aislop 0.7.0 → 0.8.0
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.
- package/README.md +39 -3
- package/dist/cli.js +1423 -84
- package/dist/expo-doctor-T4DswmX5.js +136 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1265 -65
- package/dist/{json-DwAcCqqG.js → json-BJGLCIK-.js} +1 -1
- package/dist/mcp.js +5115 -0
- package/dist/subprocess-CCnnN_oQ.js +60 -0
- package/dist/typecheck-B1MXNAy-.js +102 -0
- package/dist/typecheck-By967nny.js +102 -0
- package/dist/typecheck-XJMuCczG.js +101 -0
- package/dist/{version-BOJR1S8l.js → version-B9ZchFMv.js} +1 -1
- package/package.json +5 -3
- /package/dist/{json-B51etWTw.js → json-BbMwrgyd.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-
|
|
1
|
+
import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-B9ZchFMv.js";
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
|
|
3
3
|
import { r as runGenericLinter, t as fixRubyLint } from "./generic-BrcWMW7E.js";
|
|
4
4
|
import { n as runExpoDoctor } from "./expo-doctor-Bz0LZhQ6.js";
|
|
5
|
-
import { createRequire } from "node:module";
|
|
5
|
+
import { createRequire, isBuiltin } from "node:module";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import YAML from "yaml";
|
|
@@ -41,6 +41,7 @@ const DEFAULT_CONFIG = {
|
|
|
41
41
|
maxNesting: 5,
|
|
42
42
|
maxParams: 6
|
|
43
43
|
},
|
|
44
|
+
lint: { typecheck: false },
|
|
44
45
|
security: {
|
|
45
46
|
audit: true,
|
|
46
47
|
auditTimeout: 25e3
|
|
@@ -169,6 +170,7 @@ const QualitySchema = z.object({
|
|
|
169
170
|
maxNesting: z.number().positive().default(5),
|
|
170
171
|
maxParams: z.number().positive().default(6)
|
|
171
172
|
});
|
|
173
|
+
const LintConfigSchema = z.object({ typecheck: z.boolean().default(false) });
|
|
172
174
|
const SecurityConfigSchema = z.object({
|
|
173
175
|
audit: z.boolean().default(true),
|
|
174
176
|
auditTimeout: z.number().positive().default(25e3)
|
|
@@ -206,6 +208,7 @@ const AislopConfigSchema = z.object({
|
|
|
206
208
|
maxNesting: 5,
|
|
207
209
|
maxParams: 6
|
|
208
210
|
})),
|
|
211
|
+
lint: LintConfigSchema.default(() => ({ typecheck: false })),
|
|
209
212
|
security: SecurityConfigSchema.default(() => ({
|
|
210
213
|
audit: true,
|
|
211
214
|
auditTimeout: 25e3
|
|
@@ -606,7 +609,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
|
606
609
|
return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
|
|
607
610
|
};
|
|
608
611
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
609
|
-
const isTestFile = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
612
|
+
const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
610
613
|
const getIgnoredPaths = (rootDirectory, files) => {
|
|
611
614
|
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
612
615
|
const result = spawnSync("git", [
|
|
@@ -680,7 +683,7 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
|
|
|
680
683
|
return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
|
|
681
684
|
};
|
|
682
685
|
return normalizedFiles.filter(({ absolutePath, relativePath }) => {
|
|
683
|
-
return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
|
|
686
|
+
return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile$2(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
|
|
684
687
|
}).map(({ absolutePath }) => absolutePath);
|
|
685
688
|
};
|
|
686
689
|
const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
|
|
@@ -989,16 +992,29 @@ const planFormat = (ctx) => {
|
|
|
989
992
|
skipReason: "no supported language"
|
|
990
993
|
};
|
|
991
994
|
};
|
|
992
|
-
const
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
|
|
995
|
+
const findLocalTsc = (root) => {
|
|
996
|
+
const candidate = path.join(root, "node_modules", ".bin", "tsc");
|
|
997
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
998
|
+
};
|
|
999
|
+
const withTypecheckSuffix = (baseTool, ctx) => {
|
|
1000
|
+
if (!ctx.config.lint?.typecheck) return {
|
|
1001
|
+
tool: baseTool,
|
|
996
1002
|
status: "ok"
|
|
997
1003
|
};
|
|
998
|
-
if (
|
|
999
|
-
tool:
|
|
1004
|
+
if (findLocalTsc(ctx.rootDirectory)) return {
|
|
1005
|
+
tool: `${baseTool} + tsc`,
|
|
1000
1006
|
status: "ok"
|
|
1001
1007
|
};
|
|
1008
|
+
return {
|
|
1009
|
+
tool: `${baseTool} + tsc not found`,
|
|
1010
|
+
status: "missing",
|
|
1011
|
+
remediation: "Install TypeScript locally (pnpm add -D typescript), or set lint.typecheck: false in .aislop/config.yml."
|
|
1012
|
+
};
|
|
1013
|
+
};
|
|
1014
|
+
const planLint = (ctx) => {
|
|
1015
|
+
const { languages, frameworks, installedTools } = ctx.projectInfo;
|
|
1016
|
+
if (frameworks.includes("expo")) return withTypecheckSuffix("expo-doctor + oxlint (bundled)", ctx);
|
|
1017
|
+
if (hasJsLike(languages)) return withTypecheckSuffix("oxlint (bundled)", ctx);
|
|
1002
1018
|
return firstMatching(languages, installedTools, LINT_SPECS) ?? {
|
|
1003
1019
|
tool: "no linter",
|
|
1004
1020
|
status: "skipped",
|
|
@@ -1258,13 +1274,14 @@ const detectOverAbstraction = async (context) => {
|
|
|
1258
1274
|
|
|
1259
1275
|
//#endregion
|
|
1260
1276
|
//#region src/engines/ai-slop/comments.ts
|
|
1277
|
+
const NON_PRODUCTION_DIR_PATTERN$2 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
|
|
1261
1278
|
const TRIVIAL_VERB_STEMS = "Import|Defin|Initializ|Setting|Set\\s+up|Setup|Return|Check|Loop|Iterat|Creat|Updat|Delet|Remov|Handl|Get|Fetch|Increment|Decrement|Writ|Runn|Run|Pars|Execut|Extract|Sav|Load|Build|Start|Stopp|Stop|Clean(?:up|\\s+up)?|Configur|Validat|Process|Queue|Fire|Emit|Dispatch|Log|Print|Render";
|
|
1262
1279
|
const TRIVIAL_JS_COMMENT_PATTERNS = [/\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i, new RegExp(`\\/\\/\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
1263
1280
|
const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, new RegExp(`^#\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
1264
1281
|
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
|
|
1265
1282
|
const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
1266
1283
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
1267
|
-
const isJsComment = (trimmed) => trimmed.startsWith("//");
|
|
1284
|
+
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
1268
1285
|
const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
|
|
1269
1286
|
/**
|
|
1270
1287
|
* Extract just the comment text after the comment marker.
|
|
@@ -1313,13 +1330,14 @@ const detectTrivialComments = async (context) => {
|
|
|
1313
1330
|
const diagnostics = [];
|
|
1314
1331
|
for (const filePath of files) {
|
|
1315
1332
|
if (isAutoGenerated(filePath)) continue;
|
|
1333
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1334
|
+
if (NON_PRODUCTION_DIR_PATTERN$2.test(relativePath)) continue;
|
|
1316
1335
|
let content;
|
|
1317
1336
|
try {
|
|
1318
1337
|
content = fs.readFileSync(filePath, "utf-8");
|
|
1319
1338
|
} catch {
|
|
1320
1339
|
continue;
|
|
1321
1340
|
}
|
|
1322
|
-
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1323
1341
|
diagnostics.push(...scanFileForTrivialComments(content, relativePath));
|
|
1324
1342
|
}
|
|
1325
1343
|
return diagnostics;
|
|
@@ -1327,7 +1345,7 @@ const detectTrivialComments = async (context) => {
|
|
|
1327
1345
|
|
|
1328
1346
|
//#endregion
|
|
1329
1347
|
//#region src/engines/ai-slop/dead-patterns.ts
|
|
1330
|
-
const JS_EXTENSIONS$
|
|
1348
|
+
const JS_EXTENSIONS$4 = new Set([
|
|
1331
1349
|
".ts",
|
|
1332
1350
|
".tsx",
|
|
1333
1351
|
".js",
|
|
@@ -1349,11 +1367,11 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
|
1349
1367
|
fixable
|
|
1350
1368
|
});
|
|
1351
1369
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
1352
|
-
const
|
|
1370
|
+
const NON_PRODUCTION_DIR_PATTERN$1 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|cli|cli-[\w-]+|[\w-]+-cli)\//;
|
|
1353
1371
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
1354
|
-
if (!JS_EXTENSIONS$
|
|
1372
|
+
if (!JS_EXTENSIONS$4.has(ext)) return [];
|
|
1355
1373
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
1356
|
-
if (
|
|
1374
|
+
if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
|
|
1357
1375
|
const diagnostics = [];
|
|
1358
1376
|
const lines = content.split("\n");
|
|
1359
1377
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1393,9 +1411,9 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1393
1411
|
for (let i = 0; i < lines.length; i++) {
|
|
1394
1412
|
const trimmed = lines[i].trim();
|
|
1395
1413
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1396
|
-
if (JS_EXTENSIONS$
|
|
1414
|
+
if (JS_EXTENSIONS$4.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !nextLine.startsWith("}") && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push(slop(relativePath, i + 2, "ai-slop/unreachable-code", "warning", "Code after return/throw statement is unreachable", "Remove the unreachable code or restructure the control flow", false));
|
|
1397
1415
|
if (/\bif\s*\(\s*(?:false|true|0|1)\s*\)/.test(trimmed) && !trimmed.startsWith("//") && !trimmed.startsWith("*") && !/["'`].*\bif\s*\(/.test(trimmed) && !/\/.*\bif\s*\(/.test(trimmed.replace(/\/\/.*$/, ""))) diagnostics.push(slop(relativePath, i + 1, "ai-slop/constant-condition", "warning", "Conditional with a constant value — likely debugging leftover", "Remove the constant condition or replace with proper logic", false));
|
|
1398
|
-
if (JS_EXTENSIONS$
|
|
1416
|
+
if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
|
|
1399
1417
|
}
|
|
1400
1418
|
return diagnostics;
|
|
1401
1419
|
};
|
|
@@ -1403,6 +1421,7 @@ const asAnyPattern = new RegExp(`\\bas\\s+any\\b`);
|
|
|
1403
1421
|
const doubleAssertPattern = new RegExp(`\\bas\\s+unknown\\s+as\\s+`);
|
|
1404
1422
|
const detectUnsafeTypePatterns = (content, relativePath, ext) => {
|
|
1405
1423
|
if (ext !== ".ts" && ext !== ".tsx") return [];
|
|
1424
|
+
if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
|
|
1406
1425
|
const diagnostics = [];
|
|
1407
1426
|
const lines = content.split("\n");
|
|
1408
1427
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1439,6 +1458,74 @@ const detectDeadPatterns = async (context) => {
|
|
|
1439
1458
|
return diagnostics;
|
|
1440
1459
|
};
|
|
1441
1460
|
|
|
1461
|
+
//#endregion
|
|
1462
|
+
//#region src/engines/ai-slop/duplicate-imports.ts
|
|
1463
|
+
const JS_EXTENSIONS$3 = new Set([
|
|
1464
|
+
".ts",
|
|
1465
|
+
".tsx",
|
|
1466
|
+
".js",
|
|
1467
|
+
".jsx",
|
|
1468
|
+
".mjs",
|
|
1469
|
+
".cjs"
|
|
1470
|
+
]);
|
|
1471
|
+
const IMPORT_FROM_RE$1 = /^\s*import\s+[^;]*?from\s+["']([^"']+)["']/;
|
|
1472
|
+
const extractImportLines = (content) => {
|
|
1473
|
+
const lines = content.split("\n");
|
|
1474
|
+
const results = [];
|
|
1475
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1476
|
+
const line = lines[i];
|
|
1477
|
+
const match = IMPORT_FROM_RE$1.exec(line);
|
|
1478
|
+
if (!match) continue;
|
|
1479
|
+
results.push({
|
|
1480
|
+
spec: match[1],
|
|
1481
|
+
line: i + 1
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
return results;
|
|
1485
|
+
};
|
|
1486
|
+
const detectDuplicateImports = async (context) => {
|
|
1487
|
+
const diagnostics = [];
|
|
1488
|
+
const files = getSourceFiles(context);
|
|
1489
|
+
for (const filePath of files) {
|
|
1490
|
+
if (!JS_EXTENSIONS$3.has(path.extname(filePath))) continue;
|
|
1491
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1492
|
+
let content;
|
|
1493
|
+
try {
|
|
1494
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1495
|
+
} catch {
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
const imports = extractImportLines(content);
|
|
1499
|
+
if (imports.length < 2) continue;
|
|
1500
|
+
const bySpec = /* @__PURE__ */ new Map();
|
|
1501
|
+
for (const imp of imports) {
|
|
1502
|
+
const list = bySpec.get(imp.spec) ?? [];
|
|
1503
|
+
list.push(imp);
|
|
1504
|
+
bySpec.set(imp.spec, list);
|
|
1505
|
+
}
|
|
1506
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
1507
|
+
for (const [spec, occurrences] of bySpec) {
|
|
1508
|
+
if (occurrences.length < 2) continue;
|
|
1509
|
+
for (const dup of occurrences.slice(1)) {
|
|
1510
|
+
const firstLine = occurrences[0].line;
|
|
1511
|
+
diagnostics.push({
|
|
1512
|
+
filePath: relPath,
|
|
1513
|
+
engine: "ai-slop",
|
|
1514
|
+
rule: "ai-slop/duplicate-import",
|
|
1515
|
+
severity: "warning",
|
|
1516
|
+
message: `"${spec}" is also imported on line ${firstLine}. Merge into a single import statement.`,
|
|
1517
|
+
help: "Two imports from the same module split readers' attention and grow the import block. Run aislop fix to merge them automatically.",
|
|
1518
|
+
line: dup.line,
|
|
1519
|
+
column: 1,
|
|
1520
|
+
category: "AI Slop",
|
|
1521
|
+
fixable: true
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return diagnostics;
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1442
1529
|
//#endregion
|
|
1443
1530
|
//#region src/engines/ai-slop/exceptions.ts
|
|
1444
1531
|
const SWALLOWED_EXCEPTION_PATTERNS = [
|
|
@@ -1529,6 +1616,600 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
1529
1616
|
return diagnostics;
|
|
1530
1617
|
};
|
|
1531
1618
|
|
|
1619
|
+
//#endregion
|
|
1620
|
+
//#region src/engines/ai-slop/go-patterns.ts
|
|
1621
|
+
const GO_EXTENSIONS = new Set([".go"]);
|
|
1622
|
+
const PACKAGE_DECL_RE = /^\s*package\s+(\w+)/;
|
|
1623
|
+
const PANIC_CALL_RE = /\bpanic\s*\(/;
|
|
1624
|
+
const COMMENT_LINE_RE$1 = /^\s*\/\//;
|
|
1625
|
+
const NIL_GUARD_RE = /^\s*if\s+[\w.]+(?:\(\))?\s*==\s*nil\s*\{?\s*$/;
|
|
1626
|
+
const SHORT_STRING_PANIC_RE = /\bpanic\s*\(\s*"[^"]{1,40}"\s*\)/;
|
|
1627
|
+
const detectPackageName = (lines) => {
|
|
1628
|
+
for (const line of lines) {
|
|
1629
|
+
const m = PACKAGE_DECL_RE.exec(line);
|
|
1630
|
+
if (m) return m[1];
|
|
1631
|
+
}
|
|
1632
|
+
return null;
|
|
1633
|
+
};
|
|
1634
|
+
const PANIC_INTENT_LOOKBACK = 3;
|
|
1635
|
+
const hasIntentComment$1 = (lines, panicLineIdx) => {
|
|
1636
|
+
for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - PANIC_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE$1.test(lines[j])) return true;
|
|
1637
|
+
return false;
|
|
1638
|
+
};
|
|
1639
|
+
const isNilGuardPanic = (lines, panicLineIdx, line) => {
|
|
1640
|
+
if (!SHORT_STRING_PANIC_RE.test(line)) return false;
|
|
1641
|
+
for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - 2); j--) {
|
|
1642
|
+
const prev = lines[j];
|
|
1643
|
+
if (prev.trim() === "") continue;
|
|
1644
|
+
return NIL_GUARD_RE.test(prev);
|
|
1645
|
+
}
|
|
1646
|
+
return false;
|
|
1647
|
+
};
|
|
1648
|
+
const flagLibraryPanic = (lines, relPath, pkg, out) => {
|
|
1649
|
+
if (pkg === "main") return;
|
|
1650
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1651
|
+
const line = lines[i];
|
|
1652
|
+
if (COMMENT_LINE_RE$1.test(line)) continue;
|
|
1653
|
+
PANIC_CALL_RE.lastIndex = 0;
|
|
1654
|
+
if (!PANIC_CALL_RE.test(line)) continue;
|
|
1655
|
+
if (hasIntentComment$1(lines, i)) continue;
|
|
1656
|
+
if (isNilGuardPanic(lines, i, line)) continue;
|
|
1657
|
+
out.push({
|
|
1658
|
+
filePath: relPath,
|
|
1659
|
+
engine: "ai-slop",
|
|
1660
|
+
rule: "ai-slop/go-library-panic",
|
|
1661
|
+
severity: "warning",
|
|
1662
|
+
message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
|
|
1663
|
+
help: "Convert to `return fmt.Errorf(...)` (or a wrapped error) and let the caller decide. Reserve `panic` for genuinely-impossible states (corrupt internal invariants), and mark those with a comment so future readers know it's intentional.",
|
|
1664
|
+
line: i + 1,
|
|
1665
|
+
column: 1,
|
|
1666
|
+
category: "AI Slop",
|
|
1667
|
+
fixable: false
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
const detectGoPatterns = async (context) => {
|
|
1672
|
+
const diagnostics = [];
|
|
1673
|
+
const files = getSourceFiles(context);
|
|
1674
|
+
for (const filePath of files) {
|
|
1675
|
+
if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
1676
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1677
|
+
if (filePath.endsWith("_test.go")) continue;
|
|
1678
|
+
let content;
|
|
1679
|
+
try {
|
|
1680
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1681
|
+
} catch {
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
const lines = content.split("\n");
|
|
1685
|
+
const pkg = detectPackageName(lines);
|
|
1686
|
+
if (!pkg) continue;
|
|
1687
|
+
flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
|
|
1688
|
+
}
|
|
1689
|
+
return diagnostics;
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
//#endregion
|
|
1693
|
+
//#region src/engines/ai-slop/python-data.ts
|
|
1694
|
+
const PYTHON_STDLIB = new Set([
|
|
1695
|
+
"__future__",
|
|
1696
|
+
"_thread",
|
|
1697
|
+
"abc",
|
|
1698
|
+
"argparse",
|
|
1699
|
+
"array",
|
|
1700
|
+
"ast",
|
|
1701
|
+
"asyncio",
|
|
1702
|
+
"atexit",
|
|
1703
|
+
"base64",
|
|
1704
|
+
"binascii",
|
|
1705
|
+
"bisect",
|
|
1706
|
+
"builtins",
|
|
1707
|
+
"bz2",
|
|
1708
|
+
"calendar",
|
|
1709
|
+
"codecs",
|
|
1710
|
+
"collections",
|
|
1711
|
+
"concurrent",
|
|
1712
|
+
"configparser",
|
|
1713
|
+
"contextlib",
|
|
1714
|
+
"contextvars",
|
|
1715
|
+
"copy",
|
|
1716
|
+
"csv",
|
|
1717
|
+
"ctypes",
|
|
1718
|
+
"dataclasses",
|
|
1719
|
+
"datetime",
|
|
1720
|
+
"decimal",
|
|
1721
|
+
"difflib",
|
|
1722
|
+
"dis",
|
|
1723
|
+
"doctest",
|
|
1724
|
+
"email",
|
|
1725
|
+
"encodings",
|
|
1726
|
+
"enum",
|
|
1727
|
+
"errno",
|
|
1728
|
+
"faulthandler",
|
|
1729
|
+
"filecmp",
|
|
1730
|
+
"fileinput",
|
|
1731
|
+
"fnmatch",
|
|
1732
|
+
"fractions",
|
|
1733
|
+
"functools",
|
|
1734
|
+
"gc",
|
|
1735
|
+
"getopt",
|
|
1736
|
+
"getpass",
|
|
1737
|
+
"gettext",
|
|
1738
|
+
"glob",
|
|
1739
|
+
"graphlib",
|
|
1740
|
+
"gzip",
|
|
1741
|
+
"hashlib",
|
|
1742
|
+
"heapq",
|
|
1743
|
+
"hmac",
|
|
1744
|
+
"html",
|
|
1745
|
+
"http",
|
|
1746
|
+
"imaplib",
|
|
1747
|
+
"importlib",
|
|
1748
|
+
"inspect",
|
|
1749
|
+
"io",
|
|
1750
|
+
"ipaddress",
|
|
1751
|
+
"itertools",
|
|
1752
|
+
"json",
|
|
1753
|
+
"keyword",
|
|
1754
|
+
"linecache",
|
|
1755
|
+
"locale",
|
|
1756
|
+
"logging",
|
|
1757
|
+
"lzma",
|
|
1758
|
+
"mailbox",
|
|
1759
|
+
"math",
|
|
1760
|
+
"mimetypes",
|
|
1761
|
+
"mmap",
|
|
1762
|
+
"multiprocessing",
|
|
1763
|
+
"numbers",
|
|
1764
|
+
"operator",
|
|
1765
|
+
"os",
|
|
1766
|
+
"pathlib",
|
|
1767
|
+
"pdb",
|
|
1768
|
+
"pickle",
|
|
1769
|
+
"platform",
|
|
1770
|
+
"plistlib",
|
|
1771
|
+
"pprint",
|
|
1772
|
+
"profile",
|
|
1773
|
+
"pstats",
|
|
1774
|
+
"pty",
|
|
1775
|
+
"queue",
|
|
1776
|
+
"quopri",
|
|
1777
|
+
"random",
|
|
1778
|
+
"re",
|
|
1779
|
+
"readline",
|
|
1780
|
+
"reprlib",
|
|
1781
|
+
"resource",
|
|
1782
|
+
"secrets",
|
|
1783
|
+
"select",
|
|
1784
|
+
"selectors",
|
|
1785
|
+
"shelve",
|
|
1786
|
+
"shlex",
|
|
1787
|
+
"shutil",
|
|
1788
|
+
"signal",
|
|
1789
|
+
"site",
|
|
1790
|
+
"smtplib",
|
|
1791
|
+
"socket",
|
|
1792
|
+
"socketserver",
|
|
1793
|
+
"sqlite3",
|
|
1794
|
+
"ssl",
|
|
1795
|
+
"stat",
|
|
1796
|
+
"statistics",
|
|
1797
|
+
"string",
|
|
1798
|
+
"stringprep",
|
|
1799
|
+
"struct",
|
|
1800
|
+
"subprocess",
|
|
1801
|
+
"sunau",
|
|
1802
|
+
"symtable",
|
|
1803
|
+
"sys",
|
|
1804
|
+
"sysconfig",
|
|
1805
|
+
"syslog",
|
|
1806
|
+
"tarfile",
|
|
1807
|
+
"telnetlib",
|
|
1808
|
+
"tempfile",
|
|
1809
|
+
"termios",
|
|
1810
|
+
"test",
|
|
1811
|
+
"textwrap",
|
|
1812
|
+
"threading",
|
|
1813
|
+
"time",
|
|
1814
|
+
"timeit",
|
|
1815
|
+
"tkinter",
|
|
1816
|
+
"token",
|
|
1817
|
+
"tokenize",
|
|
1818
|
+
"tomllib",
|
|
1819
|
+
"trace",
|
|
1820
|
+
"traceback",
|
|
1821
|
+
"tracemalloc",
|
|
1822
|
+
"tty",
|
|
1823
|
+
"turtle",
|
|
1824
|
+
"types",
|
|
1825
|
+
"typing",
|
|
1826
|
+
"unicodedata",
|
|
1827
|
+
"unittest",
|
|
1828
|
+
"urllib",
|
|
1829
|
+
"uu",
|
|
1830
|
+
"uuid",
|
|
1831
|
+
"venv",
|
|
1832
|
+
"warnings",
|
|
1833
|
+
"wave",
|
|
1834
|
+
"weakref",
|
|
1835
|
+
"webbrowser",
|
|
1836
|
+
"winreg",
|
|
1837
|
+
"winsound",
|
|
1838
|
+
"wsgiref",
|
|
1839
|
+
"xml",
|
|
1840
|
+
"xmlrpc",
|
|
1841
|
+
"zipapp",
|
|
1842
|
+
"zipfile",
|
|
1843
|
+
"zipimport",
|
|
1844
|
+
"zlib",
|
|
1845
|
+
"zoneinfo"
|
|
1846
|
+
]);
|
|
1847
|
+
const PYTHON_IMPORT_TO_PIP = {
|
|
1848
|
+
yaml: "pyyaml",
|
|
1849
|
+
PIL: "pillow",
|
|
1850
|
+
dateutil: "python-dateutil",
|
|
1851
|
+
cv2: "opencv-python",
|
|
1852
|
+
sklearn: "scikit-learn",
|
|
1853
|
+
bs4: "beautifulsoup4",
|
|
1854
|
+
typing_extensions: "typing-extensions",
|
|
1855
|
+
google: "google-api-python-client",
|
|
1856
|
+
jose: "python-jose",
|
|
1857
|
+
jwt: "pyjwt",
|
|
1858
|
+
OpenSSL: "pyopenssl",
|
|
1859
|
+
magic: "python-magic",
|
|
1860
|
+
docx: "python-docx",
|
|
1861
|
+
pptx: "python-pptx",
|
|
1862
|
+
git: "gitpython",
|
|
1863
|
+
socks: "pysocks",
|
|
1864
|
+
redis: "redis"
|
|
1865
|
+
};
|
|
1866
|
+
|
|
1867
|
+
//#endregion
|
|
1868
|
+
//#region src/engines/ai-slop/hallucinated-imports.ts
|
|
1869
|
+
const JS_EXTENSIONS$2 = new Set([
|
|
1870
|
+
".ts",
|
|
1871
|
+
".tsx",
|
|
1872
|
+
".js",
|
|
1873
|
+
".jsx",
|
|
1874
|
+
".mjs",
|
|
1875
|
+
".cjs"
|
|
1876
|
+
]);
|
|
1877
|
+
const PY_EXTENSIONS$2 = new Set([".py"]);
|
|
1878
|
+
const readJson = (filePath) => {
|
|
1879
|
+
try {
|
|
1880
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1881
|
+
} catch {
|
|
1882
|
+
return null;
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
const PKG_DEP_SECTIONS = [
|
|
1886
|
+
"dependencies",
|
|
1887
|
+
"devDependencies",
|
|
1888
|
+
"peerDependencies",
|
|
1889
|
+
"optionalDependencies"
|
|
1890
|
+
];
|
|
1891
|
+
const addDepsFromPkg = (pkg, jsDeps) => {
|
|
1892
|
+
for (const section of PKG_DEP_SECTIONS) {
|
|
1893
|
+
const deps = pkg[section];
|
|
1894
|
+
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
1898
|
+
const globs = [];
|
|
1899
|
+
if (rootPkg && typeof rootPkg === "object") {
|
|
1900
|
+
const ws = rootPkg.workspaces;
|
|
1901
|
+
if (Array.isArray(ws)) {
|
|
1902
|
+
for (const g of ws) if (typeof g === "string") globs.push(g);
|
|
1903
|
+
} else if (ws && typeof ws === "object") {
|
|
1904
|
+
const pkgs = ws.packages;
|
|
1905
|
+
if (Array.isArray(pkgs)) {
|
|
1906
|
+
for (const g of pkgs) if (typeof g === "string") globs.push(g);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
const lerna = readJson(path.join(rootDir, "lerna.json"));
|
|
1911
|
+
if (lerna && Array.isArray(lerna.packages)) {
|
|
1912
|
+
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
1913
|
+
}
|
|
1914
|
+
try {
|
|
1915
|
+
const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
|
|
1916
|
+
let inPackages = false;
|
|
1917
|
+
for (const rawLine of pnpmWs.split("\n")) {
|
|
1918
|
+
if (/^packages\s*:\s*$/.test(rawLine)) {
|
|
1919
|
+
inPackages = true;
|
|
1920
|
+
continue;
|
|
1921
|
+
}
|
|
1922
|
+
if (!inPackages) continue;
|
|
1923
|
+
if (/^\S/.test(rawLine)) break;
|
|
1924
|
+
const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
|
|
1925
|
+
if (m) globs.push(m[1].trim());
|
|
1926
|
+
}
|
|
1927
|
+
} catch {}
|
|
1928
|
+
return globs;
|
|
1929
|
+
};
|
|
1930
|
+
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
1931
|
+
const dirs = [];
|
|
1932
|
+
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
1933
|
+
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
1934
|
+
try {
|
|
1935
|
+
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
1936
|
+
} catch {
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
1940
|
+
return dirs;
|
|
1941
|
+
};
|
|
1942
|
+
const SKIP_DIRS = new Set([
|
|
1943
|
+
"node_modules",
|
|
1944
|
+
".git",
|
|
1945
|
+
"dist",
|
|
1946
|
+
"build",
|
|
1947
|
+
"out",
|
|
1948
|
+
"target",
|
|
1949
|
+
"coverage"
|
|
1950
|
+
]);
|
|
1951
|
+
const NESTED_PKG_JSON_DEPTH = 4;
|
|
1952
|
+
const collectNestedManifests = (rootDir, jsDeps) => {
|
|
1953
|
+
const walk = (dir, depth) => {
|
|
1954
|
+
if (depth > NESTED_PKG_JSON_DEPTH) return;
|
|
1955
|
+
let entries;
|
|
1956
|
+
try {
|
|
1957
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1958
|
+
} catch {
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
for (const entry of entries) {
|
|
1962
|
+
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
1963
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1964
|
+
const full = path.join(dir, entry.name);
|
|
1965
|
+
if (entry.isDirectory()) walk(full, depth + 1);
|
|
1966
|
+
else if (entry.name === "package.json" && depth > 0) {
|
|
1967
|
+
const wsPkg = readJson(full);
|
|
1968
|
+
if (!wsPkg) continue;
|
|
1969
|
+
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
1970
|
+
addDepsFromPkg(wsPkg, jsDeps);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
walk(rootDir, 0);
|
|
1975
|
+
};
|
|
1976
|
+
const collectJsDeps = (rootDir, jsDeps) => {
|
|
1977
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
1978
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
1979
|
+
const pkg = readJson(pkgPath);
|
|
1980
|
+
if (!pkg || typeof pkg !== "object") return false;
|
|
1981
|
+
addDepsFromPkg(pkg, jsDeps);
|
|
1982
|
+
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
1983
|
+
const workspaceDirs = expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, pkg));
|
|
1984
|
+
for (const wsDir of workspaceDirs) {
|
|
1985
|
+
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
1986
|
+
if (!wsPkg) continue;
|
|
1987
|
+
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
1988
|
+
addDepsFromPkg(wsPkg, jsDeps);
|
|
1989
|
+
}
|
|
1990
|
+
collectNestedManifests(rootDir, jsDeps);
|
|
1991
|
+
return true;
|
|
1992
|
+
};
|
|
1993
|
+
const addPyDep = (pyDeps, name) => {
|
|
1994
|
+
const normalized = name.toLowerCase().replace(/_/g, "-");
|
|
1995
|
+
pyDeps.add(normalized);
|
|
1996
|
+
};
|
|
1997
|
+
const collectFromRequirementsTxt = (rootDir, pyDeps) => {
|
|
1998
|
+
const reqPath = path.join(rootDir, "requirements.txt");
|
|
1999
|
+
if (!fs.existsSync(reqPath)) return false;
|
|
2000
|
+
try {
|
|
2001
|
+
const content = fs.readFileSync(reqPath, "utf-8");
|
|
2002
|
+
for (const line of content.split("\n")) {
|
|
2003
|
+
const trimmed = line.trim();
|
|
2004
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
2005
|
+
const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
|
|
2006
|
+
if (match) addPyDep(pyDeps, match[1]);
|
|
2007
|
+
}
|
|
2008
|
+
return true;
|
|
2009
|
+
} catch {
|
|
2010
|
+
return false;
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
const collectFromPyproject = (rootDir, pyDeps) => {
|
|
2014
|
+
const pyprojPath = path.join(rootDir, "pyproject.toml");
|
|
2015
|
+
if (!fs.existsSync(pyprojPath)) return false;
|
|
2016
|
+
try {
|
|
2017
|
+
const content = fs.readFileSync(pyprojPath, "utf-8");
|
|
2018
|
+
const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
2019
|
+
if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
|
|
2020
|
+
const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
2021
|
+
if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
|
|
2022
|
+
const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
2023
|
+
if (pep621) for (const line of pep621[1].split("\n")) {
|
|
2024
|
+
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
2025
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
2026
|
+
}
|
|
2027
|
+
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
2028
|
+
let match = poetryRe.exec(content);
|
|
2029
|
+
while (match !== null) {
|
|
2030
|
+
for (const line of match[1].split("\n")) {
|
|
2031
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
2032
|
+
if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
|
|
2033
|
+
}
|
|
2034
|
+
match = poetryRe.exec(content);
|
|
2035
|
+
}
|
|
2036
|
+
return true;
|
|
2037
|
+
} catch {
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
const collectFromPipfile = (rootDir, pyDeps) => {
|
|
2042
|
+
const pipfilePath = path.join(rootDir, "Pipfile");
|
|
2043
|
+
if (!fs.existsSync(pipfilePath)) return false;
|
|
2044
|
+
try {
|
|
2045
|
+
const content = fs.readFileSync(pipfilePath, "utf-8");
|
|
2046
|
+
const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
|
|
2047
|
+
let match = sectionRe.exec(content);
|
|
2048
|
+
while (match !== null) {
|
|
2049
|
+
for (const line of match[2].split("\n")) {
|
|
2050
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
2051
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
2052
|
+
}
|
|
2053
|
+
match = sectionRe.exec(content);
|
|
2054
|
+
}
|
|
2055
|
+
return true;
|
|
2056
|
+
} catch {
|
|
2057
|
+
return false;
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
const loadManifest = (rootDir) => {
|
|
2061
|
+
const jsDeps = /* @__PURE__ */ new Set();
|
|
2062
|
+
const pyDeps = /* @__PURE__ */ new Set();
|
|
2063
|
+
const hasJsManifest = collectJsDeps(rootDir, jsDeps);
|
|
2064
|
+
const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
|
|
2065
|
+
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
2066
|
+
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
2067
|
+
return {
|
|
2068
|
+
jsDeps,
|
|
2069
|
+
pyDeps,
|
|
2070
|
+
hasJsManifest,
|
|
2071
|
+
hasPyManifest: hasReq || hasPyproject || hasPipfile
|
|
2072
|
+
};
|
|
2073
|
+
};
|
|
2074
|
+
const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
|
|
2075
|
+
const isJsBuiltin = (spec) => {
|
|
2076
|
+
return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
|
|
2077
|
+
};
|
|
2078
|
+
const VIRTUAL_MODULE_PREFIXES = [
|
|
2079
|
+
"astro:",
|
|
2080
|
+
"virtual:",
|
|
2081
|
+
"bun:"
|
|
2082
|
+
];
|
|
2083
|
+
const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
|
|
2084
|
+
const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
|
|
2085
|
+
const isLikelyRealImportSpec = (spec) => {
|
|
2086
|
+
if (spec.length === 0) return false;
|
|
2087
|
+
if (TEMPLATE_PLACEHOLDER_RE.test(spec)) return false;
|
|
2088
|
+
if (spec.includes("\\")) return false;
|
|
2089
|
+
if (/\s/.test(spec)) return false;
|
|
2090
|
+
return true;
|
|
2091
|
+
};
|
|
2092
|
+
const packageNameFromImport = (spec) => {
|
|
2093
|
+
if (spec.startsWith("@")) {
|
|
2094
|
+
const parts = spec.split("/");
|
|
2095
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec;
|
|
2096
|
+
}
|
|
2097
|
+
return spec.split("/")[0];
|
|
2098
|
+
};
|
|
2099
|
+
const STATIC_IMPORT_RE = /^\s*import\s+(?:[\w*{},\s]+\s+from\s+)?["']([^"']+)["']/;
|
|
2100
|
+
const DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*["']([^"']+)["']/g;
|
|
2101
|
+
const extractJsImports = (content) => {
|
|
2102
|
+
const lines = content.split("\n");
|
|
2103
|
+
const results = [];
|
|
2104
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2105
|
+
const line = lines[i];
|
|
2106
|
+
const trimmed = line.trim();
|
|
2107
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
2108
|
+
const staticMatch = STATIC_IMPORT_RE.exec(line);
|
|
2109
|
+
if (staticMatch && isLikelyRealImportSpec(staticMatch[1])) results.push({
|
|
2110
|
+
spec: staticMatch[1],
|
|
2111
|
+
line: i + 1
|
|
2112
|
+
});
|
|
2113
|
+
DYNAMIC_IMPORT_RE.lastIndex = 0;
|
|
2114
|
+
let dyn = DYNAMIC_IMPORT_RE.exec(line);
|
|
2115
|
+
while (dyn !== null) {
|
|
2116
|
+
if (isLikelyRealImportSpec(dyn[1])) results.push({
|
|
2117
|
+
spec: dyn[1],
|
|
2118
|
+
line: i + 1
|
|
2119
|
+
});
|
|
2120
|
+
dyn = DYNAMIC_IMPORT_RE.exec(line);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
return results;
|
|
2124
|
+
};
|
|
2125
|
+
const extractPyImports = (content) => {
|
|
2126
|
+
const lines = content.split("\n");
|
|
2127
|
+
const results = [];
|
|
2128
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2129
|
+
const line = lines[i].trim();
|
|
2130
|
+
if (line.startsWith("#")) continue;
|
|
2131
|
+
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
2132
|
+
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
2133
|
+
results.push({
|
|
2134
|
+
spec: fromMatch[1],
|
|
2135
|
+
line: i + 1
|
|
2136
|
+
});
|
|
2137
|
+
continue;
|
|
2138
|
+
}
|
|
2139
|
+
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
2140
|
+
if (importMatch) for (const raw of importMatch[1].split(",")) {
|
|
2141
|
+
const cleaned = raw.trim().split(/\s+as\s+/)[0];
|
|
2142
|
+
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
2143
|
+
spec: cleaned,
|
|
2144
|
+
line: i + 1
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return results;
|
|
2149
|
+
};
|
|
2150
|
+
const checkJsImport = (spec, manifest) => {
|
|
2151
|
+
if (isJsRelativeOrAbsolute(spec)) return null;
|
|
2152
|
+
if (isJsBuiltin(spec)) return null;
|
|
2153
|
+
if (isJsVirtualModule(spec)) return null;
|
|
2154
|
+
const pkg = packageNameFromImport(spec);
|
|
2155
|
+
if (manifest.jsDeps.has(pkg)) return null;
|
|
2156
|
+
if (pkg.startsWith("@types/")) {
|
|
2157
|
+
const realPkg = pkg.slice(7);
|
|
2158
|
+
if (manifest.jsDeps.has(realPkg)) return null;
|
|
2159
|
+
}
|
|
2160
|
+
return pkg;
|
|
2161
|
+
};
|
|
2162
|
+
const checkPyImport = (spec, manifest) => {
|
|
2163
|
+
const root = spec.split(".")[0];
|
|
2164
|
+
if (PYTHON_STDLIB.has(root)) return null;
|
|
2165
|
+
const normalized = root.toLowerCase().replace(/_/g, "-");
|
|
2166
|
+
if (manifest.pyDeps.has(normalized)) return null;
|
|
2167
|
+
const pipName = PYTHON_IMPORT_TO_PIP[root];
|
|
2168
|
+
if (pipName && manifest.pyDeps.has(pipName)) return null;
|
|
2169
|
+
return root;
|
|
2170
|
+
};
|
|
2171
|
+
const detectHallucinatedImports = async (context) => {
|
|
2172
|
+
const manifest = loadManifest(context.rootDirectory);
|
|
2173
|
+
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
2174
|
+
const diagnostics = [];
|
|
2175
|
+
const files = getSourceFiles(context);
|
|
2176
|
+
for (const filePath of files) {
|
|
2177
|
+
const ext = path.extname(filePath);
|
|
2178
|
+
const isJs = JS_EXTENSIONS$2.has(ext);
|
|
2179
|
+
const isPy = PY_EXTENSIONS$2.has(ext);
|
|
2180
|
+
if (!isJs && !isPy) continue;
|
|
2181
|
+
if (isJs && !manifest.hasJsManifest) continue;
|
|
2182
|
+
if (isPy && !manifest.hasPyManifest) continue;
|
|
2183
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2184
|
+
let content;
|
|
2185
|
+
try {
|
|
2186
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2187
|
+
} catch {
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2191
|
+
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2192
|
+
for (const { spec, line } of imports) {
|
|
2193
|
+
const hallucinated = isJs ? checkJsImport(spec, manifest) : checkPyImport(spec, manifest);
|
|
2194
|
+
if (!hallucinated) continue;
|
|
2195
|
+
const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
|
|
2196
|
+
diagnostics.push({
|
|
2197
|
+
filePath: relPath,
|
|
2198
|
+
engine: "ai-slop",
|
|
2199
|
+
rule: "ai-slop/hallucinated-import",
|
|
2200
|
+
severity: "error",
|
|
2201
|
+
message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
|
|
2202
|
+
help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
|
|
2203
|
+
line,
|
|
2204
|
+
column: 1,
|
|
2205
|
+
category: "AI Slop",
|
|
2206
|
+
fixable: false
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
return diagnostics;
|
|
2211
|
+
};
|
|
2212
|
+
|
|
1532
2213
|
//#endregion
|
|
1533
2214
|
//#region src/engines/ai-slop/narrative-comments-patterns.ts
|
|
1534
2215
|
const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
|
|
@@ -1645,6 +2326,7 @@ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|re
|
|
|
1645
2326
|
|
|
1646
2327
|
//#endregion
|
|
1647
2328
|
//#region src/engines/ai-slop/narrative-comments.ts
|
|
2329
|
+
const NON_PRODUCTION_DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
|
|
1648
2330
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
1649
2331
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
1650
2332
|
const getCommentSyntax = (ext) => {
|
|
@@ -1673,6 +2355,10 @@ const getMatchedLinePrefix = (line, syntax) => {
|
|
|
1673
2355
|
}
|
|
1674
2356
|
return null;
|
|
1675
2357
|
};
|
|
2358
|
+
const isRustDocCommentLine = (line) => {
|
|
2359
|
+
const trimmed = line.trimStart();
|
|
2360
|
+
return trimmed.startsWith("///") || trimmed.startsWith("//!");
|
|
2361
|
+
};
|
|
1676
2362
|
const collectBlocks = (sourceLines, syntax) => {
|
|
1677
2363
|
const blocks = [];
|
|
1678
2364
|
let i = 0;
|
|
@@ -1688,6 +2374,8 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1688
2374
|
}
|
|
1689
2375
|
let next = i;
|
|
1690
2376
|
while (next < sourceLines.length && sourceLines[next].trim() === "") next += 1;
|
|
2377
|
+
const docCandidates = raw.filter((l) => l.trim().length > 0);
|
|
2378
|
+
const isRustDoc = docCandidates.length > 0 && docCandidates.every((l) => isRustDocCommentLine(l));
|
|
1691
2379
|
blocks.push({
|
|
1692
2380
|
kind: "line",
|
|
1693
2381
|
startLine: start + 1,
|
|
@@ -1695,6 +2383,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1695
2383
|
rawLines: raw,
|
|
1696
2384
|
prose: raw.map(stripLineComment),
|
|
1697
2385
|
hasMeaningfulJsdocTag: false,
|
|
2386
|
+
isRustDoc,
|
|
1698
2387
|
nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
|
|
1699
2388
|
});
|
|
1700
2389
|
continue;
|
|
@@ -1728,6 +2417,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1728
2417
|
rawLines: raw,
|
|
1729
2418
|
prose,
|
|
1730
2419
|
hasMeaningfulJsdocTag: hasMeaningful,
|
|
2420
|
+
isRustDoc: false,
|
|
1731
2421
|
nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
|
|
1732
2422
|
});
|
|
1733
2423
|
continue;
|
|
@@ -1778,6 +2468,22 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
|
1778
2468
|
return false;
|
|
1779
2469
|
};
|
|
1780
2470
|
const looksLikeSuppressDirective = (block) => block.rawLines.some((l) => /\b(biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(l));
|
|
2471
|
+
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
2472
|
+
const looksLikeGoDocComment = (block, ext) => {
|
|
2473
|
+
if (ext !== ".go" || block.kind !== "line") return false;
|
|
2474
|
+
const next = block.nextNonBlankLine;
|
|
2475
|
+
if (!next) return false;
|
|
2476
|
+
const declMatch = GO_DECL_NAME_RE.exec(next.trim());
|
|
2477
|
+
if (!declMatch) return false;
|
|
2478
|
+
return ((block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "") === declMatch[1];
|
|
2479
|
+
};
|
|
2480
|
+
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):/i;
|
|
2481
|
+
const hasDocIndicator = (block) => {
|
|
2482
|
+
const joined = block.prose.join(" ");
|
|
2483
|
+
if (DOC_INDICATOR_RE.test(joined)) return true;
|
|
2484
|
+
for (const l of block.prose) if (/^[-]\s/.test(l)) return true;
|
|
2485
|
+
return false;
|
|
2486
|
+
};
|
|
1781
2487
|
const detectNarrativeInBlock = (block, ext) => {
|
|
1782
2488
|
if (looksLikeLicenseHeader(block)) return {
|
|
1783
2489
|
matched: false,
|
|
@@ -1791,6 +2497,14 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1791
2497
|
matched: false,
|
|
1792
2498
|
reason: ""
|
|
1793
2499
|
};
|
|
2500
|
+
if (block.isRustDoc) return {
|
|
2501
|
+
matched: false,
|
|
2502
|
+
reason: ""
|
|
2503
|
+
};
|
|
2504
|
+
if (looksLikeGoDocComment(block, ext)) return {
|
|
2505
|
+
matched: false,
|
|
2506
|
+
reason: ""
|
|
2507
|
+
};
|
|
1794
2508
|
if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
|
|
1795
2509
|
matched: true,
|
|
1796
2510
|
reason: "decorative separator"
|
|
@@ -1803,11 +2517,16 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1803
2517
|
matched: true,
|
|
1804
2518
|
reason: "bare section label"
|
|
1805
2519
|
};
|
|
2520
|
+
const joined = block.prose.join(" ");
|
|
2521
|
+
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
2522
|
+
if ((hasWhyMarker || hasDocIndicator(block)) && block.kind === "jsdoc") return {
|
|
2523
|
+
matched: false,
|
|
2524
|
+
reason: ""
|
|
2525
|
+
};
|
|
1806
2526
|
if (block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
1807
2527
|
matched: true,
|
|
1808
2528
|
reason: block.kind === "jsdoc" ? "JSDoc preamble before declaration" : "multi-line preamble before declaration"
|
|
1809
2529
|
};
|
|
1810
|
-
const joined = block.prose.join(" ");
|
|
1811
2530
|
if (CROSS_REFERENCE_PHRASES.some((re) => re.test(joined))) return {
|
|
1812
2531
|
matched: true,
|
|
1813
2532
|
reason: "cross-reference commentary"
|
|
@@ -1823,8 +2542,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1823
2542
|
reason: "explanatory preamble"
|
|
1824
2543
|
};
|
|
1825
2544
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
1826
|
-
const joinedProse = block.prose.join(" ");
|
|
1827
|
-
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joinedProse);
|
|
1828
2545
|
if (nonEmptyProseCount >= 5) return {
|
|
1829
2546
|
matched: true,
|
|
1830
2547
|
reason: "long narrative block"
|
|
@@ -1847,6 +2564,8 @@ const detectNarrativeComments = async (context) => {
|
|
|
1847
2564
|
if (isAutoGenerated(filePath)) continue;
|
|
1848
2565
|
const syntax = getCommentSyntax(ext);
|
|
1849
2566
|
if (!syntax) continue;
|
|
2567
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2568
|
+
if (NON_PRODUCTION_DIR_PATTERN.test(relativePath)) continue;
|
|
1850
2569
|
let content;
|
|
1851
2570
|
try {
|
|
1852
2571
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -1854,7 +2573,6 @@ const detectNarrativeComments = async (context) => {
|
|
|
1854
2573
|
continue;
|
|
1855
2574
|
}
|
|
1856
2575
|
const blocks = collectBlocks(content.split("\n"), syntax);
|
|
1857
|
-
const relativePath = filePath.replace(`${context.rootDirectory}/`, "");
|
|
1858
2576
|
for (const block of blocks) {
|
|
1859
2577
|
const { matched, reason } = detectNarrativeInBlock(block, ext);
|
|
1860
2578
|
if (!matched) continue;
|
|
@@ -1874,48 +2592,293 @@ const detectNarrativeComments = async (context) => {
|
|
|
1874
2592
|
}
|
|
1875
2593
|
return diagnostics;
|
|
1876
2594
|
};
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
2595
|
+
|
|
2596
|
+
//#endregion
|
|
2597
|
+
//#region src/engines/ai-slop/python-patterns.ts
|
|
2598
|
+
const PY_EXTENSIONS$1 = new Set([".py"]);
|
|
2599
|
+
const BARE_EXCEPT_RE = /^\s*except\s*:\s*(?:#.*)?$/;
|
|
2600
|
+
const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\s*:\s*(?:#.*)?$/;
|
|
2601
|
+
const PRINT_RE = /^\s*print\s*\(/;
|
|
2602
|
+
const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
|
|
2603
|
+
const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
|
|
2604
|
+
const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
|
|
2605
|
+
const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
|
|
2606
|
+
const SCRIPT_DIR_NAMES = new Set([
|
|
2607
|
+
"scripts",
|
|
2608
|
+
"bin",
|
|
2609
|
+
".github",
|
|
2610
|
+
"action",
|
|
2611
|
+
"docs",
|
|
2612
|
+
"docs_src",
|
|
2613
|
+
"examples",
|
|
2614
|
+
"example"
|
|
2615
|
+
]);
|
|
2616
|
+
const isInScriptDir = (relPath) => relPath.split(path.sep).some((seg) => SCRIPT_DIR_NAMES.has(seg));
|
|
2617
|
+
const isTutorialFile = (basename) => basename.startsWith("tutorial") && basename.endsWith(".py");
|
|
2618
|
+
const MAIN_GUARD_RE = /^\s*if\s+__name__\s*==\s*["']__main__["']\s*:/;
|
|
2619
|
+
const hasMainGuard = (lines) => lines.some((l) => MAIN_GUARD_RE.test(l));
|
|
2620
|
+
const buildDocstringRanges = (lines) => {
|
|
2621
|
+
const inside = /* @__PURE__ */ new Set();
|
|
2622
|
+
let openDelim = null;
|
|
2623
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2624
|
+
const line = lines[i];
|
|
2625
|
+
if (openDelim) {
|
|
2626
|
+
inside.add(i);
|
|
2627
|
+
if (line.includes(openDelim)) openDelim = null;
|
|
2628
|
+
continue;
|
|
2629
|
+
}
|
|
2630
|
+
for (const delim of ["\"\"\"", "'''"]) {
|
|
2631
|
+
const first = line.indexOf(delim);
|
|
2632
|
+
if (first === -1) continue;
|
|
2633
|
+
if (line.indexOf(delim, first + 3) === -1) {
|
|
2634
|
+
openDelim = delim;
|
|
2635
|
+
inside.add(i);
|
|
2636
|
+
break;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
1886
2639
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2640
|
+
return inside;
|
|
2641
|
+
};
|
|
2642
|
+
const pushFinding = (out, a) => {
|
|
2643
|
+
out.push({
|
|
2644
|
+
filePath: a.relPath,
|
|
2645
|
+
engine: "ai-slop",
|
|
2646
|
+
rule: a.rule,
|
|
2647
|
+
severity: a.severity,
|
|
2648
|
+
message: a.message,
|
|
2649
|
+
help: a.help,
|
|
2650
|
+
line: a.line,
|
|
2651
|
+
column: 1,
|
|
2652
|
+
category: "AI Slop",
|
|
2653
|
+
fixable: false
|
|
2654
|
+
});
|
|
2655
|
+
};
|
|
2656
|
+
const flagBareExcept = (lines, relPath, out) => {
|
|
2657
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2658
|
+
if (!BARE_EXCEPT_RE.test(lines[i])) continue;
|
|
2659
|
+
pushFinding(out, {
|
|
2660
|
+
relPath,
|
|
2661
|
+
rule: "ai-slop/python-bare-except",
|
|
2662
|
+
severity: "warning",
|
|
2663
|
+
message: "Bare `except:` swallows every exception including KeyboardInterrupt and SystemExit.",
|
|
2664
|
+
help: "Catch the specific exception type you actually expect (`except ValueError:`, `except (KeyError, IndexError):`). If you genuinely want everything, `except BaseException:` plus a re-raise or log makes the intent explicit.",
|
|
2665
|
+
line: i + 1
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
};
|
|
2669
|
+
const flagBroadExceptWithSilentBody = (lines, relPath, out) => {
|
|
2670
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2671
|
+
const match = BROAD_EXCEPT_RE.exec(lines[i]);
|
|
2672
|
+
if (!match) continue;
|
|
2673
|
+
const trimmedNext = (lines[i + 1] ?? "").trim();
|
|
2674
|
+
if (!(trimmedNext === "pass" || trimmedNext.startsWith("#") && (lines[i + 2] ?? "").trim() === "pass")) continue;
|
|
2675
|
+
pushFinding(out, {
|
|
2676
|
+
relPath,
|
|
2677
|
+
rule: "ai-slop/python-broad-except",
|
|
2678
|
+
severity: "warning",
|
|
2679
|
+
message: `\`except ${match[1]}: pass\` silently drops every exception. Failures vanish without a trace.`,
|
|
2680
|
+
help: "Either narrow the exception class (`except ValueError:`), log the error, or re-raise. If you genuinely intend to swallow, add a comment naming the specific failure mode you're handling — auditors will thank you.",
|
|
2681
|
+
line: i + 1
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
const flagMutableDefaults = (lines, relPath, out) => {
|
|
2686
|
+
let i = 0;
|
|
2687
|
+
while (i < lines.length) {
|
|
2688
|
+
if (!DEF_RE.test(lines[i])) {
|
|
2689
|
+
i++;
|
|
2690
|
+
continue;
|
|
2691
|
+
}
|
|
2692
|
+
const startLine = i;
|
|
2693
|
+
let signature = lines[i];
|
|
2694
|
+
let parenDepth = 0;
|
|
2695
|
+
for (const ch of signature) if (ch === "(") parenDepth++;
|
|
2696
|
+
else if (ch === ")") parenDepth--;
|
|
2697
|
+
while (parenDepth > 0 && i + 1 < lines.length) {
|
|
2698
|
+
i++;
|
|
2699
|
+
signature += `\n${lines[i]}`;
|
|
2700
|
+
for (const ch of lines[i]) if (ch === "(") parenDepth++;
|
|
2701
|
+
else if (ch === ")") parenDepth--;
|
|
2702
|
+
}
|
|
2703
|
+
MUTABLE_DEFAULT_RE.lastIndex = 0;
|
|
2704
|
+
const found = MUTABLE_DEFAULT_RE.exec(signature);
|
|
2705
|
+
if (found) pushFinding(out, {
|
|
2706
|
+
relPath,
|
|
2707
|
+
rule: "ai-slop/python-mutable-default",
|
|
2708
|
+
severity: "warning",
|
|
2709
|
+
message: `Mutable default argument \`${found[1]}=${found[2]}\`. The default is shared across all calls — bugs that look like state-leakage.`,
|
|
2710
|
+
help: "Use `None` as the default and create the mutable value inside the body: `def f(items=None): items = items if items is not None else []`. Standard Python idiom; anything else is the AI agent shortcutting.",
|
|
2711
|
+
line: startLine + 1
|
|
2712
|
+
});
|
|
2713
|
+
i++;
|
|
2714
|
+
}
|
|
2715
|
+
};
|
|
2716
|
+
const flagPrintInProduction = (lines, relPath, basename, out) => {
|
|
2717
|
+
if (isTestFile$1(relPath, basename) || isScriptOrEntrypoint(basename)) return;
|
|
2718
|
+
if (isInScriptDir(relPath)) return;
|
|
2719
|
+
if (isTutorialFile(basename)) return;
|
|
2720
|
+
if (hasMainGuard(lines)) return;
|
|
2721
|
+
const docstringLines = buildDocstringRanges(lines);
|
|
2722
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2723
|
+
const line = lines[i];
|
|
2724
|
+
if (!PRINT_RE.test(line)) continue;
|
|
2725
|
+
if (line.trim().startsWith("#")) continue;
|
|
2726
|
+
if (docstringLines.has(i)) continue;
|
|
2727
|
+
pushFinding(out, {
|
|
2728
|
+
relPath,
|
|
2729
|
+
rule: "ai-slop/python-print-debug",
|
|
2730
|
+
severity: "warning",
|
|
2731
|
+
message: "`print()` in production code — usually a leftover debug statement.",
|
|
2732
|
+
help: "Use the project's logger (`logging.getLogger(__name__).info(...)`). If this file is genuinely a CLI entry point (typer/click/argparse), it's safe to ignore — but rename to `__main__.py` or move under `scripts/` so the rule skips it next time.",
|
|
2733
|
+
line: i + 1
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
};
|
|
2737
|
+
const detectPythonPatterns = async (context) => {
|
|
2738
|
+
const diagnostics = [];
|
|
2739
|
+
const files = getSourceFiles(context);
|
|
2740
|
+
for (const filePath of files) {
|
|
2741
|
+
if (!PY_EXTENSIONS$1.has(path.extname(filePath))) continue;
|
|
2742
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1890
2743
|
let content;
|
|
1891
2744
|
try {
|
|
1892
2745
|
content = fs.readFileSync(filePath, "utf-8");
|
|
1893
2746
|
} catch {
|
|
1894
2747
|
continue;
|
|
1895
2748
|
}
|
|
2749
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2750
|
+
const basename = path.basename(filePath);
|
|
1896
2751
|
const lines = content.split("\n");
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2752
|
+
flagBareExcept(lines, relPath, diagnostics);
|
|
2753
|
+
flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
|
|
2754
|
+
flagMutableDefaults(lines, relPath, diagnostics);
|
|
2755
|
+
flagPrintInProduction(lines, relPath, basename, diagnostics);
|
|
2756
|
+
}
|
|
2757
|
+
return diagnostics;
|
|
2758
|
+
};
|
|
2759
|
+
|
|
2760
|
+
//#endregion
|
|
2761
|
+
//#region src/engines/ai-slop/rust-patterns.ts
|
|
2762
|
+
const RUST_EXTENSIONS = new Set([".rs"]);
|
|
2763
|
+
const UNWRAP_CALL_RE = /\.unwrap\s*\(\s*\)/;
|
|
2764
|
+
const TODO_MACRO_RE = /\b(todo|unimplemented)\s*!\s*\(/;
|
|
2765
|
+
const COMMENT_LINE_RE = /^\s*\/\//;
|
|
2766
|
+
const TEST_ATTR_RE = /^\s*#\s*\[\s*(?:cfg\s*\(\s*test\s*\)|test|tokio::test)/;
|
|
2767
|
+
const WRITELN_UNWRAP_RE = /\b(?:writeln|write)\s*!\s*\([^)]*\)\s*\.unwrap\s*\(\s*\)/;
|
|
2768
|
+
const TEST_BASENAMES = new Set([
|
|
2769
|
+
"tests.rs",
|
|
2770
|
+
"testutil.rs",
|
|
2771
|
+
"test_util.rs",
|
|
2772
|
+
"test_utils.rs",
|
|
2773
|
+
"build.rs"
|
|
2774
|
+
]);
|
|
2775
|
+
const TEST_CRATE_SEGMENT_RE = /(?:^|[-_])tests?(?:$|[-_])/;
|
|
2776
|
+
const isTestFile = (relPath) => {
|
|
2777
|
+
const segments = relPath.split(path.sep);
|
|
2778
|
+
if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
|
|
2779
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
2780
|
+
if (TEST_BASENAMES.has(basename)) return true;
|
|
2781
|
+
return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
|
|
2782
|
+
};
|
|
2783
|
+
const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
|
|
2784
|
+
const UNWRAP_INTENT_LOOKBACK = 2;
|
|
2785
|
+
const hasIntentComment = (lines, lineIdx) => {
|
|
2786
|
+
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - UNWRAP_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE.test(lines[j])) return true;
|
|
2787
|
+
return false;
|
|
2788
|
+
};
|
|
2789
|
+
const buildTestRanges = (lines) => {
|
|
2790
|
+
const ranges = [];
|
|
2791
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2792
|
+
if (!TEST_ATTR_RE.test(lines[i])) continue;
|
|
2793
|
+
const openLine = i;
|
|
2794
|
+
let depth = 0;
|
|
2795
|
+
let started = false;
|
|
2796
|
+
for (let j = i; j < lines.length; j++) {
|
|
2797
|
+
const line = lines[j];
|
|
2798
|
+
for (const ch of line) if (ch === "{") {
|
|
2799
|
+
depth++;
|
|
2800
|
+
started = true;
|
|
2801
|
+
} else if (ch === "}") depth--;
|
|
2802
|
+
if (started && depth === 0) {
|
|
2803
|
+
ranges.push([openLine, j]);
|
|
2804
|
+
i = j;
|
|
2805
|
+
break;
|
|
2806
|
+
}
|
|
1908
2807
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2808
|
+
}
|
|
2809
|
+
return ranges;
|
|
2810
|
+
};
|
|
2811
|
+
const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
|
|
2812
|
+
const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
|
|
2813
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2814
|
+
const line = lines[i];
|
|
2815
|
+
if (COMMENT_LINE_RE.test(line)) continue;
|
|
2816
|
+
if (isInRange(testRanges, i)) continue;
|
|
2817
|
+
if (!UNWRAP_CALL_RE.test(line)) continue;
|
|
2818
|
+
if (WRITELN_UNWRAP_RE.test(line)) continue;
|
|
2819
|
+
if (hasIntentComment(lines, i)) continue;
|
|
2820
|
+
out.push({
|
|
2821
|
+
filePath: relPath,
|
|
2822
|
+
engine: "ai-slop",
|
|
2823
|
+
rule: "ai-slop/rust-non-test-unwrap",
|
|
2824
|
+
severity: "warning",
|
|
2825
|
+
message: "`.unwrap()` in non-test code panics on None/Err. Surfaces as a hard crash for the caller.",
|
|
2826
|
+
help: "Use `?` to propagate, `.expect(\"context\")` if you really mean it (and the message names the invariant), or pattern-match the variant you care about. Reserve raw `.unwrap()` for tests and prototypes.",
|
|
2827
|
+
line: i + 1,
|
|
2828
|
+
column: 1,
|
|
2829
|
+
category: "AI Slop",
|
|
2830
|
+
fixable: false
|
|
2831
|
+
});
|
|
1913
2832
|
}
|
|
1914
2833
|
};
|
|
2834
|
+
const flagTodoMacro = (lines, relPath, out) => {
|
|
2835
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2836
|
+
const line = lines[i];
|
|
2837
|
+
if (COMMENT_LINE_RE.test(line)) continue;
|
|
2838
|
+
const match = TODO_MACRO_RE.exec(line);
|
|
2839
|
+
if (!match) continue;
|
|
2840
|
+
out.push({
|
|
2841
|
+
filePath: relPath,
|
|
2842
|
+
engine: "ai-slop",
|
|
2843
|
+
rule: "ai-slop/rust-todo-stub",
|
|
2844
|
+
severity: "warning",
|
|
2845
|
+
message: `\`${match[1]}!()\` panics at runtime — almost certainly a stub the agent forgot to fill in.`,
|
|
2846
|
+
help: "Implement the missing path or remove it. If the work is genuinely deferred, file a ticket and put the number in a comment next to the macro so it doesn't ship invisibly.",
|
|
2847
|
+
line: i + 1,
|
|
2848
|
+
column: 1,
|
|
2849
|
+
category: "AI Slop",
|
|
2850
|
+
fixable: false
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
};
|
|
2854
|
+
const detectRustPatterns = async (context) => {
|
|
2855
|
+
const diagnostics = [];
|
|
2856
|
+
const files = getSourceFiles(context);
|
|
2857
|
+
for (const filePath of files) {
|
|
2858
|
+
if (!RUST_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
2859
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2860
|
+
let content;
|
|
2861
|
+
try {
|
|
2862
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2863
|
+
} catch {
|
|
2864
|
+
continue;
|
|
2865
|
+
}
|
|
2866
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2867
|
+
const lines = content.split("\n");
|
|
2868
|
+
if (isExampleFile(relPath)) continue;
|
|
2869
|
+
if (isTestFile(relPath)) {
|
|
2870
|
+
flagTodoMacro(lines, relPath, diagnostics);
|
|
2871
|
+
continue;
|
|
2872
|
+
}
|
|
2873
|
+
flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
|
|
2874
|
+
flagTodoMacro(lines, relPath, diagnostics);
|
|
2875
|
+
}
|
|
2876
|
+
return diagnostics;
|
|
2877
|
+
};
|
|
1915
2878
|
|
|
1916
2879
|
//#endregion
|
|
1917
2880
|
//#region src/engines/ai-slop/unused-imports.ts
|
|
1918
|
-
const JS_EXTENSIONS = new Set([
|
|
2881
|
+
const JS_EXTENSIONS$1 = new Set([
|
|
1919
2882
|
".ts",
|
|
1920
2883
|
".tsx",
|
|
1921
2884
|
".js",
|
|
@@ -2045,7 +3008,7 @@ const analyzeFile = (filePath) => {
|
|
|
2045
3008
|
const lines = content.split("\n");
|
|
2046
3009
|
let symbols;
|
|
2047
3010
|
let importLines;
|
|
2048
|
-
if (JS_EXTENSIONS.has(ext)) {
|
|
3011
|
+
if (JS_EXTENSIONS$1.has(ext)) {
|
|
2049
3012
|
const result = extractJsImportedSymbols(lines);
|
|
2050
3013
|
symbols = result.symbols;
|
|
2051
3014
|
importLines = result.importLines;
|
|
@@ -2101,7 +3064,12 @@ const aiSlopEngine = {
|
|
|
2101
3064
|
detectOverAbstraction(context),
|
|
2102
3065
|
detectDeadPatterns(context),
|
|
2103
3066
|
detectUnusedImports(context),
|
|
2104
|
-
detectNarrativeComments(context)
|
|
3067
|
+
detectNarrativeComments(context),
|
|
3068
|
+
detectDuplicateImports(context),
|
|
3069
|
+
detectPythonPatterns(context),
|
|
3070
|
+
detectGoPatterns(context),
|
|
3071
|
+
detectRustPatterns(context),
|
|
3072
|
+
detectHallucinatedImports(context)
|
|
2105
3073
|
]);
|
|
2106
3074
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
2107
3075
|
return {
|
|
@@ -2485,6 +3453,12 @@ const isDataFile = (content) => {
|
|
|
2485
3453
|
const dataLinePattern = /^\s*[{}[\]"']/;
|
|
2486
3454
|
return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
|
|
2487
3455
|
};
|
|
3456
|
+
const TEST_PATH_RE = /(?:^|\/)(?:tests?|spec|specs|__tests__|__spec__|src\/test)\//i;
|
|
3457
|
+
const TEST_BASENAME_RE = /(?:^|[/.])(?:test_[\w-]+\.(?:py|rb)|[\w-]+_(?:test|spec)\.(?:py|rb|go|rs)|[\w-]+\.(?:test|spec)\.(?:[jt]sx?|mjs|cjs)|conftest\.py|[A-Z]\w*Tests?\.(?:java|cs|php))$/;
|
|
3458
|
+
const MIGRATION_PATH_RE = /(?:^|\/)(?:migrations?|migrate|prisma\/migrations|db\/migrate)\//i;
|
|
3459
|
+
const FIXTURE_PATH_RE = /(?:^|\/)(?:__fixtures__|__snapshots__|__mocks__|fixtures?|snapshots?|seeds?|stubs?)\//i;
|
|
3460
|
+
const GENERATED_PATH_RE = /(?:^|\/)(?:generated|gen|build|dist|out|target|coverage|node_modules|vendor|\.next|\.nuxt|\.svelte-kit)\//i;
|
|
3461
|
+
const isExemptFromComplexity = (relativePath) => TEST_PATH_RE.test(relativePath) || TEST_BASENAME_RE.test(relativePath) || MIGRATION_PATH_RE.test(relativePath) || FIXTURE_PATH_RE.test(relativePath) || GENERATED_PATH_RE.test(relativePath);
|
|
2488
3462
|
const analyzeFunctions = (content, ext) => {
|
|
2489
3463
|
const lines = content.split("\n");
|
|
2490
3464
|
const functions = [];
|
|
@@ -2513,13 +3487,13 @@ const checkFileDiagnostics = (relativePath, content, limits) => {
|
|
|
2513
3487
|
const lineCount = content.split("\n").length;
|
|
2514
3488
|
const ext = path.extname(relativePath).toLowerCase();
|
|
2515
3489
|
if (isDataFile(content)) return results;
|
|
2516
|
-
const
|
|
2517
|
-
if (lineCount >
|
|
3490
|
+
const configuredMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
|
|
3491
|
+
if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
|
|
2518
3492
|
filePath: relativePath,
|
|
2519
3493
|
engine: "code-quality",
|
|
2520
3494
|
rule: "complexity/file-too-large",
|
|
2521
3495
|
severity: "warning",
|
|
2522
|
-
message: `File has ${lineCount} lines (max: ${
|
|
3496
|
+
message: `File has ${lineCount} lines (max: ${configuredMax})`,
|
|
2523
3497
|
help: "Consider splitting this file into smaller modules",
|
|
2524
3498
|
line: 0,
|
|
2525
3499
|
column: 0,
|
|
@@ -2569,13 +3543,14 @@ const checkFunctionDiagnostics = (relativePath, fn, limits) => {
|
|
|
2569
3543
|
return results;
|
|
2570
3544
|
};
|
|
2571
3545
|
const checkFileComplexity = (filePath, rootDirectory, limits) => {
|
|
3546
|
+
const relativePath = path.relative(rootDirectory, filePath);
|
|
3547
|
+
if (isExemptFromComplexity(relativePath)) return [];
|
|
2572
3548
|
let content;
|
|
2573
3549
|
try {
|
|
2574
3550
|
content = fs.readFileSync(filePath, "utf-8");
|
|
2575
3551
|
} catch {
|
|
2576
3552
|
return [];
|
|
2577
3553
|
}
|
|
2578
|
-
const relativePath = path.relative(rootDirectory, filePath);
|
|
2579
3554
|
const ext = path.extname(filePath).toLowerCase();
|
|
2580
3555
|
const diagnostics = checkFileDiagnostics(relativePath, content, limits);
|
|
2581
3556
|
for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
|
|
@@ -3957,7 +4932,10 @@ const lintEngine = {
|
|
|
3957
4932
|
const diagnostics = [];
|
|
3958
4933
|
const { languages, installedTools } = context;
|
|
3959
4934
|
const promises = [];
|
|
3960
|
-
if (languages.includes("typescript") || languages.includes("javascript"))
|
|
4935
|
+
if (languages.includes("typescript") || languages.includes("javascript")) {
|
|
4936
|
+
promises.push(runOxlint(context));
|
|
4937
|
+
if (context.config.lint.typecheck) promises.push(import("./typecheck-XJMuCczG.js").then((mod) => mod.runTypecheck(context)));
|
|
4938
|
+
}
|
|
3961
4939
|
if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-Bz0LZhQ6.js").then((n) => n.t).then((mod) => mod.runExpoDoctor(context)));
|
|
3962
4940
|
if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
|
|
3963
4941
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
@@ -5315,6 +6293,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
5315
6293
|
const engineConfig = {
|
|
5316
6294
|
quality: config.quality,
|
|
5317
6295
|
security: config.security,
|
|
6296
|
+
lint: config.lint,
|
|
5318
6297
|
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
5319
6298
|
};
|
|
5320
6299
|
const gridRows = ALL_ENGINE_NAMES.filter((engine) => config.engines[engine] !== false).map((engine) => ({
|
|
@@ -5382,7 +6361,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
5382
6361
|
});
|
|
5383
6362
|
}
|
|
5384
6363
|
if (options.json) {
|
|
5385
|
-
const { buildJsonOutput } = await import("./json-
|
|
6364
|
+
const { buildJsonOutput } = await import("./json-BJGLCIK-.js");
|
|
5386
6365
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
5387
6366
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
5388
6367
|
return { exitCode };
|
|
@@ -5737,6 +6716,212 @@ const applyEditsAndCollapse = (lines, linesToRemove, lineReplacements) => {
|
|
|
5737
6716
|
return collapsed.join("\n");
|
|
5738
6717
|
};
|
|
5739
6718
|
|
|
6719
|
+
//#endregion
|
|
6720
|
+
//#region src/engines/ai-slop/duplicate-imports-fix.ts
|
|
6721
|
+
const JS_EXTENSIONS = new Set([
|
|
6722
|
+
".ts",
|
|
6723
|
+
".tsx",
|
|
6724
|
+
".js",
|
|
6725
|
+
".jsx",
|
|
6726
|
+
".mjs",
|
|
6727
|
+
".cjs"
|
|
6728
|
+
]);
|
|
6729
|
+
const IMPORT_FROM_RE = /^\s*import\s+(.*?)\s+from\s+["']([^"']+)["']\s*;?\s*$/;
|
|
6730
|
+
const SIDE_EFFECT_RE = /^\s*import\s+["']([^"']+)["']\s*;?\s*$/;
|
|
6731
|
+
const parseNamedClause = (clause) => {
|
|
6732
|
+
const inner = clause.trim().slice(1, -1).trim();
|
|
6733
|
+
if (inner.length === 0) return [];
|
|
6734
|
+
const items = [];
|
|
6735
|
+
for (const part of inner.split(",")) {
|
|
6736
|
+
const trimmed = part.trim();
|
|
6737
|
+
if (!trimmed) continue;
|
|
6738
|
+
let isType = false;
|
|
6739
|
+
let working = trimmed;
|
|
6740
|
+
if (/^type\s+/.test(working)) {
|
|
6741
|
+
isType = true;
|
|
6742
|
+
working = working.replace(/^type\s+/, "");
|
|
6743
|
+
}
|
|
6744
|
+
const aliasMatch = working.match(/^(\w+)\s+as\s+(\w+)$/);
|
|
6745
|
+
if (aliasMatch) {
|
|
6746
|
+
items.push({
|
|
6747
|
+
name: aliasMatch[1],
|
|
6748
|
+
alias: aliasMatch[2],
|
|
6749
|
+
isType
|
|
6750
|
+
});
|
|
6751
|
+
continue;
|
|
6752
|
+
}
|
|
6753
|
+
if (/^\w+$/.test(working)) items.push({
|
|
6754
|
+
name: working,
|
|
6755
|
+
isType
|
|
6756
|
+
});
|
|
6757
|
+
}
|
|
6758
|
+
return items;
|
|
6759
|
+
};
|
|
6760
|
+
const parseImportClause = (clause) => {
|
|
6761
|
+
let rest = clause.trim();
|
|
6762
|
+
let isTypeOnly = false;
|
|
6763
|
+
if (/^type\s+/.test(rest)) {
|
|
6764
|
+
isTypeOnly = true;
|
|
6765
|
+
rest = rest.replace(/^type\s+/, "");
|
|
6766
|
+
}
|
|
6767
|
+
const out = {
|
|
6768
|
+
named: [],
|
|
6769
|
+
isTypeOnly
|
|
6770
|
+
};
|
|
6771
|
+
const defMatch = rest.match(/^([A-Za-z_$][\w$]*)\s*(?:,\s*(.+))?$/);
|
|
6772
|
+
if (defMatch && !rest.startsWith("{") && !rest.startsWith("*")) {
|
|
6773
|
+
out.default = defMatch[1];
|
|
6774
|
+
rest = defMatch[2]?.trim() ?? "";
|
|
6775
|
+
}
|
|
6776
|
+
if (rest.startsWith("*")) {
|
|
6777
|
+
const nsMatch = rest.match(/^\*\s+as\s+(\w+)/);
|
|
6778
|
+
if (nsMatch) out.namespace = nsMatch[1];
|
|
6779
|
+
return out;
|
|
6780
|
+
}
|
|
6781
|
+
if (rest.startsWith("{")) out.named = parseNamedClause(rest);
|
|
6782
|
+
return out;
|
|
6783
|
+
};
|
|
6784
|
+
const parseImportLine = (line, lineIndex) => {
|
|
6785
|
+
const sideEffect = line.match(SIDE_EFFECT_RE);
|
|
6786
|
+
if (sideEffect) return {
|
|
6787
|
+
lineIndex,
|
|
6788
|
+
module: sideEffect[1],
|
|
6789
|
+
named: [],
|
|
6790
|
+
isTypeOnly: false,
|
|
6791
|
+
isSideEffect: true
|
|
6792
|
+
};
|
|
6793
|
+
const m = line.match(IMPORT_FROM_RE);
|
|
6794
|
+
if (!m) return null;
|
|
6795
|
+
return {
|
|
6796
|
+
lineIndex,
|
|
6797
|
+
module: m[2],
|
|
6798
|
+
isSideEffect: false,
|
|
6799
|
+
...parseImportClause(m[1])
|
|
6800
|
+
};
|
|
6801
|
+
};
|
|
6802
|
+
const formatNamed = (n, stripType) => {
|
|
6803
|
+
const prefix = n.isType && !stripType ? "type " : "";
|
|
6804
|
+
const suffix = n.alias ? ` as ${n.alias}` : "";
|
|
6805
|
+
return `${prefix}${n.name}${suffix}`;
|
|
6806
|
+
};
|
|
6807
|
+
const mergeImports = (group) => {
|
|
6808
|
+
if (group.some((s) => s.isSideEffect)) return null;
|
|
6809
|
+
if (group.some((s) => s.namespace !== void 0)) return null;
|
|
6810
|
+
if (group.some((s) => s.isTypeOnly && s.default !== void 0)) return null;
|
|
6811
|
+
const defaults = group.map((s) => s.default).filter((d) => d !== void 0);
|
|
6812
|
+
const uniqueDefaults = Array.from(new Set(defaults));
|
|
6813
|
+
if (uniqueDefaults.length > 1) return null;
|
|
6814
|
+
const defaultName = uniqueDefaults[0];
|
|
6815
|
+
const merged = /* @__PURE__ */ new Map();
|
|
6816
|
+
for (const stmt of group) for (const nm of stmt.named) {
|
|
6817
|
+
const key = nm.alias ?? nm.name;
|
|
6818
|
+
const isType = nm.isType || stmt.isTypeOnly;
|
|
6819
|
+
const existing = merged.get(key);
|
|
6820
|
+
if (!existing) merged.set(key, {
|
|
6821
|
+
...nm,
|
|
6822
|
+
isType
|
|
6823
|
+
});
|
|
6824
|
+
else existing.isType = existing.isType && isType;
|
|
6825
|
+
}
|
|
6826
|
+
const insertionOrder = Array.from(merged.values());
|
|
6827
|
+
const namedList = [...insertionOrder.filter((n) => !n.isType), ...insertionOrder.filter((n) => n.isType)];
|
|
6828
|
+
const allTypeOnly = namedList.length > 0 && namedList.every((n) => n.isType);
|
|
6829
|
+
const module = group[0].module;
|
|
6830
|
+
if (!defaultName && namedList.length === 0) return null;
|
|
6831
|
+
if (!defaultName && allTypeOnly) return `import type { ${namedList.map((n) => formatNamed(n, true)).join(", ")} } from "${module}";`;
|
|
6832
|
+
const parts = [];
|
|
6833
|
+
if (defaultName) parts.push(defaultName);
|
|
6834
|
+
if (namedList.length > 0) {
|
|
6835
|
+
const items = namedList.map((n) => formatNamed(n, false)).join(", ");
|
|
6836
|
+
parts.push(`{ ${items} }`);
|
|
6837
|
+
}
|
|
6838
|
+
return `import ${parts.join(", ")} from "${module}";`;
|
|
6839
|
+
};
|
|
6840
|
+
const fixDuplicateImports = async (context) => {
|
|
6841
|
+
const files = getSourceFiles(context);
|
|
6842
|
+
for (const filePath of files) {
|
|
6843
|
+
if (!JS_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
6844
|
+
if (isAutoGenerated(filePath)) continue;
|
|
6845
|
+
let content;
|
|
6846
|
+
try {
|
|
6847
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
6848
|
+
} catch {
|
|
6849
|
+
continue;
|
|
6850
|
+
}
|
|
6851
|
+
const lines = content.split("\n");
|
|
6852
|
+
const imports = [];
|
|
6853
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6854
|
+
const stmt = parseImportLine(lines[i], i);
|
|
6855
|
+
if (stmt) imports.push(stmt);
|
|
6856
|
+
}
|
|
6857
|
+
if (imports.length < 2) continue;
|
|
6858
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6859
|
+
for (const stmt of imports) {
|
|
6860
|
+
const list = groups.get(stmt.module) ?? [];
|
|
6861
|
+
list.push(stmt);
|
|
6862
|
+
groups.set(stmt.module, list);
|
|
6863
|
+
}
|
|
6864
|
+
const linesToRemove = /* @__PURE__ */ new Set();
|
|
6865
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
6866
|
+
let modified = false;
|
|
6867
|
+
for (const group of groups.values()) {
|
|
6868
|
+
if (group.length < 2) continue;
|
|
6869
|
+
const merged = mergeImports(group);
|
|
6870
|
+
if (!merged) continue;
|
|
6871
|
+
replacements.set(group[0].lineIndex, merged);
|
|
6872
|
+
for (const stmt of group.slice(1)) linesToRemove.add(stmt.lineIndex);
|
|
6873
|
+
modified = true;
|
|
6874
|
+
}
|
|
6875
|
+
if (!modified) continue;
|
|
6876
|
+
const next = [...lines];
|
|
6877
|
+
for (const [idx, replacement] of replacements) next[idx] = replacement;
|
|
6878
|
+
const sortedRemove = Array.from(linesToRemove).sort((a, b) => b - a);
|
|
6879
|
+
for (const idx of sortedRemove) next.splice(idx, 1);
|
|
6880
|
+
fs.writeFileSync(filePath, next.join("\n"));
|
|
6881
|
+
}
|
|
6882
|
+
};
|
|
6883
|
+
|
|
6884
|
+
//#endregion
|
|
6885
|
+
//#region src/engines/ai-slop/narrative-comments-fix.ts
|
|
6886
|
+
const fixNarrativeComments = async (context) => {
|
|
6887
|
+
const diagnostics = await detectNarrativeComments(context);
|
|
6888
|
+
if (diagnostics.length === 0) return;
|
|
6889
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
6890
|
+
for (const d of diagnostics) {
|
|
6891
|
+
const abs = d.filePath.startsWith("/") ? d.filePath : `${context.rootDirectory}/${d.filePath}`;
|
|
6892
|
+
const list = byFile.get(abs) ?? [];
|
|
6893
|
+
list.push(d);
|
|
6894
|
+
byFile.set(abs, list);
|
|
6895
|
+
}
|
|
6896
|
+
for (const [filePath, diags] of byFile) {
|
|
6897
|
+
const syntax = getCommentSyntax(path.extname(filePath));
|
|
6898
|
+
if (!syntax) continue;
|
|
6899
|
+
let content;
|
|
6900
|
+
try {
|
|
6901
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
6902
|
+
} catch {
|
|
6903
|
+
continue;
|
|
6904
|
+
}
|
|
6905
|
+
const lines = content.split("\n");
|
|
6906
|
+
const blocks = collectBlocks(lines, syntax);
|
|
6907
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
6908
|
+
for (const d of diags) {
|
|
6909
|
+
const block = blocks.find((b) => b.startLine === d.line);
|
|
6910
|
+
if (!block) continue;
|
|
6911
|
+
for (let ln = block.startLine; ln <= block.endLine; ln += 1) toRemove.add(ln);
|
|
6912
|
+
const prev = block.startLine - 1;
|
|
6913
|
+
const next = block.endLine + 1;
|
|
6914
|
+
const prevIsBlank = prev >= 1 && lines[prev - 1]?.trim() === "";
|
|
6915
|
+
const nextIsBlank = next <= lines.length && lines[next - 1]?.trim() === "";
|
|
6916
|
+
if (prevIsBlank && nextIsBlank) toRemove.add(prev);
|
|
6917
|
+
}
|
|
6918
|
+
const kept = [];
|
|
6919
|
+
for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
|
|
6920
|
+
const newContent = kept.join("\n");
|
|
6921
|
+
if (newContent !== content) fs.writeFileSync(filePath, newContent);
|
|
6922
|
+
}
|
|
6923
|
+
};
|
|
6924
|
+
|
|
5740
6925
|
//#endregion
|
|
5741
6926
|
//#region src/engines/ai-slop/unused-imports-fix.ts
|
|
5742
6927
|
const fixUnusedImports = async (context) => {
|
|
@@ -5758,9 +6943,9 @@ const fixUnusedImports = async (context) => {
|
|
|
5758
6943
|
for (const [lineNo, syms] of symbolsByLine) {
|
|
5759
6944
|
const lineIdx = lineNo - 1;
|
|
5760
6945
|
const allUnused = syms.every((s) => unusedNames.has(s.name));
|
|
5761
|
-
const importSpan = JS_EXTENSIONS.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
|
|
6946
|
+
const importSpan = JS_EXTENSIONS$1.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
|
|
5762
6947
|
if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
|
|
5763
|
-
else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
6948
|
+
else if (JS_EXTENSIONS$1.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
5764
6949
|
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
|
|
5765
6950
|
}
|
|
5766
6951
|
if (linesToRemove.size === 0 && unused.length === 0) continue;
|
|
@@ -6363,6 +7548,7 @@ const hasJsOrTs = (projectInfo) => projectInfo.languages.includes("typescript")
|
|
|
6363
7548
|
const runAiSlopSteps = async (deps) => {
|
|
6364
7549
|
if (!deps.config.engines["ai-slop"]) return;
|
|
6365
7550
|
await deps.runStep("Unused imports", () => detectUnusedImports(deps.context), () => fixUnusedImports(deps.context));
|
|
7551
|
+
await deps.runStep("Duplicate imports", () => detectDuplicateImports(deps.context), () => fixDuplicateImports(deps.context));
|
|
6366
7552
|
const detectFixableSlop = async () => {
|
|
6367
7553
|
const [comments, dead, narrative] = await Promise.all([
|
|
6368
7554
|
detectTrivialComments(deps.context),
|
|
@@ -6468,7 +7654,8 @@ const createEngineContext = (rootDirectory, projectInfo, config) => ({
|
|
|
6468
7654
|
installedTools: projectInfo.installedTools,
|
|
6469
7655
|
config: {
|
|
6470
7656
|
quality: config.quality,
|
|
6471
|
-
security: config.security
|
|
7657
|
+
security: config.security,
|
|
7658
|
+
lint: config.lint
|
|
6472
7659
|
}
|
|
6473
7660
|
});
|
|
6474
7661
|
const fixCommand = async (directory, config, options = {
|
|
@@ -6531,6 +7718,7 @@ const fixCommand = async (directory, config, options = {
|
|
|
6531
7718
|
const engineConfig = {
|
|
6532
7719
|
quality: config.quality,
|
|
6533
7720
|
security: config.security,
|
|
7721
|
+
lint: config.lint,
|
|
6534
7722
|
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
6535
7723
|
};
|
|
6536
7724
|
rail.start("Verifying results");
|
|
@@ -6835,8 +8023,10 @@ const buildRulesRender = (input) => {
|
|
|
6835
8023
|
const AI_SLOP_FIXABLE = new Set([
|
|
6836
8024
|
"ai-slop/trivial-comment",
|
|
6837
8025
|
"ai-slop/unused-import",
|
|
6838
|
-
"ai-slop/narrative-comment"
|
|
8026
|
+
"ai-slop/narrative-comment",
|
|
8027
|
+
"ai-slop/duplicate-import"
|
|
6839
8028
|
]);
|
|
8029
|
+
const AI_SLOP_ERRORS = new Set(["ai-slop/hallucinated-import"]);
|
|
6840
8030
|
const BUILTIN_RULES = [
|
|
6841
8031
|
{
|
|
6842
8032
|
engine: "format",
|
|
@@ -6857,7 +8047,8 @@ const BUILTIN_RULES = [
|
|
|
6857
8047
|
"ruff/*",
|
|
6858
8048
|
"go/*",
|
|
6859
8049
|
"clippy/*",
|
|
6860
|
-
"rubocop/*"
|
|
8050
|
+
"rubocop/*",
|
|
8051
|
+
"typescript/*"
|
|
6861
8052
|
]
|
|
6862
8053
|
},
|
|
6863
8054
|
{
|
|
@@ -6893,7 +8084,16 @@ const BUILTIN_RULES = [
|
|
|
6893
8084
|
"ai-slop/unsafe-type-assertion",
|
|
6894
8085
|
"ai-slop/double-type-assertion",
|
|
6895
8086
|
"ai-slop/ts-directive",
|
|
6896
|
-
"ai-slop/narrative-comment"
|
|
8087
|
+
"ai-slop/narrative-comment",
|
|
8088
|
+
"ai-slop/duplicate-import",
|
|
8089
|
+
"ai-slop/python-bare-except",
|
|
8090
|
+
"ai-slop/python-broad-except",
|
|
8091
|
+
"ai-slop/python-mutable-default",
|
|
8092
|
+
"ai-slop/python-print-debug",
|
|
8093
|
+
"ai-slop/go-library-panic",
|
|
8094
|
+
"ai-slop/rust-non-test-unwrap",
|
|
8095
|
+
"ai-slop/rust-todo-stub",
|
|
8096
|
+
"ai-slop/hallucinated-import"
|
|
6897
8097
|
]
|
|
6898
8098
|
},
|
|
6899
8099
|
{
|
|
@@ -6924,7 +8124,7 @@ const toRuleEntry = (engine, ruleId) => {
|
|
|
6924
8124
|
if (engine === "ai-slop") return {
|
|
6925
8125
|
id: ruleId,
|
|
6926
8126
|
engine,
|
|
6927
|
-
severity: "warning",
|
|
8127
|
+
severity: AI_SLOP_ERRORS.has(ruleId) ? "error" : "warning",
|
|
6928
8128
|
fixable: AI_SLOP_FIXABLE.has(ruleId)
|
|
6929
8129
|
};
|
|
6930
8130
|
return {
|