Skip to content

Commit

Permalink
Stop parsing string annotations when no longer in a typing call (#546)
Browse files Browse the repository at this point in the history
* Fix ScopeProvider when string type annotation is unparsable

* Handle nested function calls w/in type declarations

* Edit stack in place

* Add unparsed test to test_cast
  • Loading branch information
lpetre authored Nov 17, 2021
1 parent 7db6ec5 commit 56386d7
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 16 deletions.
34 changes: 19 additions & 15 deletions libcst/metadata/scope_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,10 +789,8 @@ def __init__(self, provider: "ScopeProvider") -> None:
self.scope: Scope = GlobalScope()
self.__deferred_accesses: List[DeferredAccess] = []
self.__top_level_attribute_stack: List[Optional[cst.Attribute]] = [None]
self.__in_annotation: Set[
Union[cst.Call, cst.Annotation, cst.Subscript]
] = set()
self.__in_type_hint: Set[Union[cst.Call, cst.Annotation, cst.Subscript]] = set()
self.__in_annotation_stack: List[bool] = [False]
self.__in_type_hint_stack: List[bool] = [False]
self.__in_ignored_subscript: Set[cst.Subscript] = set()
self.__last_string_annotation: Optional[cst.BaseString] = None
self.__ignore_annotation: int = 0
Expand Down Expand Up @@ -851,33 +849,36 @@ def visit_Attribute(self, node: cst.Attribute) -> Optional[bool]:

def visit_Call(self, node: cst.Call) -> Optional[bool]:
self.__top_level_attribute_stack.append(None)
self.__in_type_hint_stack.append(False)
self.__in_annotation_stack.append(False)
qnames = {qn.name for qn in self.scope.get_qualified_names_for(node)}
if "typing.NewType" in qnames or "typing.TypeVar" in qnames:
node.func.visit(self)
self.__in_type_hint.add(node)
self.__in_type_hint_stack[-1] = True
for arg in node.args[1:]:
arg.visit(self)
return False
if "typing.cast" in qnames:
node.func.visit(self)
if len(node.args) > 0:
self.__in_type_hint.add(node)
self.__in_type_hint_stack.append(True)
node.args[0].visit(self)
self.__in_type_hint.discard(node)
self.__in_type_hint_stack.pop()
for arg in node.args[1:]:
arg.visit(self)
return False
return True

def leave_Call(self, original_node: cst.Call) -> None:
self.__top_level_attribute_stack.pop()
self.__in_type_hint.discard(original_node)
self.__in_type_hint_stack.pop()
self.__in_annotation_stack.pop()

def visit_Annotation(self, node: cst.Annotation) -> Optional[bool]:
self.__in_annotation.add(node)
self.__in_annotation_stack.append(True)

def leave_Annotation(self, original_node: cst.Annotation) -> None:
self.__in_annotation.discard(original_node)
self.__in_annotation_stack.pop()

def visit_SimpleString(self, node: cst.SimpleString) -> Optional[bool]:
self._handle_string_annotation(node)
Expand All @@ -891,7 +892,7 @@ def _handle_string_annotation(
) -> bool:
"""Returns whether it successfully handled the string annotation"""
if (
self.__in_type_hint or self.__in_annotation
self.__in_type_hint_stack[-1] or self.__in_annotation_stack[-1]
) and not self.__in_ignored_subscript:
value = node.evaluated_value
if value:
Expand All @@ -911,16 +912,19 @@ def _handle_string_annotation(
return False

def visit_Subscript(self, node: cst.Subscript) -> Optional[bool]:
in_type_hint = False
if isinstance(node.value, cst.Name):
qnames = {qn.name for qn in self.scope.get_qualified_names_for(node.value)}
if any(qn.startswith(("typing.", "typing_extensions.")) for qn in qnames):
self.__in_type_hint.add(node)
in_type_hint = True
if "typing.Literal" in qnames or "typing_extensions.Literal" in qnames:
self.__in_ignored_subscript.add(node)

self.__in_type_hint_stack.append(in_type_hint)
return True

def leave_Subscript(self, original_node: cst.Subscript) -> None:
self.__in_type_hint.discard(original_node)
self.__in_type_hint_stack.pop()
self.__in_ignored_subscript.discard(original_node)

def visit_Name(self, node: cst.Name) -> Optional[bool]:
Expand All @@ -933,9 +937,9 @@ def visit_Name(self, node: cst.Name) -> Optional[bool]:
node,
self.scope,
is_annotation=bool(
self.__in_annotation and not self.__ignore_annotation
self.__in_annotation_stack[-1] and not self.__ignore_annotation
),
is_type_hint=bool(self.__in_type_hint),
is_type_hint=bool(self.__in_type_hint_stack[-1]),
)
self.__deferred_accesses.append(
DeferredAccess(
Expand Down
26 changes: 25 additions & 1 deletion libcst/metadata/tests/test_scope_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1191,7 +1191,7 @@ class Test(Generic[J]):
def test_insane_annotation_access(self) -> None:
m, scopes = get_scope_metadata_provider(
r"""
from typing import TypeVar
from typing import TypeVar, Optional
from a import G
TypeVar("G2", bound="Optional[\"G\"]")
"""
Expand Down Expand Up @@ -1760,6 +1760,30 @@ def assert_parsed(code, *calls):
mock.call("int"),
)

assert_parsed(
"""
from typing import TypeVar
TypeVar("Name", func("int"))
""",
)

assert_parsed(
"""
from typing import Literal
Literal[\"G\"]
""",
)

assert_parsed(
r"""
from typing import TypeVar, Optional
from a import G
TypeVar("G2", bound="Optional[\"G\"]")
""",
mock.call('Optional["G"]'),
mock.call("G"),
)

def test_builtin_scope(self) -> None:
m, scopes = get_scope_metadata_provider(
"""
Expand Down

0 comments on commit 56386d7

Please sign in to comment.