aislop 0.6.2 → 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 +76 -6
- package/dist/cli.js +1581 -110
- package/dist/expo-doctor-T4DswmX5.js +136 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1308 -67
- package/dist/{json-ZItDVIZL.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-AmNwcw_U.js → version-B9ZchFMv.js} +1 -1
- package/package.json +10 -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
|
|
@@ -103,6 +104,48 @@ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
|
|
|
103
104
|
# severity: error
|
|
104
105
|
`;
|
|
105
106
|
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/config/extends.ts
|
|
109
|
+
const MAX_DEPTH = 5;
|
|
110
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
111
|
+
const deepMerge = (...sources) => {
|
|
112
|
+
const result = {};
|
|
113
|
+
for (const source of sources) for (const key of Object.keys(source)) {
|
|
114
|
+
const a = result[key];
|
|
115
|
+
const b = source[key];
|
|
116
|
+
result[key] = isPlainObject(a) && isPlainObject(b) ? deepMerge(a, b) : b;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
};
|
|
120
|
+
const resolveExtendsRef = (ref, fromDir) => {
|
|
121
|
+
if (ref.startsWith("http://") || ref.startsWith("https://")) throw new Error(`URL-based extends not yet supported: ${ref}`);
|
|
122
|
+
if (ref.startsWith("./") || ref.startsWith("../") || path.isAbsolute(ref)) return path.resolve(fromDir, ref);
|
|
123
|
+
throw new Error(`Package-name extends not yet supported: ${ref} (use a relative path for now)`);
|
|
124
|
+
};
|
|
125
|
+
const normalizeExtends = (raw) => {
|
|
126
|
+
if (raw === void 0 || raw === null) return [];
|
|
127
|
+
if (typeof raw === "string") return [raw];
|
|
128
|
+
if (Array.isArray(raw) && raw.every((s) => typeof s === "string")) return raw;
|
|
129
|
+
throw new Error("`extends` must be a string or array of strings");
|
|
130
|
+
};
|
|
131
|
+
const loadConfigChain = (configPath, visited = /* @__PURE__ */ new Set(), depth = 0) => {
|
|
132
|
+
if (depth > MAX_DEPTH) throw new Error(`extends depth exceeded ${MAX_DEPTH} (cycle or runaway chain): ${configPath}`);
|
|
133
|
+
const absPath = path.resolve(configPath);
|
|
134
|
+
if (visited.has(absPath)) throw new Error(`circular extends detected: ${absPath}`);
|
|
135
|
+
if (!fs.existsSync(absPath)) throw new Error(`extends target not found: ${absPath}`);
|
|
136
|
+
const nextVisited = new Set(visited);
|
|
137
|
+
nextVisited.add(absPath);
|
|
138
|
+
const raw = fs.readFileSync(absPath, "utf-8");
|
|
139
|
+
const parsed = YAML.parse(raw) ?? {};
|
|
140
|
+
const refs = normalizeExtends(parsed.extends);
|
|
141
|
+
const fromDir = path.dirname(absPath);
|
|
142
|
+
const parents = refs.map((ref) => {
|
|
143
|
+
return loadConfigChain(resolveExtendsRef(ref, fromDir), nextVisited, depth + 1);
|
|
144
|
+
});
|
|
145
|
+
const { extends: _drop, ...own } = parsed;
|
|
146
|
+
return deepMerge(...parents, own);
|
|
147
|
+
};
|
|
148
|
+
|
|
106
149
|
//#endregion
|
|
107
150
|
//#region src/config/schema.ts
|
|
108
151
|
const DEFAULT_WEIGHTS = {
|
|
@@ -127,6 +170,7 @@ const QualitySchema = z.object({
|
|
|
127
170
|
maxNesting: z.number().positive().default(5),
|
|
128
171
|
maxParams: z.number().positive().default(6)
|
|
129
172
|
});
|
|
173
|
+
const LintConfigSchema = z.object({ typecheck: z.boolean().default(false) });
|
|
130
174
|
const SecurityConfigSchema = z.object({
|
|
131
175
|
audit: z.boolean().default(true),
|
|
132
176
|
auditTimeout: z.number().positive().default(25e3)
|
|
@@ -164,6 +208,7 @@ const AislopConfigSchema = z.object({
|
|
|
164
208
|
maxNesting: 5,
|
|
165
209
|
maxParams: 6
|
|
166
210
|
})),
|
|
211
|
+
lint: LintConfigSchema.default(() => ({ typecheck: false })),
|
|
167
212
|
security: SecurityConfigSchema.default(() => ({
|
|
168
213
|
audit: true,
|
|
169
214
|
auditTimeout: 25e3
|
|
@@ -237,8 +282,7 @@ const loadConfig = (directory) => {
|
|
|
237
282
|
const configPath = path.join(configDir, CONFIG_FILE);
|
|
238
283
|
if (!fs.existsSync(configPath)) return DEFAULT_CONFIG;
|
|
239
284
|
try {
|
|
240
|
-
|
|
241
|
-
return parseConfig(YAML.parse(raw));
|
|
285
|
+
return parseConfig(loadConfigChain(configPath));
|
|
242
286
|
} catch (error) {
|
|
243
287
|
const msg = error instanceof Error ? error.message : String(error);
|
|
244
288
|
process.stderr.write(` ⚠ Failed to parse ${configPath}: ${msg}\n ⚠ Using default configuration.\n`);
|
|
@@ -565,7 +609,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
|
565
609
|
return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
|
|
566
610
|
};
|
|
567
611
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
568
|
-
const isTestFile = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
612
|
+
const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
569
613
|
const getIgnoredPaths = (rootDirectory, files) => {
|
|
570
614
|
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
571
615
|
const result = spawnSync("git", [
|
|
@@ -639,7 +683,7 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
|
|
|
639
683
|
return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
|
|
640
684
|
};
|
|
641
685
|
return normalizedFiles.filter(({ absolutePath, relativePath }) => {
|
|
642
|
-
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);
|
|
643
687
|
}).map(({ absolutePath }) => absolutePath);
|
|
644
688
|
};
|
|
645
689
|
const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
|
|
@@ -948,16 +992,29 @@ const planFormat = (ctx) => {
|
|
|
948
992
|
skipReason: "no supported language"
|
|
949
993
|
};
|
|
950
994
|
};
|
|
951
|
-
const
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
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,
|
|
955
1002
|
status: "ok"
|
|
956
1003
|
};
|
|
957
|
-
if (
|
|
958
|
-
tool:
|
|
1004
|
+
if (findLocalTsc(ctx.rootDirectory)) return {
|
|
1005
|
+
tool: `${baseTool} + tsc`,
|
|
959
1006
|
status: "ok"
|
|
960
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);
|
|
961
1018
|
return firstMatching(languages, installedTools, LINT_SPECS) ?? {
|
|
962
1019
|
tool: "no linter",
|
|
963
1020
|
status: "skipped",
|
|
@@ -1217,13 +1274,14 @@ const detectOverAbstraction = async (context) => {
|
|
|
1217
1274
|
|
|
1218
1275
|
//#endregion
|
|
1219
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;
|
|
1220
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";
|
|
1221
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")];
|
|
1222
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")];
|
|
1223
1281
|
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
|
|
1224
1282
|
const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
1225
1283
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
1226
|
-
const isJsComment = (trimmed) => trimmed.startsWith("//");
|
|
1284
|
+
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
1227
1285
|
const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
|
|
1228
1286
|
/**
|
|
1229
1287
|
* Extract just the comment text after the comment marker.
|
|
@@ -1272,13 +1330,14 @@ const detectTrivialComments = async (context) => {
|
|
|
1272
1330
|
const diagnostics = [];
|
|
1273
1331
|
for (const filePath of files) {
|
|
1274
1332
|
if (isAutoGenerated(filePath)) continue;
|
|
1333
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1334
|
+
if (NON_PRODUCTION_DIR_PATTERN$2.test(relativePath)) continue;
|
|
1275
1335
|
let content;
|
|
1276
1336
|
try {
|
|
1277
1337
|
content = fs.readFileSync(filePath, "utf-8");
|
|
1278
1338
|
} catch {
|
|
1279
1339
|
continue;
|
|
1280
1340
|
}
|
|
1281
|
-
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1282
1341
|
diagnostics.push(...scanFileForTrivialComments(content, relativePath));
|
|
1283
1342
|
}
|
|
1284
1343
|
return diagnostics;
|
|
@@ -1286,7 +1345,7 @@ const detectTrivialComments = async (context) => {
|
|
|
1286
1345
|
|
|
1287
1346
|
//#endregion
|
|
1288
1347
|
//#region src/engines/ai-slop/dead-patterns.ts
|
|
1289
|
-
const JS_EXTENSIONS$
|
|
1348
|
+
const JS_EXTENSIONS$4 = new Set([
|
|
1290
1349
|
".ts",
|
|
1291
1350
|
".tsx",
|
|
1292
1351
|
".js",
|
|
@@ -1308,11 +1367,11 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
|
1308
1367
|
fixable
|
|
1309
1368
|
});
|
|
1310
1369
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
1311
|
-
const
|
|
1370
|
+
const NON_PRODUCTION_DIR_PATTERN$1 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|cli|cli-[\w-]+|[\w-]+-cli)\//;
|
|
1312
1371
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
1313
|
-
if (!JS_EXTENSIONS$
|
|
1372
|
+
if (!JS_EXTENSIONS$4.has(ext)) return [];
|
|
1314
1373
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
1315
|
-
if (
|
|
1374
|
+
if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
|
|
1316
1375
|
const diagnostics = [];
|
|
1317
1376
|
const lines = content.split("\n");
|
|
1318
1377
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1352,9 +1411,9 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1352
1411
|
for (let i = 0; i < lines.length; i++) {
|
|
1353
1412
|
const trimmed = lines[i].trim();
|
|
1354
1413
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1355
|
-
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));
|
|
1356
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));
|
|
1357
|
-
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));
|
|
1358
1417
|
}
|
|
1359
1418
|
return diagnostics;
|
|
1360
1419
|
};
|
|
@@ -1362,6 +1421,7 @@ const asAnyPattern = new RegExp(`\\bas\\s+any\\b`);
|
|
|
1362
1421
|
const doubleAssertPattern = new RegExp(`\\bas\\s+unknown\\s+as\\s+`);
|
|
1363
1422
|
const detectUnsafeTypePatterns = (content, relativePath, ext) => {
|
|
1364
1423
|
if (ext !== ".ts" && ext !== ".tsx") return [];
|
|
1424
|
+
if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
|
|
1365
1425
|
const diagnostics = [];
|
|
1366
1426
|
const lines = content.split("\n");
|
|
1367
1427
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1398,6 +1458,74 @@ const detectDeadPatterns = async (context) => {
|
|
|
1398
1458
|
return diagnostics;
|
|
1399
1459
|
};
|
|
1400
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
|
+
|
|
1401
1529
|
//#endregion
|
|
1402
1530
|
//#region src/engines/ai-slop/exceptions.ts
|
|
1403
1531
|
const SWALLOWED_EXCEPTION_PATTERNS = [
|
|
@@ -1488,6 +1616,600 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
1488
1616
|
return diagnostics;
|
|
1489
1617
|
};
|
|
1490
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
|
+
|
|
1491
2213
|
//#endregion
|
|
1492
2214
|
//#region src/engines/ai-slop/narrative-comments-patterns.ts
|
|
1493
2215
|
const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
|
|
@@ -1604,6 +2326,7 @@ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|re
|
|
|
1604
2326
|
|
|
1605
2327
|
//#endregion
|
|
1606
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;
|
|
1607
2330
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
1608
2331
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
1609
2332
|
const getCommentSyntax = (ext) => {
|
|
@@ -1632,6 +2355,10 @@ const getMatchedLinePrefix = (line, syntax) => {
|
|
|
1632
2355
|
}
|
|
1633
2356
|
return null;
|
|
1634
2357
|
};
|
|
2358
|
+
const isRustDocCommentLine = (line) => {
|
|
2359
|
+
const trimmed = line.trimStart();
|
|
2360
|
+
return trimmed.startsWith("///") || trimmed.startsWith("//!");
|
|
2361
|
+
};
|
|
1635
2362
|
const collectBlocks = (sourceLines, syntax) => {
|
|
1636
2363
|
const blocks = [];
|
|
1637
2364
|
let i = 0;
|
|
@@ -1647,6 +2374,8 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1647
2374
|
}
|
|
1648
2375
|
let next = i;
|
|
1649
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));
|
|
1650
2379
|
blocks.push({
|
|
1651
2380
|
kind: "line",
|
|
1652
2381
|
startLine: start + 1,
|
|
@@ -1654,6 +2383,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1654
2383
|
rawLines: raw,
|
|
1655
2384
|
prose: raw.map(stripLineComment),
|
|
1656
2385
|
hasMeaningfulJsdocTag: false,
|
|
2386
|
+
isRustDoc,
|
|
1657
2387
|
nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
|
|
1658
2388
|
});
|
|
1659
2389
|
continue;
|
|
@@ -1687,6 +2417,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1687
2417
|
rawLines: raw,
|
|
1688
2418
|
prose,
|
|
1689
2419
|
hasMeaningfulJsdocTag: hasMeaningful,
|
|
2420
|
+
isRustDoc: false,
|
|
1690
2421
|
nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
|
|
1691
2422
|
});
|
|
1692
2423
|
continue;
|
|
@@ -1737,6 +2468,22 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
|
1737
2468
|
return false;
|
|
1738
2469
|
};
|
|
1739
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
|
+
};
|
|
1740
2487
|
const detectNarrativeInBlock = (block, ext) => {
|
|
1741
2488
|
if (looksLikeLicenseHeader(block)) return {
|
|
1742
2489
|
matched: false,
|
|
@@ -1750,6 +2497,14 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1750
2497
|
matched: false,
|
|
1751
2498
|
reason: ""
|
|
1752
2499
|
};
|
|
2500
|
+
if (block.isRustDoc) return {
|
|
2501
|
+
matched: false,
|
|
2502
|
+
reason: ""
|
|
2503
|
+
};
|
|
2504
|
+
if (looksLikeGoDocComment(block, ext)) return {
|
|
2505
|
+
matched: false,
|
|
2506
|
+
reason: ""
|
|
2507
|
+
};
|
|
1753
2508
|
if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
|
|
1754
2509
|
matched: true,
|
|
1755
2510
|
reason: "decorative separator"
|
|
@@ -1762,11 +2517,16 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1762
2517
|
matched: true,
|
|
1763
2518
|
reason: "bare section label"
|
|
1764
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
|
+
};
|
|
1765
2526
|
if (block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
1766
2527
|
matched: true,
|
|
1767
2528
|
reason: block.kind === "jsdoc" ? "JSDoc preamble before declaration" : "multi-line preamble before declaration"
|
|
1768
2529
|
};
|
|
1769
|
-
const joined = block.prose.join(" ");
|
|
1770
2530
|
if (CROSS_REFERENCE_PHRASES.some((re) => re.test(joined))) return {
|
|
1771
2531
|
matched: true,
|
|
1772
2532
|
reason: "cross-reference commentary"
|
|
@@ -1782,8 +2542,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1782
2542
|
reason: "explanatory preamble"
|
|
1783
2543
|
};
|
|
1784
2544
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
1785
|
-
const joinedProse = block.prose.join(" ");
|
|
1786
|
-
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joinedProse);
|
|
1787
2545
|
if (nonEmptyProseCount >= 5) return {
|
|
1788
2546
|
matched: true,
|
|
1789
2547
|
reason: "long narrative block"
|
|
@@ -1806,6 +2564,8 @@ const detectNarrativeComments = async (context) => {
|
|
|
1806
2564
|
if (isAutoGenerated(filePath)) continue;
|
|
1807
2565
|
const syntax = getCommentSyntax(ext);
|
|
1808
2566
|
if (!syntax) continue;
|
|
2567
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2568
|
+
if (NON_PRODUCTION_DIR_PATTERN.test(relativePath)) continue;
|
|
1809
2569
|
let content;
|
|
1810
2570
|
try {
|
|
1811
2571
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -1813,7 +2573,6 @@ const detectNarrativeComments = async (context) => {
|
|
|
1813
2573
|
continue;
|
|
1814
2574
|
}
|
|
1815
2575
|
const blocks = collectBlocks(content.split("\n"), syntax);
|
|
1816
|
-
const relativePath = filePath.replace(`${context.rootDirectory}/`, "");
|
|
1817
2576
|
for (const block of blocks) {
|
|
1818
2577
|
const { matched, reason } = detectNarrativeInBlock(block, ext);
|
|
1819
2578
|
if (!matched) continue;
|
|
@@ -1833,48 +2592,293 @@ const detectNarrativeComments = async (context) => {
|
|
|
1833
2592
|
}
|
|
1834
2593
|
return diagnostics;
|
|
1835
2594
|
};
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
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
|
+
}
|
|
1845
2639
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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;
|
|
1849
2743
|
let content;
|
|
1850
2744
|
try {
|
|
1851
2745
|
content = fs.readFileSync(filePath, "utf-8");
|
|
1852
2746
|
} catch {
|
|
1853
2747
|
continue;
|
|
1854
2748
|
}
|
|
2749
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2750
|
+
const basename = path.basename(filePath);
|
|
1855
2751
|
const lines = content.split("\n");
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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
|
+
}
|
|
1867
2807
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
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
|
+
});
|
|
2832
|
+
}
|
|
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
|
+
});
|
|
1872
2852
|
}
|
|
1873
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
|
+
};
|
|
1874
2878
|
|
|
1875
2879
|
//#endregion
|
|
1876
2880
|
//#region src/engines/ai-slop/unused-imports.ts
|
|
1877
|
-
const JS_EXTENSIONS = new Set([
|
|
2881
|
+
const JS_EXTENSIONS$1 = new Set([
|
|
1878
2882
|
".ts",
|
|
1879
2883
|
".tsx",
|
|
1880
2884
|
".js",
|
|
@@ -2004,7 +3008,7 @@ const analyzeFile = (filePath) => {
|
|
|
2004
3008
|
const lines = content.split("\n");
|
|
2005
3009
|
let symbols;
|
|
2006
3010
|
let importLines;
|
|
2007
|
-
if (JS_EXTENSIONS.has(ext)) {
|
|
3011
|
+
if (JS_EXTENSIONS$1.has(ext)) {
|
|
2008
3012
|
const result = extractJsImportedSymbols(lines);
|
|
2009
3013
|
symbols = result.symbols;
|
|
2010
3014
|
importLines = result.importLines;
|
|
@@ -2060,7 +3064,12 @@ const aiSlopEngine = {
|
|
|
2060
3064
|
detectOverAbstraction(context),
|
|
2061
3065
|
detectDeadPatterns(context),
|
|
2062
3066
|
detectUnusedImports(context),
|
|
2063
|
-
detectNarrativeComments(context)
|
|
3067
|
+
detectNarrativeComments(context),
|
|
3068
|
+
detectDuplicateImports(context),
|
|
3069
|
+
detectPythonPatterns(context),
|
|
3070
|
+
detectGoPatterns(context),
|
|
3071
|
+
detectRustPatterns(context),
|
|
3072
|
+
detectHallucinatedImports(context)
|
|
2064
3073
|
]);
|
|
2065
3074
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
2066
3075
|
return {
|
|
@@ -2444,6 +3453,12 @@ const isDataFile = (content) => {
|
|
|
2444
3453
|
const dataLinePattern = /^\s*[{}[\]"']/;
|
|
2445
3454
|
return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
|
|
2446
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);
|
|
2447
3462
|
const analyzeFunctions = (content, ext) => {
|
|
2448
3463
|
const lines = content.split("\n");
|
|
2449
3464
|
const functions = [];
|
|
@@ -2472,13 +3487,13 @@ const checkFileDiagnostics = (relativePath, content, limits) => {
|
|
|
2472
3487
|
const lineCount = content.split("\n").length;
|
|
2473
3488
|
const ext = path.extname(relativePath).toLowerCase();
|
|
2474
3489
|
if (isDataFile(content)) return results;
|
|
2475
|
-
const
|
|
2476
|
-
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({
|
|
2477
3492
|
filePath: relativePath,
|
|
2478
3493
|
engine: "code-quality",
|
|
2479
3494
|
rule: "complexity/file-too-large",
|
|
2480
3495
|
severity: "warning",
|
|
2481
|
-
message: `File has ${lineCount} lines (max: ${
|
|
3496
|
+
message: `File has ${lineCount} lines (max: ${configuredMax})`,
|
|
2482
3497
|
help: "Consider splitting this file into smaller modules",
|
|
2483
3498
|
line: 0,
|
|
2484
3499
|
column: 0,
|
|
@@ -2528,13 +3543,14 @@ const checkFunctionDiagnostics = (relativePath, fn, limits) => {
|
|
|
2528
3543
|
return results;
|
|
2529
3544
|
};
|
|
2530
3545
|
const checkFileComplexity = (filePath, rootDirectory, limits) => {
|
|
3546
|
+
const relativePath = path.relative(rootDirectory, filePath);
|
|
3547
|
+
if (isExemptFromComplexity(relativePath)) return [];
|
|
2531
3548
|
let content;
|
|
2532
3549
|
try {
|
|
2533
3550
|
content = fs.readFileSync(filePath, "utf-8");
|
|
2534
3551
|
} catch {
|
|
2535
3552
|
return [];
|
|
2536
3553
|
}
|
|
2537
|
-
const relativePath = path.relative(rootDirectory, filePath);
|
|
2538
3554
|
const ext = path.extname(filePath).toLowerCase();
|
|
2539
3555
|
const diagnostics = checkFileDiagnostics(relativePath, content, limits);
|
|
2540
3556
|
for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
|
|
@@ -3916,7 +4932,10 @@ const lintEngine = {
|
|
|
3916
4932
|
const diagnostics = [];
|
|
3917
4933
|
const { languages, installedTools } = context;
|
|
3918
4934
|
const promises = [];
|
|
3919
|
-
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
|
+
}
|
|
3920
4939
|
if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-Bz0LZhQ6.js").then((n) => n.t).then((mod) => mod.runExpoDoctor(context)));
|
|
3921
4940
|
if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
|
|
3922
4941
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
@@ -5274,6 +6293,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
5274
6293
|
const engineConfig = {
|
|
5275
6294
|
quality: config.quality,
|
|
5276
6295
|
security: config.security,
|
|
6296
|
+
lint: config.lint,
|
|
5277
6297
|
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
5278
6298
|
};
|
|
5279
6299
|
const gridRows = ALL_ENGINE_NAMES.filter((engine) => config.engines[engine] !== false).map((engine) => ({
|
|
@@ -5341,7 +6361,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
5341
6361
|
});
|
|
5342
6362
|
}
|
|
5343
6363
|
if (options.json) {
|
|
5344
|
-
const { buildJsonOutput } = await import("./json-
|
|
6364
|
+
const { buildJsonOutput } = await import("./json-BJGLCIK-.js");
|
|
5345
6365
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
5346
6366
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
5347
6367
|
return { exitCode };
|
|
@@ -5696,6 +6716,212 @@ const applyEditsAndCollapse = (lines, linesToRemove, lineReplacements) => {
|
|
|
5696
6716
|
return collapsed.join("\n");
|
|
5697
6717
|
};
|
|
5698
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
|
+
|
|
5699
6925
|
//#endregion
|
|
5700
6926
|
//#region src/engines/ai-slop/unused-imports-fix.ts
|
|
5701
6927
|
const fixUnusedImports = async (context) => {
|
|
@@ -5717,9 +6943,9 @@ const fixUnusedImports = async (context) => {
|
|
|
5717
6943
|
for (const [lineNo, syms] of symbolsByLine) {
|
|
5718
6944
|
const lineIdx = lineNo - 1;
|
|
5719
6945
|
const allUnused = syms.every((s) => unusedNames.has(s.name));
|
|
5720
|
-
const importSpan = JS_EXTENSIONS.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
|
|
6946
|
+
const importSpan = JS_EXTENSIONS$1.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
|
|
5721
6947
|
if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
|
|
5722
|
-
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);
|
|
5723
6949
|
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
|
|
5724
6950
|
}
|
|
5725
6951
|
if (linesToRemove.size === 0 && unused.length === 0) continue;
|
|
@@ -6322,6 +7548,7 @@ const hasJsOrTs = (projectInfo) => projectInfo.languages.includes("typescript")
|
|
|
6322
7548
|
const runAiSlopSteps = async (deps) => {
|
|
6323
7549
|
if (!deps.config.engines["ai-slop"]) return;
|
|
6324
7550
|
await deps.runStep("Unused imports", () => detectUnusedImports(deps.context), () => fixUnusedImports(deps.context));
|
|
7551
|
+
await deps.runStep("Duplicate imports", () => detectDuplicateImports(deps.context), () => fixDuplicateImports(deps.context));
|
|
6325
7552
|
const detectFixableSlop = async () => {
|
|
6326
7553
|
const [comments, dead, narrative] = await Promise.all([
|
|
6327
7554
|
detectTrivialComments(deps.context),
|
|
@@ -6427,7 +7654,8 @@ const createEngineContext = (rootDirectory, projectInfo, config) => ({
|
|
|
6427
7654
|
installedTools: projectInfo.installedTools,
|
|
6428
7655
|
config: {
|
|
6429
7656
|
quality: config.quality,
|
|
6430
|
-
security: config.security
|
|
7657
|
+
security: config.security,
|
|
7658
|
+
lint: config.lint
|
|
6431
7659
|
}
|
|
6432
7660
|
});
|
|
6433
7661
|
const fixCommand = async (directory, config, options = {
|
|
@@ -6490,6 +7718,7 @@ const fixCommand = async (directory, config, options = {
|
|
|
6490
7718
|
const engineConfig = {
|
|
6491
7719
|
quality: config.quality,
|
|
6492
7720
|
security: config.security,
|
|
7721
|
+
lint: config.lint,
|
|
6493
7722
|
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
6494
7723
|
};
|
|
6495
7724
|
rail.start("Verifying results");
|
|
@@ -6794,8 +8023,10 @@ const buildRulesRender = (input) => {
|
|
|
6794
8023
|
const AI_SLOP_FIXABLE = new Set([
|
|
6795
8024
|
"ai-slop/trivial-comment",
|
|
6796
8025
|
"ai-slop/unused-import",
|
|
6797
|
-
"ai-slop/narrative-comment"
|
|
8026
|
+
"ai-slop/narrative-comment",
|
|
8027
|
+
"ai-slop/duplicate-import"
|
|
6798
8028
|
]);
|
|
8029
|
+
const AI_SLOP_ERRORS = new Set(["ai-slop/hallucinated-import"]);
|
|
6799
8030
|
const BUILTIN_RULES = [
|
|
6800
8031
|
{
|
|
6801
8032
|
engine: "format",
|
|
@@ -6816,7 +8047,8 @@ const BUILTIN_RULES = [
|
|
|
6816
8047
|
"ruff/*",
|
|
6817
8048
|
"go/*",
|
|
6818
8049
|
"clippy/*",
|
|
6819
|
-
"rubocop/*"
|
|
8050
|
+
"rubocop/*",
|
|
8051
|
+
"typescript/*"
|
|
6820
8052
|
]
|
|
6821
8053
|
},
|
|
6822
8054
|
{
|
|
@@ -6852,7 +8084,16 @@ const BUILTIN_RULES = [
|
|
|
6852
8084
|
"ai-slop/unsafe-type-assertion",
|
|
6853
8085
|
"ai-slop/double-type-assertion",
|
|
6854
8086
|
"ai-slop/ts-directive",
|
|
6855
|
-
"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"
|
|
6856
8097
|
]
|
|
6857
8098
|
},
|
|
6858
8099
|
{
|
|
@@ -6883,7 +8124,7 @@ const toRuleEntry = (engine, ruleId) => {
|
|
|
6883
8124
|
if (engine === "ai-slop") return {
|
|
6884
8125
|
id: ruleId,
|
|
6885
8126
|
engine,
|
|
6886
|
-
severity: "warning",
|
|
8127
|
+
severity: AI_SLOP_ERRORS.has(ruleId) ? "error" : "warning",
|
|
6887
8128
|
fixable: AI_SLOP_FIXABLE.has(ruleId)
|
|
6888
8129
|
};
|
|
6889
8130
|
return {
|