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