Skip to content

Add orelse_lineno and orelse_col_offset to nodes.If #1480

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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 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
3 changes: 3 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Release date: TBA

* ``astroid`` now requires Python 3.7.2 to run.

* Add ``orelse_lineno`` and ``orelse_col_offset`` attributes to ``nodes.If``,
``nodes.For``, ``nodes.While``, and ``nodes.TryExcept``.

* 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
60 changes: 60 additions & 0 deletions astroid/nodes/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2661,6 +2661,12 @@ def __init__(
self.orelse: typing.List[NodeNG] = []
"""The contents of the ``else`` block of the loop."""

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

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

self.type_annotation: Optional[NodeNG] = None # can be None
"""If present, this will contain the type annotation passed by a type comment"""

Expand All @@ -2680,6 +2686,9 @@ def postinit(
body: Optional[typing.List[NodeNG]] = None,
orelse: Optional[typing.List[NodeNG]] = None,
type_annotation: Optional[NodeNG] = None,
*,
orelse_lineno: Optional[int] = None,
orelse_col_offset: Optional[int] = None,
) -> None:
"""Do some setup after initialisation.

Expand All @@ -2690,13 +2699,19 @@ def postinit(
:param body: The contents of the body of the loop.

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

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

:param orelse_lineno: The column offset of the ``else`` keyword.
"""
self.target = target
self.iter = iter
if body is not None:
self.body = body
if orelse is not None:
self.orelse = orelse
self.orelse_lineno = orelse_lineno
self.orelse_col_offset = orelse_col_offset
self.type_annotation = type_annotation

assigned_stmts: ClassVar[AssignedStmtsCall["For"]]
Expand Down Expand Up @@ -3033,6 +3048,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 @@ -3046,6 +3067,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 @@ -3054,6 +3078,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 @@ -3062,6 +3090,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 Expand Up @@ -3964,6 +3994,12 @@ def __init__(
self.orelse: typing.List[NodeNG] = []
"""The contents of the ``else`` block."""

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

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

super().__init__(
lineno=lineno,
col_offset=col_offset,
Expand All @@ -3977,6 +4013,9 @@ def postinit(
body: Optional[typing.List[NodeNG]] = None,
handlers: Optional[typing.List[ExceptHandler]] = 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 @@ -3985,13 +4024,19 @@ def postinit(
:param handlers: The exception handlers.

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

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

:param orelse_lineno: The column offset of the ``else`` keyword.
"""
if body is not None:
self.body = body
if handlers is not None:
self.handlers = handlers
if orelse is not None:
self.orelse = orelse
self.orelse_lineno = orelse_lineno
self.orelse_col_offset = orelse_col_offset

def _infer_name(self, frame, name):
return name
Expand Down Expand Up @@ -4326,6 +4371,12 @@ def __init__(
self.orelse: typing.List[NodeNG] = []
"""The contents of the ``else`` block."""

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

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

super().__init__(
lineno=lineno,
col_offset=col_offset,
Expand All @@ -4339,6 +4390,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 @@ -4347,12 +4401,18 @@ def postinit(
:param body: The contents of the loop.

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

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

:param orelse_lineno: The column offset of the ``else`` keyword.
"""
self.test = test
if body is not None:
self.body = body
if orelse is not None:
self.orelse = orelse
self.orelse_lineno = orelse_lineno
self.orelse_col_offset = orelse_col_offset

@cached_property
def blockstart_tolineno(self):
Expand Down
39 changes: 39 additions & 0 deletions astroid/rebuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,26 @@ def _reset_end_lineno(self, newnode: nodes.NodeNG) -> None:
for child_node in newnode.get_children():
self._reset_end_lineno(child_node)

def _find_orelse_keyword(
self, node: Union["ast.If", "ast.Try", "ast.For", "ast.AsyncFor", "ast.While"]
) -> 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 : node.lineno : -1]):
if line.lstrip().startswith("else"):
# if start - index == 37:
# breakpoint()
return start - index, line.index("else")
if line.lstrip().startswith("elif"):
return start - index, line.index("elif")

return None, None

def visit_module(
self, node: "ast.Module", modname: str, modpath: str, package: bool
) -> nodes.Module:
Expand Down Expand Up @@ -1175,6 +1195,8 @@ def _visit_for(
# pylint: disable-next=unsubscriptable-object
col_offset = self._data[node.lineno - 1].index("async")

orelse_lineno, orelse_col_offset = self._find_orelse_keyword(node)

newnode = cls(
lineno=node.lineno,
col_offset=col_offset,
Expand All @@ -1190,6 +1212,8 @@ def _visit_for(
body=[self.visit(child, newnode) for child in node.body],
orelse=[self.visit(child, newnode) for child in node.orelse],
type_annotation=type_annotation,
orelse_lineno=orelse_lineno,
orelse_col_offset=orelse_col_offset,
)
return newnode

Expand Down Expand Up @@ -1381,10 +1405,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 Expand Up @@ -1797,10 +1826,15 @@ def visit_tryexcept(self, node: "ast.Try", parent: NodeNG) -> nodes.TryExcept:
)
else:
newnode = nodes.TryExcept(node.lineno, node.col_offset, parent)

orelse_lineno, orelse_col_offset = self._find_orelse_keyword(node)

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

Expand Down Expand Up @@ -1868,10 +1902,15 @@ def visit_while(self, node: "ast.While", parent: NodeNG) -> nodes.While:
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
93 changes: 89 additions & 4 deletions tests/unittest_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,12 +322,64 @@ class IfNodeTest(_NodeTest):
pass
else:
raise

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

for x in range(3):
print()
else:
print()

for x in range(3):
print()
if x == 3:
break
else:
print()
else:
print()

while True:
print()
else:
print()

try:
1 / 0
except ZeroDivisionError:
print()
else:
print()
finally:
print()

try:
1 / 0
except ZeroDivisionError:
try:
1 / 0
except:
print()
else:
print()
else:
print()

"""

def test_if_elif_else_node(self) -> None:
"""test transformation for If node"""
self.assertEqual(len(self.astroid.body), 4)
for stmt in self.astroid.body:
self.assertEqual(len(self.astroid.body), 10)
for stmt in self.astroid.body[:5]:
self.assertIsInstance(stmt, nodes.If)
self.assertFalse(self.astroid.body[0].orelse) # simple If
self.assertIsInstance(self.astroid.body[1].orelse[0], nodes.Pass) # If / else
Expand All @@ -336,13 +388,46 @@ 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, 73))
self.assertEqual(self.astroid.block_range(10), (0, 73)) # XXX (10, 73) ?
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
assert self.astroid.body[5].orelse_lineno == 37
assert self.astroid.body[5].orelse_col_offset == 0
assert self.astroid.body[6].orelse_lineno == 46
assert self.astroid.body[6].orelse_col_offset == 0
assert self.astroid.body[6].body[1].orelse_lineno == 44
assert self.astroid.body[6].body[1].orelse_col_offset == 4
assert self.astroid.body[7].orelse_lineno == 51
assert self.astroid.body[7].orelse_col_offset == 0
assert self.astroid.body[8].body[0].orelse_lineno == 58
assert self.astroid.body[8].body[0].orelse_col_offset == 0
assert self.astroid.body[9].orelse_lineno == 72
assert self.astroid.body[9].orelse_col_offset == 0
assert self.astroid.body[9].handlers[0].body[0].orelse_lineno == 70
assert self.astroid.body[9].handlers[0].body[0].orelse_col_offset == 4

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