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

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# 

6 

7from typing import Collection, Iterable, Set, Tuple, Union 

8 

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 

19 

20MODULES_IGNORED_BY_DEFAULT = {"__future__"} 

21 

22 

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. 

28 

29 Note that imports that are only used indirectly (from other modules) are 

30 still collected. 

31 

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 """ 

36 

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 ) 

45 

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) 

53 

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() 

63 

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 

74 

75 def visit_Import(self, node: cst.Import) -> bool: 

76 self.handle_import(node) 

77 return False 

78 

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 

88 

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 

92 

93 for alias in names: 

94 self.unused_imports.add((alias, node)) 

95 

96 def leave_Module(self, original_node: cst.Module) -> None: 

97 self.unused_imports = self.filter_unused_imports(self.unused_imports) 

98 

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. 

105 

106 This function implements the main logic of this visitor, and is called after traversal. It calls :meth:`~is_in_use` on each import. 

107 

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 

118 

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``. 

122 

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 ) 

131 

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 

138 

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