deep-slop 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1422 @@
1
+ import { _ as parseFile, a as findNodesOfType, i as findAncestorOfType, n as extractImportFromNode, o as findNodesOfTypes, r as findAncestor, y as walkAST } from "./tree-sitter-CM-cP0nl.js";
2
+ import { i as toLines, r as readFileContent } from "./file-utils-B_HFXhCs.js";
3
+ import { extname, join, relative } from "node:path";
4
+ import { readdir } from "node:fs/promises";
5
+
6
+ //#region src/engines/dead-flow/ast-detect.ts
7
+ function makeDiagnostic$1(overrides) {
8
+ return {
9
+ engine: "dead-flow",
10
+ severity: "warning",
11
+ column: 1,
12
+ category: "dead-code",
13
+ fixable: true,
14
+ help: "",
15
+ ...overrides
16
+ };
17
+ }
18
+ /** Get 1-indexed line from an AST node */
19
+ function nodeLine(node) {
20
+ return node.startRow + 1;
21
+ }
22
+ /** Check if a node is inside a catch or finally clause */
23
+ function isInCatchOrFinally(node) {
24
+ return findAncestor(node, (n) => n.type === "catch_clause" || n.type === "finally_clause") !== null;
25
+ }
26
+ /** Check if a node is inside an arrow function body */
27
+ function isInArrowFunction(node) {
28
+ return findAncestor(node, (n) => n.type === "arrow_function") !== null;
29
+ }
30
+ /** Check if a return/throw is a guard return (inside an if without else, body is only terminators) */
31
+ function isGuardReturn$1(node) {
32
+ const ifAncestor = findAncestorOfType(node, "if_statement");
33
+ if (!ifAncestor) return false;
34
+ if (ifAncestor.children.find((c) => c.type === "else" || c.type === "else_clause")) return false;
35
+ const consequence = ifAncestor.children.find((c) => c.type === "statement_block" || c.type === "consequence");
36
+ if (!consequence) return true;
37
+ const nonTrivial = consequence.children.filter((c) => c.type !== "comment" && c.type !== "{" && c.type !== "}" && c.text.trim() !== "" && c.text.trim() !== ";");
38
+ const terminators = nonTrivial.filter((c) => c.type === "return_statement" || c.type === "throw_statement");
39
+ return nonTrivial.length > 0 && nonTrivial.length === terminators.length;
40
+ }
41
+ /** Get subsequent sibling statements after a terminator in the same block */
42
+ function getSiblingsAfter(node) {
43
+ const parent = node.parent;
44
+ if (!parent) return [];
45
+ const siblings = parent.children;
46
+ const idx = siblings.indexOf(node);
47
+ if (idx < 0) return [];
48
+ return siblings.slice(idx + 1).filter((s) => s.type !== "comment" && s.type !== "{" && s.type !== "}" && s.text.trim() !== "" && s.text.trim() !== ";");
49
+ }
50
+ /** Evaluate whether an if condition is statically always truthy/falsy */
51
+ function evaluateCondition(conditionNode) {
52
+ const text = conditionNode.text.trim();
53
+ if (text.startsWith("(") && text.endsWith(")")) {
54
+ const inner = conditionNode.children.find((c) => c.type !== "(" && c.type !== ")");
55
+ if (inner) return evaluateCondition(inner);
56
+ }
57
+ if (conditionNode.type === "unary_expression" && text.startsWith("!")) {
58
+ const operand = conditionNode.children.find((c) => c.type !== "!");
59
+ if (operand) {
60
+ const inner = evaluateCondition(operand);
61
+ if (inner === "always-truthy") return "always-falsy";
62
+ if (inner === "always-falsy") return "always-truthy";
63
+ }
64
+ }
65
+ if (conditionNode.type === "true") return "always-truthy";
66
+ if (conditionNode.type === "false") return "always-falsy";
67
+ if (conditionNode.type === "number") return parseFloat(text) === 0 ? "always-falsy" : "always-truthy";
68
+ if (conditionNode.type === "string" || conditionNode.type === "template_string") return text.replace(/^['"`]|['"`]$/g, "").length === 0 ? "always-falsy" : "unknown";
69
+ if (conditionNode.type === "null" || text === "undefined") return "always-falsy";
70
+ return "unknown";
71
+ }
72
+ async function parseWithTreeSitter(content, filePath) {
73
+ return parseFile(content, filePath.endsWith(".tsx"));
74
+ }
75
+ function detectDeadAfterThrow(ast, filePath) {
76
+ const diagnostics = [];
77
+ const throws = findNodesOfType(ast, "throw_statement");
78
+ for (const throwNode of throws) {
79
+ if (isInCatchOrFinally(throwNode)) continue;
80
+ if (isGuardReturn$1(throwNode)) continue;
81
+ const after = getSiblingsAfter(throwNode);
82
+ for (const dead of after) {
83
+ diagnostics.push(makeDiagnostic$1({
84
+ filePath,
85
+ rule: "dead-flow/dead-after-throw",
86
+ message: `Unreachable code after throw on line ${nodeLine(throwNode)}`,
87
+ line: nodeLine(dead),
88
+ severity: "error",
89
+ help: `Remove the unreachable code after the throw statement on line ${nodeLine(throwNode)}`,
90
+ suggestion: {
91
+ type: "delete",
92
+ text: "",
93
+ confidence: .95,
94
+ reason: "Code after throw in same block can never execute",
95
+ range: {
96
+ startLine: nodeLine(dead),
97
+ startCol: dead.startCol + 1,
98
+ endLine: dead.endRow + 1,
99
+ endCol: dead.endCol + 1
100
+ }
101
+ },
102
+ detail: {
103
+ terminatorKind: "throw",
104
+ terminatorLine: nodeLine(throwNode)
105
+ }
106
+ }));
107
+ break;
108
+ }
109
+ }
110
+ return diagnostics;
111
+ }
112
+ function detectDeadAfterReturn(ast, filePath) {
113
+ const diagnostics = [];
114
+ const returns = findNodesOfType(ast, "return_statement");
115
+ for (const returnNode of returns) {
116
+ if (isInCatchOrFinally(returnNode)) continue;
117
+ if (isInArrowFunction(returnNode)) {
118
+ const arrowAncestor = findAncestorOfType(returnNode, "arrow_function");
119
+ if (arrowAncestor) {
120
+ const body = arrowAncestor.children.find((c) => c.type === "statement_block");
121
+ if (body && body.children.includes(returnNode)) {
122
+ if (returnNode.parent === body) {
123
+ const after = getSiblingsAfter(returnNode);
124
+ for (const dead of after) {
125
+ diagnostics.push(makeDiagnostic$1({
126
+ filePath,
127
+ rule: "dead-flow/dead-after-return",
128
+ message: `Unreachable code after return on line ${nodeLine(returnNode)}`,
129
+ line: nodeLine(dead),
130
+ severity: "warning",
131
+ help: `Remove the unreachable code after the return statement on line ${nodeLine(returnNode)}`,
132
+ suggestion: {
133
+ type: "delete",
134
+ text: "",
135
+ confidence: .9,
136
+ reason: "Code after return in same block can never execute"
137
+ },
138
+ detail: {
139
+ terminatorKind: "return",
140
+ terminatorLine: nodeLine(returnNode)
141
+ }
142
+ }));
143
+ break;
144
+ }
145
+ }
146
+ }
147
+ }
148
+ continue;
149
+ }
150
+ if (isGuardReturn$1(returnNode)) continue;
151
+ const after = getSiblingsAfter(returnNode);
152
+ for (const dead of after) {
153
+ diagnostics.push(makeDiagnostic$1({
154
+ filePath,
155
+ rule: "dead-flow/dead-after-return",
156
+ message: `Unreachable code after return on line ${nodeLine(returnNode)}`,
157
+ line: nodeLine(dead),
158
+ severity: "warning",
159
+ help: `Remove the unreachable code after the return statement on line ${nodeLine(returnNode)}`,
160
+ suggestion: {
161
+ type: "delete",
162
+ text: "",
163
+ confidence: .9,
164
+ reason: "Code after return in same block can never execute"
165
+ },
166
+ detail: {
167
+ terminatorKind: "return",
168
+ terminatorLine: nodeLine(returnNode)
169
+ }
170
+ }));
171
+ break;
172
+ }
173
+ }
174
+ return diagnostics;
175
+ }
176
+ function detectDeadAfterBreak(ast, filePath) {
177
+ const diagnostics = [];
178
+ const breaks = findNodesOfTypes(ast, ["break_statement", "continue_statement"]);
179
+ for (const breakNode of breaks) {
180
+ const after = getSiblingsAfter(breakNode);
181
+ for (const dead of after) {
182
+ const kind = breakNode.type === "break_statement" ? "break" : "continue";
183
+ diagnostics.push(makeDiagnostic$1({
184
+ filePath,
185
+ rule: "dead-flow/dead-after-break",
186
+ message: `Unreachable code after ${kind} on line ${nodeLine(breakNode)}`,
187
+ line: nodeLine(dead),
188
+ severity: "error",
189
+ help: `Remove the unreachable code after the ${kind} statement on line ${nodeLine(breakNode)}`,
190
+ suggestion: {
191
+ type: "delete",
192
+ text: "",
193
+ confidence: .95,
194
+ reason: `Code after ${kind} in same block can never execute`
195
+ },
196
+ detail: {
197
+ terminatorKind: kind,
198
+ terminatorLine: nodeLine(breakNode)
199
+ }
200
+ }));
201
+ break;
202
+ }
203
+ }
204
+ return diagnostics;
205
+ }
206
+ function detectUnreachableAfterTerminatorAST(ast, filePath) {
207
+ const diagnostics = [];
208
+ const terminators = findNodesOfTypes(ast, [
209
+ "return_statement",
210
+ "throw_statement",
211
+ "break_statement",
212
+ "continue_statement"
213
+ ]);
214
+ for (const term of terminators) {
215
+ if (isInCatchOrFinally(term)) continue;
216
+ if ((term.type === "return_statement" || term.type === "throw_statement") && isGuardReturn$1(term)) continue;
217
+ const after = getSiblingsAfter(term);
218
+ if (after.length === 0) continue;
219
+ const kind = term.type === "return_statement" ? "return" : term.type === "throw_statement" ? "throw" : term.type === "break_statement" ? "break" : "continue";
220
+ const severity = kind === "return" ? "warning" : "error";
221
+ diagnostics.push(makeDiagnostic$1({
222
+ filePath,
223
+ rule: "dead-flow/unreachable-after-terminator",
224
+ message: `Unreachable code after ${kind} on line ${nodeLine(term)}`,
225
+ line: nodeLine(after[0]),
226
+ severity,
227
+ help: `Remove or move the unreachable code after the ${kind} statement on line ${nodeLine(term)}`,
228
+ suggestion: {
229
+ type: "delete",
230
+ text: "",
231
+ confidence: .9,
232
+ reason: `Code after ${kind} can never execute`,
233
+ range: {
234
+ startLine: nodeLine(after[0]),
235
+ startCol: after[0].startCol + 1,
236
+ endLine: after[0].endRow + 1,
237
+ endCol: after[0].endCol + 1
238
+ }
239
+ },
240
+ detail: {
241
+ terminatorKind: kind,
242
+ terminatorLine: nodeLine(term)
243
+ }
244
+ }));
245
+ }
246
+ return diagnostics;
247
+ }
248
+ function detectUnusedVariablesAST(ast, filePath) {
249
+ const diagnostics = [];
250
+ const declarations = /* @__PURE__ */ new Map();
251
+ const lexicalDecls = findNodesOfTypes(ast, ["lexical_declaration", "variable_declaration"]);
252
+ for (const decl of lexicalDecls) {
253
+ const isExported = findAncestor(decl, (n) => n.type === "export_statement") !== null;
254
+ for (const child of decl.children) {
255
+ if (child.type !== "variable_declarator") continue;
256
+ const nameNode = child.children.find((c) => c.type === "identifier" && c.fieldName === "name") || child.children.find((c) => c.type === "identifier");
257
+ if (!nameNode) continue;
258
+ const name = nameNode.text;
259
+ if (name.includes(",") || name.includes("{") || name.includes("[")) continue;
260
+ declarations.set(name, {
261
+ node: child,
262
+ line: nodeLine(nameNode),
263
+ isExported,
264
+ isParameter: false,
265
+ isType: false,
266
+ isFunction: false
267
+ });
268
+ }
269
+ }
270
+ const funcDecls = findNodesOfTypes(ast, [
271
+ "function_declaration",
272
+ "generator_function_declaration",
273
+ "method_definition"
274
+ ]);
275
+ for (const fn of funcDecls) {
276
+ const nameNode = fn.children.find((c) => c.type === "identifier" || c.type === "property_identifier");
277
+ if (!nameNode) continue;
278
+ const isExported = findAncestor(fn, (n) => n.type === "export_statement") !== null;
279
+ declarations.set(nameNode.text, {
280
+ node: fn,
281
+ line: nodeLine(nameNode),
282
+ isExported,
283
+ isParameter: false,
284
+ isType: false,
285
+ isFunction: true
286
+ });
287
+ }
288
+ const typeDecls = findNodesOfTypes(ast, ["type_alias_declaration", "interface_declaration"]);
289
+ for (const td of typeDecls) {
290
+ const nameNode = td.children.find((c) => c.type === "type_identifier");
291
+ if (!nameNode) continue;
292
+ const isExported = findAncestor(td, (n) => n.type === "export_statement") !== null;
293
+ declarations.set(nameNode.text, {
294
+ node: td,
295
+ line: nodeLine(nameNode),
296
+ isExported,
297
+ isParameter: false,
298
+ isType: true,
299
+ isFunction: false
300
+ });
301
+ }
302
+ const params = findNodesOfTypes(ast, [
303
+ "required_parameter",
304
+ "optional_parameter",
305
+ "rest_parameter"
306
+ ]);
307
+ for (const param of params) {
308
+ const nameNode = param.children.find((c) => c.type === "identifier" && c.fieldName === "name") || param.children.find((c) => c.type === "identifier");
309
+ if (!nameNode) continue;
310
+ declarations.set(nameNode.text, {
311
+ node: param,
312
+ line: nodeLine(nameNode),
313
+ isExported: false,
314
+ isParameter: true,
315
+ isType: false,
316
+ isFunction: false
317
+ });
318
+ }
319
+ const references = /* @__PURE__ */ new Set();
320
+ walkAST(ast, (node) => {
321
+ if (node.type !== "identifier") return;
322
+ if (!(node.fieldName === "name" && node.parent && (node.parent.type === "variable_declarator" || node.parent.type === "function_declaration" || node.parent.type === "generator_function_declaration" || node.parent.type === "method_definition" || node.parent.type === "required_parameter" || node.parent.type === "optional_parameter" || node.parent.type === "rest_parameter" || node.parent.type === "type_alias_declaration" || node.parent.type === "interface_declaration" || node.parent.type === "class_declaration" || node.parent.type === "import_specifier" || node.parent.type === "export_specifier"))) references.add(node.text);
323
+ });
324
+ for (const [name, info] of declarations) {
325
+ if (name.startsWith("_")) continue;
326
+ if (info.isExported) continue;
327
+ if (info.isType) continue;
328
+ if (info.isParameter) continue;
329
+ if (/^[A-Z]/.test(name) && info.isFunction) continue;
330
+ if (!references.has(name)) diagnostics.push(makeDiagnostic$1({
331
+ filePath,
332
+ rule: "dead-flow/unused-variable",
333
+ message: `Variable \`${name}\` is declared but never used`,
334
+ line: info.line,
335
+ severity: "suggestion",
336
+ fixable: true,
337
+ help: `Remove the unused variable \`${name}\` or prefix with _ if intentionally unused`,
338
+ suggestion: {
339
+ type: "delete",
340
+ text: "",
341
+ confidence: .8,
342
+ reason: `Variable \`${name}\` is never referenced after its declaration (AST-verified)`
343
+ },
344
+ detail: {
345
+ variableName: name,
346
+ source: "ast"
347
+ }
348
+ }));
349
+ }
350
+ return diagnostics;
351
+ }
352
+ function detectUnusedExportsAST(astMap, rootDir) {
353
+ const diagnostics = [];
354
+ const exportMap = /* @__PURE__ */ new Map();
355
+ const importedSymbols = /* @__PURE__ */ new Set();
356
+ for (const [filePath, ast] of astMap) {
357
+ const exportStmts = findNodesOfType(ast, "export_statement");
358
+ for (const exp of exportStmts) {
359
+ const declChildren = exp.children.filter((c) => c.type === "lexical_declaration" || c.type === "variable_declaration" || c.type === "function_declaration" || c.type === "class_declaration" || c.type === "generator_function_declaration" || c.type === "type_alias_declaration" || c.type === "interface_declaration");
360
+ for (const decl of declChildren) {
361
+ const nameNode = decl.children.find((c) => c.type === "identifier" || c.type === "type_identifier");
362
+ if (!nameNode) continue;
363
+ const name = nameNode.text;
364
+ if (exp.text.includes("export default")) continue;
365
+ if (decl.type === "type_alias_declaration" || decl.type === "interface_declaration") continue;
366
+ if (!exportMap.has(name)) exportMap.set(name, []);
367
+ exportMap.get(name).push({
368
+ filePath,
369
+ line: nodeLine(nameNode)
370
+ });
371
+ }
372
+ const exportSpecifiers = findNodesOfType(exp, "export_specifier");
373
+ for (const spec of exportSpecifiers) {
374
+ const nameNode = spec.children.find((c) => c.type === "identifier");
375
+ if (!nameNode) continue;
376
+ const name = nameNode.text;
377
+ if (!exportMap.has(name)) exportMap.set(name, []);
378
+ exportMap.get(name).push({
379
+ filePath,
380
+ line: nodeLine(spec)
381
+ });
382
+ }
383
+ }
384
+ const importStmts = findNodesOfTypes(ast, ["import_statement", "import_declaration"]);
385
+ for (const imp of importStmts) {
386
+ const info = extractImportFromNode(imp);
387
+ if (info) for (const sym of info.symbols) importedSymbols.add(sym);
388
+ }
389
+ const callExprs = findNodesOfType(ast, "call_expression");
390
+ for (const call of callExprs) {
391
+ const func = call.children[0];
392
+ if (func && func.type === "import") {
393
+ const thenMatch = call.text.match(/\.then\s*\(\s*\((\w+)\)\s*=>\s*\1\.(\w+)/);
394
+ if (thenMatch) importedSymbols.add(thenMatch[2]);
395
+ const thenMatch2 = call.text.match(/\.then\s*\(\s*(\w+)\s*=>\s*\1\.(\w+)/);
396
+ if (thenMatch2) importedSymbols.add(thenMatch2[2]);
397
+ }
398
+ }
399
+ }
400
+ for (const [name, entries] of exportMap) {
401
+ if (/^[A-Z]/.test(name)) continue;
402
+ if (/Engine$/.test(name)) continue;
403
+ if (!importedSymbols.has(name)) for (const entry of entries) diagnostics.push(makeDiagnostic$1({
404
+ filePath: entry.filePath,
405
+ rule: "dead-flow/unused-export",
406
+ message: `Exported \`${name}\` is never imported by any other file`,
407
+ line: entry.line,
408
+ severity: "info",
409
+ fixable: true,
410
+ help: `Consider removing the unused export \`${name}\` or adding it to the public API explicitly`,
411
+ suggestion: {
412
+ type: "delete",
413
+ text: "",
414
+ confidence: .7,
415
+ reason: "This symbol is exported but never imported elsewhere (AST-verified)"
416
+ },
417
+ detail: {
418
+ symbolName: name,
419
+ source: "ast"
420
+ }
421
+ }));
422
+ }
423
+ return diagnostics;
424
+ }
425
+ function detectDeadBranchesAST(ast, filePath) {
426
+ const diagnostics = [];
427
+ const ifStmts = findNodesOfType(ast, "if_statement");
428
+ for (const ifStmt of ifStmts) {
429
+ const conditionNode = ifStmt.children.find((c) => c.type === "parenthesized_expression");
430
+ if (!conditionNode) continue;
431
+ const innerExpr = conditionNode.children.find((c) => c.type !== "(" && c.type !== ")");
432
+ if (!innerExpr) continue;
433
+ const eval_ = evaluateCondition(innerExpr);
434
+ if (eval_ === "unknown") continue;
435
+ const deadBranch = eval_ === "always-falsy" ? "then" : "else";
436
+ const branchDesc = deadBranch === "then" ? "if-block" : "else-block";
437
+ const conditionText = conditionNode.text.replace(/^\(|\)$/g, "").trim();
438
+ diagnostics.push(makeDiagnostic$1({
439
+ filePath,
440
+ rule: "dead-flow/dead-conditional",
441
+ message: `Condition \`${conditionText}\` is always ${eval_ === "always-falsy" ? "falsy" : "truthy"}, making the ${branchDesc} unreachable`,
442
+ line: nodeLine(ifStmt),
443
+ severity: "warning",
444
+ help: `Simplify the conditional — the ${branchDesc} can never execute`,
445
+ suggestion: {
446
+ type: "refactor",
447
+ text: deadBranch === "else" ? "// remove else branch, keep if-body" : "// remove if block, keep else body as direct code",
448
+ confidence: .85,
449
+ reason: `Condition is statically determined to always be ${eval_ === "always-falsy" ? "falsy" : "truthy"} (AST-verified)`
450
+ },
451
+ detail: {
452
+ condition: conditionText,
453
+ deadBranch,
454
+ source: "ast"
455
+ }
456
+ }));
457
+ }
458
+ return diagnostics;
459
+ }
460
+ /**
461
+ * Run all AST-enhanced detections on a single file.
462
+ * Returns null if tree-sitter is unavailable.
463
+ */
464
+ async function detectAllAST(content, filePath) {
465
+ const ast = await parseWithTreeSitter(content, filePath);
466
+ if (!ast) return null;
467
+ const diagnostics = [];
468
+ const astRules = /* @__PURE__ */ new Set();
469
+ const deadThrow = detectDeadAfterThrow(ast, filePath);
470
+ diagnostics.push(...deadThrow);
471
+ if (deadThrow.length > 0) astRules.add("dead-after-throw");
472
+ const deadReturn = detectDeadAfterReturn(ast, filePath);
473
+ diagnostics.push(...deadReturn);
474
+ if (deadReturn.length > 0) astRules.add("dead-after-return");
475
+ const deadBreak = detectDeadAfterBreak(ast, filePath);
476
+ diagnostics.push(...deadBreak);
477
+ if (deadBreak.length > 0) astRules.add("dead-after-break");
478
+ const unreachable = detectUnreachableAfterTerminatorAST(ast, filePath);
479
+ diagnostics.push(...unreachable);
480
+ astRules.add("unreachable-after-terminator");
481
+ const unusedVars = detectUnusedVariablesAST(ast, filePath);
482
+ diagnostics.push(...unusedVars);
483
+ astRules.add("unused-variable");
484
+ const deadBranches = detectDeadBranchesAST(ast, filePath);
485
+ diagnostics.push(...deadBranches);
486
+ astRules.add("dead-conditional");
487
+ return {
488
+ diagnostics,
489
+ astRules,
490
+ astAvailable: true
491
+ };
492
+ }
493
+ /**
494
+ * Run AST cross-file detections (unused exports).
495
+ * Returns null if tree-sitter is unavailable.
496
+ */
497
+ async function detectUnusedExportsASTWrapper(astMap, rootDir) {
498
+ if (astMap.size === 0) return null;
499
+ return detectUnusedExportsAST(astMap, rootDir);
500
+ }
501
+
502
+ //#endregion
503
+ //#region src/engines/dead-flow/index.ts
504
+ const TS_JS_EXTENSIONS = new Set([
505
+ ".ts",
506
+ ".tsx",
507
+ ".js",
508
+ ".jsx",
509
+ ".mjs",
510
+ ".cjs"
511
+ ]);
512
+ function isRelevantFile(filePath) {
513
+ const ext = extname(filePath);
514
+ return TS_JS_EXTENSIONS.has(ext);
515
+ }
516
+ /** Recursively collect file paths under root, respecting exclude list */
517
+ async function collectFiles(root, exclude) {
518
+ const results = [];
519
+ async function walk(dir) {
520
+ let entries;
521
+ try {
522
+ entries = await readdir(dir, { withFileTypes: true });
523
+ } catch {
524
+ return;
525
+ }
526
+ for (const entry of entries) {
527
+ const full = join(dir, entry.name);
528
+ if (exclude.some((pat) => full.includes(pat))) continue;
529
+ if (entry.isDirectory()) await walk(full);
530
+ else if (entry.isFile() && isRelevantFile(full)) results.push(full);
531
+ }
532
+ }
533
+ await walk(root);
534
+ return results;
535
+ }
536
+ /** Make a diagnostic with sensible defaults */
537
+ function makeDiagnostic(overrides) {
538
+ return {
539
+ engine: "dead-flow",
540
+ severity: "warning",
541
+ column: 1,
542
+ category: "dead-code",
543
+ fixable: true,
544
+ help: "",
545
+ ...overrides
546
+ };
547
+ }
548
+ /** Check if a trimmed line is just a closing brace (with optional trailing punctuation) */
549
+ function isClosingBraceLine(trimmed) {
550
+ return /^\}[;,)\s]*$/.test(trimmed);
551
+ }
552
+ function detectUnreachableAfterTerminator(content, filePath) {
553
+ const diagnostics = [];
554
+ const lines = toLines(content);
555
+ const terminatorRe = /^\s*(return\b|throw\b|break\b|continue\b)/;
556
+ const startDepths = [];
557
+ let depth = 0;
558
+ for (let i = 0; i < lines.length; i++) {
559
+ startDepths.push(depth);
560
+ for (const ch of lines[i].text) {
561
+ if (ch === "{") depth++;
562
+ if (ch === "}") depth--;
563
+ }
564
+ }
565
+ const inCallback = /* @__PURE__ */ new Set();
566
+ const callbackMethodRe = /\.(forEach|map|filter|reduce|find|findIndex|some|every|flatMap|sort)\s*\(/;
567
+ for (let i = 0; i < lines.length; i++) {
568
+ if (!callbackMethodRe.test(lines[i].text)) continue;
569
+ for (let s = i; s < Math.min(lines.length, i + 6); s++) {
570
+ const lineText = lines[s].text;
571
+ if ((lineText.includes("=>") || /function\s*\(/.test(lineText)) && lineText.includes("{")) {
572
+ let depthAfterLine = startDepths[s];
573
+ for (const ch of lineText) {
574
+ if (ch === "{") depthAfterLine++;
575
+ if (ch === "}") depthAfterLine--;
576
+ }
577
+ if (depthAfterLine > startDepths[s]) for (let mark = s + 1; mark < lines.length; mark++) {
578
+ if (startDepths[mark] <= startDepths[s]) break;
579
+ inCallback.add(mark);
580
+ }
581
+ break;
582
+ }
583
+ }
584
+ }
585
+ for (let i = 0; i < lines.length; i++) {
586
+ if (!lines[i].text.includes("=>")) continue;
587
+ if (!/=>\s*\{/.test(lines[i].text)) continue;
588
+ let depthAfterLine = startDepths[i];
589
+ for (const ch of lines[i].text) {
590
+ if (ch === "{") depthAfterLine++;
591
+ if (ch === "}") depthAfterLine--;
592
+ }
593
+ if (depthAfterLine > startDepths[i]) for (let mark = i + 1; mark < lines.length; mark++) {
594
+ if (startDepths[mark] <= startDepths[i]) break;
595
+ inCallback.add(mark);
596
+ }
597
+ }
598
+ const inCatchFinally = /* @__PURE__ */ new Set();
599
+ for (let i = 0; i < lines.length; i++) {
600
+ const trimmed = lines[i].text.trim();
601
+ if (!(/(?:^|})\s*catch\b/.test(trimmed) || /(?:^|})\s*finally\b/.test(trimmed))) continue;
602
+ let braceLineIdx = -1;
603
+ if (lines[i].text.includes("{")) braceLineIdx = i;
604
+ else for (let s = i + 1; s < Math.min(lines.length, i + 3); s++) if (lines[s].text.includes("{")) {
605
+ braceLineIdx = s;
606
+ break;
607
+ }
608
+ if (braceLineIdx === -1) continue;
609
+ let depthAfterLine = startDepths[braceLineIdx];
610
+ for (const ch of lines[braceLineIdx].text) {
611
+ if (ch === "{") depthAfterLine++;
612
+ if (ch === "}") depthAfterLine--;
613
+ }
614
+ if (depthAfterLine > startDepths[braceLineIdx]) for (let mark = braceLineIdx + 1; mark < lines.length; mark++) {
615
+ if (startDepths[mark] <= startDepths[braceLineIdx]) break;
616
+ inCatchFinally.add(mark);
617
+ }
618
+ }
619
+ const guardReturnLines = /* @__PURE__ */ new Set();
620
+ for (let i = 0; i < lines.length; i++) {
621
+ const trimmed = lines[i].text.trim();
622
+ if (!/^if\s*\(/.test(trimmed)) continue;
623
+ let braceLineIdx = -1;
624
+ if (lines[i].text.includes("{")) braceLineIdx = i;
625
+ else for (let s = i + 1; s < Math.min(lines.length, i + 3); s++) if (lines[s].text.includes("{")) {
626
+ braceLineIdx = s;
627
+ break;
628
+ }
629
+ if (braceLineIdx === -1) continue;
630
+ const ifBodyStart = braceLineIdx + 1;
631
+ let ifBlockEnd = -1;
632
+ let onlyTerminators = true;
633
+ for (let j = ifBodyStart; j < lines.length; j++) {
634
+ if (startDepths[j] <= startDepths[braceLineIdx]) {
635
+ ifBlockEnd = j;
636
+ break;
637
+ }
638
+ const bodyTrimmed = lines[j].text.trim();
639
+ if (bodyTrimmed === "" || bodyTrimmed.startsWith("//") || bodyTrimmed.startsWith("/*") || bodyTrimmed.startsWith("*")) continue;
640
+ if (isClosingBraceLine(bodyTrimmed)) continue;
641
+ if (/^return\b|^throw\b/.test(bodyTrimmed)) continue;
642
+ onlyTerminators = false;
643
+ }
644
+ if (!onlyTerminators || ifBlockEnd === -1) continue;
645
+ let nextNonEmpty = ifBlockEnd;
646
+ while (nextNonEmpty < lines.length) {
647
+ const t = lines[nextNonEmpty].text.trim();
648
+ if (t === "" || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) {
649
+ nextNonEmpty++;
650
+ continue;
651
+ }
652
+ break;
653
+ }
654
+ if (nextNonEmpty < lines.length) {
655
+ const nextTrimmed = lines[nextNonEmpty].text.trim();
656
+ if (/^}\s*else\b|^else\b/.test(nextTrimmed)) continue;
657
+ }
658
+ for (let j = ifBodyStart; j < ifBlockEnd; j++) {
659
+ const bodyTrimmed = lines[j].text.trim();
660
+ if (/^return\b|^throw\b/.test(bodyTrimmed)) guardReturnLines.add(j);
661
+ }
662
+ }
663
+ for (let i = 0; i < lines.length; i++) {
664
+ const match = lines[i].text.match(terminatorRe);
665
+ if (!match) continue;
666
+ if (guardReturnLines.has(i)) continue;
667
+ const terminatorKind = match[1];
668
+ if (terminatorKind !== "return" && terminatorKind !== "throw") continue;
669
+ if (isGuardReturn(lines, startDepths, i)) guardReturnLines.add(i);
670
+ }
671
+ for (let i = 0; i < lines.length; i++) {
672
+ const match = lines[i].text.match(terminatorRe);
673
+ if (!match) continue;
674
+ const terminatorKind = match[1];
675
+ const terminatorStartDepth = startDepths[i];
676
+ if (inCatchFinally.has(i)) continue;
677
+ if (inCallback.has(i)) continue;
678
+ if (callbackMethodRe.test(lines[i].text)) continue;
679
+ if (/=>\s*\{/.test(lines[i].text)) continue;
680
+ if (guardReturnLines.has(i)) continue;
681
+ let endLine = i;
682
+ if (!lines[i].text.trimEnd().endsWith(";") && !lines[i].text.trimEnd().endsWith("}")) for (let j = i + 1; j < lines.length && j <= i + 5; j++) {
683
+ endLine = j;
684
+ if (lines[j].text.includes(";") || lines[j].text.trimEnd().endsWith("}")) break;
685
+ }
686
+ for (let j = endLine + 1; j < lines.length; j++) {
687
+ const text = lines[j].text.trim();
688
+ if (text.startsWith("} catch") || text.startsWith("} else") || text.startsWith("} finally")) break;
689
+ if (isClosingBraceLine(text)) continue;
690
+ if (text === "" || text.startsWith("//") || text.startsWith("/*") || text.startsWith("*")) continue;
691
+ if (startDepths[j] !== terminatorStartDepth) break;
692
+ const severity = terminatorKind === "return" ? "warning" : "error";
693
+ diagnostics.push(makeDiagnostic({
694
+ filePath,
695
+ rule: "dead-flow/unreachable-after-terminator",
696
+ message: `Unreachable code after ${terminatorKind} on line ${lines[i].num}`,
697
+ line: lines[j].num,
698
+ severity,
699
+ help: `Remove or move the unreachable code after the ${terminatorKind} statement on line ${lines[i].num}`,
700
+ suggestion: {
701
+ type: "delete",
702
+ text: "",
703
+ confidence: .9,
704
+ reason: `Code after ${terminatorKind} can never execute`,
705
+ range: {
706
+ startLine: lines[j].num,
707
+ startCol: 1,
708
+ endLine: lines[j].num,
709
+ endCol: lines[j].text.length + 1
710
+ }
711
+ },
712
+ detail: {
713
+ terminatorKind,
714
+ terminatorLine: lines[i].num
715
+ }
716
+ }));
717
+ break;
718
+ }
719
+ }
720
+ return diagnostics;
721
+ }
722
+ /** Check if the terminator at line index `idx` is a braceless guard return
723
+ * (inside a braceless `if` without `else`). A guard return like
724
+ * `if (!x) return;` makes subsequent code the normal path, not unreachable. */
725
+ function isGuardReturn(lines, startDepths, idx) {
726
+ let prevIdx = idx - 1;
727
+ while (prevIdx >= 0) {
728
+ const prevTrimmed = lines[prevIdx].text.trim();
729
+ if (prevTrimmed === "" || prevTrimmed.startsWith("//") || prevTrimmed.startsWith("/*") || prevTrimmed.startsWith("*")) {
730
+ prevIdx--;
731
+ continue;
732
+ }
733
+ break;
734
+ }
735
+ if (prevIdx < 0) return false;
736
+ const prevTrimmed = lines[prevIdx].text.trim();
737
+ if (/^if\s*\(/.test(prevTrimmed) && !prevTrimmed.includes("{")) {
738
+ let nextIdx = idx + 1;
739
+ while (nextIdx < lines.length) {
740
+ const nextTrimmed = lines[nextIdx].text.trim();
741
+ if (nextTrimmed === "" || nextTrimmed.startsWith("//") || nextTrimmed.startsWith("/*") || nextTrimmed.startsWith("*")) {
742
+ nextIdx++;
743
+ continue;
744
+ }
745
+ if (nextTrimmed.startsWith("else")) return false;
746
+ break;
747
+ }
748
+ return true;
749
+ }
750
+ return false;
751
+ }
752
+ function detectUnreachableAfterIfElseReturn(content, filePath) {
753
+ const diagnostics = [];
754
+ const lines = toLines(content);
755
+ const startDepths = [];
756
+ let depth = 0;
757
+ for (let i = 0; i < lines.length; i++) {
758
+ startDepths.push(depth);
759
+ for (const ch of lines[i].text) {
760
+ if (ch === "{") depth++;
761
+ if (ch === "}") depth--;
762
+ }
763
+ }
764
+ for (let i = 0; i < lines.length; i++) {
765
+ const text = lines[i].text.trim();
766
+ if (!text.startsWith("if") && !text.startsWith("} else if")) continue;
767
+ const ifStartLine = i;
768
+ let braceDepth = 0;
769
+ let ifBlockEnd = -1;
770
+ let hasIfTerminator = false;
771
+ let j = i;
772
+ while (j < lines.length && !lines[j].text.includes("{")) j++;
773
+ if (j >= lines.length) continue;
774
+ braceDepth = 1;
775
+ j++;
776
+ while (j < lines.length && braceDepth > 0) {
777
+ lines[j].text.trim();
778
+ for (const ch of lines[j].text) {
779
+ if (ch === "{") braceDepth++;
780
+ if (ch === "}") braceDepth--;
781
+ }
782
+ if (braceDepth === 0) {
783
+ ifBlockEnd = j;
784
+ break;
785
+ }
786
+ if (/^\s*(return\b|throw\b)/.test(lines[j].text)) hasIfTerminator = true;
787
+ j++;
788
+ }
789
+ if (!hasIfTerminator || ifBlockEnd === -1) continue;
790
+ let elseStart = ifBlockEnd + 1;
791
+ while (elseStart < lines.length) {
792
+ const t = lines[elseStart].text.trim();
793
+ if (t === "" || t.startsWith("//")) {
794
+ elseStart++;
795
+ continue;
796
+ }
797
+ break;
798
+ }
799
+ if (elseStart >= lines.length) continue;
800
+ if (!lines[elseStart].text.trim().startsWith("else")) continue;
801
+ braceDepth = 0;
802
+ let elseBlockEnd = -1;
803
+ let hasElseTerminator = false;
804
+ let k = elseStart;
805
+ while (k < lines.length && !lines[k].text.includes("{")) k++;
806
+ if (k >= lines.length) continue;
807
+ braceDepth = 1;
808
+ k++;
809
+ while (k < lines.length && braceDepth > 0) {
810
+ for (const ch of lines[k].text) {
811
+ if (ch === "{") braceDepth++;
812
+ if (ch === "}") braceDepth--;
813
+ }
814
+ if (braceDepth === 0) {
815
+ elseBlockEnd = k;
816
+ break;
817
+ }
818
+ if (/^\s*(return\b|throw\b)/.test(lines[k].text)) hasElseTerminator = true;
819
+ k++;
820
+ }
821
+ if (!hasElseTerminator || elseBlockEnd === -1) continue;
822
+ const constructDepth = startDepths[ifStartLine];
823
+ for (let m = elseBlockEnd + 1; m < lines.length; m++) {
824
+ const t = lines[m].text.trim();
825
+ if (isClosingBraceLine(t)) break;
826
+ if (t === "" || t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) continue;
827
+ if (startDepths[m] < constructDepth) break;
828
+ if (startDepths[m] > constructDepth) continue;
829
+ diagnostics.push(makeDiagnostic({
830
+ filePath,
831
+ rule: "dead-flow/unreachable-after-if-else-return",
832
+ message: `Unreachable code: both if/else branches terminate (lines ${ifStartLine + 1}-${elseBlockEnd + 1})`,
833
+ line: lines[m].num,
834
+ severity: "warning",
835
+ help: "Remove the unreachable code after the if/else that both return/throw",
836
+ suggestion: {
837
+ type: "delete",
838
+ text: "",
839
+ confidence: .85,
840
+ reason: "Code after if/else where both branches terminate is unreachable",
841
+ range: {
842
+ startLine: lines[m].num,
843
+ startCol: 1,
844
+ endLine: lines[m].num,
845
+ endCol: lines[m].text.length + 1
846
+ }
847
+ }
848
+ }));
849
+ break;
850
+ }
851
+ }
852
+ return diagnostics;
853
+ }
854
+ function detectDeadConditionals(content, filePath) {
855
+ const diagnostics = [];
856
+ const lines = toLines(content);
857
+ const alwaysTruthy = /^(true|!false|[1-9]\d*|![0]+)$/;
858
+ const alwaysFalsy = /^(false|!true|0+|null|undefined|!1)$/;
859
+ for (const { num, text } of lines) {
860
+ const ifMatch = text.trim().match(/^if\s*\(\s*(.+?)\s*\)\s*\{?$/);
861
+ if (!ifMatch) continue;
862
+ const condition = ifMatch[1].trim();
863
+ if (condition.includes("&&") || condition.includes("||") || condition.includes("==") || condition.includes("!=") || condition.includes(">") || condition.includes("<")) continue;
864
+ let deadBranch = null;
865
+ if (alwaysTruthy.test(condition)) deadBranch = "else";
866
+ else if (alwaysFalsy.test(condition)) deadBranch = "then";
867
+ if (deadBranch) {
868
+ const branchDesc = deadBranch === "then" ? "if-block" : "else-block";
869
+ diagnostics.push(makeDiagnostic({
870
+ filePath,
871
+ rule: "dead-flow/dead-conditional",
872
+ message: `Condition \`${condition}\` is always ${deadBranch === "then" ? "falsy" : "truthy"}, making the ${branchDesc} unreachable`,
873
+ line: num,
874
+ severity: "warning",
875
+ help: `Simplify the conditional — the ${branchDesc} can never execute`,
876
+ suggestion: {
877
+ type: "refactor",
878
+ text: deadBranch === "else" ? "// remove else branch, keep if-body" : "// remove if block, keep else body as direct code",
879
+ confidence: .8,
880
+ reason: `Condition is statically determined to always be ${deadBranch === "then" ? "falsy" : "truthy"}`
881
+ },
882
+ detail: {
883
+ condition,
884
+ deadBranch
885
+ }
886
+ }));
887
+ }
888
+ }
889
+ return diagnostics;
890
+ }
891
+ function extractExports(content) {
892
+ const exports = [];
893
+ const lines = toLines(content);
894
+ for (const { num, text } of lines) {
895
+ const trimmed = text.trim();
896
+ const namedExport = trimmed.match(/^export\s+(?:default\s+)?(?:function|const|let|var|class|enum|interface|type)\s+(\w+)/);
897
+ if (namedExport) {
898
+ exports.push({
899
+ name: namedExport[1],
900
+ line: num,
901
+ isTypeExport: trimmed.includes("export type ") || trimmed.includes("export interface "),
902
+ isDefault: trimmed.includes("export default")
903
+ });
904
+ continue;
905
+ }
906
+ const braceExport = trimmed.match(/^export\s+(?:type\s+)?\{([^}]+)\}/);
907
+ if (braceExport) {
908
+ const names = braceExport[1].split(",").map((s) => {
909
+ const parts = s.trim().split(/\s+as\s+/);
910
+ return parts[parts.length - 1].trim();
911
+ }).filter(Boolean);
912
+ for (const name of names) exports.push({
913
+ name,
914
+ line: num,
915
+ isTypeExport: trimmed.includes("export type {"),
916
+ isDefault: false
917
+ });
918
+ continue;
919
+ }
920
+ if (trimmed.match(/^export\s+default\s+/)) exports.push({
921
+ name: "default",
922
+ line: num,
923
+ isTypeExport: false,
924
+ isDefault: true
925
+ });
926
+ }
927
+ return exports;
928
+ }
929
+ function detectUnusedExports(files, rootDir) {
930
+ const diagnostics = [];
931
+ const exportMap = /* @__PURE__ */ new Map();
932
+ const importedSymbols = /* @__PURE__ */ new Set();
933
+ for (const [filePath, content] of files) {
934
+ const relPath = relative(rootDir, filePath);
935
+ const exports = extractExports(content);
936
+ for (const exp of exports) {
937
+ const key = `${relPath}::${exp.name}`;
938
+ if (!exportMap.has(key)) exportMap.set(key, []);
939
+ exportMap.get(key).push({
940
+ filePath: relPath,
941
+ line: exp.line,
942
+ isType: exp.isTypeExport,
943
+ isDefault: exp.isDefault
944
+ });
945
+ }
946
+ const lines = toLines(content);
947
+ for (let li = 0; li < lines.length; li++) {
948
+ const trimmed = lines[li].text.trim();
949
+ const braceImport = trimmed.match(/^import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"][^'"]+['"]/);
950
+ if (braceImport) {
951
+ const names = braceImport[1].split(",").map((s) => {
952
+ return s.trim().split(/\s+as\s+/)[0].trim();
953
+ }).filter(Boolean);
954
+ for (const name of names) importedSymbols.add(name);
955
+ }
956
+ if (trimmed.match(/^import\s+(?:type\s+)?\{\s*$/)) for (let next = li + 1; next < lines.length; next++) {
957
+ const nextTrimmed = lines[next].text.trim();
958
+ if (nextTrimmed.startsWith("}")) break;
959
+ const nameParts = nextTrimmed.split(",").map((s) => {
960
+ return s.trim().split(/\s+as\s+/)[0].trim();
961
+ }).filter(Boolean);
962
+ for (const name of nameParts) importedSymbols.add(name);
963
+ }
964
+ const defaultImport = trimmed.match(/^import\s+(\w+)\s+from\s+['"][^'"]+['"]/);
965
+ if (defaultImport && !trimmed.includes("{")) importedSymbols.add(defaultImport[1]);
966
+ const nsImport = trimmed.match(/^import\s+\*\s+as\s+(\w+)\s+from/);
967
+ if (nsImport) importedSymbols.add(nsImport[1]);
968
+ const dynamicImport = trimmed.match(/import\s*\([^)]*\)\s*\.then\s*\(\s*(?:\((\w+)\)|(\w+))\s*=>\s*\2?\.?(\w+)/);
969
+ if (dynamicImport) {
970
+ const symbolName = dynamicImport[3];
971
+ if (symbolName) importedSymbols.add(symbolName);
972
+ }
973
+ const dynamicThenAccess = trimmed.match(/\.then\s*\(\s*\((\w+)\)\s*=>\s*\1\.(\w+)/);
974
+ if (dynamicThenAccess) importedSymbols.add(dynamicThenAccess[2]);
975
+ const dynamicThenAccessNoParens = trimmed.match(/\.then\s*\(\s*(\w+)\s*=>\s*\1\.(\w+)/);
976
+ if (dynamicThenAccessNoParens) importedSymbols.add(dynamicThenAccessNoParens[2]);
977
+ }
978
+ }
979
+ for (const [key, entries] of exportMap) for (const entry of entries) {
980
+ const symbolName = key.split("::").pop();
981
+ if (entry.isType) continue;
982
+ if (entry.isDefault) continue;
983
+ if (/^[A-Z]/.test(symbolName)) continue;
984
+ if (/Engine$/.test(symbolName)) continue;
985
+ if (!importedSymbols.has(symbolName)) diagnostics.push(makeDiagnostic({
986
+ filePath: entry.filePath,
987
+ rule: "dead-flow/unused-export",
988
+ message: `Exported \`${symbolName}\` is never imported by any other file`,
989
+ line: entry.line,
990
+ severity: "info",
991
+ fixable: true,
992
+ help: `Consider removing the unused export \`${symbolName}\` or adding it to the public API explicitly`,
993
+ suggestion: {
994
+ type: "delete",
995
+ text: "",
996
+ confidence: .6,
997
+ reason: "This symbol is exported but never imported elsewhere in the project"
998
+ },
999
+ detail: { symbolName }
1000
+ }));
1001
+ }
1002
+ return diagnostics;
1003
+ }
1004
+ function detectUnusedVariables(content, filePath) {
1005
+ const diagnostics = [];
1006
+ const lines = toLines(content);
1007
+ const declarations = /* @__PURE__ */ new Map();
1008
+ for (const { num, text } of lines) {
1009
+ const trimmed = text.trim();
1010
+ const varMatch = trimmed.match(/^(?:export\s+)?(?:const|let|var)\s+(\w+)/);
1011
+ if (varMatch) {
1012
+ const name = varMatch[1];
1013
+ declarations.set(name, {
1014
+ line: num,
1015
+ isExported: trimmed.startsWith("export"),
1016
+ isReactComponent: /^[A-Z]/.test(name) && (trimmed.includes("=>") || trimmed.includes("function")),
1017
+ isType: false,
1018
+ isParameter: false
1019
+ });
1020
+ continue;
1021
+ }
1022
+ const fnMatch = trimmed.match(/^(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
1023
+ if (fnMatch) {
1024
+ const name = fnMatch[1];
1025
+ declarations.set(name, {
1026
+ line: num,
1027
+ isExported: trimmed.startsWith("export"),
1028
+ isReactComponent: /^[A-Z]/.test(name),
1029
+ isType: false,
1030
+ isParameter: false
1031
+ });
1032
+ continue;
1033
+ }
1034
+ const typeMatch = trimmed.match(/^(?:export\s+)?(?:type|interface)\s+(\w+)/);
1035
+ if (typeMatch) {
1036
+ declarations.set(typeMatch[1], {
1037
+ line: num,
1038
+ isExported: trimmed.startsWith("export"),
1039
+ isReactComponent: false,
1040
+ isType: true,
1041
+ isParameter: false
1042
+ });
1043
+ continue;
1044
+ }
1045
+ const arrowParamMatch = trimmed.match(/^(?:export\s+)?(?:const|let)\s+\w+\s*=\s*\(\s*([^)]+)\)\s*=>/);
1046
+ if (arrowParamMatch) {
1047
+ const params = arrowParamMatch[1].split(",").map((p) => {
1048
+ return p.trim().split(":")[0].trim().replace(/^\.\.\./, "").trim();
1049
+ });
1050
+ for (const param of params) if (param && /^\w+$/.test(param)) declarations.set(param, {
1051
+ line: num,
1052
+ isExported: false,
1053
+ isReactComponent: false,
1054
+ isType: false,
1055
+ isParameter: true
1056
+ });
1057
+ }
1058
+ }
1059
+ const allContent = content;
1060
+ for (const [name, info] of declarations) {
1061
+ if (name.startsWith("_")) continue;
1062
+ if (info.isExported) continue;
1063
+ if (info.isReactComponent) continue;
1064
+ if (info.isType) continue;
1065
+ if (info.isParameter) continue;
1066
+ const re = new RegExp(`\\b${escapeRegExp(name)}\\b`, "g");
1067
+ const occurrences = (allContent.match(re) ?? []).length;
1068
+ if (occurrences <= 1) diagnostics.push(makeDiagnostic({
1069
+ filePath,
1070
+ rule: "dead-flow/unused-variable",
1071
+ message: `Variable \`${name}\` is declared but never used`,
1072
+ line: info.line,
1073
+ severity: "suggestion",
1074
+ fixable: true,
1075
+ help: `Remove the unused variable \`${name}\` or prefix with _ if intentionally unused`,
1076
+ suggestion: {
1077
+ type: "delete",
1078
+ text: "",
1079
+ confidence: .7,
1080
+ reason: `Variable \`${name}\` is never referenced after its declaration`
1081
+ },
1082
+ detail: {
1083
+ variableName: name,
1084
+ referenceCount: occurrences
1085
+ }
1086
+ }));
1087
+ }
1088
+ return diagnostics;
1089
+ }
1090
+ /** Escape string for use in RegExp */
1091
+ function escapeRegExp(str) {
1092
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1093
+ }
1094
+ function detectEmptyBlocks(content, filePath) {
1095
+ const diagnostics = [];
1096
+ const lines = toLines(content);
1097
+ for (let i = 0; i < lines.length; i++) {
1098
+ const trimmed = lines[i].text.trim();
1099
+ const sameLineEmpty = trimmed.match(/^(?:if|else|for|while|do|try|catch|finally|switch)\s*\([^)]*\)\s*\{(.*)\}\s*$/);
1100
+ if (sameLineEmpty) {
1101
+ const innerContent = sameLineEmpty[1].trim();
1102
+ const construct = trimmed.split("{")[0].trim();
1103
+ if (/catch\b/.test(construct) || /finally\b/.test(construct)) {
1104
+ const catchMatch = construct.match(/catch\s*\(\s*(\w+)\s*\)/);
1105
+ const errorVar = catchMatch ? catchMatch[1] : "error";
1106
+ if (/catch\b/.test(construct) && innerContent === "") diagnostics.push(makeDiagnostic({
1107
+ filePath,
1108
+ rule: "dead-flow/empty-block",
1109
+ message: `Empty catch block — error is silently swallowed`,
1110
+ line: lines[i].num,
1111
+ severity: "warning",
1112
+ fixable: true,
1113
+ help: "Empty catch blocks silently swallow errors. Add console.error() or a TODO comment to handle the error.",
1114
+ suggestion: {
1115
+ type: "replace",
1116
+ text: `${construct} { console.error(${errorVar}) }`,
1117
+ range: {
1118
+ startLine: lines[i].num,
1119
+ startCol: 1,
1120
+ endLine: lines[i].num,
1121
+ endCol: lines[i].text.length + 1
1122
+ },
1123
+ confidence: .75,
1124
+ reason: "Empty catch blocks hide errors. Adding console.error() ensures errors are at least logged."
1125
+ }
1126
+ }));
1127
+ continue;
1128
+ }
1129
+ if (innerContent !== "") continue;
1130
+ diagnostics.push(makeDiagnostic({
1131
+ filePath,
1132
+ rule: "dead-flow/empty-block",
1133
+ message: `Empty block after \`${construct}\``,
1134
+ line: lines[i].num,
1135
+ severity: "info",
1136
+ fixable: false,
1137
+ help: "Empty blocks may indicate swallowed logic or placeholder code. Add implementation or a comment explaining intent.",
1138
+ suggestion: {
1139
+ type: "refactor",
1140
+ text: "// TODO: implement",
1141
+ confidence: .5,
1142
+ reason: "Empty block likely indicates missing implementation"
1143
+ }
1144
+ }));
1145
+ continue;
1146
+ }
1147
+ const elseEmpty = trimmed.match(/^else\s*\{(.*)\}\s*$/);
1148
+ if (elseEmpty) {
1149
+ if (elseEmpty[1].trim() === "") diagnostics.push(makeDiagnostic({
1150
+ filePath,
1151
+ rule: "dead-flow/empty-block",
1152
+ message: "Empty else block",
1153
+ line: lines[i].num,
1154
+ severity: "info",
1155
+ fixable: false,
1156
+ help: "Empty else block may indicate swallowed logic. Add implementation or remove the else clause.",
1157
+ suggestion: {
1158
+ type: "refactor",
1159
+ text: "// TODO: implement",
1160
+ confidence: .5,
1161
+ reason: "Empty else block likely indicates missing implementation"
1162
+ }
1163
+ }));
1164
+ continue;
1165
+ }
1166
+ if (trimmed.endsWith("{") && !trimmed.includes("}")) {
1167
+ if (!(/^(?:if|else|for|while|do|try|catch|finally|switch)/.test(trimmed) || trimmed === "{")) continue;
1168
+ let nextLine = i + 1;
1169
+ let isEmpty = true;
1170
+ let hasComment = false;
1171
+ while (nextLine < lines.length) {
1172
+ const nextTrimmed = lines[nextLine].text.trim();
1173
+ if (nextTrimmed.startsWith("}") || nextTrimmed.startsWith("});")) break;
1174
+ if (nextTrimmed !== "") if (nextTrimmed.startsWith("//") || nextTrimmed.startsWith("/*") || nextTrimmed.startsWith("*")) hasComment = true;
1175
+ else {
1176
+ isEmpty = false;
1177
+ break;
1178
+ }
1179
+ nextLine++;
1180
+ }
1181
+ if (isEmpty) {
1182
+ const construct = trimmed.split("{")[0].trim() || "block";
1183
+ const isCatchOrFinallyBlock = construct.startsWith("catch") || construct.startsWith("finally") || trimmed === "{" && i > 0 && (/^\s*}\s*catch\b/.test(lines[i - 1].text) || /^\s*}\s*finally\b/.test(lines[i - 1].text));
1184
+ if (isCatchOrFinallyBlock && hasComment) continue;
1185
+ if (isCatchOrFinallyBlock && construct.startsWith("catch") && !hasComment) {
1186
+ const catchMatch = construct.match(/catch\s*\(\s*(\w+)\s*\)/);
1187
+ const errorVar = catchMatch ? catchMatch[1] : "error";
1188
+ diagnostics.push(makeDiagnostic({
1189
+ filePath,
1190
+ rule: "dead-flow/empty-block",
1191
+ message: `Empty catch block — error is silently swallowed`,
1192
+ line: lines[i].num,
1193
+ severity: "warning",
1194
+ fixable: true,
1195
+ help: "Empty catch blocks silently swallow errors. Add console.error() or a TODO comment to handle the error.",
1196
+ suggestion: {
1197
+ type: "replace",
1198
+ text: `${construct} {\n console.error(${errorVar})\n}`,
1199
+ range: {
1200
+ startLine: lines[i].num,
1201
+ startCol: 1,
1202
+ endLine: nextLine + 1,
1203
+ endCol: 1
1204
+ },
1205
+ confidence: .7,
1206
+ reason: "Empty catch blocks hide errors. Adding console.error() ensures errors are at least logged."
1207
+ }
1208
+ }));
1209
+ continue;
1210
+ }
1211
+ diagnostics.push(makeDiagnostic({
1212
+ filePath,
1213
+ rule: "dead-flow/empty-block",
1214
+ message: `Empty ${construct} block`,
1215
+ line: lines[i].num,
1216
+ severity: "info",
1217
+ fixable: false,
1218
+ help: "Empty blocks may indicate swallowed logic or placeholder code. Add implementation or a comment.",
1219
+ suggestion: {
1220
+ type: "refactor",
1221
+ text: "// TODO: implement",
1222
+ confidence: .5,
1223
+ reason: "Empty block likely indicates missing implementation"
1224
+ }
1225
+ }));
1226
+ }
1227
+ }
1228
+ }
1229
+ return diagnostics;
1230
+ }
1231
+ function detectDeadSwitchCases(content, filePath) {
1232
+ const diagnostics = [];
1233
+ const lines = toLines(content);
1234
+ for (let i = 0; i < lines.length; i++) {
1235
+ if (!lines[i].text.trim().startsWith("switch")) continue;
1236
+ let braceDepth = 0;
1237
+ let j = i;
1238
+ while (j < lines.length && !lines[j].text.includes("{")) j++;
1239
+ if (j >= lines.length) continue;
1240
+ braceDepth = 1;
1241
+ j++;
1242
+ const cases = [];
1243
+ while (j < lines.length && braceDepth > 0) {
1244
+ const lineText = lines[j].text;
1245
+ for (const ch of lineText) {
1246
+ if (ch === "{") braceDepth++;
1247
+ if (ch === "}") braceDepth--;
1248
+ }
1249
+ const t = lineText.trim();
1250
+ if (t.startsWith("case ") || t.startsWith("default:")) cases.push({
1251
+ keyword: t.startsWith("default") ? "default" : "case",
1252
+ line: lines[j].num
1253
+ });
1254
+ if (/^\s*(break;|return\b|throw\b|continue;)/.test(lineText) && braceDepth > 0) {
1255
+ let nextLineIdx = j + 1;
1256
+ while (nextLineIdx < lines.length && braceDepth > 0) {
1257
+ const nextTrimmed = lines[nextLineIdx].text.trim();
1258
+ for (const ch of lines[nextLineIdx].text) {
1259
+ if (ch === "{") braceDepth++;
1260
+ if (ch === "}") braceDepth--;
1261
+ }
1262
+ if (braceDepth <= 0) break;
1263
+ if (nextTrimmed.startsWith("case ") || nextTrimmed.startsWith("default:")) break;
1264
+ if (nextTrimmed === "" || nextTrimmed.startsWith("//") || nextTrimmed.startsWith("/*") || nextTrimmed.startsWith("*") || nextTrimmed.startsWith("break;")) {
1265
+ nextLineIdx++;
1266
+ continue;
1267
+ }
1268
+ diagnostics.push(makeDiagnostic({
1269
+ filePath,
1270
+ rule: "dead-flow/dead-switch-code",
1271
+ message: `Unreachable code in switch after break/return on line ${lines[j].num}`,
1272
+ line: lines[nextLineIdx].num,
1273
+ severity: "warning",
1274
+ help: "Remove the unreachable code in this switch case after the terminator statement",
1275
+ suggestion: {
1276
+ type: "delete",
1277
+ text: "",
1278
+ confidence: .9,
1279
+ reason: "Code after break/return/throw in a switch case is unreachable",
1280
+ range: {
1281
+ startLine: lines[nextLineIdx].num,
1282
+ startCol: 1,
1283
+ endLine: lines[nextLineIdx].num,
1284
+ endCol: lines[nextLineIdx].text.length + 1
1285
+ }
1286
+ }
1287
+ }));
1288
+ break;
1289
+ }
1290
+ }
1291
+ j++;
1292
+ }
1293
+ const defaultIdx = cases.findIndex((c) => c.keyword === "default");
1294
+ if (defaultIdx !== -1 && defaultIdx < cases.length - 1) for (let k = defaultIdx + 1; k < cases.length; k++) diagnostics.push(makeDiagnostic({
1295
+ filePath,
1296
+ rule: "dead-flow/dead-switch-case-after-default",
1297
+ message: `Case on line ${cases[k].line} is unreachable: it appears after the default case`,
1298
+ line: cases[k].line,
1299
+ severity: "warning",
1300
+ help: "Move the default case to the end of the switch, or remove the unreachable case",
1301
+ suggestion: {
1302
+ type: "refactor",
1303
+ text: "// move default to end of switch",
1304
+ confidence: .85,
1305
+ reason: "Cases after default can never be reached"
1306
+ }
1307
+ }));
1308
+ }
1309
+ return diagnostics;
1310
+ }
1311
+ /** Build a dedup key for a diagnostic */
1312
+ function dedupKey(d) {
1313
+ return `${d.filePath}:${d.line}:${d.rule}`;
1314
+ }
1315
+ /** Merge AST and regex diagnostics, preferring AST when both match.
1316
+ * AST-only rules always pass through.
1317
+ * For rules that both AST and regex can produce, AST wins on same file+line. */
1318
+ function mergeASTAndRegex(astDiags, regexDiags, astRulesRun) {
1319
+ const result = [];
1320
+ const seen = /* @__PURE__ */ new Set();
1321
+ for (const d of astDiags) {
1322
+ const key = dedupKey(d);
1323
+ if (!seen.has(key)) {
1324
+ seen.add(key);
1325
+ result.push(d);
1326
+ }
1327
+ }
1328
+ for (const d of regexDiags) {
1329
+ const key = dedupKey(d);
1330
+ if (seen.has(key)) continue;
1331
+ const ruleBase = d.rule.replace("dead-flow/", "");
1332
+ if (astRulesRun.has(ruleBase)) continue;
1333
+ seen.add(key);
1334
+ result.push(d);
1335
+ }
1336
+ return result;
1337
+ }
1338
+ const deadFlowEngine = {
1339
+ name: "dead-flow",
1340
+ description: "Detects dead/unreachable code using AST (tree-sitter) with regex fallback: unreachable code after terminators, dead conditionals, unused exports/variables, empty blocks, and dead switch cases",
1341
+ supportedLanguages: ["typescript", "javascript"],
1342
+ async run(context) {
1343
+ const start = Date.now();
1344
+ const { rootDirectory, config, files: specifiedFiles } = context;
1345
+ const filePaths = specifiedFiles ? specifiedFiles.filter(isRelevantFile) : await collectFiles(rootDirectory, config.exclude);
1346
+ if (filePaths.length === 0) return {
1347
+ engine: "dead-flow",
1348
+ diagnostics: [],
1349
+ elapsed: Date.now() - start,
1350
+ skipped: true,
1351
+ skipReason: "No TypeScript/JavaScript files found to analyze"
1352
+ };
1353
+ const fileContents = /* @__PURE__ */ new Map();
1354
+ for (const fp of filePaths) try {
1355
+ const content = await readFileContent(fp);
1356
+ fileContents.set(fp, content);
1357
+ } catch {}
1358
+ const astDiagnostics = [];
1359
+ const astMap = /* @__PURE__ */ new Map();
1360
+ const perFileASTRules = /* @__PURE__ */ new Map();
1361
+ let astAvailable = false;
1362
+ for (const [fp, content] of fileContents) {
1363
+ const relPath = relative(rootDirectory, fp);
1364
+ try {
1365
+ const astResult = await detectAllAST(content, relPath);
1366
+ if (astResult) {
1367
+ astAvailable = true;
1368
+ astDiagnostics.push(...astResult.diagnostics);
1369
+ perFileASTRules.set(relPath, astResult.astRules);
1370
+ const ast = await parseWithTreeSitter(content, relPath);
1371
+ if (ast) astMap.set(relPath, ast);
1372
+ }
1373
+ } catch {}
1374
+ }
1375
+ let astExportDiags = [];
1376
+ let astExportRulesRun = false;
1377
+ if (config.deadCode.unusedExports && astMap.size > 0) try {
1378
+ const exportResult = await detectUnusedExportsASTWrapper(astMap, rootDirectory);
1379
+ if (exportResult) {
1380
+ astExportDiags = exportResult;
1381
+ astExportRulesRun = true;
1382
+ }
1383
+ } catch {}
1384
+ const regexDiagnostics = [];
1385
+ for (const [fp, content] of fileContents) {
1386
+ const relPath = relative(rootDirectory, fp);
1387
+ if (config.deadCode.unreachableBranches) regexDiagnostics.push(...detectUnreachableAfterTerminator(content, relPath));
1388
+ if (config.deadCode.unreachableBranches) regexDiagnostics.push(...detectUnreachableAfterIfElseReturn(content, relPath));
1389
+ if (config.deadCode.unreachableBranches) regexDiagnostics.push(...detectDeadConditionals(content, relPath));
1390
+ if (config.deadCode.unusedVariables) regexDiagnostics.push(...detectUnusedVariables(content, relPath));
1391
+ regexDiagnostics.push(...detectEmptyBlocks(content, relPath));
1392
+ if (config.deadCode.unreachableBranches) regexDiagnostics.push(...detectDeadSwitchCases(content, relPath));
1393
+ }
1394
+ if (config.deadCode.unusedExports) regexDiagnostics.push(...detectUnusedExports(fileContents, rootDirectory));
1395
+ const globalASTRules = /* @__PURE__ */ new Set();
1396
+ if (astAvailable) {
1397
+ globalASTRules.add("unreachable-after-terminator");
1398
+ globalASTRules.add("unused-variable");
1399
+ globalASTRules.add("dead-conditional");
1400
+ globalASTRules.add("dead-after-throw");
1401
+ globalASTRules.add("dead-after-return");
1402
+ globalASTRules.add("dead-after-break");
1403
+ }
1404
+ if (astExportRulesRun) globalASTRules.add("unused-export");
1405
+ let merged = mergeASTAndRegex([...astDiagnostics, ...astExportDiags], regexDiagnostics, globalASTRules);
1406
+ const seen = /* @__PURE__ */ new Set();
1407
+ return {
1408
+ engine: "dead-flow",
1409
+ diagnostics: merged.filter((d) => {
1410
+ const key = `${d.filePath}:${d.line}:${d.rule}`;
1411
+ if (seen.has(key)) return false;
1412
+ seen.add(key);
1413
+ return true;
1414
+ }),
1415
+ elapsed: Date.now() - start,
1416
+ skipped: false
1417
+ };
1418
+ }
1419
+ };
1420
+
1421
+ //#endregion
1422
+ export { deadFlowEngine };