Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split QualifiedNameProvider out from libcst.metadata.scope_provider #464

Merged
merged 3 commits into from
Mar 12, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libcst/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
ExpressionContextProvider,
)
from libcst.metadata.full_repo_manager import FullRepoManager
from libcst.metadata.name_provider import QualifiedNameProvider
from libcst.metadata.parent_node_provider import ParentNodeProvider
from libcst.metadata.position_provider import (
PositionProvider,
Expand All @@ -37,7 +38,6 @@
FunctionScope,
GlobalScope,
QualifiedName,
QualifiedNameProvider,
QualifiedNameSource,
Scope,
ScopeProvider,
Expand Down
78 changes: 78 additions & 0 deletions libcst/metadata/name_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from typing import Collection, Optional, Union

import libcst as cst
from libcst._metadata_dependent import MetadataDependent
from libcst.metadata.base_provider import BatchableMetadataProvider
from libcst.metadata.scope_provider import QualifiedName, ScopeProvider


class QualifiedNameProvider(BatchableMetadataProvider[Collection[QualifiedName]]):
"""
Compute possible qualified names of a variable CSTNode
(extends `PEP-3155 <https://www.python.org/dev/peps/pep-3155/>`_).
It uses the
:func:`~libcst.metadata.Scope.get_qualified_names_for` underlying to get qualified names.
Multiple qualified names may be returned, such as when we have conditional imports or an
import shadows another. E.g., the provider finds ``a.b``, ``d.e`` and
``f.g`` as possible qualified names of ``c``::

>>> wrapper = MetadataWrapper(
>>> cst.parse_module(dedent(
>>> '''
>>> if something:
>>> from a import b as c
>>> elif otherthing:
>>> from d import e as c
>>> else:
>>> from f import g as c
>>> c()
>>> '''
>>> ))
>>> )
>>> call = wrapper.module.body[1].body[0].value
>>> wrapper.resolve(QualifiedNameProvider)[call],
{
QualifiedName(name="a.b", source=QualifiedNameSource.IMPORT),
QualifiedName(name="d.e", source=QualifiedNameSource.IMPORT),
QualifiedName(name="f.g", source=QualifiedNameSource.IMPORT),
}

For qualified name of a variable in a function or a comprehension, please refer
:func:`~libcst.metadata.Scope.get_qualified_names_for` for more detail.
"""

METADATA_DEPENDENCIES = (ScopeProvider,)

def visit_Module(self, node: cst.Module) -> Optional[bool]:
visitor = QualifiedNameVisitor(self)
node.visit(visitor)

@staticmethod
def has_name(
visitor: MetadataDependent, node: cst.CSTNode, name: Union[str, QualifiedName]
) -> bool:
"""Check if any of qualified name has the str name or :class:`~libcst.metadata.QualifiedName` name."""
qualified_names = visitor.get_metadata(QualifiedNameProvider, node, set())
if isinstance(name, str):
return any(qn.name == name for qn in qualified_names)
else:
return any(qn == name for qn in qualified_names)


class QualifiedNameVisitor(cst.CSTVisitor):
def __init__(self, provider: "QualifiedNameProvider") -> None:
self.provider: QualifiedNameProvider = provider

def on_visit(self, node: cst.CSTNode) -> bool:
scope = self.provider.get_metadata(ScopeProvider, node, None)
if scope:
self.provider.set_metadata(node, scope.get_qualified_names_for(node))
else:
self.provider.set_metadata(node, set())
super().on_visit(node)
return True
68 changes: 0 additions & 68 deletions libcst/metadata/scope_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import libcst as cst
from libcst import ensure_type
from libcst._add_slots import add_slots
from libcst._metadata_dependent import MetadataDependent
from libcst.helpers import get_full_name_for_node
from libcst.metadata.base_provider import BatchableMetadataProvider
from libcst.metadata.expression_context_provider import (
Expand Down Expand Up @@ -1051,70 +1050,3 @@ def visit_Module(self, node: cst.Module) -> Optional[bool]:
visitor = ScopeVisitor(self)
node.visit(visitor)
visitor.infer_accesses()


class QualifiedNameVisitor(cst.CSTVisitor):
def __init__(self, provider: "QualifiedNameProvider") -> None:
self.provider: QualifiedNameProvider = provider

def on_visit(self, node: cst.CSTNode) -> bool:
scope = self.provider.get_metadata(ScopeProvider, node, None)
if scope:
self.provider.set_metadata(node, scope.get_qualified_names_for(node))
else:
self.provider.set_metadata(node, set())
super().on_visit(node)
return True


class QualifiedNameProvider(BatchableMetadataProvider[Collection[QualifiedName]]):
"""
Compute possible qualified names of a variable CSTNode
(extends `PEP-3155 <https://www.python.org/dev/peps/pep-3155/>`_).
It uses the
:func:`~libcst.metadata.Scope.get_qualified_names_for` underlying to get qualified names.
Multiple qualified names may be returned, such as when we have conditional imports or an
import shadows another. E.g., the provider finds ``a.b``, ``d.e`` and
``f.g`` as possible qualified names of ``c``::

>>> wrapper = MetadataWrapper(
>>> cst.parse_module(dedent(
>>> '''
>>> if something:
>>> from a import b as c
>>> elif otherthing:
>>> from d import e as c
>>> else:
>>> from f import g as c
>>> c()
>>> '''
>>> ))
>>> )
>>> call = wrapper.module.body[1].body[0].value
>>> wrapper.resolve(QualifiedNameProvider)[call],
{
QualifiedName(name="a.b", source=QualifiedNameSource.IMPORT),
QualifiedName(name="d.e", source=QualifiedNameSource.IMPORT),
QualifiedName(name="f.g", source=QualifiedNameSource.IMPORT),
}

For qualified name of a variable in a function or a comprehension, please refer
:func:`~libcst.metadata.Scope.get_qualified_names_for` for more detail.
"""

METADATA_DEPENDENCIES = (ScopeProvider,)

def visit_Module(self, node: cst.Module) -> Optional[bool]:
visitor = QualifiedNameVisitor(self)
node.visit(visitor)

@staticmethod
def has_name(
visitor: MetadataDependent, node: cst.CSTNode, name: Union[str, QualifiedName]
) -> bool:
"""Check if any of qualified name has the str name or :class:`~libcst.metadata.QualifiedName` name."""
qualified_names = visitor.get_metadata(QualifiedNameProvider, node, set())
if isinstance(name, str):
return any(qn.name == name for qn in qualified_names)
else:
return any(qn == name for qn in qualified_names)
54 changes: 54 additions & 0 deletions libcst/metadata/tests/test_name_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a test_qualified_name_provider.py which is duplicated with this test_name_provider.py
We should consolidate and provide a better file naming.
https://github.com/Instagram/LibCST/blob/6731aa5d2940ce82dd221963b64a496955bfd07a/libcst/metadata/tests/test_qualified_name_provider.py

# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from textwrap import dedent
from typing import Set

import libcst as cst
from libcst.metadata.name_provider import QualifiedNameProvider
from libcst.metadata.scope_provider import QualifiedName, QualifiedNameSource
from libcst.testing.utils import UnitTest


def get_qualified_names(module_str: str) -> Set[QualifiedName]:
wrapper = cst.MetadataWrapper(cst.parse_module(dedent(module_str)))
qnames = wrapper.resolve(QualifiedNameProvider)
return set().union(*qnames.values())


class QualifiedNameProviderTest(UnitTest):
def test_imports(self) -> None:
qnames = get_qualified_names(
"""
from a.b import c as d
d
"""
)
self.assertEqual({"a.b.c"}, {qname.name for qname in qnames})
for qname in qnames:
self.assertEqual(qname.source, QualifiedNameSource.IMPORT, msg=f"{qname}")

def test_builtins(self) -> None:
qnames = get_qualified_names(
"""
int(None)
"""
)
self.assertEqual(
{"builtins.int", "builtins.None"}, {qname.name for qname in qnames}
)
for qname in qnames:
self.assertEqual(qname.source, QualifiedNameSource.BUILTIN, msg=f"{qname}")

def test_locals(self) -> None:
qnames = get_qualified_names(
"""
class X:
a: "X"
"""
)
self.assertEqual({"X", "X.a"}, {qname.name for qname in qnames})
for qname in qnames:
self.assertEqual(qname.source, QualifiedNameSource.LOCAL, msg=f"{qname}")