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 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