|
31 | 31 | """
|
32 | 32 |
|
33 | 33 | import sys
|
| 34 | +import token |
| 35 | +from io import StringIO |
| 36 | +from tokenize import TokenInfo, generate_tokens |
34 | 37 | from typing import (
|
35 | 38 | TYPE_CHECKING,
|
36 | 39 | Callable,
|
|
48 | 51 |
|
49 | 52 | from astroid import nodes
|
50 | 53 | from astroid._ast import ParserModule, get_parser_module, parse_function_type_comment
|
51 |
| -from astroid.const import PY38, PY38_PLUS, Context |
| 54 | +from astroid.const import PY36, PY38, PY38_PLUS, Context |
52 | 55 | from astroid.manager import AstroidManager
|
53 | 56 | from astroid.nodes import NodeNG
|
| 57 | +from astroid.nodes.utils import Position |
54 | 58 |
|
55 | 59 | if sys.version_info >= (3, 8):
|
56 | 60 | from typing import Final
|
@@ -88,9 +92,13 @@ class TreeRebuilder:
|
88 | 92 | """Rebuilds the _ast tree to become an Astroid tree"""
|
89 | 93 |
|
90 | 94 | def __init__(
|
91 |
| - self, manager: AstroidManager, parser_module: Optional[ParserModule] = None |
92 |
| - ): |
| 95 | + self, |
| 96 | + manager: AstroidManager, |
| 97 | + parser_module: Optional[ParserModule] = None, |
| 98 | + data: Optional[str] = None, |
| 99 | + ) -> None: |
93 | 100 | self._manager = manager
|
| 101 | + self._data = data.split("\n") if data else None |
94 | 102 | self._global_names: List[Dict[str, List[nodes.Global]]] = []
|
95 | 103 | self._import_from_nodes: List[nodes.ImportFrom] = []
|
96 | 104 | self._delayed_assattr: List[nodes.AssignAttr] = []
|
@@ -133,6 +141,68 @@ def _get_context(
|
133 | 141 | ) -> Context:
|
134 | 142 | return self._parser_module.context_classes.get(type(node.ctx), Context.Load)
|
135 | 143 |
|
| 144 | + def _get_position_info( |
| 145 | + self, |
| 146 | + node: Union["ast.ClassDef", "ast.FunctionDef", "ast.AsyncFunctionDef"], |
| 147 | + parent: Union[nodes.ClassDef, nodes.FunctionDef, nodes.AsyncFunctionDef], |
| 148 | + ) -> Optional[Position]: |
| 149 | + """Return position information for ClassDef and FunctionDef nodes. |
| 150 | +
|
| 151 | + In contrast to AST positions, these only include the actual keyword(s) |
| 152 | + and the class / function name. |
| 153 | +
|
| 154 | + >>> @decorator |
| 155 | + >>> async def some_func(var: int) -> None: |
| 156 | + >>> ^^^^^^^^^^^^^^^^^^^ |
| 157 | + """ |
| 158 | + if not self._data: |
| 159 | + return None |
| 160 | + end_lineno: Optional[int] = getattr(node, "end_lineno", None) |
| 161 | + if node.body: |
| 162 | + end_lineno = node.body[0].lineno |
| 163 | + # pylint: disable-next=unsubscriptable-object |
| 164 | + data = "\n".join(self._data[node.lineno - 1 : end_lineno]) |
| 165 | + |
| 166 | + start_token: Optional[TokenInfo] = None |
| 167 | + keyword_tokens: Tuple[int, ...] = (token.NAME,) |
| 168 | + if isinstance(parent, nodes.AsyncFunctionDef): |
| 169 | + search_token = "async" |
| 170 | + if PY36: |
| 171 | + # In Python 3.6, the token type for 'async' was 'ASYNC' |
| 172 | + # In Python 3.7, the type was changed to 'NAME' and 'ASYNC' removed |
| 173 | + # Python 3.8 added it back. However, if we use it unconditionally |
| 174 | + # we would break 3.7. |
| 175 | + keyword_tokens = (token.NAME, token.ASYNC) |
| 176 | + elif isinstance(parent, nodes.FunctionDef): |
| 177 | + search_token = "def" |
| 178 | + else: |
| 179 | + search_token = "class" |
| 180 | + |
| 181 | + for t in generate_tokens(StringIO(data).readline): |
| 182 | + if ( |
| 183 | + start_token is not None |
| 184 | + and t.type == token.NAME |
| 185 | + and t.string == node.name |
| 186 | + ): |
| 187 | + break |
| 188 | + if t.type in keyword_tokens: |
| 189 | + if t.string == search_token: |
| 190 | + start_token = t |
| 191 | + continue |
| 192 | + if t.string in {"def"}: |
| 193 | + continue |
| 194 | + start_token = None |
| 195 | + else: |
| 196 | + return None |
| 197 | + |
| 198 | + # pylint: disable=undefined-loop-variable |
| 199 | + return Position( |
| 200 | + lineno=node.lineno - 1 + start_token.start[0], |
| 201 | + col_offset=start_token.start[1], |
| 202 | + end_lineno=node.lineno - 1 + t.end[0], |
| 203 | + end_col_offset=t.end[1], |
| 204 | + ) |
| 205 | + |
136 | 206 | def visit_module(
|
137 | 207 | self, node: "ast.Module", modname: str, modpath: str, package: bool
|
138 | 208 | ) -> nodes.Module:
|
@@ -1203,6 +1273,7 @@ def visit_classdef(
|
1203 | 1273 | for kwd in node.keywords
|
1204 | 1274 | if kwd.arg != "metaclass"
|
1205 | 1275 | ],
|
| 1276 | + position=self._get_position_info(node, newnode), |
1206 | 1277 | )
|
1207 | 1278 | return newnode
|
1208 | 1279 |
|
@@ -1551,6 +1622,7 @@ def _visit_functiondef(
|
1551 | 1622 | returns=returns,
|
1552 | 1623 | type_comment_returns=type_comment_returns,
|
1553 | 1624 | type_comment_args=type_comment_args,
|
| 1625 | + position=self._get_position_info(node, newnode), |
1554 | 1626 | )
|
1555 | 1627 | self._global_names.pop()
|
1556 | 1628 | return newnode
|
|
0 commit comments