Coverage for tests / test_dead_code_typescript.py: 99%

170 statements  

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

1"""Tests for dead code detection in TypeScript.""" 

2 

3from __future__ import annotations 

4 

5from pathlib import Path 

6 

7from parsers.typescript import parse 

8from dead_code.parsers.typescript import TypeScriptDeadCodeParser 

9from dead_code.core import build_cross_reference_graph 

10 

11 

12def test_unused_named_imports(): 

13 code = """ 

14import { foo, bar } from './module'; 

15 

16function test() { 

17 foo(); 

18} 

19""" 

20 parser = TypeScriptDeadCodeParser() 

21 root = parse(code) 

22 imports = parser.extract_imports(root, code) 

23 

24 import_names = [imp.name for imp in imports] 

25 assert "foo" in import_names 

26 assert "bar" in import_names 

27 

28 

29def test_unused_namespace_imports(): 

30 code = """ 

31import * as utils from './utils'; 

32import { unused } from './other'; 

33 

34function test() { 

35 utils.foo(); 

36} 

37""" 

38 parser = TypeScriptDeadCodeParser() 

39 root = parse(code) 

40 imports = parser.extract_imports(root, code) 

41 

42 import_names = [imp.name for imp in imports] 

43 assert "utils" in import_names 

44 assert "unused" in import_names 

45 

46 

47def test_unused_default_imports(): 

48 code = """ 

49import Foo from './foo'; 

50import Bar from './bar'; 

51 

52function test() { 

53 new Foo(); 

54} 

55""" 

56 parser = TypeScriptDeadCodeParser() 

57 root = parse(code) 

58 imports = parser.extract_imports(root, code) 

59 

60 import_names = [imp.name for imp in imports] 

61 assert "Foo" in import_names 

62 assert "Bar" in import_names 

63 

64 

65def test_type_only_imports_skipped(): 

66 code = """ 

67import type { SomeType } from './types'; 

68 

69function test() { 

70 // Type-only imports shouldn't be tracked 

71} 

72""" 

73 parser = TypeScriptDeadCodeParser() 

74 root = parse(code) 

75 imports = parser.extract_imports(root, code) 

76 

77 # Type-only imports should be skipped 

78 import_names = [imp.name for imp in imports] 

79 assert len(import_names) == 0 

80 

81 

82def test_dead_function_declarations(): 

83 code = """ 

84function used() {} 

85 

86function unused() {} 

87 

88used(); 

89""" 

90 parser = TypeScriptDeadCodeParser() 

91 root = parse(code) 

92 defs = parser.extract_definitions(root, code) 

93 

94 assert len(defs) == 2 

95 def_names = [d.name for d in defs] 

96 assert "used" in def_names 

97 assert "unused" in def_names 

98 

99 

100def test_dead_arrow_functions(): 

101 code = """ 

102const used = () => {}; 

103const unused = () => {} 

104 

105used(); 

106""" 

107 parser = TypeScriptDeadCodeParser() 

108 root = parse(code) 

109 defs = parser.extract_definitions(root, code) 

110 

111 assert len(defs) == 2 

112 def_names = [d.name for d in defs] 

113 assert "used" in def_names 

114 assert "unused" in def_names 

115 

116 

117def test_dead_classes(): 

118 code = """ 

119class Used {} 

120class Unused {} 

121 

122new Used(); 

123""" 

124 parser = TypeScriptDeadCodeParser() 

125 root = parse(code) 

126 defs = parser.extract_definitions(root, code) 

127 

128 assert len(defs) == 2 

129 def_names = [d.name for d in defs] 

130 assert "Used" in def_names 

131 assert "Unused" in def_names 

132 

133 

134def test_exported_functions_not_flagged(): 

135 code = """ 

136 export function publicFunc() {} 

137  

138 export class PublicClass {} 

139  

140 // These shouldn't be flagged as unused 

141 """ 

142 parser = TypeScriptDeadCodeParser() 

143 root = parse(code) 

144 

145 for child in root.children: 

146 if child.type == "export_statement": 

147 for grandchild in child.children: 

148 if grandchild.type in ("function_declaration", "class_declaration"): 

149 assert parser.is_exported(grandchild) 

150 

151 

152def test_extract_method_definitions(): 

153 code = """ 

154class MyClass { 

155 public method1() {} 

156 private method2() {} 

157} 

158""" 

159 parser = TypeScriptDeadCodeParser() 

160 root = parse(code) 

161 defs = parser.extract_definitions(root, code) 

162 

163 def_names = [d.name for d in defs] 

164 method_names = [d.name for d in defs if d.definition_type == "function"] 

165 # Methods should be tracked by property_identifier 

166 # The query handles method_definition nodes 

167 assert len(defs) >= 1 

168 

169 

170def test_unused_local_variables(): 

171 code = """ 

172 function test() { 

173 const used = 1; 

174 const unused = 2; 

175 return used; 

176 } 

177 """ 

178 parser = TypeScriptDeadCodeParser() 

179 root = parse(code) 

180 refs = parser.extract_references(root, code) 

181 

182 ref_names = [r.name for r in refs] 

183 assert "used" in ref_names 

184 assert "unused" in ref_names 

185 

