Skip to content
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