Coverage for dead_code/parsers/typescript.py: 71%

1"""TypeScript dead code parser."""

2

3 from __future__ import annotations

4

5 from tree_sitter import Node

6

7 from parsers.typescript import TS_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 TypeScriptDeadCodeParser(DeadCodeParser):

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

20 imports = []

21 query_str = "(import_statement) @import_stmt"

22 query = create_query(TS_LANGUAGE, query_str)

23 captures = run_captures(query, root)

24

25 for stmt_node, _ in captures:

26 self._process_import_statement(stmt_node, imports)

27 return imports

28

29 def _process_import_statement(

30 self, stmt_node: Node, imports: list[ImportedSymbol]

31 ) -> None:

32 for child in stmt_node.children:

33 if child.type == "import" and len(stmt_node.children) > 1:

34 next_child = stmt_node.children[1]

35 if (

36 next_child.type == "type"

37 and next_child.text

38 and next_child.text.decode() == "type"

39 ):

40 return

41 if child.type == "import_clause":

42 self._process_import_clause(child, imports)

43 break

44

45 def _process_import_clause(

46 self, clause_node: Node, imports: list[ImportedSymbol]

47 ) -> None:

48 for child in clause_node.children:

49 if child.type == "named_imports":

50 self._process_named_imports(child, imports)

51 elif child.type == "namespace_import":

52 self._process_namespace_import(child, imports)

53 elif child.type == "identifier":

54 name = child.text.decode() if child.text else ""

55 if name and name[0].isupper():

56 imports.append(

57 ImportedSymbol(name, child.start_point[0] + 1, None, None)

58 )

59

60 def _process_named_imports(

61 self, named_imports: Node, imports: list[ImportedSymbol]

62 ) -> None:

63 for child in named_imports.children:

64 if child.type == "import_specifier":

65 for nested in child.children:

66 if nested.type == "identifier":

67 name = nested.text.decode() if nested.text else ""

68 if name:

69 imports.append(

70 ImportedSymbol(

71 name, nested.start_point[0] + 1, None, None

72 )

73 )

74

75 def _process_namespace_import(

76 self, namespace_node: Node, imports: list[ImportedSymbol]

77 ) -> None:

78 for child in namespace_node.children:

79 if child.type == "identifier":

80 name = child.text.decode() if child.text else ""

81 if name:

82 imports.append(

83 ImportedSymbol(name, child.start_point[0] + 1, None, None)

84 )

85

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

87 defs = []

88 query_str = "(function_declaration) @func_decl"

89 try:

90 query = create_query(TS_LANGUAGE, query_str)

91 captures = run_captures(query, root)

92 for node, _ in captures:

93 for child in node.children:

94 if child.type == "identifier":

95 text = child.text.decode() if child.text else ""

96 is_exported = self.is_exported(node)

97 defs.append(

98 SymbolDefinition(

99 text, node.start_point[0] + 1, "function", is_exported

100 )

101 )

102 break

103 except Exception:

104 pass

105

106 query_str = "(generator_function_declaration) @func_decl"

107 try:

108 query = create_query(TS_LANGUAGE, query_str)

109 captures = run_captures(query, root)

110 for node, _ in captures:

111 for child in node.children:

112 if child.type == "identifier":

113 text = child.text.decode() if child.text else ""

114 is_exported = self.is_exported(node)

115 defs.append(

116 SymbolDefinition(

117 text, node.start_point[0] + 1, "function", is_exported

118 )

119 )

120 break

121 except Exception:

122 pass

123

124 query_str = "(class_declaration) @class_decl"

125 try:

126 query = create_query(TS_LANGUAGE, query_str)

127 captures = run_captures(query, root)

128 for node, _ in captures:

129 for child in node.children:

130 if child.type == "type_identifier":

131 text = child.text.decode() if child.text else ""

132 is_exported = self.is_exported(node)

133 defs.append(

134 SymbolDefinition(

135 text, node.start_point[0] + 1, "class", is_exported

136 )

137 )

138 break

139 except Exception:

140 pass

141

142 query_str = "(method_definition) @method_def"

143 try:

144 query = create_query(TS_LANGUAGE, query_str)

145 captures = run_captures(query, root)

146 for node, _ in captures:

147 for child in node.children:

148 if child.type == "property_identifier":

149 text = child.text.decode() if child.text else ""

150 is_exported = self.is_exported(node)

151 defs.append(

152 SymbolDefinition(

153 text, node.start_point[0] + 1, "function", is_exported

154 )

155 )

156 break

157 except Exception:

158 pass

159

160 query_str = "(lexical_declaration (variable_declarator name: (identifier) @name value: (arrow_function)))"

161 try:

162 query = create_query(TS_LANGUAGE, query_str)

163 captures = run_captures(query, root)

164 for node, capture_name in captures:

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

166 if capture_name == "name":

167 lexical_parent = node.parent.parent

168 is_exported = (

169 self.is_exported(lexical_parent) if lexical_parent else False

170 )

171 defs.append(

172 SymbolDefinition(

173 text, node.start_point[0] + 1, "function", is_exported

174 )

175 )

176 except Exception:

177 pass

178

179 return defs

180

181 def extract_abstract_info(

182 self, root: Node, source: str

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

184 abstract_classes = set()

185 abstract_methods = set()

186

187 query_str = "(abstract_class_declaration name: (type_identifier) @class_name)"

188 try:

189 query = create_query(TS_LANGUAGE, query_str)

190 captures = run_captures(query, root)

191 for node, capture_name in captures:

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

193 if capture_name == "class_name":

194 abstract_classes.add(text)

195 except Exception:

196 pass

197

198 query_str = (

199 "(abstract_method_signature name: (property_identifier) @method_name)"

200 )

201 try:

202 query = create_query(TS_LANGUAGE, query_str)

203 captures = run_captures(query, root)

204 for node, capture_name in captures:

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

206 if capture_name == "method_name":

207 abstract_methods.add(text)

208 except Exception:

209 pass

210

211 return abstract_classes, abstract_methods

212

213 def extract_variable_definitions(

214 self, root: Node, source: str

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

216 query_str = (

217 "(lexical_declaration (variable_declarator name: (identifier) @var_name))"

218 )

219 query = create_query(TS_LANGUAGE, query_str)

220 captures = run_captures(query, root)

221

222 result: dict[tuple, list[VariableDefinition]] = {}

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

224

225 for node, capture_name in captures:

226 if capture_name == "var_name":

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

228 scope = self._find_enclosing_scope(node, source_lines)

229 if scope is None:

230 continue

231 if scope not in result:

232 result[scope] = []

233 result[scope].append(

234 VariableDefinition(

235 name=text,

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

237 scope_type="local",

238 definition_type="variable",

239 )

240 )

241

242 return result

243

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

245 query_str = "(identifier) @usage_ref"

246 query = create_query(TS_LANGUAGE, query_str)

247 captures = run_captures(query, root)

248

249 refs = []

250 for node, capture_name in captures:

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

252 refs.append(

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

254 )

255 return refs

256

257 def extract_scope_references(

258 self, root: Node, source: str

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

260 query_str = "(identifier) @usage_ref"

261 query = create_query(TS_LANGUAGE, query_str)

262 captures = run_captures(query, root)

263

264 result: dict[tuple, list[SymbolReference]] = {}

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

266

267 for node, capture_name in captures:

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

269 scope = self._find_enclosing_scope(node, source_lines)

270 if scope is None:

271 continue

272 if scope not in result:

273 result[scope] = []

274 result[scope].append(

275 SymbolReference(

276 name=node.text.decode(),

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

278 )

279 )

280

281 return result

282

283 def _find_enclosing_scope(

284 self, node: Node, source_lines: list[str]

285 ) -> tuple | None:

286 parent = node.parent

287 while parent:

288 if parent.type in (

289 "function_declaration",

290 "generator_function_declaration",

291 "method_definition",

292 "arrow_function",

293 ):

294 return (None, parent.start_point[0] + 1)

295 parent = parent.parent

296 return None

297

298 def is_exported(self, node: Node) -> bool:

299 parent = node.parent

300 while parent:

301 if parent.type == "export_statement":

302 return True

303 parent = parent.parent

304 return False