Coverage for dead_code / parsers / python.py: 54%

87 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-08 15:04 -0800

1"""Python dead code parser.""" 

2 

3from __future__ import annotations 

4 

5from tree_sitter import Node 

6 

7from parsers.python import PY_LANGUAGE 

8from patterns.common import create_query, run_captures 

9from dead_code.parsers.base import ( 

10 SymbolDefinition, 

11 ImportedSymbol, 

12 VariableDefinition, 

13 SymbolReference, 

14 DeadCodeParser, 

15) 

16 

17 

18class PythonDeadCodeParser(DeadCodeParser): 

19 def extract_imports(self, root: Node, source: str) -> list[ImportedSymbol]: 

20 queries = [ 

21 "(import_statement (dotted_name (identifier) @import_name))", 

22 "(import_statement (aliased_import name: (dotted_name (identifier) @import_name) alias: (identifier) @import_alias))", 

23 "(import_from_statement name: (dotted_name (identifier) @import_name))", 

24 "(import_from_statement (aliased_import name: (identifier) @import_name alias: (identifier) @import_alias))", 

25 ] 

26 

27 imports = [] 

28 for query_str in queries: 

29 try: 

30 query = create_query(PY_LANGUAGE, query_str) 

31 captures = run_captures(query, root) 

32 

33 for node, capture_name in captures: 

34 text = node.text.decode() if node.text else "" 

35 if capture_name == "import_name": 

36 imports.append( 

37 ImportedSymbol( 

38 name=text, 

39 line=node.start_point[0] + 1, 

40 imported_from=None, 

41 alias=None, 

42 ) 

43 ) 

44 elif capture_name == "import_alias": 

45 if imports: 

46 imports[-1].alias = text 

47 except Exception: 

48 pass 

49 return imports 

50 

51 def extract_definitions(self, root: Node, source: str) -> list[SymbolDefinition]: 

52 query_str = "(function_definition name: (identifier) @func_name) (class_definition name: (identifier) @class_name)" 

53 query = create_query(PY_LANGUAGE, query_str) 

54 captures = run_captures(query, root) 

55 

56 defs = [] 

57 for node, capture_name in captures: 

58 text = node.text.decode() if node.text else "" 

59 if capture_name == "func_name": 

60 defs.append(SymbolDefinition(text, node.start_point[0] + 1, "function")) 

61 elif capture_name == "class_name": 

62 defs.append(SymbolDefinition(text, node.start_point[0] + 1, "class")) 

63 return defs 

64 

65 def extract_abstract_info( 

66 self, root: Node, source: str 

67 ) -> tuple[set[str], set[str]]: 

68 """Extract abstract class names and abstract method names.""" 

69 abstract_classes = set() 

70 abstract_methods = set() 

71 

72 # Source lines to method name mapping 

73 source_lines = source.split("\n") 

74 

75 # Find classes that inherit from ABC 

76 query_str = """(class_definition 

77 name: (identifier) @class_name 

78 (argument_list (identifier) @base_name))""" 

79 

80 try: 

81 query = create_query(PY_LANGUAGE, query_str) 

82 captures = run_captures(query, root) 

83 

84 current_class = None 

85 for node, capture_name in captures: 

86 text = node.text.decode() if node.text else "" 

87 if capture_name == "class_name": 

88 current_class = text 

89 elif capture_name == "base_name" and text in ["ABC", "ABCMeta"]: 

90 if current_class: 

91 abstract_classes.add(current_class) 

92 except Exception: 

93 pass 

94 

95 # Find methods with @abstractmethod decorator 

96 query_str = """(decorator (identifier) @decorator_name)""" 

97 

98 try: 

99 query = create_query(PY_LANGUAGE, query_str) 

100 captures = run_captures(query, root) 

101 

102 for node, capture_name in captures: 

103 text = node.text.decode() if node.text else "" 

104 if capture_name == "decorator_name" and text in [ 

105 "abstractmethod", 

106 "abstractproperty", 

107 "abstractclassmethod", 

108 "abstractstaticmethod", 

109 ]: 

110 # Get line number of decorator 

111 line_num = node.start_point[0] + 1 

112 # Look for the next line for the function name 

113 if line_num < len(source_lines): 

114 next_line = source_lines[line_num] # +1 due to 0-index 

115 match = None 

116 if next_line: 

117 # Simple logic: find "def " in the line 

118 if "def " in next_line: 

119 parts = next_line.split("def ") 

120 if len(parts) > 1: 

121 method_name = parts[1].split("(")[0].strip() 

122 if method_name: 

123 abstract_methods.add(method_name) 

124 except Exception: 

125 pass 

126 

127 return abstract_classes, abstract_methods 

128 

129 def extract_variable_definitions( 

130 self, root: Node, source: str 

131 ) -> dict[tuple, list[VariableDefinition]]: 

132 return {} 

133 

134 def extract_references(self, root: Node, source: str) -> list[SymbolReference]: 

135 query_str = "(identifier) @usage_ref" 

136 query = create_query(PY_LANGUAGE, query_str) 

137 captures = run_captures(query, root) 

138 

139 refs = [] 

140 for node, capture_name in captures: 

141 if capture_name == "usage_ref" and node.text: 

142 refs.append( 

143 SymbolReference(node.text.decode(), node.start_point[0] + 1) 

144 ) 

145 return refs 

146 

147 def extract_scope_references( 

148 self, root: Node, source: str 

149 ) -> dict[tuple, list[SymbolReference]]: 

150 return {}