Handle FunctionDef blockstart_tolineno edge cases (#2880)
Getting the lineno of the start of the block for function definition(`FunctionDef.blockstart_tolineno`) can be quite tricky. Take below example:
```python
# Case A
def foo(bar: str) -> None:
pass
# should returns line=1
# Case B
def foo(
bar:str):
pass
# should returns line=2
# Case C
def foo(
bar:str
) -> None:
pass
# should returns line=3
# Case D
def foo(
bar:str
):
# should returns line=3
pass
```
Currently we only handled Case A, B. With this commit we can cover case C.
But for Case D, we will need a better solutiondiff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py
index 8035008..d769b07 100644
--- a/astroid/nodes/scoped_nodes/scoped_nodes.py
+++ b/astroid/nodes/scoped_nodes/scoped_nodes.py
@@ -1401,6 +1401,8 @@
:type: int
"""
+ if self.returns:
+ return self.returns.tolineno
return self.args.tolineno
def implicit_parameters(self) -> Literal[0, 1]:
diff --git a/tests/test_scoped_nodes.py b/tests/test_scoped_nodes.py
index c95f53f..875ee05 100644
--- a/tests/test_scoped_nodes.py
+++ b/tests/test_scoped_nodes.py
@@ -982,6 +982,46 @@
with pytest.raises(AttributeInferenceError):
func.getattr("")
+ @staticmethod
+ def test_blockstart_tolineno() -> None:
+ code = textwrap.dedent(
+ """\
+ def f1(bar: str) -> None: #@
+ pass
+
+ def f2( #@
+ bar: str) -> None:
+ pass
+
+ def f3( #@
+ bar: str
+ ) -> None:
+ pass
+
+ def f4( #@
+ bar: str
+ ):
+ pass
+
+ def f5( #@
+ bar: str):
+ pass
+ """
+ )
+ ast_nodes: list[nodes.FunctionDef] = builder.extract_node(code) # type: ignore[assignment]
+ assert len(ast_nodes) == 5
+
+ assert ast_nodes[0].blockstart_tolineno == 1
+
+ assert ast_nodes[1].blockstart_tolineno == 5
+
+ assert ast_nodes[2].blockstart_tolineno == 10
+
+ # Unimplemented, will return line 14 for now.
+ # assert ast_nodes[3].blockstart_tolineno == 15
+
+ assert ast_nodes[4].blockstart_tolineno == 19
+
class ClassNodeTest(ModuleLoader, unittest.TestCase):
def test_dict_interface(self) -> None: