Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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: 1 addition & 1 deletion ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ What's New in astroid 2.12.0?
=============================
Release date: TBA


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

What's New in astroid 2.11.1?
=============================
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 @@ -3034,6 +3034,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 @@ -3047,6 +3053,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 @@ -3055,6 +3064,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 @@ -3063,6 +3076,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
32 changes: 32 additions & 0 deletions astroid/rebuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,33 @@ 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

end_lineno = node.orelse[0].lineno

def find_keyword(begin: int, end: int) -> Tuple[Optional[int], Optional[int]]:
# pylint: disable-next=unsubscriptable-object
data = "\n".join(self._data[begin:end])

try:
tokens = list(generate_tokens(StringIO(data).readline))
except tokenize.TokenError:
# If we cut-off in the middle of multi-line if statements we
# generate a TokenError here. We just keep trying
# until the multi-line statement is closed.
return find_keyword(begin, end + 1)
for t in tokens[::-1]:
if t.type == token.NAME and t.string in {"else", "elif"}:
return node.lineno + t.start[0] - 1, t.start[1]
raise AssertionError() # pragma: no cover # Shouldn't be reached.

return find_keyword(node.lineno - 1, end_lineno)

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 @@ -1392,10 +1419,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
15 changes: 15 additions & 0 deletions tests/unittest_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,21 @@ def test_block_range(self) -> None:
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

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