Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/libcst/codemod/visitors/_gather_unused_imports.py: 32%
60 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:43 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:43 +0000
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#
7from typing import Collection, Iterable, Set, Tuple, Union
9import libcst as cst
10from libcst.codemod._context import CodemodContext
11from libcst.codemod._visitor import ContextAwareVisitor
12from libcst.codemod.visitors._gather_exports import GatherExportsVisitor
13from libcst.codemod.visitors._gather_string_annotation_names import (
14 FUNCS_CONSIDERED_AS_STRING_ANNOTATIONS,
15 GatherNamesFromStringAnnotationsVisitor,
16)
17from libcst.metadata import ProviderT, ScopeProvider
18from libcst.metadata.scope_provider import _gen_dotted_names
20MODULES_IGNORED_BY_DEFAULT = {"__future__"}
23class GatherUnusedImportsVisitor(ContextAwareVisitor):
24 """
25 Collects all imports from a module not directly used in the same module.
26 Intended to be instantiated and passed to a :class:`libcst.Module`
27 :meth:`~libcst.CSTNode.visit` method to process the full module.
29 Note that imports that are only used indirectly (from other modules) are
30 still collected.
32 After visiting a module the attribute ``unused_imports`` will contain a
33 set of unused :class:`~libcst.ImportAlias` objects, paired with their
34 parent import node.
35 """
37 # pyre-fixme[8]: Attribute has type
38 # `Tuple[typing.Type[cst.metadata.base_provider.BaseMetadataProvider[object]]]`;
39 # used as `Tuple[typing.Type[cst.metadata.name_provider.QualifiedNameProvider],
40 # typing.Type[cst.metadata.scope_provider.ScopeProvider]]`.
41 METADATA_DEPENDENCIES: Tuple[ProviderT] = (
42 *GatherNamesFromStringAnnotationsVisitor.METADATA_DEPENDENCIES,
43 ScopeProvider,
44 )
46 def __init__(
47 self,
48 context: CodemodContext,
49 ignored_modules: Collection[str] = MODULES_IGNORED_BY_DEFAULT,
50 typing_functions: Collection[str] = FUNCS_CONSIDERED_AS_STRING_ANNOTATIONS,
51 ) -> None:
52 super().__init__(context)
54 self._ignored_modules: Collection[str] = ignored_modules
55 self._typing_functions = typing_functions
56 self._string_annotation_names: Set[str] = set()
57 self._exported_names: Set[str] = set()
58 #: Contains a set of (alias, parent_import) pairs that are not used
59 #: in the module after visiting.
60 self.unused_imports: Set[
61 Tuple[cst.ImportAlias, Union[cst.Import, cst.ImportFrom]]
62 ] = set()
64 def visit_Module(self, node: cst.Module) -> bool:
65 export_collector = GatherExportsVisitor(self.context)
66 node.visit(export_collector)
67 self._exported_names = export_collector.explicit_exported_objects
68 annotation_visitor = GatherNamesFromStringAnnotationsVisitor(
69 self.context, typing_functions=self._typing_functions
70 )
71 node.visit(annotation_visitor)
72 self._string_annotation_names = annotation_visitor.names
73 return True
75 def visit_Import(self, node: cst.Import) -> bool:
76 self.handle_import(node)
77 return False
79 def visit_ImportFrom(self, node: cst.ImportFrom) -> bool:
80 module = node.module
81 if (
82 not isinstance(node.names, cst.ImportStar)
83 and module is not None
84 and module.value not in self._ignored_modules
85 ):
86 self.handle_import(node)
87 return False
89 def handle_import(self, node: Union[cst.Import, cst.ImportFrom]) -> None:
90 names = node.names
91 assert not isinstance(names, cst.ImportStar) # hello, type checker
93 for alias in names:
94 self.unused_imports.add((alias, node))
96 def leave_Module(self, original_node: cst.Module) -> None:
97 self.unused_imports = self.filter_unused_imports(self.unused_imports)
99 def filter_unused_imports(
100 self,
101 candidates: Iterable[Tuple[cst.ImportAlias, Union[cst.Import, cst.ImportFrom]]],
102 ) -> Set[Tuple[cst.ImportAlias, Union[cst.Import, cst.ImportFrom]]]:
103 """
104 Return the imports in ``candidates`` which are not used.
106 This function implements the main logic of this visitor, and is called after traversal. It calls :meth:`~is_in_use` on each import.
108 Override this in a subclass for additional filtering.
109 """
110 unused_imports = set()
111 for alias, parent in candidates:
112 scope = self.get_metadata(ScopeProvider, parent)
113 if scope is None:
114 continue
115 if not self.is_in_use(scope, alias):
116 unused_imports.add((alias, parent))
117 return unused_imports
119 def is_in_use(self, scope: cst.metadata.Scope, alias: cst.ImportAlias) -> bool:
120 """
121 Check if ``alias`` is in use in the given ``scope``.
123 An alias is in use if it's directly referenced, exported, or appears in
124 a string type annotation. Override this in a subclass for additional
125 filtering.
126 """
127 asname = alias.asname
128 names = _gen_dotted_names(
129 cst.ensure_type(asname.name, cst.Name) if asname is not None else alias.name
130 )
132 for name_or_alias, _ in names:
133 if (
134 name_or_alias in self._exported_names
135 or name_or_alias in self._string_annotation_names
136 ):
137 return True
139 for assignment in scope[name_or_alias]:
140 if (
141 isinstance(assignment, cst.metadata.ImportAssignment)
142 and len(assignment.references) > 0
143 ):
144 return True
145 return False