Coverage for tests/test_dead_code_typescript.py: 99%

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

2

3 from __future__ import annotations

4

5 from pathlib import Path

6

7 from parsers.typescript import parse

8 from dead_code.parsers.typescript import TypeScriptDeadCodeParser

9 from dead_code.core import build_cross_reference_graph

10

11

12 def test_unused_named_imports():

13 code = """

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

15

16 function 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

29 def test_unused_namespace_imports():

30 code = """

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

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

33

34 function 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

47 def test_unused_default_imports():

48 code = """

49 import Foo from './foo';

50 import Bar from './bar';

51

52 function 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

65 def test_type_only_imports_skipped():

66 code = """

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

68

69 function 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

82 def test_dead_function_declarations():

83 code = """

84 function used() {}

85

86 function unused() {}

87

88 used();

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

100 def test_dead_arrow_functions():

101 code = """

102 const used = () => {};

103 const unused = () => {}

104

105 used();

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

117 def test_dead_classes():

118 code = """

119 class Used {}

120 class Unused {}

121

122 new 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

134 def 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

152 def test_extract_method_definitions():

153 code = """

154 class 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

170 def 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

187 def test_variables_used_in_nested_blocks():

188 code = """

189 function 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

206 def test_shadowing_separate_tracking():

207 code = """

208 function 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

226 def test_class_implementing_interface():

227 code = """

228 interface MyInterface {

229 method(x: number): void;

230}

231

232 class 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

247 def 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

267 def test_extract_references():

268 code = """

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

270 const result = x + y;

271 return result;

272}

273

274 const 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

287 def test_abstract_class_detection():

288 code = """

289 abstract class AbstractClass {

290 abstract method(): void;

291}

292

293 class 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

305 def test_dead_function_with_export():

306 code = """

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

308 const unused = () => {};

309

310 used();

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

321 def test_default_parameters():

322 code = """

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

324 return `Hello, ${name}`;

325}

326

327 console.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

339 def test_for_loop_variables():

340 code = """

341 function 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

355 def 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