1# Copyright (c) Meta Platforms, Inc. and affiliates.
2#
3# This source code is licensed under the MIT license found in the
4# LICENSE file in the root directory of this source tree.
5
6import dataclasses
7from pathlib import Path
8from typing import Any, Collection, List, Mapping, Optional, Union
9
10import libcst as cst
11from libcst._metadata_dependent import LazyValue, MetadataDependent
12from libcst.helpers.module import calculate_module_and_package, ModuleNameAndPackage
13from libcst.metadata.base_provider import BatchableMetadataProvider
14from libcst.metadata.scope_provider import (
15 QualifiedName,
16 QualifiedNameSource,
17 ScopeProvider,
18)
19
20
21class QualifiedNameProvider(BatchableMetadataProvider[Collection[QualifiedName]]):
22 """
23 Compute possible qualified names of a variable CSTNode
24 (extends `PEP-3155 <https://www.python.org/dev/peps/pep-3155/>`_).
25 It uses the
26 :func:`~libcst.metadata.Scope.get_qualified_names_for` underlying to get qualified names.
27 Multiple qualified names may be returned, such as when we have conditional imports or an
28 import shadows another. E.g., the provider finds ``a.b``, ``d.e`` and
29 ``f.g`` as possible qualified names of ``c``::
30
31 >>> wrapper = MetadataWrapper(
32 >>> cst.parse_module(dedent(
33 >>> '''
34 >>> if something:
35 >>> from a import b as c
36 >>> elif otherthing:
37 >>> from d import e as c
38 >>> else:
39 >>> from f import g as c
40 >>> c()
41 >>> '''
42 >>> ))
43 >>> )
44 >>> call = wrapper.module.body[1].body[0].value
45 >>> wrapper.resolve(QualifiedNameProvider)[call],
46 {
47 QualifiedName(name="a.b", source=QualifiedNameSource.IMPORT),
48 QualifiedName(name="d.e", source=QualifiedNameSource.IMPORT),
49 QualifiedName(name="f.g", source=QualifiedNameSource.IMPORT),
50 }
51
52 For qualified name of a variable in a function or a comprehension, please refer
53 :func:`~libcst.metadata.Scope.get_qualified_names_for` for more detail.
54 """
55
56 METADATA_DEPENDENCIES = (ScopeProvider,)
57
58 def visit_Module(self, node: cst.Module) -> Optional[bool]:
59 visitor = QualifiedNameVisitor(self)
60 node.visit(visitor)
61
62 @staticmethod
63 def has_name(
64 visitor: MetadataDependent, node: cst.CSTNode, name: Union[str, QualifiedName]
65 ) -> bool:
66 """Check if any of qualified name has the str name or :class:`~libcst.metadata.QualifiedName` name."""
67 qualified_names = visitor.get_metadata(QualifiedNameProvider, node, set())
68 if isinstance(name, str):
69 return any(qn.name == name for qn in qualified_names)
70 else:
71 return any(qn == name for qn in qualified_names)
72
73
74class QualifiedNameVisitor(cst.CSTVisitor):
75 def __init__(self, provider: "QualifiedNameProvider") -> None:
76 self.provider: QualifiedNameProvider = provider
77
78 def on_visit(self, node: cst.CSTNode) -> bool:
79 scope = self.provider.get_metadata(ScopeProvider, node, None)
80 if scope:
81 self.provider.set_metadata(
82 node, LazyValue(lambda: scope.get_qualified_names_for(node))
83 )
84 else:
85 self.provider.set_metadata(node, set())
86 super().on_visit(node)
87 return True
88
89
90class FullyQualifiedNameProvider(BatchableMetadataProvider[Collection[QualifiedName]]):
91 """
92 Provide fully qualified names for CST nodes. Like :class:`QualifiedNameProvider`,
93 but the provided :class:`QualifiedName` instances have absolute identifier names
94 instead of local to the current module.
95
96 This provider is initialized with the current module's fully qualified name, and can
97 be used with :class:`~libcst.metadata.FullRepoManager`. The module's fully qualified
98 name itself is stored as a metadata of the :class:`~libcst.Module` node. Compared to
99 :class:`QualifiedNameProvider`, it also resolves relative imports.
100
101 Example usage::
102
103 >>> mgr = FullRepoManager(".", {"dir/a.py"}, {FullyQualifiedNameProvider})
104 >>> wrapper = mgr.get_metadata_wrapper_for_path("dir/a.py")
105 >>> fqnames = wrapper.resolve(FullyQualifiedNameProvider)
106 >>> {type(k): v for (k, v) in fqnames.items()}
107 {<class 'libcst._nodes.module.Module'>: {QualifiedName(name='dir.a', source=<QualifiedNameSource.LOCAL: 3>)}}
108
109 """
110
111 METADATA_DEPENDENCIES = (QualifiedNameProvider,)
112
113 @classmethod
114 def gen_cache(
115 cls,
116 root_path: Path,
117 paths: List[str],
118 *,
119 use_pyproject_toml: bool = False,
120 **kwargs: Any,
121 ) -> Mapping[str, ModuleNameAndPackage]:
122 cache = {
123 path: calculate_module_and_package(
124 root_path, path, use_pyproject_toml=use_pyproject_toml
125 )
126 for path in paths
127 }
128 return cache
129
130 def __init__(self, cache: ModuleNameAndPackage) -> None:
131 super().__init__(cache)
132 self.module_name: str = cache.name
133 self.package_name: str = cache.package
134
135 def visit_Module(self, node: cst.Module) -> bool:
136 visitor = FullyQualifiedNameVisitor(self, self.module_name, self.package_name)
137 node.visit(visitor)
138 self.set_metadata(
139 node,
140 {QualifiedName(name=self.module_name, source=QualifiedNameSource.LOCAL)},
141 )
142 return True
143
144
145class FullyQualifiedNameVisitor(cst.CSTVisitor):
146 @staticmethod
147 def _fully_qualify_local(module_name: str, package_name: str, name: str) -> str:
148 abs_name = name.lstrip(".")
149 num_dots = len(name) - len(abs_name)
150 # handle relative import
151 if num_dots > 0:
152 name = abs_name
153 # see importlib._bootstrap._resolve_name
154 # https://github.com/python/cpython/blob/3.10/Lib/importlib/_bootstrap.py#L902
155 bits = package_name.rsplit(".", num_dots - 1)
156 if len(bits) < num_dots:
157 raise ImportError("attempted relative import beyond top-level package")
158 module_name = bits[0]
159
160 return f"{module_name}.{name}"
161
162 @staticmethod
163 def _fully_qualify(
164 module_name: str, package_name: str, qname: QualifiedName
165 ) -> QualifiedName:
166 if qname.source == QualifiedNameSource.BUILTIN:
167 # builtins are already fully qualified
168 return qname
169 name = qname.name
170 if qname.source == QualifiedNameSource.IMPORT and not name.startswith("."):
171 # non-relative imports are already fully qualified
172 return qname
173 new_name = FullyQualifiedNameVisitor._fully_qualify_local(
174 module_name, package_name, qname.name
175 )
176 return dataclasses.replace(qname, name=new_name)
177
178 def __init__(
179 self, provider: FullyQualifiedNameProvider, module_name: str, package_name: str
180 ) -> None:
181 self.module_name = module_name
182 self.package_name = package_name
183 self.provider = provider
184
185 def on_visit(self, node: cst.CSTNode) -> bool:
186 qnames = self.provider.get_metadata(QualifiedNameProvider, node)
187 if qnames is not None:
188 self.provider.set_metadata(
189 node,
190 {
191 FullyQualifiedNameVisitor._fully_qualify(
192 self.module_name, self.package_name, qname
193 )
194 for qname in qnames
195 },
196 )
197 return True