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,1839 @@
1
+ import { _ as parseFile, a as findNodesOfType, c as findPythonImports, d as initParser, f as initPythonParser, g as isPythonAvailable, h as isInsideCatch, l as getAsExpressionContext, m as isCatchBodyEmpty, n as extractImportFromNode, p as isAvailable, s as findPythonClasses, t as detectPythonAIPatterns, u as getAsExpressionType, v as parsePython } from "./tree-sitter-CM-cP0nl.js";
2
+ import { i as toLines, n as extractImports, r as readFileContent } from "./file-utils-B_HFXhCs.js";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { extname, join, relative, resolve } from "node:path";
5
+ import { readFile } from "node:fs/promises";
6
+
7
+ //#region src/engines/ast-slop/index.ts
8
+ /** Build a diagnostic with common fields filled */
9
+ function diag(opts) {
10
+ return {
11
+ filePath: opts.filePath,
12
+ engine: "ast-slop",
13
+ rule: opts.rule,
14
+ severity: opts.severity,
15
+ message: opts.message,
16
+ help: opts.help,
17
+ line: opts.line,
18
+ column: opts.column,
19
+ category: "ai-slop",
20
+ fixable: opts.fixable,
21
+ suggestion: opts.suggestion,
22
+ detail: opts.detail
23
+ };
24
+ }
25
+ /** Determine language from file extension */
26
+ function languageFromPath(filePath) {
27
+ const ext = extname(filePath);
28
+ return {
29
+ ".ts": "typescript",
30
+ ".tsx": "typescript",
31
+ ".js": "javascript",
32
+ ".jsx": "javascript",
33
+ ".mjs": "javascript",
34
+ ".cjs": "javascript",
35
+ ".py": "python"
36
+ }[ext] ?? null;
37
+ }
38
+ /** Determine TS/TSX language hint for tree-sitter parsing */
39
+ function tsLangHint(filePath) {
40
+ const ext = extname(filePath);
41
+ if (ext === ".tsx" || ext === ".jsx") return "tsx";
42
+ if (ext === ".ts") return "typescript";
43
+ return "javascript";
44
+ }
45
+ /** Check whether an import source is a bare specifier (not relative, not absolute) */
46
+ function isBareSpecifier(source) {
47
+ return !source.startsWith(".") && !source.startsWith("/");
48
+ }
49
+ /** Check whether a bare specifier is scoped (@org/pkg) and return the package name */
50
+ function scopedPackageName(source) {
51
+ const match = source.match(/^(@[^/]+\/[^/]+)/);
52
+ return match ? match[1] : null;
53
+ }
54
+ /** Load tsconfig.json compilerOptions.paths + baseUrl from project root */
55
+ function loadTsconfigPaths(rootDir) {
56
+ const tsconfigPath = join(rootDir, "tsconfig.json");
57
+ if (!existsSync(tsconfigPath)) return null;
58
+ try {
59
+ const stripped = readFileSync(tsconfigPath, "utf-8").replace(/\/\/.*$/gm, "");
60
+ const compilerOptions = JSON.parse(stripped).compilerOptions ?? {};
61
+ const baseUrl = compilerOptions.baseUrl;
62
+ const paths = compilerOptions.paths ?? {};
63
+ if (!baseUrl && Object.keys(paths).length === 0) return null;
64
+ return {
65
+ baseUrl,
66
+ paths
67
+ };
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ /** Check if an import source matches a tsconfig path alias.
73
+ * Returns the resolved filesystem path if it matches, or null. */
74
+ function resolveTsconfigAlias(importSource, tsconfigPaths, rootDir) {
75
+ const { baseUrl, paths } = tsconfigPaths;
76
+ const baseDir = baseUrl ? resolve(rootDir, baseUrl) : rootDir;
77
+ for (const [pattern, targets] of Object.entries(paths)) if (pattern.endsWith("/*")) {
78
+ const prefix = pattern.slice(0, -2);
79
+ if (importSource === prefix || importSource.startsWith(prefix + "/")) {
80
+ const suffix = importSource.slice(prefix.length + 1);
81
+ for (const target of targets) {
82
+ const resolved = resolve(baseDir, target.endsWith("/*") ? target.slice(0, -2) : target, suffix);
83
+ if (existsSync(resolved) || existsSync(resolved + ".ts") || existsSync(resolved + ".tsx") || existsSync(resolved + ".js") || existsSync(resolved + ".jsx") || existsSync(resolved + "/index.ts") || existsSync(resolved + "/index.tsx") || existsSync(resolved + "/index.js")) return resolved;
84
+ }
85
+ }
86
+ } else if (importSource === pattern) for (const target of targets) {
87
+ const resolved = resolve(baseDir, target);
88
+ if (existsSync(resolved) || existsSync(resolved + ".ts") || existsSync(resolved + ".tsx") || existsSync(resolved + ".js") || existsSync(resolved + ".jsx") || existsSync(resolved + "/index.ts") || existsSync(resolved + "/index.tsx") || existsSync(resolved + "/index.js")) return resolved;
89
+ }
90
+ if (baseUrl && !importSource.startsWith("@") && !importSource.startsWith(".")) {
91
+ const resolved = resolve(baseDir, importSource);
92
+ if (existsSync(resolved) || existsSync(resolved + ".ts") || existsSync(resolved + ".tsx") || existsSync(resolved + ".js") || existsSync(resolved + "/index.ts") || existsSync(resolved + "/index.tsx") || existsSync(resolved + "/index.js")) return resolved;
93
+ }
94
+ return null;
95
+ }
96
+ /** Load package.json dependencies (including devDependencies) */
97
+ async function loadPackageDeps(rootDir) {
98
+ try {
99
+ const raw = await readFile(join(rootDir, "package.json"), "utf-8");
100
+ const pkg = JSON.parse(raw);
101
+ return new Set([
102
+ ...Object.keys(pkg.dependencies ?? {}),
103
+ ...Object.keys(pkg.devDependencies ?? {}),
104
+ ...Object.keys(pkg.peerDependencies ?? {}),
105
+ ...Object.keys(pkg.optionalDependencies ?? {})
106
+ ]);
107
+ } catch {
108
+ return /* @__PURE__ */ new Set();
109
+ }
110
+ }
111
+ /** Load requirements.txt / pyproject.toml for Python */
112
+ async function loadPythonDeps(rootDir) {
113
+ const deps = /* @__PURE__ */ new Set();
114
+ try {
115
+ const raw = await readFile(join(rootDir, "requirements.txt"), "utf-8");
116
+ for (const line of raw.split("\n")) {
117
+ const trimmed = line.trim();
118
+ if (!trimmed || trimmed.startsWith("#")) continue;
119
+ const name = trimmed.split(/[=<>!\[]/)[0].split("[")[0].trim();
120
+ if (name) deps.add(name);
121
+ }
122
+ } catch {}
123
+ try {
124
+ const raw = await readFile(join(rootDir, "pyproject.toml"), "utf-8");
125
+ for (const line of raw.split("\n")) {
126
+ const depMatch = line.trim().match(/^"?([a-zA-Z0-9_-]+)"?\s*[=<>]/);
127
+ if (depMatch) deps.add(depMatch[1]);
128
+ }
129
+ } catch {}
130
+ return deps;
131
+ }
132
+ const NARRATIVE_PATTERNS = [
133
+ {
134
+ regex: /\/\/\s*Initialize/i,
135
+ label: "Initialize"
136
+ },
137
+ {
138
+ regex: /\/\/\s*Set up/i,
139
+ label: "Set up"
140
+ },
141
+ {
142
+ regex: /\/\/\s*Handle/i,
143
+ label: "Handle"
144
+ },
145
+ {
146
+ regex: /\/\/\s*Process/i,
147
+ label: "Process"
148
+ },
149
+ {
150
+ regex: /\/\/\s*Create/i,
151
+ label: "Create"
152
+ },
153
+ {
154
+ regex: /\/\/\s*Update/i,
155
+ label: "Update"
156
+ },
157
+ {
158
+ regex: /\/\/\s*Calculate/i,
159
+ label: "Calculate"
160
+ },
161
+ {
162
+ regex: /\/\/\s*Check if/i,
163
+ label: "Check if"
164
+ },
165
+ {
166
+ regex: /\/\/\s*Define/i,
167
+ label: "Define"
168
+ },
169
+ {
170
+ regex: /\/\*\s*We need to/i,
171
+ label: "We need to"
172
+ },
173
+ {
174
+ regex: /\/\*\s*This function/i,
175
+ label: "This function"
176
+ }
177
+ ];
178
+ const NARRATIVE_PATTERNS_PY = [
179
+ {
180
+ regex: /#\s*Initialize/i,
181
+ label: "Initialize"
182
+ },
183
+ {
184
+ regex: /#\s*Set up/i,
185
+ label: "Set up"
186
+ },
187
+ {
188
+ regex: /#\s*Handle/i,
189
+ label: "Handle"
190
+ },
191
+ {
192
+ regex: /#\s*Process/i,
193
+ label: "Process"
194
+ },
195
+ {
196
+ regex: /#\s*Create/i,
197
+ label: "Create"
198
+ },
199
+ {
200
+ regex: /#\s*Update/i,
201
+ label: "Update"
202
+ },
203
+ {
204
+ regex: /#\s*Calculate/i,
205
+ label: "Calculate"
206
+ },
207
+ {
208
+ regex: /#\s*Check if/i,
209
+ label: "Check if"
210
+ },
211
+ {
212
+ regex: /#\s*Define/i,
213
+ label: "Define"
214
+ },
215
+ {
216
+ regex: /"""\s*We need to/i,
217
+ label: "We need to"
218
+ },
219
+ {
220
+ regex: /"""\s*This function/i,
221
+ label: "This function"
222
+ }
223
+ ];
224
+ function detectNarrativeComments(lines, filePath, language) {
225
+ const patterns = language === "python" ? NARRATIVE_PATTERNS_PY : NARRATIVE_PATTERNS;
226
+ const results = [];
227
+ for (const { num, text } of lines) {
228
+ const trimmed = text.trim();
229
+ for (const { regex, label } of patterns) if (regex.test(trimmed)) {
230
+ const col = text.indexOf(trimmed.charAt(0)) + 1;
231
+ results.push(diag({
232
+ filePath,
233
+ rule: "ast-slop/narrative-comment",
234
+ severity: "suggestion",
235
+ message: `Narrative comment: "${label}" — describes WHAT, not WHY`,
236
+ help: "Remove or replace with a comment explaining the reasoning (WHY), not the mechanics (WHAT). Code should be self-documenting for the WHAT.",
237
+ line: num,
238
+ column: col,
239
+ fixable: true,
240
+ suggestion: {
241
+ type: "delete",
242
+ text: "",
243
+ range: {
244
+ startLine: num,
245
+ startCol: 1,
246
+ endLine: num,
247
+ endCol: text.length + 1
248
+ },
249
+ confidence: .7,
250
+ reason: "Narrative comments that only describe what the code does add noise. Delete or replace with a WHY comment."
251
+ }
252
+ }));
253
+ break;
254
+ }
255
+ }
256
+ return results;
257
+ }
258
+ const DECORATIVE_PATTERNS = [
259
+ /\/\/\s*[=]{3,}/,
260
+ /\/\/\s*[─━]{3,}/,
261
+ /\/\/\s*[*]{3,}/,
262
+ /\/\/\s*[~]{3,}/,
263
+ /\/\/\s*[-]{3,}\s*$/,
264
+ /#\s*[=]{3,}/,
265
+ /#\s*[─━]{3,}/,
266
+ /#\s*[*]{3,}/,
267
+ /#\s*[~]{3,}/,
268
+ /#\s*[-]{3,}\s*$/
269
+ ];
270
+ function detectDecorativeComments(lines, filePath) {
271
+ const results = [];
272
+ for (const { num, text } of lines) {
273
+ const trimmed = text.trim();
274
+ for (const pattern of DECORATIVE_PATTERNS) if (pattern.test(trimmed)) {
275
+ const col = text.indexOf(trimmed.charAt(0)) + 1;
276
+ results.push(diag({
277
+ filePath,
278
+ rule: "ast-slop/decorative-comment",
279
+ severity: "info",
280
+ message: "Decorative comment block — visual noise typical of AI-generated code",
281
+ help: "Remove decorative separators. Use blank lines to separate logical sections instead.",
282
+ line: num,
283
+ column: col,
284
+ fixable: true,
285
+ suggestion: {
286
+ type: "delete",
287
+ text: "",
288
+ range: {
289
+ startLine: num,
290
+ startCol: 1,
291
+ endLine: num,
292
+ endCol: text.length + 1
293
+ },
294
+ confidence: .9,
295
+ reason: "Decorative comment lines add visual clutter without conveying information."
296
+ }
297
+ }));
298
+ break;
299
+ }
300
+ }
301
+ return results;
302
+ }
303
+ function detectTrivialComments(lines, filePath, language) {
304
+ const results = [];
305
+ const commentPrefix = language === "python" ? "#" : "//";
306
+ for (let i = 0; i < lines.length - 1; i++) {
307
+ const current = lines[i].text.trim();
308
+ const next = lines[i + 1].text.trim();
309
+ if (!current.startsWith(commentPrefix)) continue;
310
+ if (!next || next.startsWith(commentPrefix)) continue;
311
+ const commentText = current.replace(new RegExp(`^\\s*${commentPrefix.replace("/", "\\/")}\\s*`), "").trim().toLowerCase();
312
+ if (!commentText) continue;
313
+ const normalizedComment = commentText.replace(/^(initialize|set up|handle|process|create|update|calculate|check if|define|get|set|return|add|remove|delete|fetch|load|save|validate|parse|reset|clear|log|assign|declare|call|invoke)\s+/i, "").replace(/^(the |a |an |this |that |these |those )/i, "").trim();
314
+ if (!normalizedComment || normalizedComment.length < 3) continue;
315
+ const codeLower = next.toLowerCase();
316
+ const commentWords = normalizedComment.split(/\s+/).filter((w) => w.length > 2);
317
+ if (commentWords.length === 0) continue;
318
+ const matchCount = commentWords.filter((w) => codeLower.includes(w)).length;
319
+ if (matchCount / commentWords.length >= .6 && matchCount >= 2) {
320
+ const col = lines[i].text.indexOf(current.charAt(0)) + 1;
321
+ results.push(diag({
322
+ filePath,
323
+ rule: "ast-slop/trivial-comment",
324
+ severity: "suggestion",
325
+ message: `Comment restates the obvious: next line already expresses "${normalizedComment}"`,
326
+ help: "Remove comments that simply restate what the code does. If the code isn't clear enough, improve the code instead.",
327
+ line: lines[i].num,
328
+ column: col,
329
+ fixable: true,
330
+ suggestion: {
331
+ type: "delete",
332
+ text: "",
333
+ range: {
334
+ startLine: lines[i].num,
335
+ startCol: 1,
336
+ endLine: lines[i].num,
337
+ endCol: lines[i].text.length + 1
338
+ },
339
+ confidence: .65,
340
+ reason: "The comment merely restates what the next line of code already makes obvious."
341
+ }
342
+ }));
343
+ }
344
+ }
345
+ return results;
346
+ }
347
+ function detectConsoleLeftovers(lines, filePath, language) {
348
+ const results = [];
349
+ if (/[/__]tests?[/__]/i.test(filePath)) return results;
350
+ if (/\.test\.(?:ts|tsx|js|jsx)$/.test(filePath)) return results;
351
+ if (/\.spec\.(?:ts|tsx|js|jsx)$/.test(filePath)) return results;
352
+ for (const { num, text } of lines) {
353
+ const trimmed = text.trim();
354
+ if (language !== "python") {
355
+ if (filePath.includes(".test.") || filePath.includes(".spec.") || filePath.includes("__tests__")) continue;
356
+ const logMatch = trimmed.match(/console\.(log|debug)\s*\(/);
357
+ if (logMatch) {
358
+ if (isInCatchBlock(lines, num)) continue;
359
+ const col = text.indexOf("console") + 1;
360
+ results.push(diag({
361
+ filePath,
362
+ rule: "ast-slop/console-leftover",
363
+ severity: "suggestion",
364
+ message: `console.${logMatch[1]}() leftover — likely debugging artifact`,
365
+ help: "Remove debug logging before committing. Use a proper logging library for production, or guard with environment checks.",
366
+ line: num,
367
+ column: col,
368
+ fixable: true,
369
+ suggestion: {
370
+ type: "delete",
371
+ text: "",
372
+ range: {
373
+ startLine: num,
374
+ startCol: 1,
375
+ endLine: num,
376
+ endCol: text.length + 1
377
+ },
378
+ confidence: .85,
379
+ reason: "console.log/console.debug statements are typically debugging artifacts that should not be in committed code."
380
+ }
381
+ }));
382
+ }
383
+ }
384
+ if (language === "python") {
385
+ if (trimmed.match(/^print\s*\(/)) {
386
+ if (lines.filter((l) => l.num < num && l.num >= num - 5).some((l) => l.text.includes("if __name__"))) continue;
387
+ const col = text.indexOf("print") + 1;
388
+ results.push(diag({
389
+ filePath,
390
+ rule: "ast-slop/console-leftover",
391
+ severity: "suggestion",
392
+ message: "print() leftover — likely debugging artifact",
393
+ help: "Replace print() with proper logging (logging.debug, logger.debug) or remove entirely.",
394
+ line: num,
395
+ column: col,
396
+ fixable: true,
397
+ suggestion: {
398
+ type: "replace",
399
+ text: trimmed.replace(/^print\s*\((.+)\)/, "logger.debug($1)"),
400
+ range: {
401
+ startLine: num,
402
+ startCol: 1,
403
+ endLine: num,
404
+ endCol: text.length + 1
405
+ },
406
+ confidence: .6,
407
+ reason: "Replace bare print() with structured logging for maintainability."
408
+ }
409
+ }));
410
+ }
411
+ }
412
+ }
413
+ return results;
414
+ }
415
+ function isInCatchBlock(lines, lineNum) {
416
+ let depth = 0;
417
+ for (let i = lineNum - 1; i >= 1; i--) {
418
+ const line = lines.find((l) => l.num === i);
419
+ if (!line) continue;
420
+ const t = line.text.trim();
421
+ for (const ch of t) {
422
+ if (ch === "}") depth--;
423
+ if (ch === "{") depth++;
424
+ }
425
+ if (t.includes("catch") && depth >= 0) return true;
426
+ if (depth < 0) return false;
427
+ }
428
+ return false;
429
+ }
430
+ const TODO_PATTERNS = [
431
+ /\bTODO\b/i,
432
+ /\bFIXME\b/i,
433
+ /\bHACK\b/i,
434
+ /\bXXX\b/i
435
+ ];
436
+ function detectTodoStubs(lines, filePath, language) {
437
+ const results = [];
438
+ const commentPrefix = language === "python" ? "#" : "//";
439
+ for (const { num, text } of lines) {
440
+ const trimmed = text.trim();
441
+ if (!(trimmed.startsWith(commentPrefix) || trimmed.startsWith("/*") || trimmed.startsWith("*"))) continue;
442
+ for (const pattern of TODO_PATTERNS) if (pattern.test(trimmed)) {
443
+ const col = text.indexOf(trimmed.charAt(0)) + 1;
444
+ const tag = trimmed.match(/\b(TODO|FIXME|HACK|XXX)\b/i)?.[1] ?? "TODO";
445
+ if (!/(?:#\d+|@[a-zA-Z0-9_-]+|\(\d{4}-\d{2}-\d{2}\)|https?:\/\/)/.test(trimmed)) results.push(diag({
446
+ filePath,
447
+ rule: "ast-slop/todo-stub",
448
+ severity: "info",
449
+ message: `${tag} comment without ticket reference or assignee — likely a stub`,
450
+ help: "Add a ticket/issue number (e.g. TODO(#123)) or assignee (e.g. TODO(@dev)), or remove if not actionable.",
451
+ line: num,
452
+ column: col,
453
+ fixable: false,
454
+ detail: { tag }
455
+ }));
456
+ break;
457
+ }
458
+ }
459
+ return results;
460
+ }
461
+ const GENERIC_NAMES = new Set([
462
+ "var1",
463
+ "var2",
464
+ "var3",
465
+ "temp",
466
+ "tmp",
467
+ "retval",
468
+ "foo",
469
+ "bar",
470
+ "baz",
471
+ "qux",
472
+ "quux",
473
+ "x",
474
+ "y",
475
+ "z",
476
+ "a",
477
+ "b",
478
+ "c",
479
+ "stuff",
480
+ "thing",
481
+ "something",
482
+ "whatever",
483
+ "misc",
484
+ "obj",
485
+ "itm"
486
+ ]);
487
+ function isGenericNameAcceptable(name, fullLine, prevLine, nextLine) {
488
+ if (/\b(?:name|id)\s*=\s*["']/.test(fullLine)) return true;
489
+ if (/\b(?:function|=>|callback|handler)\b/.test(fullLine) && /\(\s*\w*\s*,?\s*\b/.test(fullLine)) return true;
490
+ if (/\b(?:query|params|req|request|ctx|context)\s*[.\[]\s*/.test(fullLine)) return true;
491
+ if (/\{\s*[^}]*\b\w+\b[^}]*\}\s*=/.test(fullLine) && fullLine.includes(name)) {
492
+ if (/\b(?:response|res|result|axios|fetch|api)\b/.test(fullLine)) return true;
493
+ }
494
+ if (/\b(?:FormData|event|CustomEvent)\b/.test(fullLine)) return true;
495
+ if (/\b(?:useQuery|useMutation|useSWR|useFetch)\b/.test(fullLine)) return true;
496
+ return false;
497
+ }
498
+ function detectGenericNames(lines, filePath, language) {
499
+ const results = [];
500
+ for (let i = 0; i < lines.length; i++) {
501
+ const { num, text } = lines[i];
502
+ const trimmed = text.trim();
503
+ const varPattern = language === "python" ? /(?:^|\s)(\w+)\s*=\s*/ : /(?:const|let|var)\s+(\w+)\s*[=:]?/;
504
+ const match = trimmed.match(varPattern);
505
+ if (!match) continue;
506
+ const varName = match[1];
507
+ if (!GENERIC_NAMES.has(varName)) continue;
508
+ if (isGenericNameAcceptable(varName, trimmed, i > 0 ? lines[i - 1].text.trim() : void 0, i < lines.length - 1 ? lines[i + 1].text.trim() : void 0)) continue;
509
+ const col = text.indexOf(varName) + 1;
510
+ results.push(diag({
511
+ filePath,
512
+ rule: "ast-slop/generic-name",
513
+ severity: "suggestion",
514
+ message: `Generic variable name "${varName}" — lacks descriptive intent`,
515
+ help: `Rename "${varName}" to convey its purpose (e.g. "userData", "fetchResult", "configInfo"). Generic names are a hallmark of AI-generated code.`,
516
+ line: num,
517
+ column: col,
518
+ fixable: false,
519
+ detail: { variableName: varName }
520
+ }));
521
+ }
522
+ return results;
523
+ }
524
+ function detectDefensivePatterns(lines, filePath, language) {
525
+ const results = [];
526
+ for (const { num, text } of lines) {
527
+ const trimmed = text.trim();
528
+ if (language === "typescript") {
529
+ const typeofMatch = trimmed.match(/typeof\s+(\w+)\s*===?\s*['"]undefined['"]/);
530
+ if (typeofMatch) {
531
+ const col = text.indexOf("typeof") + 1;
532
+ results.push(diag({
533
+ filePath,
534
+ rule: "ast-slop/defensive-typeof",
535
+ severity: "info",
536
+ message: `typeof ${typeofMatch[1]} === 'undefined' — unnecessary in TypeScript; use optional chaining or type guards instead`,
537
+ help: "In TypeScript, variables are type-checked at compile time. Use optional chaining (?.), type narrowing, or explicit null checks instead of runtime typeof guards for declared variables.",
538
+ line: num,
539
+ column: col,
540
+ fixable: true,
541
+ suggestion: {
542
+ type: "refactor",
543
+ text: `${typeofMatch[1]} != null`,
544
+ confidence: .6,
545
+ reason: "Replace typeof undefined check with a simpler null check when the variable is already typed."
546
+ }
547
+ }));
548
+ }
549
+ }
550
+ if (language === "python") {
551
+ const isinstanceMatch = trimmed.match(/isinstance\s*\(\s*(\w+)\s*,/);
552
+ if (isinstanceMatch && trimmed.includes("type: ignore")) {
553
+ const col = text.indexOf("isinstance") + 1;
554
+ results.push(diag({
555
+ filePath,
556
+ rule: "ast-slop/defensive-isinstance",
557
+ severity: "info",
558
+ message: `Defensive isinstance check for "${isinstanceMatch[1]}" — contradicts type hints`,
559
+ help: "If the variable has a type annotation, isinstance checks at runtime indicate distrust of the type system. Strengthen the types or use a TypeGuard instead.",
560
+ line: num,
561
+ column: col,
562
+ fixable: false
563
+ }));
564
+ }
565
+ }
566
+ }
567
+ return results;
568
+ }
569
+ function detectSwallowedExceptions(lines, filePath, language) {
570
+ const results = [];
571
+ for (let i = 0; i < lines.length; i++) {
572
+ const { num, text } = lines[i];
573
+ const trimmed = text.trim();
574
+ if (language === "python") {
575
+ if (trimmed.match(/^except\s*(?:\w+(?:\s+as\s+\w+)?)?\s*:/)) for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
576
+ const nextTrimmed = lines[j].text.trim();
577
+ if (nextTrimmed === "pass" || nextTrimmed === "...") {
578
+ const col = text.indexOf("except") + 1;
579
+ results.push(diag({
580
+ filePath,
581
+ rule: "ast-slop/swallowed-exception",
582
+ severity: "info",
583
+ message: "Swallowed exception: except block contains only pass/ellipsis",
584
+ help: "At minimum, log the error. Silently swallowing exceptions hides bugs. Consider: logger.error(f'...: {e}', exc_info=True)",
585
+ line: num,
586
+ column: col,
587
+ fixable: true,
588
+ suggestion: {
589
+ type: "insert",
590
+ text: " logger.error(f'Unexpected error: {e}', exc_info=True)",
591
+ range: {
592
+ startLine: lines[j].num,
593
+ startCol: 1,
594
+ endLine: lines[j].num,
595
+ endCol: lines[j].text.length + 1
596
+ },
597
+ confidence: .7,
598
+ reason: "Replace bare pass with error logging to avoid silently hiding failures."
599
+ }
600
+ }));
601
+ break;
602
+ }
603
+ if (nextTrimmed && !nextTrimmed.startsWith("#") && nextTrimmed !== "pass" && nextTrimmed !== "...") break;
604
+ }
605
+ } else if (trimmed.match(/catch\s*(?:\(\s*\w+\s*\))?\s*\{\s*\}\s*$/)) {
606
+ const col = text.indexOf("catch") + 1;
607
+ results.push(diag({
608
+ filePath,
609
+ rule: "ast-slop/swallowed-exception",
610
+ severity: "info",
611
+ message: "Swallowed exception: empty catch block",
612
+ help: "Handle the error (log, rethrow, or recover). Empty catch blocks silently swallow errors, making bugs invisible.",
613
+ line: num,
614
+ column: col,
615
+ fixable: true,
616
+ suggestion: {
617
+ type: "refactor",
618
+ text: "catch (error) { console.error(error); }",
619
+ confidence: .6,
620
+ reason: "Add at least error logging to avoid silently swallowing exceptions."
621
+ }
622
+ }));
623
+ } else {
624
+ const catchStartMatch = trimmed.match(/catch\s*(?:\(\s*(\w+)\s*\))?\s*\{\s*$/);
625
+ if (catchStartMatch) {
626
+ const catchVar = catchStartMatch[1] ?? "error";
627
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
628
+ const nextTrimmed = lines[j].text.trim();
629
+ if (nextTrimmed === "}") {
630
+ const col = text.indexOf("catch") + 1;
631
+ results.push(diag({
632
+ filePath,
633
+ rule: "ast-slop/swallowed-exception",
634
+ severity: "info",
635
+ message: "Swallowed exception: empty catch block",
636
+ help: "Handle the error (log, rethrow, or recover). Empty catch blocks silently swallow errors, making bugs invisible.",
637
+ line: num,
638
+ column: col,
639
+ fixable: true,
640
+ suggestion: {
641
+ type: "insert",
642
+ text: ` console.error(${catchVar});`,
643
+ range: {
644
+ startLine: lines[j].num,
645
+ startCol: 1,
646
+ endLine: lines[j].num,
647
+ endCol: 1
648
+ },
649
+ confidence: .65,
650
+ reason: "Add at least error logging to the empty catch block."
651
+ }
652
+ }));
653
+ break;
654
+ }
655
+ if (nextTrimmed && nextTrimmed !== "" && nextTrimmed !== "}" && !nextTrimmed.startsWith("//")) break;
656
+ }
657
+ }
658
+ }
659
+ }
660
+ return results;
661
+ }
662
+ function detectUnsafeCasts(lines, filePath) {
663
+ const results = [];
664
+ for (const { num, text } of lines) {
665
+ const trimmed = text.trim();
666
+ const doubleMatch = trimmed.match(/\bas\s+unknown\s+as\s+(\w+)/);
667
+ if (doubleMatch) {
668
+ const col = text.indexOf("as unknown") + 1;
669
+ results.push(diag({
670
+ filePath,
671
+ rule: "ast-slop/double-assertion",
672
+ severity: "warning",
673
+ message: `Double type assertion: as unknown as ${doubleMatch[1]} — bypasses type safety`,
674
+ help: "Use a proper type guard, type predicate, or adjust the source/target types. Double assertions defeat the purpose of TypeScript.",
675
+ line: num,
676
+ column: col,
677
+ fixable: true,
678
+ suggestion: {
679
+ type: "refactor",
680
+ text: `as ${doubleMatch[1]}`,
681
+ confidence: .5,
682
+ reason: "Prefer a direct cast with a type guard over bypassing the type system with double assertion."
683
+ }
684
+ }));
685
+ }
686
+ if (trimmed.match(/\bas\s+any\b/) && !doubleMatch) {
687
+ const col = text.indexOf("as any") + 1;
688
+ results.push(diag({
689
+ filePath,
690
+ rule: "ast-slop/as-any",
691
+ severity: "warning",
692
+ message: "Unsafe cast: as any — opts out of type checking entirely",
693
+ help: "Replace `as any` with a more specific type, a type guard, or `as unknown as SpecificType` if truly needed (though that has its own issues).",
694
+ line: num,
695
+ column: col,
696
+ fixable: true,
697
+ suggestion: {
698
+ type: "refactor",
699
+ text: "/* replace with specific type */",
700
+ confidence: .4,
701
+ reason: "`as any` disables type checking. Replace with the actual expected type."
702
+ }
703
+ }));
704
+ }
705
+ }
706
+ return results;
707
+ }
708
+ function detectHallucinatedImports(content, lines, filePath, language, knownDeps, tsconfigPaths, rootDir) {
709
+ const results = [];
710
+ const imports = extractImports(content, language);
711
+ for (const imp of imports) {
712
+ if (!isBareSpecifier(imp.source)) continue;
713
+ if (tsconfigPaths) {
714
+ if (resolveTsconfigAlias(imp.source, tsconfigPaths, rootDir)) continue;
715
+ }
716
+ let pkgName;
717
+ if (imp.source.startsWith("@")) {
718
+ const scoped = scopedPackageName(imp.source);
719
+ if (!scoped) continue;
720
+ pkgName = scoped;
721
+ } else pkgName = imp.source.split("/")[0];
722
+ if (!knownDeps.has(pkgName)) {
723
+ if ((language === "python" ? new Set([
724
+ "os",
725
+ "sys",
726
+ "json",
727
+ "re",
728
+ "math",
729
+ "datetime",
730
+ "collections",
731
+ "functools",
732
+ "itertools",
733
+ "logging",
734
+ "pathlib",
735
+ "typing",
736
+ "dataclasses",
737
+ "abc",
738
+ "io",
739
+ "hashlib",
740
+ "copy",
741
+ "enum",
742
+ "subprocess",
743
+ "argparse",
744
+ "unittest",
745
+ "asyncio",
746
+ "threading",
747
+ "multiprocessing",
748
+ "http",
749
+ "urllib",
750
+ "socket",
751
+ "struct",
752
+ "csv",
753
+ "sqlite3",
754
+ "random",
755
+ "string",
756
+ "textwrap",
757
+ "tempfile"
758
+ ]) : new Set([
759
+ "fs",
760
+ "path",
761
+ "http",
762
+ "https",
763
+ "url",
764
+ "util",
765
+ "crypto",
766
+ "os",
767
+ "stream",
768
+ "buffer",
769
+ "events",
770
+ "child_process",
771
+ "cluster",
772
+ "dns",
773
+ "net",
774
+ "tls",
775
+ "zlib",
776
+ "assert",
777
+ "async_hooks",
778
+ "perf_hooks",
779
+ "worker_threads",
780
+ "readline",
781
+ "vm",
782
+ "module",
783
+ "process",
784
+ "timers",
785
+ "dgram",
786
+ "fs/promises",
787
+ "node:fs",
788
+ "node:path",
789
+ "node:http",
790
+ "node:https",
791
+ "node:url",
792
+ "node:util",
793
+ "node:crypto",
794
+ "node:os",
795
+ "node:stream",
796
+ "node:buffer",
797
+ "node:events",
798
+ "node:child_process",
799
+ "node:fs/promises",
800
+ "node:perf_hooks",
801
+ "node:assert"
802
+ ])).has(pkgName)) continue;
803
+ if (pkgName === "typescript" && imp.isTypeOnly) continue;
804
+ const line = lines.find((l) => l.num === imp.line);
805
+ const col = line ? line.text.indexOf(imp.source) + 1 : 1;
806
+ results.push(diag({
807
+ filePath,
808
+ rule: "ast-slop/hallucinated-import",
809
+ severity: "error",
810
+ message: `Import "${imp.source}" not found in project dependencies`,
811
+ help: `Package "${pkgName}" is not listed in package.json/requirements.txt. This may be a hallucinated import. Install it (npm install ${pkgName}) or remove the import if it was incorrectly generated.`,
812
+ line: imp.line,
813
+ column: col,
814
+ fixable: true,
815
+ suggestion: {
816
+ type: "delete",
817
+ text: "",
818
+ range: {
819
+ startLine: imp.line,
820
+ startCol: 1,
821
+ endLine: imp.line,
822
+ endCol: (line?.text.length ?? 80) + 1
823
+ },
824
+ confidence: .8,
825
+ reason: `The imported package "${pkgName}" is not in project dependencies and may not exist.`
826
+ },
827
+ detail: {
828
+ importSource: imp.source,
829
+ packageName: pkgName
830
+ }
831
+ }));
832
+ }
833
+ }
834
+ return results;
835
+ }
836
+ function detectUnnecessaryAbstraction(content, lines, filePath, language) {
837
+ const results = [];
838
+ if (language === "python") return results;
839
+ const interfaceRe = /^\s*(?:export\s+)?interface\s+(\w+)/;
840
+ const abstractClassRe = /^\s*(?:export\s+)?abstract\s+class\s+(\w+)/;
841
+ const implementsRe = /\bimplements\s+(\w+)/;
842
+ const extendsRe = /\bextends\s+(\w+)/;
843
+ const interfaces = /* @__PURE__ */ new Map();
844
+ const abstractClasses = /* @__PURE__ */ new Map();
845
+ const implementors = /* @__PURE__ */ new Map();
846
+ const subclasses = /* @__PURE__ */ new Map();
847
+ for (const { num, text } of lines) {
848
+ const trimmed = text.trim();
849
+ const ifaceMatch = trimmed.match(interfaceRe);
850
+ if (ifaceMatch) interfaces.set(ifaceMatch[1], {
851
+ line: num,
852
+ col: text.indexOf(ifaceMatch[1]) + 1
853
+ });
854
+ const absMatch = trimmed.match(abstractClassRe);
855
+ if (absMatch) abstractClasses.set(absMatch[1], {
856
+ line: num,
857
+ col: text.indexOf(absMatch[1]) + 1
858
+ });
859
+ const implMatch = trimmed.match(implementsRe);
860
+ if (implMatch) implementors.set(implMatch[1], (implementors.get(implMatch[1]) ?? 0) + 1);
861
+ const extMatch = trimmed.match(extendsRe);
862
+ if (extMatch) {
863
+ if (abstractClasses.has(extMatch[1])) subclasses.set(extMatch[1], (subclasses.get(extMatch[1]) ?? 0) + 1);
864
+ }
865
+ }
866
+ for (const [name, pos] of interfaces) {
867
+ const count = implementors.get(name) ?? 0;
868
+ if (count === 1) results.push(diag({
869
+ filePath,
870
+ rule: "ast-slop/unnecessary-abstraction",
871
+ severity: "info",
872
+ message: `Interface "${name}" has only 1 implementor — unnecessary abstraction`,
873
+ help: "Consider removing the interface and using the concrete type directly, or add more implementors to justify the abstraction layer.",
874
+ line: pos.line,
875
+ column: pos.col,
876
+ fixable: false,
877
+ detail: {
878
+ interfaceName: name,
879
+ implementorCount: count
880
+ }
881
+ }));
882
+ }
883
+ for (const [name, pos] of abstractClasses) {
884
+ const count = subclasses.get(name) ?? 0;
885
+ if (count === 1) results.push(diag({
886
+ filePath,
887
+ rule: "ast-slop/unnecessary-abstraction",
888
+ severity: "info",
889
+ message: `Abstract class "${name}" has only 1 subclass — unnecessary abstraction`,
890
+ help: "Consider removing the abstract class and using the concrete type directly, or add more subclasses to justify the abstraction layer.",
891
+ line: pos.line,
892
+ column: pos.col,
893
+ fixable: false,
894
+ detail: {
895
+ abstractClassName: name,
896
+ subclassCount: count
897
+ }
898
+ }));
899
+ }
900
+ return results;
901
+ }
902
+ function detectSilentRecovery(lines, filePath, language) {
903
+ const results = [];
904
+ for (let i = 0; i < lines.length; i++) {
905
+ const { num, text } = lines[i];
906
+ const trimmed = text.trim();
907
+ if (language === "python") {
908
+ if (trimmed.match(/^except\s*(?:\w+(?:\s+as\s+\w+)?)?\s*:/)) {
909
+ let hasCode = false;
910
+ let hasComment = false;
911
+ for (let j = i + 1; j < Math.min(i + 8, lines.length); j++) {
912
+ const nextTrimmed = lines[j].text.trim();
913
+ if (nextTrimmed === "") continue;
914
+ if (lines[j].text.length - lines[j].text.trimStart().length <= text.length - text.trimStart().length && nextTrimmed.length > 0) break;
915
+ if (nextTrimmed.startsWith("#")) hasComment = true;
916
+ else if (nextTrimmed !== "pass" && nextTrimmed !== "...") {
917
+ hasCode = true;
918
+ break;
919
+ }
920
+ }
921
+ if (hasComment && !hasCode) {
922
+ const col = text.indexOf("except") + 1;
923
+ results.push(diag({
924
+ filePath,
925
+ rule: "ast-slop/silent-recovery",
926
+ severity: "info",
927
+ message: "Silent recovery: except block contains only comments — errors are neither logged nor rethrown",
928
+ help: "At minimum, log the error or rethrow. Comment-only catch blocks silently swallow errors while appearing to handle them.",
929
+ line: num,
930
+ column: col,
931
+ fixable: true,
932
+ suggestion: {
933
+ type: "insert",
934
+ text: " logger.error(f'Unexpected error: {e}', exc_info=True)",
935
+ range: {
936
+ startLine: num + 1,
937
+ startCol: 1,
938
+ endLine: num + 1,
939
+ endCol: 1
940
+ },
941
+ confidence: .7,
942
+ reason: "Replace comment-only catch body with error logging to avoid silently hiding failures."
943
+ }
944
+ }));
945
+ }
946
+ }
947
+ } else {
948
+ const catchStartMatch = trimmed.match(/catch\s*(?:\(\s*(\w+)\s*\))?\s*\{\s*$/);
949
+ if (catchStartMatch) {
950
+ let hasCode = false;
951
+ let hasCommentOnly = false;
952
+ for (let j = i + 1; j < Math.min(i + 8, lines.length); j++) {
953
+ const nextTrimmed = lines[j].text.trim();
954
+ if (nextTrimmed === "}") {
955
+ if (hasCommentOnly && !hasCode) {
956
+ const col = text.indexOf("catch") + 1;
957
+ results.push(diag({
958
+ filePath,
959
+ rule: "ast-slop/silent-recovery",
960
+ severity: "info",
961
+ message: "Silent recovery: catch block contains only comments — errors are neither logged nor rethrown",
962
+ help: "At minimum, log the error or rethrow. Comment-only catch blocks silently swallow errors while appearing to handle them.",
963
+ line: num,
964
+ column: col,
965
+ fixable: true,
966
+ suggestion: {
967
+ type: "refactor",
968
+ text: `catch (${catchStartMatch[1] ?? "error"}) { console.error(${catchStartMatch[1] ?? "error"}); }`,
969
+ confidence: .7,
970
+ reason: "Replace comment-only catch body with error logging to avoid silently hiding failures."
971
+ }
972
+ }));
973
+ }
974
+ break;
975
+ }
976
+ if (nextTrimmed === "") continue;
977
+ if (nextTrimmed.startsWith("//") || nextTrimmed.startsWith("/*") || nextTrimmed.startsWith("*")) hasCommentOnly = true;
978
+ else {
979
+ hasCode = true;
980
+ break;
981
+ }
982
+ }
983
+ }
984
+ }
985
+ }
986
+ return results;
987
+ }
988
+ function detectHardcodedConfig(lines, filePath, language) {
989
+ const results = [];
990
+ if (/[/\\](?:config|conf|settings|env)[/\\]/i.test(filePath)) return results;
991
+ if (/[/\\]\.env/i.test(filePath)) return results;
992
+ const urlRe = /['"`]((?:https?:\/\/)[^'"`\s]+)['"`]/;
993
+ const portRe = /:(\d{4,5})\b/;
994
+ for (const { num, text } of lines) {
995
+ const trimmed = text.trim();
996
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("/*") || /^\s*import\b/.test(trimmed)) continue;
997
+ const urlMatch = trimmed.match(urlRe);
998
+ if (urlMatch) {
999
+ const url = urlMatch[1];
1000
+ if (/localhost|127\.0\.0\.1|0\.0\.0\.0|example\.com|example\.org/i.test(url)) continue;
1001
+ if (/\.test\.|\.spec\.|__tests__|test-utils/i.test(filePath)) continue;
1002
+ const col = text.indexOf(urlMatch[0]) + 1;
1003
+ results.push(diag({
1004
+ filePath,
1005
+ rule: "ast-slop/hardcoded-config",
1006
+ severity: "warning",
1007
+ message: `Hardcoded URL: "${url}" — should be in config/environment`,
1008
+ help: "Move URLs to environment variables or a config file. Hardcoded URLs make deployment across environments error-prone.",
1009
+ line: num,
1010
+ column: col,
1011
+ fixable: false,
1012
+ detail: { url }
1013
+ }));
1014
+ }
1015
+ const portMatch = trimmed.match(portRe);
1016
+ if (portMatch) {
1017
+ const port = portMatch[1];
1018
+ if (/\bcase\s+\d+:/i.test(trimmed)) continue;
1019
+ if (/\bport\b/i.test(trimmed) && !/process\.env/.test(trimmed)) {
1020
+ const col = text.indexOf(portMatch[0]) + 1;
1021
+ results.push(diag({
1022
+ filePath,
1023
+ rule: "ast-slop/hardcoded-config",
1024
+ severity: "info",
1025
+ message: `Hardcoded port: ${port} — should be in config/environment`,
1026
+ help: "Move port numbers to environment variables or a config file. Hardcoded ports make deployment across environments error-prone.",
1027
+ line: num,
1028
+ column: col,
1029
+ fixable: false,
1030
+ detail: { port }
1031
+ }));
1032
+ }
1033
+ }
1034
+ }
1035
+ return results;
1036
+ }
1037
+ const META_COMMENT_PATTERNS = [
1038
+ /\/\/\s*Generated\s+by/i,
1039
+ /\/\/\s*AI[- ]?assisted/i,
1040
+ /\/\/\s*Created\s+with\s+(?:Claude|GPT|ChatGPT|Copilot|Gemini|AI)/i,
1041
+ /\/\/\s*Auto[- ]?generated/i,
1042
+ /\/\/\s*Written\s+by\s+(?:Claude|GPT|ChatGPT|Copilot|Gemini|AI)/i,
1043
+ /\/\*\s*Generated\s+by/i,
1044
+ /\/\*\s*AI[- ]?assisted/i,
1045
+ /\/\*\s*Auto[- ]?generated/i,
1046
+ /#\s*Generated\s+by/i,
1047
+ /#\s*AI[- ]?assisted/i,
1048
+ /#\s*Auto[- ]?generated/i,
1049
+ /#\s*Created\s+with\s+(?:Claude|GPT|ChatGPT|Copilot|Gemini|AI)/i
1050
+ ];
1051
+ function detectMetaComments(lines, filePath) {
1052
+ const results = [];
1053
+ for (const { num, text } of lines) {
1054
+ const trimmed = text.trim();
1055
+ for (const pattern of META_COMMENT_PATTERNS) if (pattern.test(trimmed)) {
1056
+ const col = text.indexOf(trimmed.charAt(0)) + 1;
1057
+ const label = trimmed.match(/(Generated by|AI.assisted|Created with|Auto.generated|Written by)/i)?.[1] ?? "AI attribution";
1058
+ results.push(diag({
1059
+ filePath,
1060
+ rule: "ast-slop/meta-comment",
1061
+ severity: "info",
1062
+ message: `AI attribution comment: "${label}" — metadata that should not be in committed code`,
1063
+ help: "Remove AI attribution comments. Use git history or .ai-metadata files if tracking is needed. These comments add noise and reveal tooling choices.",
1064
+ line: num,
1065
+ column: col,
1066
+ fixable: true,
1067
+ suggestion: {
1068
+ type: "delete",
1069
+ text: "",
1070
+ range: {
1071
+ startLine: num,
1072
+ startCol: 1,
1073
+ endLine: num,
1074
+ endCol: text.length + 1
1075
+ },
1076
+ confidence: .95,
1077
+ reason: "AI attribution comments are metadata noise that should not be in committed source code."
1078
+ }
1079
+ }));
1080
+ break;
1081
+ }
1082
+ }
1083
+ return results;
1084
+ }
1085
+ function detectDebugPath(lines, filePath, language) {
1086
+ const results = [];
1087
+ if (/[/\\](?:test|tests|__tests__|spec)[/\\]/i.test(filePath)) return results;
1088
+ if (/\.(?:test|spec)\.(?:ts|tsx|js|jsx|py)$/.test(filePath)) return results;
1089
+ for (const { num, text } of lines) {
1090
+ const trimmed = text.trim();
1091
+ if (language !== "python") {
1092
+ if (/\bdebugger\b/.test(trimmed) && !trimmed.startsWith("//")) {
1093
+ const col = text.indexOf("debugger") + 1;
1094
+ results.push(diag({
1095
+ filePath,
1096
+ rule: "ast-slop/debug-path",
1097
+ severity: "warning",
1098
+ message: "debugger statement in production code — will pause execution in devtools",
1099
+ help: "Remove debugger statements before committing. They cause unexpected breakpoints in production.",
1100
+ line: num,
1101
+ column: col,
1102
+ fixable: true,
1103
+ suggestion: {
1104
+ type: "delete",
1105
+ text: "",
1106
+ range: {
1107
+ startLine: num,
1108
+ startCol: 1,
1109
+ endLine: num,
1110
+ endCol: text.length + 1
1111
+ },
1112
+ confidence: .95,
1113
+ reason: "debugger statements should never be in committed code."
1114
+ }
1115
+ }));
1116
+ }
1117
+ if (/process\.env\.DEBUG/.test(trimmed) && !trimmed.startsWith("//")) {
1118
+ const col = text.indexOf("process.env.DEBUG") + 1;
1119
+ results.push(diag({
1120
+ filePath,
1121
+ rule: "ast-slop/debug-path",
1122
+ severity: "info",
1123
+ message: "process.env.DEBUG reference in production code — likely a debug path leak",
1124
+ help: "Use a proper logging library with level configuration instead of raw process.env.DEBUG checks.",
1125
+ line: num,
1126
+ column: col,
1127
+ fixable: false
1128
+ }));
1129
+ }
1130
+ }
1131
+ }
1132
+ return results;
1133
+ }
1134
+ const GENERIC_ALIAS_NAMES = new Set([
1135
+ "data",
1136
+ "result",
1137
+ "item",
1138
+ "value",
1139
+ "info",
1140
+ "obj",
1141
+ "config",
1142
+ "handler",
1143
+ "callback",
1144
+ "util",
1145
+ "utils",
1146
+ "helper",
1147
+ "helpers"
1148
+ ]);
1149
+ function detectSuspiciousAlias(content, lines, filePath, language) {
1150
+ const results = [];
1151
+ if (language === "python") return results;
1152
+ const aliasRe = /import\s+(?:type\s+)?(?:\{[^}]*\}|\*|\w+)\s+as\s+(\w+)/g;
1153
+ let m;
1154
+ while ((m = aliasRe.exec(content)) !== null) {
1155
+ const aliasName = m[1];
1156
+ if (!GENERIC_ALIAS_NAMES.has(aliasName)) continue;
1157
+ const lineNum = (content.slice(0, m.index).match(/\n/g) ?? []).length + 1;
1158
+ const line = lines.find((l) => l.num === lineNum);
1159
+ if (!line) continue;
1160
+ const col = line.text.indexOf(`as ${aliasName}`) + 1;
1161
+ results.push(diag({
1162
+ filePath,
1163
+ rule: "ast-slop/suspicious-alias",
1164
+ severity: "info",
1165
+ message: `Import aliased to generic name "${aliasName}" — obscures the original module intent`,
1166
+ help: `Use a more descriptive alias or keep the original name. Generic aliases like "${aliasName}" lose the semantic meaning of the imported module.`,
1167
+ line: lineNum,
1168
+ column: col,
1169
+ fixable: false,
1170
+ detail: { aliasName }
1171
+ }));
1172
+ }
1173
+ return results;
1174
+ }
1175
+ async function detectWorkspaceMisconfig(rootDir, filePath) {
1176
+ const results = [];
1177
+ if (!filePath.endsWith("package.json")) return results;
1178
+ const { dirname } = await import("node:path");
1179
+ const pkgDir = dirname(filePath);
1180
+ if (pkgDir !== rootDir && pkgDir !== ".") return results;
1181
+ try {
1182
+ const raw = await readFileContent(filePath);
1183
+ const workspaces = JSON.parse(raw).workspaces ?? [];
1184
+ if (workspaces.length === 0) return results;
1185
+ const { access } = await import("node:fs/promises");
1186
+ for (const ws of workspaces) try {
1187
+ await access(join(rootDir, ws));
1188
+ } catch {
1189
+ results.push(diag({
1190
+ filePath,
1191
+ rule: "ast-slop/workspace-misconfig",
1192
+ severity: "warning",
1193
+ message: `Workspace "${ws}" does not exist — package.json points to missing directory`,
1194
+ help: "Remove the workspace entry or create the directory with a package.json. Stale workspace references cause confusing build errors.",
1195
+ line: 1,
1196
+ column: 1,
1197
+ fixable: false,
1198
+ detail: { workspace: ws }
1199
+ }));
1200
+ }
1201
+ } catch {}
1202
+ return results;
1203
+ }
1204
+ function detectOverdefensiveType(lines, filePath, language) {
1205
+ const results = [];
1206
+ if (language !== "typescript") return results;
1207
+ const typedVars = /* @__PURE__ */ new Map();
1208
+ const typeAnnotationRe = /(?:const|let|var)\s+(\w+)\s*:\s*([^=;]+)/;
1209
+ for (const { num, text } of lines) {
1210
+ const typeMatch = text.trim().match(typeAnnotationRe);
1211
+ if (typeMatch) {
1212
+ const varName = typeMatch[1];
1213
+ const typeAnn = typeMatch[2].trim().replace(/\s+/g, " ");
1214
+ typedVars.set(varName, typeAnn);
1215
+ }
1216
+ }
1217
+ for (const { num, text } of lines) {
1218
+ const trimmed = text.trim();
1219
+ const typeofGuardRe = /typeof\s+(\w+)\s*===?\s*['"](string|number|boolean|object|function|symbol|bigint|undefined)['"]/g;
1220
+ let m;
1221
+ while ((m = typeofGuardRe.exec(trimmed)) !== null) {
1222
+ const varName = m[1];
1223
+ const typeCheck = m[2];
1224
+ const declaredType = typedVars.get(varName);
1225
+ if (declaredType) {
1226
+ if (typeCheck === "string" && /string/.test(declaredType) && !/\|/.test(declaredType) && !/any/.test(declaredType) || typeCheck === "number" && /number/.test(declaredType) && !/\|/.test(declaredType) && !/any/.test(declaredType) || typeCheck === "boolean" && /boolean/.test(declaredType) && !/\|/.test(declaredType) && !/any/.test(declaredType)) {
1227
+ const col = text.indexOf("typeof") + 1;
1228
+ results.push(diag({
1229
+ filePath,
1230
+ rule: "ast-slop/overdefensive-type",
1231
+ severity: "info",
1232
+ message: `typeof ${varName} === '${typeCheck}' is redundant — ${varName} is already typed as ${declaredType}`,
1233
+ help: "Remove redundant type guards on already-typed values. TypeScript enforces these types at compile time. Only use typeof checks for union types or unknown values.",
1234
+ line: num,
1235
+ column: col,
1236
+ fixable: true,
1237
+ suggestion: {
1238
+ type: "refactor",
1239
+ text: `${varName} != null`,
1240
+ confidence: .6,
1241
+ reason: `The variable ${varName} is already typed as ${declaredType}, making the typeof check redundant.`
1242
+ }
1243
+ }));
1244
+ }
1245
+ }
1246
+ }
1247
+ }
1248
+ return results;
1249
+ }
1250
+ function detectPlaceholderImpl(lines, filePath, language) {
1251
+ const results = [];
1252
+ const funcStartRe = language === "python" ? /^\s*def\s+(\w+)\s*\(/ : /^\s*(?:(?:export|public|private|protected|static|async)\s+)*(?:function\s+)?(\w+)\s*[^;]*\{\s*$/;
1253
+ const arrowFuncRe = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>\s*\{\s*$/;
1254
+ for (let i = 0; i < lines.length; i++) {
1255
+ const { num, text } = lines[i];
1256
+ const trimmed = text.trim();
1257
+ const funcMatch = trimmed.match(funcStartRe);
1258
+ const arrowMatch = trimmed.match(arrowFuncRe);
1259
+ const funcName = funcMatch?.[1] ?? arrowMatch?.[1];
1260
+ if (!funcName) continue;
1261
+ if ([
1262
+ "constructor",
1263
+ "render",
1264
+ "mount",
1265
+ "unmount"
1266
+ ].includes(funcName)) continue;
1267
+ let hasTodo = /\b(?:TODO|FIXME)\b/i.test(trimmed);
1268
+ if (language === "python") for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
1269
+ const nextTrimmed = lines[j].text.trim();
1270
+ if (lines[j].text.length - lines[j].text.trimStart().length <= text.length - text.trimStart().length && nextTrimmed.length > 0) break;
1271
+ if (nextTrimmed.startsWith("#")) {
1272
+ if (/\b(?:TODO|FIXME)\b/i.test(nextTrimmed)) hasTodo = true;
1273
+ continue;
1274
+ }
1275
+ if (nextTrimmed === "pass" || nextTrimmed === "..." || /^return\s+(None|null|0|''|\"\"|False)?\s*$/.test(nextTrimmed)) {
1276
+ if (hasTodo) {
1277
+ const col = text.indexOf(funcName) + 1;
1278
+ results.push(diag({
1279
+ filePath,
1280
+ rule: "ast-slop/placeholder-impl",
1281
+ severity: "warning",
1282
+ message: `Placeholder implementation: "${funcName}" has TODO/FIXME but only returns a stub value`,
1283
+ help: "Implement the function or explicitly throw NotImplementedError. Placeholder stubs with TODOs are a hallmark of incomplete AI-generated code.",
1284
+ line: num,
1285
+ column: col,
1286
+ fixable: false,
1287
+ detail: { functionName: funcName }
1288
+ }));
1289
+ }
1290
+ break;
1291
+ }
1292
+ break;
1293
+ }
1294
+ else {
1295
+ let hasComment = false;
1296
+ for (let j = i + 1; j < Math.min(i + 10, lines.length); j++) {
1297
+ const nextTrimmed = lines[j].text.trim();
1298
+ if (nextTrimmed === "}") {
1299
+ if (hasTodo && hasComment) {
1300
+ const col = text.indexOf(funcName) + 1;
1301
+ results.push(diag({
1302
+ filePath,
1303
+ rule: "ast-slop/placeholder-impl",
1304
+ severity: "warning",
1305
+ message: `Placeholder implementation: "${funcName}" has TODO/FIXME but only returns a stub value`,
1306
+ help: "Implement the function or explicitly throw new Error('Not implemented'). Placeholder stubs with TODOs are a hallmark of incomplete AI-generated code.",
1307
+ line: num,
1308
+ column: col,
1309
+ fixable: false,
1310
+ detail: { functionName: funcName }
1311
+ }));
1312
+ }
1313
+ break;
1314
+ }
1315
+ if (nextTrimmed === "") continue;
1316
+ if (nextTrimmed.startsWith("//") || nextTrimmed.startsWith("/*")) {
1317
+ hasComment = true;
1318
+ if (/\b(?:TODO|FIXME)\b/i.test(nextTrimmed)) hasTodo = true;
1319
+ continue;
1320
+ }
1321
+ if (/^return\s+(null|undefined|0|''|\"\"|void\s+0)?\s*;?\s*$/.test(nextTrimmed)) {
1322
+ if (hasTodo) {
1323
+ const col = text.indexOf(funcName) + 1;
1324
+ results.push(diag({
1325
+ filePath,
1326
+ rule: "ast-slop/placeholder-impl",
1327
+ severity: "warning",
1328
+ message: `Placeholder implementation: "${funcName}" has TODO/FIXME but only returns a stub value`,
1329
+ help: "Implement the function or explicitly throw new Error('Not implemented'). Placeholder stubs with TODOs are a hallmark of incomplete AI-generated code.",
1330
+ line: num,
1331
+ column: col,
1332
+ fixable: false,
1333
+ detail: { functionName: funcName }
1334
+ }));
1335
+ }
1336
+ break;
1337
+ }
1338
+ break;
1339
+ }
1340
+ }
1341
+ }
1342
+ return results;
1343
+ }
1344
+ function detectCopyPasteSignature(content, lines, filePath, language) {
1345
+ const results = [];
1346
+ if (language === "python") return results;
1347
+ const funcSigRe = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
1348
+ const signatures = [];
1349
+ let m;
1350
+ while ((m = funcSigRe.exec(content)) !== null) {
1351
+ const name = m[1];
1352
+ const rawParams = m[2];
1353
+ const paramTypes = normalizeParamTypes(rawParams);
1354
+ const lineNum = (content.slice(0, m.index).match(/\n/g) ?? []).length + 1;
1355
+ const line = lines.find((l) => l.num === lineNum);
1356
+ if (!line) continue;
1357
+ const col = line.text.indexOf(name) + 1;
1358
+ signatures.push({
1359
+ name,
1360
+ paramTypes,
1361
+ line: lineNum,
1362
+ col
1363
+ });
1364
+ }
1365
+ const byParamTypes = /* @__PURE__ */ new Map();
1366
+ for (const sig of signatures) {
1367
+ if (sig.paramTypes.length < 4) continue;
1368
+ const list = byParamTypes.get(sig.paramTypes) ?? [];
1369
+ list.push(sig);
1370
+ byParamTypes.set(sig.paramTypes, list);
1371
+ }
1372
+ for (const [, group] of byParamTypes) {
1373
+ const uniqueNames = new Set(group.map((s) => s.name));
1374
+ if (group.length < 2 || uniqueNames.size < 2) continue;
1375
+ for (let i = 0; i < group.length; i++) for (let j = i + 1; j < group.length; j++) {
1376
+ const a = group[i];
1377
+ const b = group[j];
1378
+ if (a.name === b.name) continue;
1379
+ results.push(diag({
1380
+ filePath,
1381
+ rule: "ast-slop/copy-paste-signature",
1382
+ severity: "info",
1383
+ message: `Functions "${a.name}" and "${b.name}" have identical parameter type signatures — likely copy-paste`,
1384
+ help: "If these functions serve different purposes, differentiate their signatures. If they share logic, extract the common implementation into a shared utility.",
1385
+ line: a.line,
1386
+ column: a.col,
1387
+ fixable: false,
1388
+ detail: {
1389
+ functionA: a.name,
1390
+ functionB: b.name,
1391
+ paramTypes: a.paramTypes
1392
+ }
1393
+ }));
1394
+ }
1395
+ }
1396
+ return results;
1397
+ }
1398
+ /** Normalize parameter types: strip names, keep only type annotations */
1399
+ function normalizeParamTypes(rawParams) {
1400
+ return rawParams.split(",").map((p) => {
1401
+ const trimmed = p.trim();
1402
+ if (!trimmed) return "";
1403
+ const typeMatch = trimmed.match(/\??\s*:\s*([^=]+)/);
1404
+ if (typeMatch) return typeMatch[1].trim();
1405
+ return trimmed.replace(/\?.*$/, "").trim();
1406
+ }).filter((t) => t.length > 0).join(", ");
1407
+ }
1408
+ /**
1409
+ * AST-enhanced empty catch detection.
1410
+ * Uses tree-sitter to find catch_clause nodes and checks if their body is empty.
1411
+ * More accurate than regex — catches multi-line empty catches, nested catches, etc.
1412
+ */
1413
+ function detectEmptyCatchAST(root, filePath) {
1414
+ const results = [];
1415
+ const catchNodes = findNodesOfType(root, "catch_clause");
1416
+ for (const catchNode of catchNodes) if (isCatchBodyEmpty(catchNode)) {
1417
+ const line = catchNode.startRow + 1;
1418
+ const col = catchNode.startCol + 1;
1419
+ const catchVar = catchNode.children.find((c) => c.type === "identifier" || c.fieldName === "parameter")?.text ?? "error";
1420
+ results.push(diag({
1421
+ filePath,
1422
+ rule: "ast-slop/swallowed-exception",
1423
+ severity: "info",
1424
+ message: "Swallowed exception: empty catch block (AST-confirmed)",
1425
+ help: "Handle the error (log, rethrow, or recover). Empty catch blocks silently swallow errors, making bugs invisible.",
1426
+ line,
1427
+ column: col,
1428
+ fixable: true,
1429
+ suggestion: {
1430
+ type: "insert",
1431
+ text: ` console.error(${catchVar});`,
1432
+ range: {
1433
+ startLine: line + 1,
1434
+ startCol: 1,
1435
+ endLine: line + 1,
1436
+ endCol: 1
1437
+ },
1438
+ confidence: .85,
1439
+ reason: "AST analysis confirms this catch block is empty. Add at least error logging."
1440
+ },
1441
+ detail: {
1442
+ astConfirmed: true,
1443
+ catchVariable: catchVar
1444
+ }
1445
+ }));
1446
+ }
1447
+ return results;
1448
+ }
1449
+ /**
1450
+ * AST-enhanced `as any` detection.
1451
+ * Uses tree-sitter to find as_expression nodes, checks their type,
1452
+ * and only flags `as any` outside catch/ORM/JSON contexts.
1453
+ * Suppresses false positives where `as any` is acceptable.
1454
+ */
1455
+ function detectAsAnyAST(root, filePath) {
1456
+ const results = [];
1457
+ const asExpressions = findNodesOfType(root, "as_expression");
1458
+ for (const asNode of asExpressions) {
1459
+ if (getAsExpressionType(asNode) !== "any") continue;
1460
+ const context = getAsExpressionContext(asNode);
1461
+ if (context === "catch") continue;
1462
+ if (context === "orm") continue;
1463
+ if (context === "json") continue;
1464
+ const line = asNode.startRow + 1;
1465
+ const col = asNode.startCol + 1;
1466
+ results.push(diag({
1467
+ filePath,
1468
+ rule: "ast-slop/as-any",
1469
+ severity: "warning",
1470
+ message: `Unsafe cast: as any — opts out of type checking entirely (AST-confirmed, not in catch/ORM/JSON context)`,
1471
+ help: "Replace `as any` with a more specific type, a type guard, or `as unknown as SpecificType` if truly needed.",
1472
+ line,
1473
+ column: col,
1474
+ fixable: true,
1475
+ suggestion: {
1476
+ type: "refactor",
1477
+ text: "/* replace with specific type */",
1478
+ confidence: .6,
1479
+ reason: "AST analysis confirms `as any` outside acceptable catch/ORM/JSON contexts."
1480
+ },
1481
+ detail: {
1482
+ astConfirmed: true,
1483
+ expressionContext: context
1484
+ }
1485
+ }));
1486
+ }
1487
+ return results;
1488
+ }
1489
+ /**
1490
+ * AST-enhanced import analysis.
1491
+ * Uses tree-sitter to extract import info with full accuracy,
1492
+ * enabling detection of type-only barrel imports and re-exports
1493
+ * that regex can't reliably identify.
1494
+ */
1495
+ function detectBarrelImportsAST(root, filePath) {
1496
+ const results = [];
1497
+ const importNodes = findNodesOfType(root, "import_statement");
1498
+ const importDeclNodes = findNodesOfType(root, "import_declaration");
1499
+ const allImports = [...importNodes, ...importDeclNodes];
1500
+ for (const impNode of allImports) {
1501
+ const info = extractImportFromNode(impNode);
1502
+ if (!info) continue;
1503
+ if (info.isTypeOnly && isBarrelSource(info.source)) {
1504
+ const line = info.line;
1505
+ results.push(diag({
1506
+ filePath,
1507
+ rule: "ast-slop/barrel-type-import",
1508
+ severity: "info",
1509
+ message: `Type-only import from barrel file "${info.source}" — consider importing directly from the source module`,
1510
+ help: "Import types directly from their source module instead of through a barrel (index) file. This reduces coupling and improves tree-shaking.",
1511
+ line,
1512
+ column: 1,
1513
+ fixable: false,
1514
+ detail: {
1515
+ astConfirmed: true,
1516
+ source: info.source,
1517
+ symbols: info.symbols,
1518
+ isTypeOnly: info.isTypeOnly
1519
+ }
1520
+ }));
1521
+ }
1522
+ if (impNode.text.includes("import *") && isBarrelSource(info.source)) {
1523
+ const line = info.line;
1524
+ results.push(diag({
1525
+ filePath,
1526
+ rule: "ast-slop/barrel-wildcard-import",
1527
+ severity: "info",
1528
+ message: `Wildcard import from barrel file "${info.source}" — imports more than needed`,
1529
+ help: "Use named imports instead of wildcard imports from barrel files. This improves tree-shaking and makes dependencies explicit.",
1530
+ line,
1531
+ column: 1,
1532
+ fixable: false,
1533
+ detail: {
1534
+ astConfirmed: true,
1535
+ source: info.source
1536
+ }
1537
+ }));
1538
+ }
1539
+ }
1540
+ return results;
1541
+ }
1542
+ /** Check if an import source looks like a barrel file */
1543
+ function isBarrelSource(source) {
1544
+ return /(?:^|\/)(index|src|lib|utils|helpers|types|models|services|components)(\/)?$/.test(source);
1545
+ }
1546
+ /**
1547
+ * AST-enhanced console.log suppression.
1548
+ * Finds console.log/debug calls and checks if they're inside catch blocks.
1549
+ * Suppresses false positives that regex-based detection misses.
1550
+ */
1551
+ function detectConsoleLeftoversAST(root, filePath) {
1552
+ const results = [];
1553
+ if (/[/__]tests?[/__]/i.test(filePath)) return results;
1554
+ if (/\.test\.(?:ts|tsx|js|jsx)$/.test(filePath)) return results;
1555
+ if (/\.spec\.(?:ts|tsx|js|jsx)$/.test(filePath)) return results;
1556
+ const callExprs = findNodesOfType(root, "call_expression");
1557
+ for (const callNode of callExprs) {
1558
+ const text = callNode.text;
1559
+ const logMatch = text.match(/^console\.(log|debug)\s*\(/);
1560
+ if (!logMatch) continue;
1561
+ if (isInsideCatch(callNode)) continue;
1562
+ const line = callNode.startRow + 1;
1563
+ const col = callNode.startCol + 1;
1564
+ results.push(diag({
1565
+ filePath,
1566
+ rule: "ast-slop/console-leftover",
1567
+ severity: "suggestion",
1568
+ message: `console.${logMatch[1]}() leftover — likely debugging artifact (AST-confirmed, not in catch block)`,
1569
+ help: "Remove debug logging before committing. Use a proper logging library for production, or guard with environment checks.",
1570
+ line,
1571
+ column: col,
1572
+ fixable: true,
1573
+ suggestion: {
1574
+ type: "delete",
1575
+ text: "",
1576
+ range: {
1577
+ startLine: line,
1578
+ startCol: 1,
1579
+ endLine: line,
1580
+ endCol: text.length + 1
1581
+ },
1582
+ confidence: .9,
1583
+ reason: "AST analysis confirms this console.log is not inside a catch block — it is a debugging artifact."
1584
+ },
1585
+ detail: { astConfirmed: true }
1586
+ }));
1587
+ }
1588
+ return results;
1589
+ }
1590
+ /**
1591
+ * AST-enhanced double assertion detection.
1592
+ * Uses tree-sitter to find chained `as unknown as X` patterns
1593
+ * and verify they are actual double assertions (not in test assertions).
1594
+ */
1595
+ function detectDoubleAssertionAST(root, filePath) {
1596
+ const results = [];
1597
+ const asExpressions = findNodesOfType(root, "as_expression");
1598
+ for (const asNode of asExpressions) {
1599
+ const type = getAsExpressionType(asNode);
1600
+ const innerAs = asNode.children.find((c) => c.type === "as_expression");
1601
+ if (!innerAs) continue;
1602
+ if (getAsExpressionType(innerAs) !== "unknown") continue;
1603
+ const line = asNode.startRow + 1;
1604
+ const col = asNode.startCol + 1;
1605
+ results.push(diag({
1606
+ filePath,
1607
+ rule: "ast-slop/double-assertion",
1608
+ severity: "warning",
1609
+ message: `Double type assertion: as unknown as ${type ?? "unknown"} — bypasses type safety (AST-confirmed)`,
1610
+ help: "Use a proper type guard, type predicate, or adjust the source/target types. Double assertions defeat the purpose of TypeScript.",
1611
+ line,
1612
+ column: col,
1613
+ fixable: true,
1614
+ suggestion: {
1615
+ type: "refactor",
1616
+ text: `as ${type ?? "SpecificType"}`,
1617
+ confidence: .5,
1618
+ reason: "AST analysis confirms this is a double assertion bypassing the type system."
1619
+ },
1620
+ detail: {
1621
+ astConfirmed: true,
1622
+ targetType: type
1623
+ }
1624
+ }));
1625
+ }
1626
+ return results;
1627
+ }
1628
+ /** Deduplicate diagnostics: prefer AST-confirmed over regex on same line+rule */
1629
+ function dedupDiagnostics(diagnostics) {
1630
+ const byKey = /* @__PURE__ */ new Map();
1631
+ for (const d of diagnostics) {
1632
+ const key = `${d.filePath}::${d.rule}::${d.line}`;
1633
+ const list = byKey.get(key) ?? [];
1634
+ list.push(d);
1635
+ byKey.set(key, list);
1636
+ }
1637
+ const results = [];
1638
+ for (const [, group] of byKey) {
1639
+ if (group.length === 1) {
1640
+ results.push(group[0]);
1641
+ continue;
1642
+ }
1643
+ const astConfirmed = group.find((d) => d.detail?.astConfirmed === true);
1644
+ if (astConfirmed) results.push(astConfirmed);
1645
+ else results.push(group[0]);
1646
+ }
1647
+ return results;
1648
+ }
1649
+ /** Detect Python AI patterns using tree-sitter AST */
1650
+ function detectPythonAIPatternsAST(root, filePath) {
1651
+ const diagnostics = [];
1652
+ const patterns = detectPythonAIPatterns(root);
1653
+ for (const p of patterns) {
1654
+ const rule = p.type === "python-stub-function" ? "ast-slop/placeholder-impl" : p.type === "python-bare-except" ? "ast-slop/swallowed-exception" : p.type === "python-todo-stub" ? "ast-slop/todo-stub" : p.type === "python-print-leftover" ? "ast-slop/console-leftover" : "ast-slop/placeholder-impl";
1655
+ const severity = p.type === "python-bare-except" ? "warning" : p.type === "python-print-leftover" ? "warning" : p.type === "python-stub-function" ? "warning" : "info";
1656
+ diagnostics.push(diag({
1657
+ filePath,
1658
+ rule,
1659
+ severity,
1660
+ message: p.message,
1661
+ help: p.type === "python-stub-function" ? "Replace stub with actual implementation or use abc.ABC for abstract methods." : p.type === "python-bare-except" ? "Catch specific exceptions instead of bare except. Use `except Exception as e:` at minimum." : p.type === "python-todo-stub" ? "Resolve the TODO/FIXME or remove the comment." : "Remove debug print() statements before committing.",
1662
+ line: p.line,
1663
+ column: 1,
1664
+ fixable: p.type === "python-print-leftover" || p.type === "python-stub-function",
1665
+ detail: {
1666
+ astConfirmed: true,
1667
+ patternType: p.type
1668
+ }
1669
+ }));
1670
+ }
1671
+ const classes = findPythonClasses(root);
1672
+ for (const cls of classes) if (cls.methods.length === 0 && cls.bases.length === 0) diagnostics.push(diag({
1673
+ filePath,
1674
+ rule: "ast-slop/placeholder-impl",
1675
+ severity: "info",
1676
+ message: `Class '${cls.name}' has no methods or base classes — likely a placeholder`,
1677
+ help: "Implement the class, inherit from a base, or mark it as abstract with abc.ABC.",
1678
+ line: cls.line,
1679
+ column: 1,
1680
+ fixable: false,
1681
+ detail: {
1682
+ astConfirmed: true,
1683
+ patternType: "python-empty-class"
1684
+ }
1685
+ }));
1686
+ const imports = findPythonImports(root);
1687
+ for (const imp of imports) if (imp.symbols.includes("*")) diagnostics.push(diag({
1688
+ filePath,
1689
+ rule: "ast-slop/placeholder-impl",
1690
+ severity: "info",
1691
+ message: `Wildcard import from '${imp.module}' — pollutes namespace`,
1692
+ help: "Import only the specific symbols you need: `from module import symbol1, symbol2`",
1693
+ line: imp.line,
1694
+ column: 1,
1695
+ fixable: false,
1696
+ detail: {
1697
+ astConfirmed: true,
1698
+ patternType: "python-wildcard-import"
1699
+ }
1700
+ }));
1701
+ return diagnostics;
1702
+ }
1703
+ async function analyzeFile(filePath, rootDir, knownDeps, tsconfigPaths) {
1704
+ const diagnostics = [];
1705
+ const language = languageFromPath(filePath);
1706
+ if (!language) return diagnostics;
1707
+ if (language !== "typescript" && language !== "javascript" && language !== "python") return diagnostics;
1708
+ let content;
1709
+ try {
1710
+ content = await readFileContent(filePath);
1711
+ } catch {
1712
+ return diagnostics;
1713
+ }
1714
+ const lines = toLines(content);
1715
+ const relPath = relative(rootDir, filePath);
1716
+ diagnostics.push(...detectNarrativeComments(lines, relPath, language));
1717
+ diagnostics.push(...detectDecorativeComments(lines, relPath));
1718
+ diagnostics.push(...detectTrivialComments(lines, relPath, language));
1719
+ diagnostics.push(...detectConsoleLeftovers(lines, relPath, language));
1720
+ diagnostics.push(...detectTodoStubs(lines, relPath, language));
1721
+ diagnostics.push(...detectGenericNames(lines, relPath, language));
1722
+ diagnostics.push(...detectDefensivePatterns(lines, relPath, language));
1723
+ diagnostics.push(...detectSwallowedExceptions(lines, relPath, language));
1724
+ if (language === "typescript") diagnostics.push(...detectUnsafeCasts(lines, relPath));
1725
+ diagnostics.push(...detectHallucinatedImports(content, lines, relPath, language, knownDeps, tsconfigPaths, rootDir));
1726
+ diagnostics.push(...detectUnnecessaryAbstraction(content, lines, relPath, language));
1727
+ diagnostics.push(...detectSilentRecovery(lines, relPath, language));
1728
+ diagnostics.push(...detectHardcodedConfig(lines, relPath, language));
1729
+ diagnostics.push(...detectMetaComments(lines, relPath));
1730
+ diagnostics.push(...detectDebugPath(lines, relPath, language));
1731
+ diagnostics.push(...detectSuspiciousAlias(content, lines, relPath, language));
1732
+ diagnostics.push(...detectOverdefensiveType(lines, relPath, language));
1733
+ diagnostics.push(...detectPlaceholderImpl(lines, relPath, language));
1734
+ diagnostics.push(...detectCopyPasteSignature(content, lines, relPath, language));
1735
+ if ((language === "typescript" || language === "javascript") && isAvailable()) {
1736
+ const isTsx = tsLangHint(filePath) === "tsx";
1737
+ const astRoot = await parseFile(content, isTsx);
1738
+ if (astRoot) {
1739
+ diagnostics.push(...detectEmptyCatchAST(astRoot, relPath));
1740
+ diagnostics.push(...detectAsAnyAST(astRoot, relPath));
1741
+ diagnostics.push(...detectConsoleLeftoversAST(astRoot, relPath));
1742
+ diagnostics.push(...detectDoubleAssertionAST(astRoot, relPath));
1743
+ diagnostics.push(...detectBarrelImportsAST(astRoot, relPath));
1744
+ }
1745
+ return dedupDiagnostics(diagnostics);
1746
+ }
1747
+ if (language === "python") {
1748
+ if (await initPythonParser() && isPythonAvailable()) {
1749
+ const astRoot = await parsePython(content);
1750
+ if (astRoot) diagnostics.push(...detectPythonAIPatternsAST(astRoot, relPath));
1751
+ return dedupDiagnostics(diagnostics);
1752
+ }
1753
+ }
1754
+ return diagnostics;
1755
+ }
1756
+ const astSlopEngine = {
1757
+ name: "ast-slop",
1758
+ description: "Detects AI-authored code patterns using regex + tree-sitter AST context analysis. Flags narrative comments, decorative blocks, trivial restating comments, debug leftovers, TODO stubs, generic variable names, defensive coding patterns, swallowed exceptions, unsafe type casts, hallucinated imports, unnecessary abstractions, silent recovery, hardcoded config, meta comments, debug paths, suspicious aliases, workspace misconfig, overdefensive types, placeholder implementations, and copy-paste signatures. Tree-sitter AST enhancements suppress false positives and add barrel-import detection when available.",
1759
+ supportedLanguages: [
1760
+ "typescript",
1761
+ "javascript",
1762
+ "python"
1763
+ ],
1764
+ async run(context) {
1765
+ const start = performance.now();
1766
+ await initParser();
1767
+ const files = context.files ?? [];
1768
+ if (files.length === 0) return {
1769
+ engine: "ast-slop",
1770
+ diagnostics: [],
1771
+ elapsed: performance.now() - start,
1772
+ skipped: true,
1773
+ skipReason: "No files to scan (context.files is empty)"
1774
+ };
1775
+ const hasJS = context.languages.includes("typescript") || context.languages.includes("javascript");
1776
+ const hasPython = context.languages.includes("python");
1777
+ let knownDeps = /* @__PURE__ */ new Set();
1778
+ if (hasJS) knownDeps = await loadPackageDeps(context.rootDirectory);
1779
+ if (hasPython) {
1780
+ const pyDeps = await loadPythonDeps(context.rootDirectory);
1781
+ knownDeps = new Set([...knownDeps, ...pyDeps]);
1782
+ }
1783
+ const tsconfigPaths = hasJS ? loadTsconfigPaths(context.rootDirectory) : null;
1784
+ const allDiagnostics = [];
1785
+ const batchSize = 20;
1786
+ for (let i = 0; i < files.length; i += batchSize) {
1787
+ const batch = files.slice(i, i + batchSize);
1788
+ const results = await Promise.all(batch.map((filePath) => analyzeFile(filePath, context.rootDirectory, knownDeps, tsconfigPaths)));
1789
+ for (const diags of results) allDiagnostics.push(...diags);
1790
+ }
1791
+ const pkgJsonPath = join(context.rootDirectory, "package.json");
1792
+ const wsDiags = await detectWorkspaceMisconfig(context.rootDirectory, pkgJsonPath);
1793
+ allDiagnostics.push(...wsDiags);
1794
+ return {
1795
+ engine: "ast-slop",
1796
+ diagnostics: allDiagnostics,
1797
+ elapsed: performance.now() - start,
1798
+ skipped: false
1799
+ };
1800
+ },
1801
+ async fix(diagnostics, context) {
1802
+ const fixableRules = new Set([
1803
+ "ast-slop/narrative-comment",
1804
+ "ast-slop/decorative-comment",
1805
+ "ast-slop/trivial-comment",
1806
+ "ast-slop/console-leftover"
1807
+ ]);
1808
+ const fixable = diagnostics.filter((d) => d.fixable && fixableRules.has(d.rule) && d.suggestion?.type === "delete");
1809
+ const remaining = diagnostics.filter((d) => !fixableRules.has(d.rule) || !d.fixable);
1810
+ const byFile = /* @__PURE__ */ new Map();
1811
+ for (const d of fixable) {
1812
+ const list = byFile.get(d.filePath) ?? [];
1813
+ list.push(d);
1814
+ d.filePath && byFile.set(d.filePath, list);
1815
+ }
1816
+ const modifiedFiles = [];
1817
+ for (const [relPath, fileDiags] of byFile) {
1818
+ const absPath = join(context.rootDirectory, relPath);
1819
+ try {
1820
+ const lines = toLines(await readFileContent(absPath));
1821
+ const linesToRemove = new Set(fileDiags.map((d) => d.line));
1822
+ const newLines = lines.filter((l) => !linesToRemove.has(l.num)).map((l) => l.text).join("\n");
1823
+ const { writeFile } = await import("node:fs/promises");
1824
+ await writeFile(absPath, newLines, "utf-8");
1825
+ modifiedFiles.push(relPath);
1826
+ } catch {
1827
+ remaining.push(...fileDiags);
1828
+ }
1829
+ }
1830
+ return {
1831
+ fixed: fixable.length - (remaining.length - (diagnostics.length - fixable.length)),
1832
+ remaining,
1833
+ modifiedFiles
1834
+ };
1835
+ }
1836
+ };
1837
+
1838
+ //#endregion
1839
+ export { astSlopEngine };