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

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

2

3 from __future__ import annotations

4

5 from tree_sitter import Node

6

7 from parsers.python import PY_LANGUAGE

8 from patterns.common import create_query, run_captures

9 from dead_code.parsers.base import (

10 SymbolDefinition,

11 ImportedSymbol,

12 VariableDefinition,

13 SymbolReference,

14 DeadCodeParser,

15)

16

17

18 class 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 {}