Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add orelse_lineno and orelse_col_offset to nodes.If #1480

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ What's New in astroid 2.12.0?
=============================
Release date: TBA

* Add ``orelse_lineno`` and ``orelse_col_offset`` attributes to ``nodes.If``.

* Fix ``re`` brain on Python ``3.11``. The flags now come from ``re._compile``.

* Build ``nodes.Module`` for frozen modules which have location information in their
Expand Down
15 changes: 15 additions & 0 deletions astroid/nodes/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3032,6 +3032,12 @@ def __init__(
self.is_orelse: bool = False
"""Whether the if-statement is the orelse-block of another if statement."""

self.orelse_lineno: Optional[int] = None
"""The line number of the ``else`` or ``elif`` keyword."""

self.orelse_col_offset: Optional[int] = None
"""The column offset of the ``else`` or ``elif`` keyword."""

super().__init__(
lineno=lineno,
col_offset=col_offset,
Expand All @@ -3045,6 +3051,9 @@ def postinit(
test: Optional[NodeNG] = None,
body: Optional[typing.List[NodeNG]] = None,
orelse: Optional[typing.List[NodeNG]] = None,
*,
orelse_lineno: Optional[int] = None,
orelse_col_offset: Optional[int] = None,
) -> None:
"""Do some setup after initialisation.

Expand All @@ -3053,6 +3062,10 @@ def postinit(
:param body: The contents of the block.

:param orelse: The contents of the ``else`` block.

:param orelse_lineno: The line number of the ``else`` or ``elif`` keyword.

:param orelse_lineno: The column offset of the ``else`` or ``elif`` keyword.
"""
self.test = test
if body is not None:
Expand All @@ -3061,6 +3074,8 @@ def postinit(
self.orelse = orelse
if isinstance(self.parent, If) and self in self.parent.orelse:
self.is_orelse = True
self.orelse_lineno = orelse_lineno
self.orelse_col_offset = orelse_col_offset

@cached_property
def blockstart_tolineno(self):
Expand Down
23 changes: 23 additions & 0 deletions astroid/rebuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,24 @@ def visit_global(self, node: "ast.Global", parent: NodeNG) -> nodes.Global:
self._global_names[-1].setdefault(name, []).append(newnode)
return newnode

def _find_orelse_keyword(
self, node: "ast.If"
) -> Tuple[Optional[int], Optional[int]]:
"""Get the line number and column offset of the `else` or `elif` keyword."""
if not self._data or not node.orelse:
return None, None

start = node.orelse[0].lineno

# pylint: disable-next=unsubscriptable-object
for index, line in enumerate(self._data[start::-1]):
if line.rstrip().startswith("else"):
return start - index + 1, line.index("else")
if line.rstrip().startswith("elif"):
return start - index + 1, line.index("elif")

return None, None

def visit_if(self, node: "ast.If", parent: NodeNG) -> nodes.If:
"""visit an If node by returning a fresh instance of it"""
newnode = nodes.If(
Expand All @@ -1397,10 +1415,15 @@ def visit_if(self, node: "ast.If", parent: NodeNG) -> nodes.If:
end_col_offset=getattr(node, "end_col_offset", None),
parent=parent,
)

orelse_lineno, orelse_col_offset = self._find_orelse_keyword(node)

newnode.postinit(
self.visit(node.test, newnode),
[self.visit(child, newnode) for child in node.body],
[self.visit(child, newnode) for child in node.orelse],
orelse_lineno=orelse_lineno,
orelse_col_offset=orelse_col_offset,
)
return newnode

Expand Down
37 changes: 34 additions & 3 deletions tests/unittest_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,23 @@ class IfNodeTest(_NodeTest):
pass
else:
raise

if 1:
print()
elif (
2
and 3
):
print()
else:
# This is using else in a comment
raise

"""

def test_if_elif_else_node(self) -> None:
"""test transformation for If node"""
self.assertEqual(len(self.astroid.body), 4)
self.assertEqual(len(self.astroid.body), 5)
for stmt in self.astroid.body:
self.assertIsInstance(stmt, nodes.If)
self.assertFalse(self.astroid.body[0].orelse) # simple If
Expand All @@ -336,13 +348,32 @@ def test_if_elif_else_node(self) -> None:

def test_block_range(self) -> None:
# XXX ensure expected values
self.assertEqual(self.astroid.block_range(1), (0, 22))
self.assertEqual(self.astroid.block_range(10), (0, 22)) # XXX (10, 22) ?
self.assertEqual(self.astroid.block_range(1), (0, 33))
self.assertEqual(self.astroid.block_range(10), (0, 33)) # XXX (10, 33) ?
self.assertEqual(self.astroid.body[1].block_range(5), (5, 6))
self.assertEqual(self.astroid.body[1].block_range(6), (6, 6))
self.assertEqual(self.astroid.body[1].orelse[0].block_range(7), (7, 8))
self.assertEqual(self.astroid.body[1].orelse[0].block_range(8), (8, 8))

def test_orelse_line_numbering(self) -> None:
"""Test the position info for the `else` keyword."""
assert self.astroid.body[0].orelse_lineno is None
assert self.astroid.body[0].orelse_col_offset is None
assert self.astroid.body[1].orelse_lineno == 7
assert self.astroid.body[1].orelse_col_offset == 0
assert self.astroid.body[2].orelse_lineno == 12
assert self.astroid.body[2].orelse_col_offset == 0
assert self.astroid.body[3].orelse_lineno == 17
assert self.astroid.body[3].orelse_col_offset == 0
assert self.astroid.body[3].orelse[0].orelse_lineno == 19
assert self.astroid.body[3].orelse[0].orelse_col_offset == 0
assert self.astroid.body[3].orelse[0].orelse[0].orelse_lineno == 21
assert self.astroid.body[3].orelse[0].orelse[0].orelse_col_offset == 0
assert self.astroid.body[4].orelse_lineno == 26
assert self.astroid.body[4].orelse_col_offset == 0
assert self.astroid.body[4].orelse[0].orelse_lineno == 31
assert self.astroid.body[4].orelse[0].orelse_col_offset == 0

@staticmethod
@pytest.mark.filterwarnings("ignore:.*is_sys_guard:DeprecationWarning")
def test_if_sys_guard() -> None:
Expand Down