Repository: python/mypy

Issue:
Provides `end_col_offset` for mypy ast nodes (pr proposal)
I can work and send a PR solving this


**Feature**
The mypy ast nodes sould have the `end_col_offset` attribute if this information is available in the original python AST nodes

**Pitch**

AST nodes from python version 3.8 and up providess the `end_col_offset` attribute. This information can be used to adress some issues in  mypy:

https://github.com/python/mypy/issues/12288
https://github.com/python/mypy/issues/4868
https://github.com/python/mypy/issues/12513

and also in LibCST

https://github.com/Instagram/LibCST/issues/451

The reason is because the `end_col_offset`  allows to associate a section in the source code with the type information.
We can latter on implement something similar to pyre query API using this feature. 
![image](https://user-images.githubusercontent.com/6979335/172017253-a13868e6-05b7-4d6d-912e-3e943688e01c.png)

I working on in a TypeVisitor that  represents a MypyFile node as json string (similar to pyre query result), which maybe in the future can be used by `dmypy`. 
But I need `end_col_offest` to be exposed, currently, I have a local copy of mypy that exposes the `end_col_offset`.  I can work in a PR to integrate this modification in mypy. I don't think that PR will change so much stuff. 


Discussion:
Seems useful! We could also use this in mypy itself for giving an output range for errors.
Hi @JelleZijlstra . Can I start working in a PR to expose that?
Sure! We'll have to think about how to use this new data in mypy's output and about what to do on mypy 3.6/3.7 where the feature is not yet available.

Resolving patch:
diff --git a/mypy/fastparse.py b/mypy/fastparse.py
--- a/mypy/fastparse.py
+++ b/mypy/fastparse.py
@@ -373,6 +373,8 @@ def set_line(self, node: N, n: AstNode) -> N:
         node.line = n.lineno
         node.column = n.col_offset
         node.end_line = getattr(n, "end_lineno", None) if isinstance(n, ast3.expr) else None
+        node.end_column = getattr(n, "end_col_offset", None) if isinstance(n, ast3.expr) else None
+
         return node
 
     def translate_opt_expr_list(self, l: Sequence[Optional[AST]]) -> List[Optional[Expression]]:
diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py
--- a/mypy/fastparse2.py
+++ b/mypy/fastparse2.py
@@ -599,6 +599,7 @@ def visit_ClassDef(self, n: ast27.ClassDef) -> ClassDef:
         cdef.line = n.lineno + len(n.decorator_list)
         cdef.column = n.col_offset
         cdef.end_line = n.lineno
+        cdef.end_column = None
         self.class_and_function_stack.pop()
         return cdef
 
diff --git a/mypy/nodes.py b/mypy/nodes.py
--- a/mypy/nodes.py
+++ b/mypy/nodes.py
@@ -23,17 +23,19 @@
 
 class Context:
     """Base type for objects that are valid as error message locations."""
-    __slots__ = ('line', 'column', 'end_line')
+    __slots__ = ('line', 'column', 'end_line', 'end_column')
 
     def __init__(self, line: int = -1, column: int = -1) -> None:
         self.line = line
         self.column = column
         self.end_line: Optional[int] = None
+        self.end_column: Optional[int] = None
 
     def set_line(self,
                  target: Union['Context', int],
                  column: Optional[int] = None,
-                 end_line: Optional[int] = None) -> None:
+                 end_line: Optional[int] = None,
+                 end_column: Optional[int] = None) -> None:
         """If target is a node, pull line (and column) information
         into this node. If column is specified, this will override any column
         information coming from a node.
@@ -44,6 +46,7 @@ def set_line(self,
             self.line = target.line
             self.column = target.column
             self.end_line = target.end_line
+            self.end_column = target.end_column
 
         if column is not None:
             self.column = column
@@ -51,6 +54,9 @@ def set_line(self,
         if end_line is not None:
             self.end_line = end_line
 
+        if end_column is not None:
+            self.end_column = end_column
+
     def get_line(self) -> int:
         """Don't use. Use x.line."""
         return self.line
@@ -631,13 +637,16 @@ def __init__(self,
     def set_line(self,
                  target: Union[Context, int],
                  column: Optional[int] = None,
-                 end_line: Optional[int] = None) -> None:
-        super().set_line(target, column, end_line)
+                 end_line: Optional[int] = None,
+                 end_column: Optional[int] = None) -> None:
+        super().set_line(target, column, end_line, end_column)
 
         if self.initializer and self.initializer.line < 0:
-            self.initializer.set_line(self.line, self.column, self.end_line)
+            self.initializer.set_line(
+                self.line, self.column, self.end_line, self.end_column)
 
-        self.variable.set_line(self.line, self.column, self.end_line)
+        self.variable.set_line(
+            self.line, self.column, self.end_line, self.end_column)
 
 
 FUNCITEM_FLAGS: Final = FUNCBASE_FLAGS + [
@@ -698,10 +707,11 @@ def max_fixed_argc(self) -> int:
     def set_line(self,
                  target: Union[Context, int],
                  column: Optional[int] = None,
-                 end_line: Optional[int] = None) -> None:
-        super().set_line(target, column, end_line)
+                 end_line: Optional[int] = None,
+                 end_column: Optional[int] = None) -> None:
+        super().set_line(target, column, end_line, end_column)
         for arg in self.arguments:
-            arg.set_line(self.line, self.column, self.end_line)
+            arg.set_line(self.line, self.column, self.end_line, end_column)
 
     def is_dynamic(self) -> bool:
         return self.type is None


Test patch: