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

192 statements  

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

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

2 

3from __future__ import annotations 

4 

5from tree_sitter import Node 

6 

7from parsers.typescript import TS_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 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