186 

187def test_variables_used_in_nested_blocks(): 

188 code = """ 

189function test(x: number) { 

190 if (x > 0) { 

191 const y = x + 1; 

192 return y; 

193 } 

194 return 0; 

195} 

196""" 

197 parser = TypeScriptDeadCodeParser() 

198 root = parse(code) 

199 refs = parser.extract_references(root, code) 

200 

201 ref_names = [r.name for r in refs] 

202 assert "x" in ref_names 

203 assert "y" in ref_names # y is used in the if block 

204 

205 

206def test_shadowing_separate_tracking(): 

207 code = """ 

208function test() { 

209 let x = 1; 

210 if (true) { 

211 let x = 2; 

212 return x; 

213 } 

214 return x; 

215} 

216""" 

217 parser = TypeScriptDeadCodeParser() 

218 root = parse(code) 

219 refs = parser.extract_references(root, code) 

220 

221 # Both x declarations should be used 

222 ref_names = [r.name for r in refs] 

223 assert ref_names.count("x") >= 2 

224 

225 

226def test_class_implementing_interface(): 

227 code = """ 

228interface MyInterface { 

229 method(x: number): void; 

230} 

231 

232class MyClass implements MyInterface { 

233 method(x: number) { 

234 return; 

235 } 

236} 

237""" 

238 parser = TypeScriptDeadCodeParser() 

239 root = parse(code) 

240 defs = parser.extract_definitions(root, code) 

241 

242 # MyClass should be extracted as a class definition 

243 class_names = [d.name for d in defs if d.definition_type == "class"] 

244 assert "MyClass" in class_names 

245 

246 

247def test_react_component_pattern(): 

248 code = """ 

249 import React from 'react'; 

250 

251 export function Component({ name }: { name: string }) { 

252 return <div>{name}</div>; 

253 } 

254 """ 

255 parser = TypeScriptDeadCodeParser() 

256 root = parse(code) 

257 

258 for child in root.children: 

259 if child.type == "export_statement": 

260 for grandchild in child.children: 

261 if grandchild.type == "function_declaration": 

262 assert parser.is_exported(grandchild) 

263 return 

264 assert False, "Exported function not found" 

265 

266 

267def test_extract_references(): 

268 code = """ 

269function foo(x: number, y: number): number { 

270 const result = x + y; 

271 return result; 

272} 

273 

274const test = foo(1, 2); 

275""" 

276 parser = TypeScriptDeadCodeParser() 

277 root = parse(code) 

278 refs = parser.extract_references(root, code) 

279 

280 ref_names = [r.name for r in refs] 

281 assert "x" in ref_names 

282 assert "y" in ref_names 

283 assert "foo" in ref_names 

284 assert "test" in ref_names 

285 

286 

287def test_abstract_class_detection(): 

288 code = """ 

289abstract class AbstractClass { 

290 abstract method(): void; 

291} 

292 

293class ConcreteClass extends AbstractClass { 

294 method(): void {} 

295} 

296""" 

297 parser = TypeScriptDeadCodeParser() 

298 root = parse(code) 

299 abstract_classes, abstract_methods = parser.extract_abstract_info(root, code) 

300 

301 assert "AbstractClass" in abstract_classes 

302 assert "method" in abstract_methods 

303 

304 

305def test_dead_function_with_export(): 

306 code = """ 

307export const used = () => {}; 

308const unused = () => {}; 

309 

310used(); 

311""" 

312 parser = TypeScriptDeadCodeParser() 

313 root = parse(code) 

314 defs = parser.extract_definitions(root, code) 

315 

316 def_names = [d.name for d in defs] 

317 assert "used" in def_names 

318 assert "unused" in def_names 

319 

320 

321def test_default_parameters(): 

322 code = """ 

323function greet(name: string = "World"): string { 

324 return `Hello, ${name}`; 

325} 

326 

327console.log(greet()); 

328""" 

329 parser = TypeScriptDeadCodeParser() 

330 root = parse(code) 

331 refs = parser.extract_references(root, code) 

332 

333 ref_names = [r.name for r in refs] 

334 assert "name" in ref_names 

335 assert "greet" in ref_names 

336 assert "console" in ref_names 

337 

338 

339def test_for_loop_variables(): 

340 code = """ 

341function test() { 

342 for (let i = 0; i < 10; i++) { 

343 console.log(i); 

344 } 

345} 

346""" 

347 parser = TypeScriptDeadCodeParser() 

348 root = parse(code) 

349 refs = parser.extract_references(root, code) 

350 

351 ref_names = [r.name for r in refs] 

352 assert "i" in ref_names # used in loop condition and body 

353 

354 

355def test_cross_file_dead_code_detection(): 

356 code1 = """ 

357 export { foo, bar } from './module'; 

358 

359 function internalFunc() {} 

360 export { internalFunc as exportedFunc }; 

361 """ 

362 

363 parser = TypeScriptDeadCodeParser() 

364 root = parse(code1) 

365 imports = parser.extract_imports(root, code1) 

366 defs = parser.extract_definitions(root, code1) 

367 

368 # local function definition should be extracted 

369 def_names = [d.name for d in defs] 

370 assert "internalFunc" in def_names