Repository: pylint-dev/astroid

Issue:
Decorator.toline is off by 1
### Steps to reproduce

I came across this inconsistency while debugging why pylint reports `missing-docstring` on the wrong line for the `g2` function in the example. As it turns out, the `toline` of the decorator seems to point to `b=3,` instead of `)`.

```python
import ast
import astroid

source = """\
@f(a=2,
   b=3,
)
def g2():
    pass
"""

[f] = ast.parse(source).body
[deco] = f.decorator_list
print("ast", deco.lineno, deco.end_lineno)

[f] = astroid.parse(source).body
[deco] = f.decorators.nodes
print("astroid", deco.fromlineno, deco.tolineno)
```

### Current behavior

```
ast 1 3
astroid 1 2
```

### Expected behavior

```
ast 1 3
astroid 1 3
```

### `python -c "from astroid import __pkginfo__; print(__pkginfo__.version)"` output

2.9.3


Discussion / hints:

Fix (patch):
diff --git a/ChangeLog b/ChangeLog
index bce6094b46..e206228968 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -30,6 +30,11 @@ Release date: TBA
 
   Closes #1330
 
+* Use the ``end_lineno`` attribute for the ``NodeNG.tolineno`` property
+  when it is available.
+
+  Closes #1350
+
 * Add ``is_dataclass`` attribute to ``ClassDef`` nodes.
 
 * Use ``sysconfig`` instead of ``distutils`` to determine the location of
diff --git a/astroid/nodes/node_ng.py b/astroid/nodes/node_ng.py
index 4688876f50..5a12925deb 100644
--- a/astroid/nodes/node_ng.py
+++ b/astroid/nodes/node_ng.py
@@ -438,6 +438,8 @@ def fromlineno(self) -> Optional[int]:
     @decorators.cachedproperty
     def tolineno(self) -> Optional[int]:
         """The last line that this node appears on in the source code."""
+        if self.end_lineno is not None:
+            return self.end_lineno
         if not self._astroid_fields:
             # can't have children
             last_child = None
diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py
index 13b007026c..94043f7ba1 100644
--- a/astroid/rebuilder.py
+++ b/astroid/rebuilder.py
@@ -2125,11 +2125,21 @@ def visit_starred(self, node: "ast.Starred", parent: NodeNG) -> nodes.Starred:
     def visit_tryexcept(self, node: "ast.Try", parent: NodeNG) -> nodes.TryExcept:
         """visit a TryExcept node by returning a fresh instance of it"""
         if sys.version_info >= (3, 8):
+            # TryExcept excludes the 'finally' but that will be included in the
+            # end_lineno from 'node'. Therefore, we check all non 'finally'
+            # children to find the correct end_lineno and column.
+            end_lineno = node.end_lineno
+            end_col_offset = node.end_col_offset
+            all_children: List["ast.AST"] = [*node.body, *node.handlers, *node.orelse]
+            for child in reversed(all_children):
+                end_lineno = child.end_lineno
+                end_col_offset = child.end_col_offset
+                break
             newnode = nodes.TryExcept(
                 lineno=node.lineno,
                 col_offset=node.col_offset,
-                end_lineno=node.end_lineno,
-                end_col_offset=node.end_col_offset,
+                end_lineno=end_lineno,
+                end_col_offset=end_col_offset,
                 parent=parent,
             )
         else:


Test changes:
diff --git a/tests/unittest_builder.py b/tests/unittest_builder.py
index eb7fbdc77f..fbcf413174 100644
--- a/tests/unittest_builder.py
+++ b/tests/unittest_builder.py
@@ -71,11 +71,14 @@ def test_callfunc_lineno(self) -> None:
         strarg = callfunc.args[0]
         self.assertIsInstance(strarg, nodes.Const)
         if hasattr(sys, "pypy_version_info"):
-            lineno = 4
+            self.assertEqual(strarg.fromlineno, 4)
+            self.assertEqual(strarg.tolineno, 4)
         else:
-            lineno = 5 if not PY38_PLUS else 4
-        self.assertEqual(strarg.fromlineno, lineno)
-        self.assertEqual(strarg.tolineno, lineno)
+            if not PY38_PLUS:
+                self.assertEqual(strarg.fromlineno, 5)
+            else:
+                self.assertEqual(strarg.fromlineno, 4)
+            self.assertEqual(strarg.tolineno, 5)
         namearg = callfunc.args[1]
         self.assertIsInstance(namearg, nodes.Name)
         self.assertEqual(namearg.fromlineno, 5)
diff --git a/tests/unittest_nodes_lineno.py b/tests/unittest_nodes_lineno.py
index 73cf0207cc..0f1b7e4ac7 100644
--- a/tests/unittest_nodes_lineno.py
+++ b/tests/unittest_nodes_lineno.py
@@ -784,7 +784,7 @@ def test_end_lineno_try() -> None:
         assert (t3.lineno, t3.col_offset) == (10, 0)
         assert (t3.end_lineno, t3.end_col_offset) == (17, 8)
         assert (t3.body[0].lineno, t3.body[0].col_offset) == (10, 0)
-        assert (t3.body[0].end_lineno, t3.body[0].end_col_offset) == (17, 8)
+        assert (t3.body[0].end_lineno, t3.body[0].end_col_offset) == (15, 8)
         assert (t3.finalbody[0].lineno, t3.finalbody[0].col_offset) == (17, 4)
         assert (t3.finalbody[0].end_lineno, t3.finalbody[0].end_col_offset) == (17, 8)
 
diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py
index 9b2bc291a6..0009278d75 100644
--- a/tests/unittest_scoped_nodes.py
+++ b/tests/unittest_scoped_nodes.py
@@ -1092,15 +1092,19 @@ def g1(x):
                 print(x)
 
             @f(a=2,
-               b=3)
+               b=3,
+            )
             def g2():
                 pass
         """
         astroid = builder.parse(data)
         self.assertEqual(astroid["g1"].fromlineno, 4)
         self.assertEqual(astroid["g1"].tolineno, 5)
-        self.assertEqual(astroid["g2"].fromlineno, 9)
-        self.assertEqual(astroid["g2"].tolineno, 10)
+        if not PY38_PLUS:
+            self.assertEqual(astroid["g2"].fromlineno, 9)
+        else:
+            self.assertEqual(astroid["g2"].fromlineno, 10)
+        self.assertEqual(astroid["g2"].tolineno, 11)
 
     def test_metaclass_error(self) -> None:
         astroid = builder.parse(