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/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire } from "node:module";
|
|
2
|
+
import { createRequire, isBuiltin } from "node:module";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import fs from "node:fs";
|
|
@@ -34,7 +34,11 @@ var __exportAll = (all, no_symbols) => {
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/hooks/feedback.ts
|
|
37
|
+
const fingerprintFinding = (f) => `${f.file}:${f.line}:${f.ruleId}`;
|
|
37
38
|
const MAX_FINDINGS = 20;
|
|
39
|
+
const MAX_NEW_SINCE_BASELINE = 10;
|
|
40
|
+
const REVIEW_TOP_N = 3;
|
|
41
|
+
const REGRESSION_FLAG_THRESHOLD = 5;
|
|
38
42
|
const toFinding = (d, rootDirectory) => {
|
|
39
43
|
if (d.severity !== "error" && d.severity !== "warning") return null;
|
|
40
44
|
const file = path.isAbsolute(d.filePath) ? path.relative(rootDirectory, d.filePath) : d.filePath;
|
|
@@ -64,6 +68,42 @@ const buildNextSteps = (findings) => {
|
|
|
64
68
|
}
|
|
65
69
|
return steps;
|
|
66
70
|
};
|
|
71
|
+
const buildSuggestedActions = (diagnostics, findings, regressed, delta) => {
|
|
72
|
+
const actions = [];
|
|
73
|
+
const fixableDiags = diagnostics.filter((d) => d.fixable);
|
|
74
|
+
if (fixableDiags.length > 0) {
|
|
75
|
+
const ruleIds = Array.from(new Set(fixableDiags.map((d) => d.rule)));
|
|
76
|
+
actions.push({
|
|
77
|
+
id: "run_aislop_fix",
|
|
78
|
+
label: `Run aislop fix to clear ${fixableDiags.length} mechanical finding${fixableDiags.length === 1 ? "" : "s"}.`,
|
|
79
|
+
command: "npx aislop fix",
|
|
80
|
+
rationale: "These findings have deterministic fixes (formatting, unused imports, trivial comments). Running this before any manual work avoids burning agent tokens on what the CLI handles for free.",
|
|
81
|
+
ruleIds
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const archErrors = findings.filter((f) => f.ruleId.startsWith("arch/") && f.severity === "error");
|
|
85
|
+
if (archErrors.length > 0) actions.push({
|
|
86
|
+
id: "review_finding",
|
|
87
|
+
label: `Review ${archErrors.length} architecture rule violation${archErrors.length === 1 ? "" : "s"} — these can't be auto-fixed.`,
|
|
88
|
+
rationale: "Architecture rules encode intentional project structure decisions. The fix usually means moving code, not editing it.",
|
|
89
|
+
ruleIds: Array.from(new Set(archErrors.map((f) => f.ruleId)))
|
|
90
|
+
});
|
|
91
|
+
if (regressed && typeof delta === "number" && delta <= -REGRESSION_FLAG_THRESHOLD && fixableDiags.length === 0) {
|
|
92
|
+
const top = findings.filter((f) => f.severity === "error" || f.severity === "warning").slice(0, REVIEW_TOP_N);
|
|
93
|
+
if (top.length > 0) actions.push({
|
|
94
|
+
id: "review_finding",
|
|
95
|
+
label: `Score dropped ${Math.abs(delta)} points — review the top ${top.length} finding${top.length === 1 ? "" : "s"} from this edit.`,
|
|
96
|
+
rationale: "None of these are auto-fixable. Read each one against the source and decide whether the fix is to change the code or to add a justified suppression with a reason.",
|
|
97
|
+
ruleIds: top.map((f) => f.ruleId)
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (actions.length === 0) actions.push({
|
|
101
|
+
id: "no_action",
|
|
102
|
+
label: typeof delta === "number" ? delta > 0 ? `Score improved by ${delta}. No action needed.` : "Score unchanged. No action needed." : "No findings. No action needed.",
|
|
103
|
+
rationale: "The current scan didn't reveal anything that requires the agent's attention."
|
|
104
|
+
});
|
|
105
|
+
return actions;
|
|
106
|
+
};
|
|
67
107
|
const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
|
|
68
108
|
const all = diagnostics.map((d) => toFinding(d, rootDirectory)).filter((x) => x !== null);
|
|
69
109
|
const capped = all.slice(0, MAX_FINDINGS);
|
|
@@ -74,15 +114,30 @@ const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
|
|
|
74
114
|
fixable: diagnostics.filter((d) => d.fixable).length,
|
|
75
115
|
total: all.length
|
|
76
116
|
};
|
|
117
|
+
const baselineSnapshot = typeof baseline === "number" ? {
|
|
118
|
+
score: baseline,
|
|
119
|
+
findingFingerprints: []
|
|
120
|
+
} : baseline;
|
|
121
|
+
const baselineScore = baselineSnapshot?.score;
|
|
122
|
+
const delta = typeof baselineScore === "number" ? score - baselineScore : void 0;
|
|
123
|
+
const regressed = typeof delta === "number" ? delta < 0 : false;
|
|
124
|
+
let newSinceBaseline;
|
|
125
|
+
if (baselineSnapshot && baselineSnapshot.findingFingerprints.length > 0) {
|
|
126
|
+
const known = new Set(baselineSnapshot.findingFingerprints);
|
|
127
|
+
newSinceBaseline = all.filter((f) => !known.has(fingerprintFinding(f))).slice(0, MAX_NEW_SINCE_BASELINE);
|
|
128
|
+
}
|
|
77
129
|
return {
|
|
78
|
-
schema: "aislop.hook.
|
|
130
|
+
schema: "aislop.hook.v2",
|
|
79
131
|
score,
|
|
80
|
-
baseline,
|
|
81
|
-
|
|
132
|
+
baseline: baselineScore,
|
|
133
|
+
delta,
|
|
134
|
+
regressed,
|
|
82
135
|
counts,
|
|
83
136
|
findings: capped,
|
|
84
137
|
elided,
|
|
85
|
-
|
|
138
|
+
newSinceBaseline,
|
|
139
|
+
nextSteps: buildNextSteps(capped),
|
|
140
|
+
suggestedActions: buildSuggestedActions(diagnostics, capped, regressed, delta)
|
|
86
141
|
};
|
|
87
142
|
};
|
|
88
143
|
|
|
@@ -147,6 +202,7 @@ const DEFAULT_CONFIG = {
|
|
|
147
202
|
maxNesting: 5,
|
|
148
203
|
maxParams: 6
|
|
149
204
|
},
|
|
205
|
+
lint: { typecheck: false },
|
|
150
206
|
security: {
|
|
151
207
|
audit: true,
|
|
152
208
|
auditTimeout: 25e3
|
|
@@ -275,6 +331,7 @@ const QualitySchema = z.object({
|
|
|
275
331
|
maxNesting: z.number().positive().default(5),
|
|
276
332
|
maxParams: z.number().positive().default(6)
|
|
277
333
|
});
|
|
334
|
+
const LintConfigSchema = z.object({ typecheck: z.boolean().default(false) });
|
|
278
335
|
const SecurityConfigSchema = z.object({
|
|
279
336
|
audit: z.boolean().default(true),
|
|
280
337
|
auditTimeout: z.number().positive().default(25e3)
|
|
@@ -312,6 +369,7 @@ const AislopConfigSchema = z.object({
|
|
|
312
369
|
maxNesting: 5,
|
|
313
370
|
maxParams: 6
|
|
314
371
|
})),
|
|
372
|
+
lint: LintConfigSchema.default(() => ({ typecheck: false })),
|
|
315
373
|
security: SecurityConfigSchema.default(() => ({
|
|
316
374
|
audit: true,
|
|
317
375
|
auditTimeout: 25e3
|
|
@@ -464,7 +522,7 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
|
464
522
|
return SOURCE_EXTENSIONS.has(extension) || extraExtensions.has(extension);
|
|
465
523
|
};
|
|
466
524
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
467
|
-
const isTestFile = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
525
|
+
const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
468
526
|
const getIgnoredPaths = (rootDirectory, files) => {
|
|
469
527
|
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
470
528
|
const result = spawnSync("git", [
|
|
@@ -538,7 +596,7 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
|
|
|
538
596
|
return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
|
|
539
597
|
};
|
|
540
598
|
return normalizedFiles.filter(({ absolutePath, relativePath }) => {
|
|
541
|
-
return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
|
|
599
|
+
return hasAllowedExtension(relativePath, extraSet) && !isExcludedPath(relativePath) && !isTestFile$2(relativePath) && !ignoredPaths.has(relativePath) && !isUserExcluded(relativePath) && fs.existsSync(absolutePath);
|
|
542
600
|
}).map(({ absolutePath }) => absolutePath);
|
|
543
601
|
};
|
|
544
602
|
const filterExplicitFiles = (rootDirectory, files, extraExtensions = []) => {
|
|
@@ -674,13 +732,14 @@ const detectOverAbstraction = async (context) => {
|
|
|
674
732
|
|
|
675
733
|
//#endregion
|
|
676
734
|
//#region src/engines/ai-slop/comments.ts
|
|
735
|
+
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;
|
|
677
736
|
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";
|
|
678
737
|
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")];
|
|
679
738
|
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")];
|
|
680
739
|
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
|
|
681
740
|
const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
682
741
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
683
|
-
const isJsComment = (trimmed) => trimmed.startsWith("//");
|
|
742
|
+
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
684
743
|
const isPythonComment = (trimmed) => trimmed.startsWith("#") && !trimmed.startsWith("#!");
|
|
685
744
|
/**
|
|
686
745
|
* Extract just the comment text after the comment marker.
|
|
@@ -729,13 +788,14 @@ const detectTrivialComments = async (context) => {
|
|
|
729
788
|
const diagnostics = [];
|
|
730
789
|
for (const filePath of files) {
|
|
731
790
|
if (isAutoGenerated(filePath)) continue;
|
|
791
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
792
|
+
if (NON_PRODUCTION_DIR_PATTERN$2.test(relativePath)) continue;
|
|
732
793
|
let content;
|
|
733
794
|
try {
|
|
734
795
|
content = fs.readFileSync(filePath, "utf-8");
|
|
735
796
|
} catch {
|
|
736
797
|
continue;
|
|
737
798
|
}
|
|
738
|
-
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
739
799
|
diagnostics.push(...scanFileForTrivialComments(content, relativePath));
|
|
740
800
|
}
|
|
741
801
|
return diagnostics;
|
|
@@ -743,7 +803,7 @@ const detectTrivialComments = async (context) => {
|
|
|
743
803
|
|
|
744
804
|
//#endregion
|
|
745
805
|
//#region src/engines/ai-slop/dead-patterns.ts
|
|
746
|
-
const JS_EXTENSIONS$
|
|
806
|
+
const JS_EXTENSIONS$4 = new Set([
|
|
747
807
|
".ts",
|
|
748
808
|
".tsx",
|
|
749
809
|
".js",
|
|
@@ -765,11 +825,11 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
|
765
825
|
fixable
|
|
766
826
|
});
|
|
767
827
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
768
|
-
const
|
|
828
|
+
const NON_PRODUCTION_DIR_PATTERN$1 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|cli|cli-[\w-]+|[\w-]+-cli)\//;
|
|
769
829
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
770
|
-
if (!JS_EXTENSIONS$
|
|
830
|
+
if (!JS_EXTENSIONS$4.has(ext)) return [];
|
|
771
831
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
772
|
-
if (
|
|
832
|
+
if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
|
|
773
833
|
const diagnostics = [];
|
|
774
834
|
const lines = content.split("\n");
|
|
775
835
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -809,9 +869,9 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
809
869
|
for (let i = 0; i < lines.length; i++) {
|
|
810
870
|
const trimmed = lines[i].trim();
|
|
811
871
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
812
|
-
if (JS_EXTENSIONS$
|
|
872
|
+
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));
|
|
813
873
|
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));
|
|
814
|
-
if (JS_EXTENSIONS$
|
|
874
|
+
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));
|
|
815
875
|
}
|
|
816
876
|
return diagnostics;
|
|
817
877
|
};
|
|
@@ -819,6 +879,7 @@ const asAnyPattern = new RegExp(`\\bas\\s+any\\b`);
|
|
|
819
879
|
const doubleAssertPattern = new RegExp(`\\bas\\s+unknown\\s+as\\s+`);
|
|
820
880
|
const detectUnsafeTypePatterns = (content, relativePath, ext) => {
|
|
821
881
|
if (ext !== ".ts" && ext !== ".tsx") return [];
|
|
882
|
+
if (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
|
|
822
883
|
const diagnostics = [];
|
|
823
884
|
const lines = content.split("\n");
|
|
824
885
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -855,6 +916,74 @@ const detectDeadPatterns = async (context) => {
|
|
|
855
916
|
return diagnostics;
|
|
856
917
|
};
|
|
857
918
|
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/engines/ai-slop/duplicate-imports.ts
|
|
921
|
+
const JS_EXTENSIONS$3 = new Set([
|
|
922
|
+
".ts",
|
|
923
|
+
".tsx",
|
|
924
|
+
".js",
|
|
925
|
+
".jsx",
|
|
926
|
+
".mjs",
|
|
927
|
+
".cjs"
|
|
928
|
+
]);
|
|
929
|
+
const IMPORT_FROM_RE$1 = /^\s*import\s+[^;]*?from\s+["']([^"']+)["']/;
|
|
930
|
+
const extractImportLines = (content) => {
|
|
931
|
+
const lines = content.split("\n");
|
|
932
|
+
const results = [];
|
|
933
|
+
for (let i = 0; i < lines.length; i++) {
|
|
934
|
+
const line = lines[i];
|
|
935
|
+
const match = IMPORT_FROM_RE$1.exec(line);
|
|
936
|
+
if (!match) continue;
|
|
937
|
+
results.push({
|
|
938
|
+
spec: match[1],
|
|
939
|
+
line: i + 1
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
return results;
|
|
943
|
+
};
|
|
944
|
+
const detectDuplicateImports = async (context) => {
|
|
945
|
+
const diagnostics = [];
|
|
946
|
+
const files = getSourceFiles(context);
|
|
947
|
+
for (const filePath of files) {
|
|
948
|
+
if (!JS_EXTENSIONS$3.has(path.extname(filePath))) continue;
|
|
949
|
+
if (isAutoGenerated(filePath)) continue;
|
|
950
|
+
let content;
|
|
951
|
+
try {
|
|
952
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
953
|
+
} catch {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
const imports = extractImportLines(content);
|
|
957
|
+
if (imports.length < 2) continue;
|
|
958
|
+
const bySpec = /* @__PURE__ */ new Map();
|
|
959
|
+
for (const imp of imports) {
|
|
960
|
+
const list = bySpec.get(imp.spec) ?? [];
|
|
961
|
+
list.push(imp);
|
|
962
|
+
bySpec.set(imp.spec, list);
|
|
963
|
+
}
|
|
964
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
965
|
+
for (const [spec, occurrences] of bySpec) {
|
|
966
|
+
if (occurrences.length < 2) continue;
|
|
967
|
+
for (const dup of occurrences.slice(1)) {
|
|
968
|
+
const firstLine = occurrences[0].line;
|
|
969
|
+
diagnostics.push({
|
|
970
|
+
filePath: relPath,
|
|
971
|
+
engine: "ai-slop",
|
|
972
|
+
rule: "ai-slop/duplicate-import",
|
|
973
|
+
severity: "warning",
|
|
974
|
+
message: `"${spec}" is also imported on line ${firstLine}. Merge into a single import statement.`,
|
|
975
|
+
help: "Two imports from the same module split readers' attention and grow the import block. Run aislop fix to merge them automatically.",
|
|
976
|
+
line: dup.line,
|
|
977
|
+
column: 1,
|
|
978
|
+
category: "AI Slop",
|
|
979
|
+
fixable: true
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return diagnostics;
|
|
985
|
+
};
|
|
986
|
+
|
|
858
987
|
//#endregion
|
|
859
988
|
//#region src/engines/ai-slop/exceptions.ts
|
|
860
989
|
const SWALLOWED_EXCEPTION_PATTERNS = [
|
|
@@ -945,6 +1074,600 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
945
1074
|
return diagnostics;
|
|
946
1075
|
};
|
|
947
1076
|
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/engines/ai-slop/go-patterns.ts
|
|
1079
|
+
const GO_EXTENSIONS = new Set([".go"]);
|
|
1080
|
+
const PACKAGE_DECL_RE = /^\s*package\s+(\w+)/;
|
|
1081
|
+
const PANIC_CALL_RE = /\bpanic\s*\(/;
|
|
1082
|
+
const COMMENT_LINE_RE$1 = /^\s*\/\//;
|
|
1083
|
+
const NIL_GUARD_RE = /^\s*if\s+[\w.]+(?:\(\))?\s*==\s*nil\s*\{?\s*$/;
|
|
1084
|
+
const SHORT_STRING_PANIC_RE = /\bpanic\s*\(\s*"[^"]{1,40}"\s*\)/;
|
|
1085
|
+
const detectPackageName = (lines) => {
|
|
1086
|
+
for (const line of lines) {
|
|
1087
|
+
const m = PACKAGE_DECL_RE.exec(line);
|
|
1088
|
+
if (m) return m[1];
|
|
1089
|
+
}
|
|
1090
|
+
return null;
|
|
1091
|
+
};
|
|
1092
|
+
const PANIC_INTENT_LOOKBACK = 3;
|
|
1093
|
+
const hasIntentComment$1 = (lines, panicLineIdx) => {
|
|
1094
|
+
for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - PANIC_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE$1.test(lines[j])) return true;
|
|
1095
|
+
return false;
|
|
1096
|
+
};
|
|
1097
|
+
const isNilGuardPanic = (lines, panicLineIdx, line) => {
|
|
1098
|
+
if (!SHORT_STRING_PANIC_RE.test(line)) return false;
|
|
1099
|
+
for (let j = panicLineIdx - 1; j >= Math.max(0, panicLineIdx - 2); j--) {
|
|
1100
|
+
const prev = lines[j];
|
|
1101
|
+
if (prev.trim() === "") continue;
|
|
1102
|
+
return NIL_GUARD_RE.test(prev);
|
|
1103
|
+
}
|
|
1104
|
+
return false;
|
|
1105
|
+
};
|
|
1106
|
+
const flagLibraryPanic = (lines, relPath, pkg, out) => {
|
|
1107
|
+
if (pkg === "main") return;
|
|
1108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1109
|
+
const line = lines[i];
|
|
1110
|
+
if (COMMENT_LINE_RE$1.test(line)) continue;
|
|
1111
|
+
PANIC_CALL_RE.lastIndex = 0;
|
|
1112
|
+
if (!PANIC_CALL_RE.test(line)) continue;
|
|
1113
|
+
if (hasIntentComment$1(lines, i)) continue;
|
|
1114
|
+
if (isNilGuardPanic(lines, i, line)) continue;
|
|
1115
|
+
out.push({
|
|
1116
|
+
filePath: relPath,
|
|
1117
|
+
engine: "ai-slop",
|
|
1118
|
+
rule: "ai-slop/go-library-panic",
|
|
1119
|
+
severity: "warning",
|
|
1120
|
+
message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
|
|
1121
|
+
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.",
|
|
1122
|
+
line: i + 1,
|
|
1123
|
+
column: 1,
|
|
1124
|
+
category: "AI Slop",
|
|
1125
|
+
fixable: false
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
const detectGoPatterns = async (context) => {
|
|
1130
|
+
const diagnostics = [];
|
|
1131
|
+
const files = getSourceFiles(context);
|
|
1132
|
+
for (const filePath of files) {
|
|
1133
|
+
if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
1134
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1135
|
+
if (filePath.endsWith("_test.go")) continue;
|
|
1136
|
+
let content;
|
|
1137
|
+
try {
|
|
1138
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1139
|
+
} catch {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
const lines = content.split("\n");
|
|
1143
|
+
const pkg = detectPackageName(lines);
|
|
1144
|
+
if (!pkg) continue;
|
|
1145
|
+
flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
|
|
1146
|
+
}
|
|
1147
|
+
return diagnostics;
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region src/engines/ai-slop/python-data.ts
|
|
1152
|
+
const PYTHON_STDLIB = new Set([
|
|
1153
|
+
"__future__",
|
|
1154
|
+
"_thread",
|
|
1155
|
+
"abc",
|
|
1156
|
+
"argparse",
|
|
1157
|
+
"array",
|
|
1158
|
+
"ast",
|
|
1159
|
+
"asyncio",
|
|
1160
|
+
"atexit",
|
|
1161
|
+
"base64",
|
|
1162
|
+
"binascii",
|
|
1163
|
+
"bisect",
|
|
1164
|
+
"builtins",
|
|
1165
|
+
"bz2",
|
|
1166
|
+
"calendar",
|
|
1167
|
+
"codecs",
|
|
1168
|
+
"collections",
|
|
1169
|
+
"concurrent",
|
|
1170
|
+
"configparser",
|
|
1171
|
+
"contextlib",
|
|
1172
|
+
"contextvars",
|
|
1173
|
+
"copy",
|
|
1174
|
+
"csv",
|
|
1175
|
+
"ctypes",
|
|
1176
|
+
"dataclasses",
|
|
1177
|
+
"datetime",
|
|
1178
|
+
"decimal",
|
|
1179
|
+
"difflib",
|
|
1180
|
+
"dis",
|
|
1181
|
+
"doctest",
|
|
1182
|
+
"email",
|
|
1183
|
+
"encodings",
|
|
1184
|
+
"enum",
|
|
1185
|
+
"errno",
|
|
1186
|
+
"faulthandler",
|
|
1187
|
+
"filecmp",
|
|
1188
|
+
"fileinput",
|
|
1189
|
+
"fnmatch",
|
|
1190
|
+
"fractions",
|
|
1191
|
+
"functools",
|
|
1192
|
+
"gc",
|
|
1193
|
+
"getopt",
|
|
1194
|
+
"getpass",
|
|
1195
|
+
"gettext",
|
|
1196
|
+
"glob",
|
|
1197
|
+
"graphlib",
|
|
1198
|
+
"gzip",
|
|
1199
|
+
"hashlib",
|
|
1200
|
+
"heapq",
|
|
1201
|
+
"hmac",
|
|
1202
|
+
"html",
|
|
1203
|
+
"http",
|
|
1204
|
+
"imaplib",
|
|
1205
|
+
"importlib",
|
|
1206
|
+
"inspect",
|
|
1207
|
+
"io",
|
|
1208
|
+
"ipaddress",
|
|
1209
|
+
"itertools",
|
|
1210
|
+
"json",
|
|
1211
|
+
"keyword",
|
|
1212
|
+
"linecache",
|
|
1213
|
+
"locale",
|
|
1214
|
+
"logging",
|
|
1215
|
+
"lzma",
|
|
1216
|
+
"mailbox",
|
|
1217
|
+
"math",
|
|
1218
|
+
"mimetypes",
|
|
1219
|
+
"mmap",
|
|
1220
|
+
"multiprocessing",
|
|
1221
|
+
"numbers",
|
|
1222
|
+
"operator",
|
|
1223
|
+
"os",
|
|
1224
|
+
"pathlib",
|
|
1225
|
+
"pdb",
|
|
1226
|
+
"pickle",
|
|
1227
|
+
"platform",
|
|
1228
|
+
"plistlib",
|
|
1229
|
+
"pprint",
|
|
1230
|
+
"profile",
|
|
1231
|
+
"pstats",
|
|
1232
|
+
"pty",
|
|
1233
|
+
"queue",
|
|
1234
|
+
"quopri",
|
|
1235
|
+
"random",
|
|
1236
|
+
"re",
|
|
1237
|
+
"readline",
|
|
1238
|
+
"reprlib",
|
|
1239
|
+
"resource",
|
|
1240
|
+
"secrets",
|
|
1241
|
+
"select",
|
|
1242
|
+
"selectors",
|
|
1243
|
+
"shelve",
|
|
1244
|
+
"shlex",
|
|
1245
|
+
"shutil",
|
|
1246
|
+
"signal",
|
|
1247
|
+
"site",
|
|
1248
|
+
"smtplib",
|
|
1249
|
+
"socket",
|
|
1250
|
+
"socketserver",
|
|
1251
|
+
"sqlite3",
|
|
1252
|
+
"ssl",
|
|
1253
|
+
"stat",
|
|
1254
|
+
"statistics",
|
|
1255
|
+
"string",
|
|
1256
|
+
"stringprep",
|
|
1257
|
+
"struct",
|
|
1258
|
+
"subprocess",
|
|
1259
|
+
"sunau",
|
|
1260
|
+
"symtable",
|
|
1261
|
+
"sys",
|
|
1262
|
+
"sysconfig",
|
|
1263
|
+
"syslog",
|
|
1264
|
+
"tarfile",
|
|
1265
|
+
"telnetlib",
|
|
1266
|
+
"tempfile",
|
|
1267
|
+
"termios",
|
|
1268
|
+
"test",
|
|
1269
|
+
"textwrap",
|
|
1270
|
+
"threading",
|
|
1271
|
+
"time",
|
|
1272
|
+
"timeit",
|
|
1273
|
+
"tkinter",
|
|
1274
|
+
"token",
|
|
1275
|
+
"tokenize",
|
|
1276
|
+
"tomllib",
|
|
1277
|
+
"trace",
|
|
1278
|
+
"traceback",
|
|
1279
|
+
"tracemalloc",
|
|
1280
|
+
"tty",
|
|
1281
|
+
"turtle",
|
|
1282
|
+
"types",
|
|
1283
|
+
"typing",
|
|
1284
|
+
"unicodedata",
|
|
1285
|
+
"unittest",
|
|
1286
|
+
"urllib",
|
|
1287
|
+
"uu",
|
|
1288
|
+
"uuid",
|
|
1289
|
+
"venv",
|
|
1290
|
+
"warnings",
|
|
1291
|
+
"wave",
|
|
1292
|
+
"weakref",
|
|
1293
|
+
"webbrowser",
|
|
1294
|
+
"winreg",
|
|
1295
|
+
"winsound",
|
|
1296
|
+
"wsgiref",
|
|
1297
|
+
"xml",
|
|
1298
|
+
"xmlrpc",
|
|
1299
|
+
"zipapp",
|
|
1300
|
+
"zipfile",
|
|
1301
|
+
"zipimport",
|
|
1302
|
+
"zlib",
|
|
1303
|
+
"zoneinfo"
|
|
1304
|
+
]);
|
|
1305
|
+
const PYTHON_IMPORT_TO_PIP = {
|
|
1306
|
+
yaml: "pyyaml",
|
|
1307
|
+
PIL: "pillow",
|
|
1308
|
+
dateutil: "python-dateutil",
|
|
1309
|
+
cv2: "opencv-python",
|
|
1310
|
+
sklearn: "scikit-learn",
|
|
1311
|
+
bs4: "beautifulsoup4",
|
|
1312
|
+
typing_extensions: "typing-extensions",
|
|
1313
|
+
google: "google-api-python-client",
|
|
1314
|
+
jose: "python-jose",
|
|
1315
|
+
jwt: "pyjwt",
|
|
1316
|
+
OpenSSL: "pyopenssl",
|
|
1317
|
+
magic: "python-magic",
|
|
1318
|
+
docx: "python-docx",
|
|
1319
|
+
pptx: "python-pptx",
|
|
1320
|
+
git: "gitpython",
|
|
1321
|
+
socks: "pysocks",
|
|
1322
|
+
redis: "redis"
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
//#endregion
|
|
1326
|
+
//#region src/engines/ai-slop/hallucinated-imports.ts
|
|
1327
|
+
const JS_EXTENSIONS$2 = new Set([
|
|
1328
|
+
".ts",
|
|
1329
|
+
".tsx",
|
|
1330
|
+
".js",
|
|
1331
|
+
".jsx",
|
|
1332
|
+
".mjs",
|
|
1333
|
+
".cjs"
|
|
1334
|
+
]);
|
|
1335
|
+
const PY_EXTENSIONS$2 = new Set([".py"]);
|
|
1336
|
+
const readJson = (filePath) => {
|
|
1337
|
+
try {
|
|
1338
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1339
|
+
} catch {
|
|
1340
|
+
return null;
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
const PKG_DEP_SECTIONS = [
|
|
1344
|
+
"dependencies",
|
|
1345
|
+
"devDependencies",
|
|
1346
|
+
"peerDependencies",
|
|
1347
|
+
"optionalDependencies"
|
|
1348
|
+
];
|
|
1349
|
+
const addDepsFromPkg = (pkg, jsDeps) => {
|
|
1350
|
+
for (const section of PKG_DEP_SECTIONS) {
|
|
1351
|
+
const deps = pkg[section];
|
|
1352
|
+
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
1356
|
+
const globs = [];
|
|
1357
|
+
if (rootPkg && typeof rootPkg === "object") {
|
|
1358
|
+
const ws = rootPkg.workspaces;
|
|
1359
|
+
if (Array.isArray(ws)) {
|
|
1360
|
+
for (const g of ws) if (typeof g === "string") globs.push(g);
|
|
1361
|
+
} else if (ws && typeof ws === "object") {
|
|
1362
|
+
const pkgs = ws.packages;
|
|
1363
|
+
if (Array.isArray(pkgs)) {
|
|
1364
|
+
for (const g of pkgs) if (typeof g === "string") globs.push(g);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const lerna = readJson(path.join(rootDir, "lerna.json"));
|
|
1369
|
+
if (lerna && Array.isArray(lerna.packages)) {
|
|
1370
|
+
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
1371
|
+
}
|
|
1372
|
+
try {
|
|
1373
|
+
const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
|
|
1374
|
+
let inPackages = false;
|
|
1375
|
+
for (const rawLine of pnpmWs.split("\n")) {
|
|
1376
|
+
if (/^packages\s*:\s*$/.test(rawLine)) {
|
|
1377
|
+
inPackages = true;
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
if (!inPackages) continue;
|
|
1381
|
+
if (/^\S/.test(rawLine)) break;
|
|
1382
|
+
const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
|
|
1383
|
+
if (m) globs.push(m[1].trim());
|
|
1384
|
+
}
|
|
1385
|
+
} catch {}
|
|
1386
|
+
return globs;
|
|
1387
|
+
};
|
|
1388
|
+
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
1389
|
+
const dirs = [];
|
|
1390
|
+
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
1391
|
+
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
1392
|
+
try {
|
|
1393
|
+
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
1394
|
+
} catch {
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
1398
|
+
return dirs;
|
|
1399
|
+
};
|
|
1400
|
+
const SKIP_DIRS = new Set([
|
|
1401
|
+
"node_modules",
|
|
1402
|
+
".git",
|
|
1403
|
+
"dist",
|
|
1404
|
+
"build",
|
|
1405
|
+
"out",
|
|
1406
|
+
"target",
|
|
1407
|
+
"coverage"
|
|
1408
|
+
]);
|
|
1409
|
+
const NESTED_PKG_JSON_DEPTH = 4;
|
|
1410
|
+
const collectNestedManifests = (rootDir, jsDeps) => {
|
|
1411
|
+
const walk = (dir, depth) => {
|
|
1412
|
+
if (depth > NESTED_PKG_JSON_DEPTH) return;
|
|
1413
|
+
let entries;
|
|
1414
|
+
try {
|
|
1415
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1416
|
+
} catch {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
for (const entry of entries) {
|
|
1420
|
+
if (entry.name.startsWith(".") && entry.name !== ".github") continue;
|
|
1421
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1422
|
+
const full = path.join(dir, entry.name);
|
|
1423
|
+
if (entry.isDirectory()) walk(full, depth + 1);
|
|
1424
|
+
else if (entry.name === "package.json" && depth > 0) {
|
|
1425
|
+
const wsPkg = readJson(full);
|
|
1426
|
+
if (!wsPkg) continue;
|
|
1427
|
+
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
1428
|
+
addDepsFromPkg(wsPkg, jsDeps);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
walk(rootDir, 0);
|
|
1433
|
+
};
|
|
1434
|
+
const collectJsDeps = (rootDir, jsDeps) => {
|
|
1435
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
1436
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
1437
|
+
const pkg = readJson(pkgPath);
|
|
1438
|
+
if (!pkg || typeof pkg !== "object") return false;
|
|
1439
|
+
addDepsFromPkg(pkg, jsDeps);
|
|
1440
|
+
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
1441
|
+
const workspaceDirs = expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, pkg));
|
|
1442
|
+
for (const wsDir of workspaceDirs) {
|
|
1443
|
+
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
1444
|
+
if (!wsPkg) continue;
|
|
1445
|
+
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
1446
|
+
addDepsFromPkg(wsPkg, jsDeps);
|
|
1447
|
+
}
|
|
1448
|
+
collectNestedManifests(rootDir, jsDeps);
|
|
1449
|
+
return true;
|
|
1450
|
+
};
|
|
1451
|
+
const addPyDep = (pyDeps, name) => {
|
|
1452
|
+
const normalized = name.toLowerCase().replace(/_/g, "-");
|
|
1453
|
+
pyDeps.add(normalized);
|
|
1454
|
+
};
|
|
1455
|
+
const collectFromRequirementsTxt = (rootDir, pyDeps) => {
|
|
1456
|
+
const reqPath = path.join(rootDir, "requirements.txt");
|
|
1457
|
+
if (!fs.existsSync(reqPath)) return false;
|
|
1458
|
+
try {
|
|
1459
|
+
const content = fs.readFileSync(reqPath, "utf-8");
|
|
1460
|
+
for (const line of content.split("\n")) {
|
|
1461
|
+
const trimmed = line.trim();
|
|
1462
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
1463
|
+
const match = trimmed.match(/^([a-zA-Z0-9_\-.]+)/);
|
|
1464
|
+
if (match) addPyDep(pyDeps, match[1]);
|
|
1465
|
+
}
|
|
1466
|
+
return true;
|
|
1467
|
+
} catch {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
const collectFromPyproject = (rootDir, pyDeps) => {
|
|
1472
|
+
const pyprojPath = path.join(rootDir, "pyproject.toml");
|
|
1473
|
+
if (!fs.existsSync(pyprojPath)) return false;
|
|
1474
|
+
try {
|
|
1475
|
+
const content = fs.readFileSync(pyprojPath, "utf-8");
|
|
1476
|
+
const projectNameMatch = content.match(/\[project\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1477
|
+
if (projectNameMatch) addPyDep(pyDeps, projectNameMatch[1]);
|
|
1478
|
+
const poetryNameMatch = content.match(/\[tool\.poetry\][\s\S]*?^\s*name\s*=\s*["']([^"']+)/m);
|
|
1479
|
+
if (poetryNameMatch) addPyDep(pyDeps, poetryNameMatch[1]);
|
|
1480
|
+
const pep621 = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
1481
|
+
if (pep621) for (const line of pep621[1].split("\n")) {
|
|
1482
|
+
const m = line.match(/["']\s*([a-zA-Z0-9_\-.]+)/);
|
|
1483
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
1484
|
+
}
|
|
1485
|
+
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1486
|
+
let match = poetryRe.exec(content);
|
|
1487
|
+
while (match !== null) {
|
|
1488
|
+
for (const line of match[1].split("\n")) {
|
|
1489
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1490
|
+
if (m && m[1] !== "python") addPyDep(pyDeps, m[1]);
|
|
1491
|
+
}
|
|
1492
|
+
match = poetryRe.exec(content);
|
|
1493
|
+
}
|
|
1494
|
+
return true;
|
|
1495
|
+
} catch {
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
const collectFromPipfile = (rootDir, pyDeps) => {
|
|
1500
|
+
const pipfilePath = path.join(rootDir, "Pipfile");
|
|
1501
|
+
if (!fs.existsSync(pipfilePath)) return false;
|
|
1502
|
+
try {
|
|
1503
|
+
const content = fs.readFileSync(pipfilePath, "utf-8");
|
|
1504
|
+
const sectionRe = /\[(packages|dev-packages)\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1505
|
+
let match = sectionRe.exec(content);
|
|
1506
|
+
while (match !== null) {
|
|
1507
|
+
for (const line of match[2].split("\n")) {
|
|
1508
|
+
const m = line.trim().match(/^([a-zA-Z0-9_\-.]+)\s*=/);
|
|
1509
|
+
if (m) addPyDep(pyDeps, m[1]);
|
|
1510
|
+
}
|
|
1511
|
+
match = sectionRe.exec(content);
|
|
1512
|
+
}
|
|
1513
|
+
return true;
|
|
1514
|
+
} catch {
|
|
1515
|
+
return false;
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
const loadManifest = (rootDir) => {
|
|
1519
|
+
const jsDeps = /* @__PURE__ */ new Set();
|
|
1520
|
+
const pyDeps = /* @__PURE__ */ new Set();
|
|
1521
|
+
const hasJsManifest = collectJsDeps(rootDir, jsDeps);
|
|
1522
|
+
const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
|
|
1523
|
+
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
1524
|
+
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
1525
|
+
return {
|
|
1526
|
+
jsDeps,
|
|
1527
|
+
pyDeps,
|
|
1528
|
+
hasJsManifest,
|
|
1529
|
+
hasPyManifest: hasReq || hasPyproject || hasPipfile
|
|
1530
|
+
};
|
|
1531
|
+
};
|
|
1532
|
+
const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~/");
|
|
1533
|
+
const isJsBuiltin = (spec) => {
|
|
1534
|
+
return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
|
|
1535
|
+
};
|
|
1536
|
+
const VIRTUAL_MODULE_PREFIXES = [
|
|
1537
|
+
"astro:",
|
|
1538
|
+
"virtual:",
|
|
1539
|
+
"bun:"
|
|
1540
|
+
];
|
|
1541
|
+
const isJsVirtualModule = (spec) => VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p));
|
|
1542
|
+
const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
|
|
1543
|
+
const isLikelyRealImportSpec = (spec) => {
|
|
1544
|
+
if (spec.length === 0) return false;
|
|
1545
|
+
if (TEMPLATE_PLACEHOLDER_RE.test(spec)) return false;
|
|
1546
|
+
if (spec.includes("\\")) return false;
|
|
1547
|
+
if (/\s/.test(spec)) return false;
|
|
1548
|
+
return true;
|
|
1549
|
+
};
|
|
1550
|
+
const packageNameFromImport = (spec) => {
|
|
1551
|
+
if (spec.startsWith("@")) {
|
|
1552
|
+
const parts = spec.split("/");
|
|
1553
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec;
|
|
1554
|
+
}
|
|
1555
|
+
return spec.split("/")[0];
|
|
1556
|
+
};
|
|
1557
|
+
const STATIC_IMPORT_RE = /^\s*import\s+(?:[\w*{},\s]+\s+from\s+)?["']([^"']+)["']/;
|
|
1558
|
+
const DYNAMIC_IMPORT_RE = /(?:import|require)\s*\(\s*["']([^"']+)["']/g;
|
|
1559
|
+
const extractJsImports = (content) => {
|
|
1560
|
+
const lines = content.split("\n");
|
|
1561
|
+
const results = [];
|
|
1562
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1563
|
+
const line = lines[i];
|
|
1564
|
+
const trimmed = line.trim();
|
|
1565
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1566
|
+
const staticMatch = STATIC_IMPORT_RE.exec(line);
|
|
1567
|
+
if (staticMatch && isLikelyRealImportSpec(staticMatch[1])) results.push({
|
|
1568
|
+
spec: staticMatch[1],
|
|
1569
|
+
line: i + 1
|
|
1570
|
+
});
|
|
1571
|
+
DYNAMIC_IMPORT_RE.lastIndex = 0;
|
|
1572
|
+
let dyn = DYNAMIC_IMPORT_RE.exec(line);
|
|
1573
|
+
while (dyn !== null) {
|
|
1574
|
+
if (isLikelyRealImportSpec(dyn[1])) results.push({
|
|
1575
|
+
spec: dyn[1],
|
|
1576
|
+
line: i + 1
|
|
1577
|
+
});
|
|
1578
|
+
dyn = DYNAMIC_IMPORT_RE.exec(line);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return results;
|
|
1582
|
+
};
|
|
1583
|
+
const extractPyImports = (content) => {
|
|
1584
|
+
const lines = content.split("\n");
|
|
1585
|
+
const results = [];
|
|
1586
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1587
|
+
const line = lines[i].trim();
|
|
1588
|
+
if (line.startsWith("#")) continue;
|
|
1589
|
+
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
1590
|
+
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
1591
|
+
results.push({
|
|
1592
|
+
spec: fromMatch[1],
|
|
1593
|
+
line: i + 1
|
|
1594
|
+
});
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
1598
|
+
if (importMatch) for (const raw of importMatch[1].split(",")) {
|
|
1599
|
+
const cleaned = raw.trim().split(/\s+as\s+/)[0];
|
|
1600
|
+
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
1601
|
+
spec: cleaned,
|
|
1602
|
+
line: i + 1
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return results;
|
|
1607
|
+
};
|
|
1608
|
+
const checkJsImport = (spec, manifest) => {
|
|
1609
|
+
if (isJsRelativeOrAbsolute(spec)) return null;
|
|
1610
|
+
if (isJsBuiltin(spec)) return null;
|
|
1611
|
+
if (isJsVirtualModule(spec)) return null;
|
|
1612
|
+
const pkg = packageNameFromImport(spec);
|
|
1613
|
+
if (manifest.jsDeps.has(pkg)) return null;
|
|
1614
|
+
if (pkg.startsWith("@types/")) {
|
|
1615
|
+
const realPkg = pkg.slice(7);
|
|
1616
|
+
if (manifest.jsDeps.has(realPkg)) return null;
|
|
1617
|
+
}
|
|
1618
|
+
return pkg;
|
|
1619
|
+
};
|
|
1620
|
+
const checkPyImport = (spec, manifest) => {
|
|
1621
|
+
const root = spec.split(".")[0];
|
|
1622
|
+
if (PYTHON_STDLIB.has(root)) return null;
|
|
1623
|
+
const normalized = root.toLowerCase().replace(/_/g, "-");
|
|
1624
|
+
if (manifest.pyDeps.has(normalized)) return null;
|
|
1625
|
+
const pipName = PYTHON_IMPORT_TO_PIP[root];
|
|
1626
|
+
if (pipName && manifest.pyDeps.has(pipName)) return null;
|
|
1627
|
+
return root;
|
|
1628
|
+
};
|
|
1629
|
+
const detectHallucinatedImports = async (context) => {
|
|
1630
|
+
const manifest = loadManifest(context.rootDirectory);
|
|
1631
|
+
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
1632
|
+
const diagnostics = [];
|
|
1633
|
+
const files = getSourceFiles(context);
|
|
1634
|
+
for (const filePath of files) {
|
|
1635
|
+
const ext = path.extname(filePath);
|
|
1636
|
+
const isJs = JS_EXTENSIONS$2.has(ext);
|
|
1637
|
+
const isPy = PY_EXTENSIONS$2.has(ext);
|
|
1638
|
+
if (!isJs && !isPy) continue;
|
|
1639
|
+
if (isJs && !manifest.hasJsManifest) continue;
|
|
1640
|
+
if (isPy && !manifest.hasPyManifest) continue;
|
|
1641
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1642
|
+
let content;
|
|
1643
|
+
try {
|
|
1644
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
1645
|
+
} catch {
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
1649
|
+
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
1650
|
+
for (const { spec, line } of imports) {
|
|
1651
|
+
const hallucinated = isJs ? checkJsImport(spec, manifest) : checkPyImport(spec, manifest);
|
|
1652
|
+
if (!hallucinated) continue;
|
|
1653
|
+
const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
|
|
1654
|
+
diagnostics.push({
|
|
1655
|
+
filePath: relPath,
|
|
1656
|
+
engine: "ai-slop",
|
|
1657
|
+
rule: "ai-slop/hallucinated-import",
|
|
1658
|
+
severity: "error",
|
|
1659
|
+
message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
|
|
1660
|
+
help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
|
|
1661
|
+
line,
|
|
1662
|
+
column: 1,
|
|
1663
|
+
category: "AI Slop",
|
|
1664
|
+
fixable: false
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return diagnostics;
|
|
1669
|
+
};
|
|
1670
|
+
|
|
948
1671
|
//#endregion
|
|
949
1672
|
//#region src/engines/ai-slop/narrative-comments-patterns.ts
|
|
950
1673
|
const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
|
|
@@ -1061,6 +1784,7 @@ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|re
|
|
|
1061
1784
|
|
|
1062
1785
|
//#endregion
|
|
1063
1786
|
//#region src/engines/ai-slop/narrative-comments.ts
|
|
1787
|
+
const NON_PRODUCTION_DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
|
|
1064
1788
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
1065
1789
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
1066
1790
|
const getCommentSyntax = (ext) => {
|
|
@@ -1089,6 +1813,10 @@ const getMatchedLinePrefix = (line, syntax) => {
|
|
|
1089
1813
|
}
|
|
1090
1814
|
return null;
|
|
1091
1815
|
};
|
|
1816
|
+
const isRustDocCommentLine = (line) => {
|
|
1817
|
+
const trimmed = line.trimStart();
|
|
1818
|
+
return trimmed.startsWith("///") || trimmed.startsWith("//!");
|
|
1819
|
+
};
|
|
1092
1820
|
const collectBlocks = (sourceLines, syntax) => {
|
|
1093
1821
|
const blocks = [];
|
|
1094
1822
|
let i = 0;
|
|
@@ -1104,6 +1832,8 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1104
1832
|
}
|
|
1105
1833
|
let next = i;
|
|
1106
1834
|
while (next < sourceLines.length && sourceLines[next].trim() === "") next += 1;
|
|
1835
|
+
const docCandidates = raw.filter((l) => l.trim().length > 0);
|
|
1836
|
+
const isRustDoc = docCandidates.length > 0 && docCandidates.every((l) => isRustDocCommentLine(l));
|
|
1107
1837
|
blocks.push({
|
|
1108
1838
|
kind: "line",
|
|
1109
1839
|
startLine: start + 1,
|
|
@@ -1111,6 +1841,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1111
1841
|
rawLines: raw,
|
|
1112
1842
|
prose: raw.map(stripLineComment),
|
|
1113
1843
|
hasMeaningfulJsdocTag: false,
|
|
1844
|
+
isRustDoc,
|
|
1114
1845
|
nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
|
|
1115
1846
|
});
|
|
1116
1847
|
continue;
|
|
@@ -1144,6 +1875,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1144
1875
|
rawLines: raw,
|
|
1145
1876
|
prose,
|
|
1146
1877
|
hasMeaningfulJsdocTag: hasMeaningful,
|
|
1878
|
+
isRustDoc: false,
|
|
1147
1879
|
nextNonBlankLine: next < sourceLines.length ? sourceLines[next] : null
|
|
1148
1880
|
});
|
|
1149
1881
|
continue;
|
|
@@ -1194,6 +1926,22 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
|
1194
1926
|
return false;
|
|
1195
1927
|
};
|
|
1196
1928
|
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));
|
|
1929
|
+
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
1930
|
+
const looksLikeGoDocComment = (block, ext) => {
|
|
1931
|
+
if (ext !== ".go" || block.kind !== "line") return false;
|
|
1932
|
+
const next = block.nextNonBlankLine;
|
|
1933
|
+
if (!next) return false;
|
|
1934
|
+
const declMatch = GO_DECL_NAME_RE.exec(next.trim());
|
|
1935
|
+
if (!declMatch) return false;
|
|
1936
|
+
return ((block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "") === declMatch[1];
|
|
1937
|
+
};
|
|
1938
|
+
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):/i;
|
|
1939
|
+
const hasDocIndicator = (block) => {
|
|
1940
|
+
const joined = block.prose.join(" ");
|
|
1941
|
+
if (DOC_INDICATOR_RE.test(joined)) return true;
|
|
1942
|
+
for (const l of block.prose) if (/^[-]\s/.test(l)) return true;
|
|
1943
|
+
return false;
|
|
1944
|
+
};
|
|
1197
1945
|
const detectNarrativeInBlock = (block, ext) => {
|
|
1198
1946
|
if (looksLikeLicenseHeader(block)) return {
|
|
1199
1947
|
matched: false,
|
|
@@ -1207,6 +1955,14 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1207
1955
|
matched: false,
|
|
1208
1956
|
reason: ""
|
|
1209
1957
|
};
|
|
1958
|
+
if (block.isRustDoc) return {
|
|
1959
|
+
matched: false,
|
|
1960
|
+
reason: ""
|
|
1961
|
+
};
|
|
1962
|
+
if (looksLikeGoDocComment(block, ext)) return {
|
|
1963
|
+
matched: false,
|
|
1964
|
+
reason: ""
|
|
1965
|
+
};
|
|
1210
1966
|
if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
|
|
1211
1967
|
matched: true,
|
|
1212
1968
|
reason: "decorative separator"
|
|
@@ -1219,11 +1975,16 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1219
1975
|
matched: true,
|
|
1220
1976
|
reason: "bare section label"
|
|
1221
1977
|
};
|
|
1978
|
+
const joined = block.prose.join(" ");
|
|
1979
|
+
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
1980
|
+
if ((hasWhyMarker || hasDocIndicator(block)) && block.kind === "jsdoc") return {
|
|
1981
|
+
matched: false,
|
|
1982
|
+
reason: ""
|
|
1983
|
+
};
|
|
1222
1984
|
if (block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
1223
1985
|
matched: true,
|
|
1224
1986
|
reason: block.kind === "jsdoc" ? "JSDoc preamble before declaration" : "multi-line preamble before declaration"
|
|
1225
1987
|
};
|
|
1226
|
-
const joined = block.prose.join(" ");
|
|
1227
1988
|
if (CROSS_REFERENCE_PHRASES.some((re) => re.test(joined))) return {
|
|
1228
1989
|
matched: true,
|
|
1229
1990
|
reason: "cross-reference commentary"
|
|
@@ -1239,8 +2000,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1239
2000
|
reason: "explanatory preamble"
|
|
1240
2001
|
};
|
|
1241
2002
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
1242
|
-
const joinedProse = block.prose.join(" ");
|
|
1243
|
-
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joinedProse);
|
|
1244
2003
|
if (nonEmptyProseCount >= 5) return {
|
|
1245
2004
|
matched: true,
|
|
1246
2005
|
reason: "long narrative block"
|
|
@@ -1263,6 +2022,8 @@ const detectNarrativeComments = async (context) => {
|
|
|
1263
2022
|
if (isAutoGenerated(filePath)) continue;
|
|
1264
2023
|
const syntax = getCommentSyntax(ext);
|
|
1265
2024
|
if (!syntax) continue;
|
|
2025
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2026
|
+
if (NON_PRODUCTION_DIR_PATTERN.test(relativePath)) continue;
|
|
1266
2027
|
let content;
|
|
1267
2028
|
try {
|
|
1268
2029
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -1270,7 +2031,6 @@ const detectNarrativeComments = async (context) => {
|
|
|
1270
2031
|
continue;
|
|
1271
2032
|
}
|
|
1272
2033
|
const blocks = collectBlocks(content.split("\n"), syntax);
|
|
1273
|
-
const relativePath = filePath.replace(`${context.rootDirectory}/`, "");
|
|
1274
2034
|
for (const block of blocks) {
|
|
1275
2035
|
const { matched, reason } = detectNarrativeInBlock(block, ext);
|
|
1276
2036
|
if (!matched) continue;
|
|
@@ -1290,48 +2050,293 @@ const detectNarrativeComments = async (context) => {
|
|
|
1290
2050
|
}
|
|
1291
2051
|
return diagnostics;
|
|
1292
2052
|
};
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
2053
|
+
|
|
2054
|
+
//#endregion
|
|
2055
|
+
//#region src/engines/ai-slop/python-patterns.ts
|
|
2056
|
+
const PY_EXTENSIONS$1 = new Set([".py"]);
|
|
2057
|
+
const BARE_EXCEPT_RE = /^\s*except\s*:\s*(?:#.*)?$/;
|
|
2058
|
+
const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\s*:\s*(?:#.*)?$/;
|
|
2059
|
+
const PRINT_RE = /^\s*print\s*\(/;
|
|
2060
|
+
const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
|
|
2061
|
+
const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
|
|
2062
|
+
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");
|
|
2063
|
+
const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
|
|
2064
|
+
const SCRIPT_DIR_NAMES = new Set([
|
|
2065
|
+
"scripts",
|
|
2066
|
+
"bin",
|
|
2067
|
+
".github",
|
|
2068
|
+
"action",
|
|
2069
|
+
"docs",
|
|
2070
|
+
"docs_src",
|
|
2071
|
+
"examples",
|
|
2072
|
+
"example"
|
|
2073
|
+
]);
|
|
2074
|
+
const isInScriptDir = (relPath) => relPath.split(path.sep).some((seg) => SCRIPT_DIR_NAMES.has(seg));
|
|
2075
|
+
const isTutorialFile = (basename) => basename.startsWith("tutorial") && basename.endsWith(".py");
|
|
2076
|
+
const MAIN_GUARD_RE = /^\s*if\s+__name__\s*==\s*["']__main__["']\s*:/;
|
|
2077
|
+
const hasMainGuard = (lines) => lines.some((l) => MAIN_GUARD_RE.test(l));
|
|
2078
|
+
const buildDocstringRanges = (lines) => {
|
|
2079
|
+
const inside = /* @__PURE__ */ new Set();
|
|
2080
|
+
let openDelim = null;
|
|
2081
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2082
|
+
const line = lines[i];
|
|
2083
|
+
if (openDelim) {
|
|
2084
|
+
inside.add(i);
|
|
2085
|
+
if (line.includes(openDelim)) openDelim = null;
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
for (const delim of ["\"\"\"", "'''"]) {
|
|
2089
|
+
const first = line.indexOf(delim);
|
|
2090
|
+
if (first === -1) continue;
|
|
2091
|
+
if (line.indexOf(delim, first + 3) === -1) {
|
|
2092
|
+
openDelim = delim;
|
|
2093
|
+
inside.add(i);
|
|
2094
|
+
break;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
1302
2097
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
2098
|
+
return inside;
|
|
2099
|
+
};
|
|
2100
|
+
const pushFinding = (out, a) => {
|
|
2101
|
+
out.push({
|
|
2102
|
+
filePath: a.relPath,
|
|
2103
|
+
engine: "ai-slop",
|
|
2104
|
+
rule: a.rule,
|
|
2105
|
+
severity: a.severity,
|
|
2106
|
+
message: a.message,
|
|
2107
|
+
help: a.help,
|
|
2108
|
+
line: a.line,
|
|
2109
|
+
column: 1,
|
|
2110
|
+
category: "AI Slop",
|
|
2111
|
+
fixable: false
|
|
2112
|
+
});
|
|
2113
|
+
};
|
|
2114
|
+
const flagBareExcept = (lines, relPath, out) => {
|
|
2115
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2116
|
+
if (!BARE_EXCEPT_RE.test(lines[i])) continue;
|
|
2117
|
+
pushFinding(out, {
|
|
2118
|
+
relPath,
|
|
2119
|
+
rule: "ai-slop/python-bare-except",
|
|
2120
|
+
severity: "warning",
|
|
2121
|
+
message: "Bare `except:` swallows every exception including KeyboardInterrupt and SystemExit.",
|
|
2122
|
+
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.",
|
|
2123
|
+
line: i + 1
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
const flagBroadExceptWithSilentBody = (lines, relPath, out) => {
|
|
2128
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2129
|
+
const match = BROAD_EXCEPT_RE.exec(lines[i]);
|
|
2130
|
+
if (!match) continue;
|
|
2131
|
+
const trimmedNext = (lines[i + 1] ?? "").trim();
|
|
2132
|
+
if (!(trimmedNext === "pass" || trimmedNext.startsWith("#") && (lines[i + 2] ?? "").trim() === "pass")) continue;
|
|
2133
|
+
pushFinding(out, {
|
|
2134
|
+
relPath,
|
|
2135
|
+
rule: "ai-slop/python-broad-except",
|
|
2136
|
+
severity: "warning",
|
|
2137
|
+
message: `\`except ${match[1]}: pass\` silently drops every exception. Failures vanish without a trace.`,
|
|
2138
|
+
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.",
|
|
2139
|
+
line: i + 1
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
const flagMutableDefaults = (lines, relPath, out) => {
|
|
2144
|
+
let i = 0;
|
|
2145
|
+
while (i < lines.length) {
|
|
2146
|
+
if (!DEF_RE.test(lines[i])) {
|
|
2147
|
+
i++;
|
|
2148
|
+
continue;
|
|
2149
|
+
}
|
|
2150
|
+
const startLine = i;
|
|
2151
|
+
let signature = lines[i];
|
|
2152
|
+
let parenDepth = 0;
|
|
2153
|
+
for (const ch of signature) if (ch === "(") parenDepth++;
|
|
2154
|
+
else if (ch === ")") parenDepth--;
|
|
2155
|
+
while (parenDepth > 0 && i + 1 < lines.length) {
|
|
2156
|
+
i++;
|
|
2157
|
+
signature += `\n${lines[i]}`;
|
|
2158
|
+
for (const ch of lines[i]) if (ch === "(") parenDepth++;
|
|
2159
|
+
else if (ch === ")") parenDepth--;
|
|
2160
|
+
}
|
|
2161
|
+
MUTABLE_DEFAULT_RE.lastIndex = 0;
|
|
2162
|
+
const found = MUTABLE_DEFAULT_RE.exec(signature);
|
|
2163
|
+
if (found) pushFinding(out, {
|
|
2164
|
+
relPath,
|
|
2165
|
+
rule: "ai-slop/python-mutable-default",
|
|
2166
|
+
severity: "warning",
|
|
2167
|
+
message: `Mutable default argument \`${found[1]}=${found[2]}\`. The default is shared across all calls — bugs that look like state-leakage.`,
|
|
2168
|
+
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.",
|
|
2169
|
+
line: startLine + 1
|
|
2170
|
+
});
|
|
2171
|
+
i++;
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
const flagPrintInProduction = (lines, relPath, basename, out) => {
|
|
2175
|
+
if (isTestFile$1(relPath, basename) || isScriptOrEntrypoint(basename)) return;
|
|
2176
|
+
if (isInScriptDir(relPath)) return;
|
|
2177
|
+
if (isTutorialFile(basename)) return;
|
|
2178
|
+
if (hasMainGuard(lines)) return;
|
|
2179
|
+
const docstringLines = buildDocstringRanges(lines);
|
|
2180
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2181
|
+
const line = lines[i];
|
|
2182
|
+
if (!PRINT_RE.test(line)) continue;
|
|
2183
|
+
if (line.trim().startsWith("#")) continue;
|
|
2184
|
+
if (docstringLines.has(i)) continue;
|
|
2185
|
+
pushFinding(out, {
|
|
2186
|
+
relPath,
|
|
2187
|
+
rule: "ai-slop/python-print-debug",
|
|
2188
|
+
severity: "warning",
|
|
2189
|
+
message: "`print()` in production code — usually a leftover debug statement.",
|
|
2190
|
+
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.",
|
|
2191
|
+
line: i + 1
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
const detectPythonPatterns = async (context) => {
|
|
2196
|
+
const diagnostics = [];
|
|
2197
|
+
const files = getSourceFiles(context);
|
|
2198
|
+
for (const filePath of files) {
|
|
2199
|
+
if (!PY_EXTENSIONS$1.has(path.extname(filePath))) continue;
|
|
2200
|
+
if (isAutoGenerated(filePath)) continue;
|
|
1306
2201
|
let content;
|
|
1307
2202
|
try {
|
|
1308
2203
|
content = fs.readFileSync(filePath, "utf-8");
|
|
1309
2204
|
} catch {
|
|
1310
2205
|
continue;
|
|
1311
2206
|
}
|
|
2207
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2208
|
+
const basename = path.basename(filePath);
|
|
1312
2209
|
const lines = content.split("\n");
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
2210
|
+
flagBareExcept(lines, relPath, diagnostics);
|
|
2211
|
+
flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
|
|
2212
|
+
flagMutableDefaults(lines, relPath, diagnostics);
|
|
2213
|
+
flagPrintInProduction(lines, relPath, basename, diagnostics);
|
|
2214
|
+
}
|
|
2215
|
+
return diagnostics;
|
|
2216
|
+
};
|
|
2217
|
+
|
|
2218
|
+
//#endregion
|
|
2219
|
+
//#region src/engines/ai-slop/rust-patterns.ts
|
|
2220
|
+
const RUST_EXTENSIONS = new Set([".rs"]);
|
|
2221
|
+
const UNWRAP_CALL_RE = /\.unwrap\s*\(\s*\)/;
|
|
2222
|
+
const TODO_MACRO_RE = /\b(todo|unimplemented)\s*!\s*\(/;
|
|
2223
|
+
const COMMENT_LINE_RE = /^\s*\/\//;
|
|
2224
|
+
const TEST_ATTR_RE = /^\s*#\s*\[\s*(?:cfg\s*\(\s*test\s*\)|test|tokio::test)/;
|
|
2225
|
+
const WRITELN_UNWRAP_RE = /\b(?:writeln|write)\s*!\s*\([^)]*\)\s*\.unwrap\s*\(\s*\)/;
|
|
2226
|
+
const TEST_BASENAMES = new Set([
|
|
2227
|
+
"tests.rs",
|
|
2228
|
+
"testutil.rs",
|
|
2229
|
+
"test_util.rs",
|
|
2230
|
+
"test_utils.rs",
|
|
2231
|
+
"build.rs"
|
|
2232
|
+
]);
|
|
2233
|
+
const TEST_CRATE_SEGMENT_RE = /(?:^|[-_])tests?(?:$|[-_])/;
|
|
2234
|
+
const isTestFile = (relPath) => {
|
|
2235
|
+
const segments = relPath.split(path.sep);
|
|
2236
|
+
if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
|
|
2237
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
2238
|
+
if (TEST_BASENAMES.has(basename)) return true;
|
|
2239
|
+
return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
|
|
2240
|
+
};
|
|
2241
|
+
const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
|
|
2242
|
+
const UNWRAP_INTENT_LOOKBACK = 2;
|
|
2243
|
+
const hasIntentComment = (lines, lineIdx) => {
|
|
2244
|
+
for (let j = lineIdx - 1; j >= Math.max(0, lineIdx - UNWRAP_INTENT_LOOKBACK); j--) if (COMMENT_LINE_RE.test(lines[j])) return true;
|
|
2245
|
+
return false;
|
|
2246
|
+
};
|
|
2247
|
+
const buildTestRanges = (lines) => {
|
|
2248
|
+
const ranges = [];
|
|
2249
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2250
|
+
if (!TEST_ATTR_RE.test(lines[i])) continue;
|
|
2251
|
+
const openLine = i;
|
|
2252
|
+
let depth = 0;
|
|
2253
|
+
let started = false;
|
|
2254
|
+
for (let j = i; j < lines.length; j++) {
|
|
2255
|
+
const line = lines[j];
|
|
2256
|
+
for (const ch of line) if (ch === "{") {
|
|
2257
|
+
depth++;
|
|
2258
|
+
started = true;
|
|
2259
|
+
} else if (ch === "}") depth--;
|
|
2260
|
+
if (started && depth === 0) {
|
|
2261
|
+
ranges.push([openLine, j]);
|
|
2262
|
+
i = j;
|
|
2263
|
+
break;
|
|
2264
|
+
}
|
|
1324
2265
|
}
|
|
1325
|
-
const kept = [];
|
|
1326
|
-
for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
|
|
1327
|
-
const newContent = kept.join("\n");
|
|
1328
|
-
if (newContent !== content) fs.writeFileSync(filePath, newContent);
|
|
1329
2266
|
}
|
|
2267
|
+
return ranges;
|
|
2268
|
+
};
|
|
2269
|
+
const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
|
|
2270
|
+
const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
|
|
2271
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2272
|
+
const line = lines[i];
|
|
2273
|
+
if (COMMENT_LINE_RE.test(line)) continue;
|
|
2274
|
+
if (isInRange(testRanges, i)) continue;
|
|
2275
|
+
if (!UNWRAP_CALL_RE.test(line)) continue;
|
|
2276
|
+
if (WRITELN_UNWRAP_RE.test(line)) continue;
|
|
2277
|
+
if (hasIntentComment(lines, i)) continue;
|
|
2278
|
+
out.push({
|
|
2279
|
+
filePath: relPath,
|
|
2280
|
+
engine: "ai-slop",
|
|
2281
|
+
rule: "ai-slop/rust-non-test-unwrap",
|
|
2282
|
+
severity: "warning",
|
|
2283
|
+
message: "`.unwrap()` in non-test code panics on None/Err. Surfaces as a hard crash for the caller.",
|
|
2284
|
+
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.",
|
|
2285
|
+
line: i + 1,
|
|
2286
|
+
column: 1,
|
|
2287
|
+
category: "AI Slop",
|
|
2288
|
+
fixable: false
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
const flagTodoMacro = (lines, relPath, out) => {
|
|
2293
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2294
|
+
const line = lines[i];
|
|
2295
|
+
if (COMMENT_LINE_RE.test(line)) continue;
|
|
2296
|
+
const match = TODO_MACRO_RE.exec(line);
|
|
2297
|
+
if (!match) continue;
|
|
2298
|
+
out.push({
|
|
2299
|
+
filePath: relPath,
|
|
2300
|
+
engine: "ai-slop",
|
|
2301
|
+
rule: "ai-slop/rust-todo-stub",
|
|
2302
|
+
severity: "warning",
|
|
2303
|
+
message: `\`${match[1]}!()\` panics at runtime — almost certainly a stub the agent forgot to fill in.`,
|
|
2304
|
+
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.",
|
|
2305
|
+
line: i + 1,
|
|
2306
|
+
column: 1,
|
|
2307
|
+
category: "AI Slop",
|
|
2308
|
+
fixable: false
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
const detectRustPatterns = async (context) => {
|
|
2313
|
+
const diagnostics = [];
|
|
2314
|
+
const files = getSourceFiles(context);
|
|
2315
|
+
for (const filePath of files) {
|
|
2316
|
+
if (!RUST_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
2317
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2318
|
+
let content;
|
|
2319
|
+
try {
|
|
2320
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2321
|
+
} catch {
|
|
2322
|
+
continue;
|
|
2323
|
+
}
|
|
2324
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2325
|
+
const lines = content.split("\n");
|
|
2326
|
+
if (isExampleFile(relPath)) continue;
|
|
2327
|
+
if (isTestFile(relPath)) {
|
|
2328
|
+
flagTodoMacro(lines, relPath, diagnostics);
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2331
|
+
flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
|
|
2332
|
+
flagTodoMacro(lines, relPath, diagnostics);
|
|
2333
|
+
}
|
|
2334
|
+
return diagnostics;
|
|
1330
2335
|
};
|
|
1331
2336
|
|
|
1332
2337
|
//#endregion
|
|
1333
2338
|
//#region src/engines/ai-slop/unused-imports.ts
|
|
1334
|
-
const JS_EXTENSIONS = new Set([
|
|
2339
|
+
const JS_EXTENSIONS$1 = new Set([
|
|
1335
2340
|
".ts",
|
|
1336
2341
|
".tsx",
|
|
1337
2342
|
".js",
|
|
@@ -1461,7 +2466,7 @@ const analyzeFile = (filePath) => {
|
|
|
1461
2466
|
const lines = content.split("\n");
|
|
1462
2467
|
let symbols;
|
|
1463
2468
|
let importLines;
|
|
1464
|
-
if (JS_EXTENSIONS.has(ext)) {
|
|
2469
|
+
if (JS_EXTENSIONS$1.has(ext)) {
|
|
1465
2470
|
const result = extractJsImportedSymbols(lines);
|
|
1466
2471
|
symbols = result.symbols;
|
|
1467
2472
|
importLines = result.importLines;
|
|
@@ -1517,7 +2522,12 @@ const aiSlopEngine = {
|
|
|
1517
2522
|
detectOverAbstraction(context),
|
|
1518
2523
|
detectDeadPatterns(context),
|
|
1519
2524
|
detectUnusedImports(context),
|
|
1520
|
-
detectNarrativeComments(context)
|
|
2525
|
+
detectNarrativeComments(context),
|
|
2526
|
+
detectDuplicateImports(context),
|
|
2527
|
+
detectPythonPatterns(context),
|
|
2528
|
+
detectGoPatterns(context),
|
|
2529
|
+
detectRustPatterns(context),
|
|
2530
|
+
detectHallucinatedImports(context)
|
|
1521
2531
|
]);
|
|
1522
2532
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
1523
2533
|
return {
|
|
@@ -1913,6 +2923,12 @@ const isDataFile = (content) => {
|
|
|
1913
2923
|
const dataLinePattern = /^\s*[{}[\]"']/;
|
|
1914
2924
|
return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
|
|
1915
2925
|
};
|
|
2926
|
+
const TEST_PATH_RE = /(?:^|\/)(?:tests?|spec|specs|__tests__|__spec__|src\/test)\//i;
|
|
2927
|
+
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))$/;
|
|
2928
|
+
const MIGRATION_PATH_RE = /(?:^|\/)(?:migrations?|migrate|prisma\/migrations|db\/migrate)\//i;
|
|
2929
|
+
const FIXTURE_PATH_RE = /(?:^|\/)(?:__fixtures__|__snapshots__|__mocks__|fixtures?|snapshots?|seeds?|stubs?)\//i;
|
|
2930
|
+
const GENERATED_PATH_RE = /(?:^|\/)(?:generated|gen|build|dist|out|target|coverage|node_modules|vendor|\.next|\.nuxt|\.svelte-kit)\//i;
|
|
2931
|
+
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);
|
|
1916
2932
|
const analyzeFunctions = (content, ext) => {
|
|
1917
2933
|
const lines = content.split("\n");
|
|
1918
2934
|
const functions = [];
|
|
@@ -1941,13 +2957,13 @@ const checkFileDiagnostics = (relativePath, content, limits) => {
|
|
|
1941
2957
|
const lineCount = content.split("\n").length;
|
|
1942
2958
|
const ext = path.extname(relativePath).toLowerCase();
|
|
1943
2959
|
if (isDataFile(content)) return results;
|
|
1944
|
-
const
|
|
1945
|
-
if (lineCount >
|
|
2960
|
+
const configuredMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
|
|
2961
|
+
if (lineCount > Math.ceil(configuredMax * 1.1)) results.push({
|
|
1946
2962
|
filePath: relativePath,
|
|
1947
2963
|
engine: "code-quality",
|
|
1948
2964
|
rule: "complexity/file-too-large",
|
|
1949
2965
|
severity: "warning",
|
|
1950
|
-
message: `File has ${lineCount} lines (max: ${
|
|
2966
|
+
message: `File has ${lineCount} lines (max: ${configuredMax})`,
|
|
1951
2967
|
help: "Consider splitting this file into smaller modules",
|
|
1952
2968
|
line: 0,
|
|
1953
2969
|
column: 0,
|
|
@@ -1997,13 +3013,14 @@ const checkFunctionDiagnostics = (relativePath, fn, limits) => {
|
|
|
1997
3013
|
return results;
|
|
1998
3014
|
};
|
|
1999
3015
|
const checkFileComplexity = (filePath, rootDirectory, limits) => {
|
|
3016
|
+
const relativePath = path.relative(rootDirectory, filePath);
|
|
3017
|
+
if (isExemptFromComplexity(relativePath)) return [];
|
|
2000
3018
|
let content;
|
|
2001
3019
|
try {
|
|
2002
3020
|
content = fs.readFileSync(filePath, "utf-8");
|
|
2003
3021
|
} catch {
|
|
2004
3022
|
return [];
|
|
2005
3023
|
}
|
|
2006
|
-
const relativePath = path.relative(rootDirectory, filePath);
|
|
2007
3024
|
const ext = path.extname(filePath).toLowerCase();
|
|
2008
3025
|
const diagnostics = checkFileDiagnostics(relativePath, content, limits);
|
|
2009
3026
|
for (const fn of analyzeFunctions(content, ext)) diagnostics.push(...checkFunctionDiagnostics(relativePath, fn, limits));
|
|
@@ -3579,7 +4596,10 @@ const lintEngine = {
|
|
|
3579
4596
|
const diagnostics = [];
|
|
3580
4597
|
const { languages, installedTools } = context;
|
|
3581
4598
|
const promises = [];
|
|
3582
|
-
if (languages.includes("typescript") || languages.includes("javascript"))
|
|
4599
|
+
if (languages.includes("typescript") || languages.includes("javascript")) {
|
|
4600
|
+
promises.push(runOxlint(context));
|
|
4601
|
+
if (context.config.lint.typecheck) promises.push(import("./typecheck-B1MXNAy-.js").then((mod) => mod.runTypecheck(context)));
|
|
4602
|
+
}
|
|
3583
4603
|
if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
|
|
3584
4604
|
if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
|
|
3585
4605
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
@@ -4597,6 +5617,7 @@ const runScopedScan = async (cwd, filePaths) => {
|
|
|
4597
5617
|
audit: false,
|
|
4598
5618
|
auditTimeout: 0
|
|
4599
5619
|
},
|
|
5620
|
+
lint: { typecheck: false },
|
|
4600
5621
|
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
4601
5622
|
}
|
|
4602
5623
|
}, {
|
|
@@ -4635,6 +5656,9 @@ const readIfExists = (targetPath) => {
|
|
|
4635
5656
|
|
|
4636
5657
|
//#endregion
|
|
4637
5658
|
//#region src/hooks/quality-gate/baseline.ts
|
|
5659
|
+
const fingerprintDiagnostic = (d, rootDirectory) => {
|
|
5660
|
+
return `${path.isAbsolute(d.filePath) ? path.relative(rootDirectory, d.filePath) : d.filePath}:${d.line}:${d.rule}`;
|
|
5661
|
+
};
|
|
4638
5662
|
const BASELINE_REL = path.join(".aislop", "baseline.json");
|
|
4639
5663
|
const baselinePath = (cwd) => path.join(cwd, BASELINE_REL);
|
|
4640
5664
|
const readBaseline = (cwd) => {
|
|
@@ -4642,8 +5666,16 @@ const readBaseline = (cwd) => {
|
|
|
4642
5666
|
if (!raw) return null;
|
|
4643
5667
|
try {
|
|
4644
5668
|
const parsed = JSON.parse(raw);
|
|
4645
|
-
if (parsed.schema !== "aislop.baseline.v1") return null;
|
|
4646
|
-
return
|
|
5669
|
+
if (parsed.schema !== "aislop.baseline.v2" && parsed.schema !== "aislop.baseline.v1") return null;
|
|
5670
|
+
return {
|
|
5671
|
+
schema: "aislop.baseline.v2",
|
|
5672
|
+
updatedAt: parsed.updatedAt ?? "",
|
|
5673
|
+
score: parsed.score ?? 0,
|
|
5674
|
+
byEngine: parsed.byEngine ?? {},
|
|
5675
|
+
fileCount: parsed.fileCount ?? 0,
|
|
5676
|
+
commit: parsed.commit,
|
|
5677
|
+
findingFingerprints: parsed.findingFingerprints ?? []
|
|
5678
|
+
};
|
|
4647
5679
|
} catch {
|
|
4648
5680
|
return null;
|
|
4649
5681
|
}
|
|
@@ -4667,7 +5699,8 @@ const captureBaseline = async (cwd) => {
|
|
|
4667
5699
|
security: {
|
|
4668
5700
|
audit: false,
|
|
4669
5701
|
auditTimeout: 0
|
|
4670
|
-
}
|
|
5702
|
+
},
|
|
5703
|
+
lint: { typecheck: false }
|
|
4671
5704
|
}
|
|
4672
5705
|
}, {
|
|
4673
5706
|
format: config.engines.format,
|
|
@@ -4684,12 +5717,14 @@ const captureBaseline = async (cwd) => {
|
|
|
4684
5717
|
const { score: engineScore } = calculateScore(diagnostics.filter((d) => r.diagnostics.includes(d)), config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
4685
5718
|
byEngine[r.engine] = engineScore;
|
|
4686
5719
|
}
|
|
5720
|
+
const findingFingerprints = diagnostics.filter((d) => d.severity === "error" || d.severity === "warning").map((d) => fingerprintDiagnostic(d, project.rootDirectory));
|
|
4687
5721
|
const target = writeBaseline(cwd, {
|
|
4688
|
-
schema: "aislop.baseline.
|
|
5722
|
+
schema: "aislop.baseline.v2",
|
|
4689
5723
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4690
5724
|
score,
|
|
4691
5725
|
byEngine,
|
|
4692
|
-
fileCount: project.sourceFileCount
|
|
5726
|
+
fileCount: project.sourceFileCount,
|
|
5727
|
+
findingFingerprints
|
|
4693
5728
|
});
|
|
4694
5729
|
return {
|
|
4695
5730
|
score,
|
|
@@ -4778,7 +5813,10 @@ const runClaudeHook = async (deps = {}) => {
|
|
|
4778
5813
|
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
|
|
4779
5814
|
const baseline = readBaseline(cwd);
|
|
4780
5815
|
appendSessionFiles(cwd, files);
|
|
4781
|
-
const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline
|
|
5816
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline ? {
|
|
5817
|
+
score: baseline.score,
|
|
5818
|
+
findingFingerprints: baseline.findingFingerprints
|
|
5819
|
+
} : void 0);
|
|
4782
5820
|
const envelope = renderClaudeOutput(JSON.stringify(feedback));
|
|
4783
5821
|
write(JSON.stringify(envelope));
|
|
4784
5822
|
return 0;
|
|
@@ -4788,6 +5826,42 @@ const runClaudeHook = async (deps = {}) => {
|
|
|
4788
5826
|
release();
|
|
4789
5827
|
}
|
|
4790
5828
|
};
|
|
5829
|
+
const parseClaudeFileChangedStdin = (raw) => {
|
|
5830
|
+
if (!raw.trim()) return {};
|
|
5831
|
+
try {
|
|
5832
|
+
return JSON.parse(raw);
|
|
5833
|
+
} catch {
|
|
5834
|
+
return {};
|
|
5835
|
+
}
|
|
5836
|
+
};
|
|
5837
|
+
const runClaudeFileChangedHook = async (deps = {}) => {
|
|
5838
|
+
const getStdin = deps.stdin ?? readStdin$2;
|
|
5839
|
+
const write = deps.write ?? ((s) => process.stdout.write(s));
|
|
5840
|
+
const input = parseClaudeFileChangedStdin(await getStdin());
|
|
5841
|
+
const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
|
|
5842
|
+
const release = acquireHookLock(cwd);
|
|
5843
|
+
if (!release) return 0;
|
|
5844
|
+
try {
|
|
5845
|
+
const result = await captureBaseline(cwd);
|
|
5846
|
+
const changed = input.file_path ? path.relative(cwd, input.file_path) || input.file_path : "<unknown>";
|
|
5847
|
+
const envelope = renderClaudeOutput(JSON.stringify({
|
|
5848
|
+
schema: "aislop.hook.v2",
|
|
5849
|
+
event: "file_changed",
|
|
5850
|
+
file: changed,
|
|
5851
|
+
message: `Watched file changed (${changed}). aislop refreshed the baseline — score: ${result.score}.`,
|
|
5852
|
+
baseline: {
|
|
5853
|
+
score: result.score,
|
|
5854
|
+
fileCount: result.fileCount
|
|
5855
|
+
}
|
|
5856
|
+
}));
|
|
5857
|
+
write(JSON.stringify(envelope));
|
|
5858
|
+
return 0;
|
|
5859
|
+
} catch {
|
|
5860
|
+
return 0;
|
|
5861
|
+
} finally {
|
|
5862
|
+
release();
|
|
5863
|
+
}
|
|
5864
|
+
};
|
|
4791
5865
|
const parseClaudeStopStdin = (raw) => {
|
|
4792
5866
|
if (!raw.trim()) return {};
|
|
4793
5867
|
try {
|
|
@@ -4810,7 +5884,10 @@ const runClaudeStopHook = async (deps = {}) => {
|
|
|
4810
5884
|
if (!release) return 0;
|
|
4811
5885
|
try {
|
|
4812
5886
|
const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, sessionFiles);
|
|
4813
|
-
const feedback = buildFeedback(diagnostics, score, rootDirectory,
|
|
5887
|
+
const feedback = buildFeedback(diagnostics, score, rootDirectory, {
|
|
5888
|
+
score: baseline.score,
|
|
5889
|
+
findingFingerprints: baseline.findingFingerprints
|
|
5890
|
+
});
|
|
4814
5891
|
if (!feedback.regressed) {
|
|
4815
5892
|
clearSessionFiles(cwd);
|
|
4816
5893
|
return 0;
|
|
@@ -4938,7 +6015,7 @@ const AISLOP_MD_BODY = `# aislop — agent instructions
|
|
|
4938
6015
|
|
|
4939
6016
|
## On every edit
|
|
4940
6017
|
|
|
4941
|
-
A PostToolUse hook runs \`aislop hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`AislopFeedback\` (schema \`aislop.hook.
|
|
6018
|
+
A PostToolUse hook runs \`aislop hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`AislopFeedback\` (schema \`aislop.hook.v2\` — score, baseline, delta, regressed, counts, findings, newSinceBaseline, suggestedActions). Act on them the same turn; the \`suggestedActions\` field tells you exactly what to run next.
|
|
4942
6019
|
|
|
4943
6020
|
## Severity ladder
|
|
4944
6021
|
|
|
@@ -5175,6 +6252,25 @@ const buildStopHookGroup = () => {
|
|
|
5175
6252
|
}]
|
|
5176
6253
|
};
|
|
5177
6254
|
};
|
|
6255
|
+
const FILE_CHANGED_MATCHER = ".aislop/config.yml|.aislop/rules.yml|package.json";
|
|
6256
|
+
const buildFileChangedHookGroup = () => {
|
|
6257
|
+
const hashBody = JSON.stringify({
|
|
6258
|
+
command: "aislop hook claude --on-file-changed",
|
|
6259
|
+
matcher: FILE_CHANGED_MATCHER
|
|
6260
|
+
});
|
|
6261
|
+
return {
|
|
6262
|
+
matcher: FILE_CHANGED_MATCHER,
|
|
6263
|
+
hooks: [{
|
|
6264
|
+
type: "command",
|
|
6265
|
+
command: "aislop hook claude --on-file-changed",
|
|
6266
|
+
[AISLOP_SENTINEL_KEY]: {
|
|
6267
|
+
v: 1,
|
|
6268
|
+
managed: true,
|
|
6269
|
+
hash: sentinelHash(hashBody)
|
|
6270
|
+
}
|
|
6271
|
+
}]
|
|
6272
|
+
};
|
|
6273
|
+
};
|
|
5178
6274
|
const renderSettings$1 = (existingRaw, qualityGate) => {
|
|
5179
6275
|
let obj = {};
|
|
5180
6276
|
if (existingRaw) try {
|
|
@@ -5183,6 +6279,7 @@ const renderSettings$1 = (existingRaw, qualityGate) => {
|
|
|
5183
6279
|
obj = {};
|
|
5184
6280
|
}
|
|
5185
6281
|
let next = upsertHookGroup(obj, "PostToolUse", buildHookGroup$1());
|
|
6282
|
+
next = upsertHookGroup(next, "FileChanged", buildFileChangedHookGroup());
|
|
5186
6283
|
if (qualityGate) next = upsertHookGroup(next, "Stop", buildStopHookGroup());
|
|
5187
6284
|
else next = removeAislopEntries(next, "Stop").next;
|
|
5188
6285
|
return `${JSON.stringify(next, null, 2)}\n`;
|
|
@@ -5191,7 +6288,7 @@ const installClaude = (opts) => {
|
|
|
5191
6288
|
const paths = resolveClaudePaths(opts);
|
|
5192
6289
|
const result = emptyResult();
|
|
5193
6290
|
const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
|
|
5194
|
-
applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse
|
|
6291
|
+
applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse + FileChanged hooks");
|
|
5195
6292
|
const mdHash = sentinelHash(AISLOP_MD_BODY);
|
|
5196
6293
|
const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, mdHash);
|
|
5197
6294
|
applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
|
|
@@ -5221,7 +6318,9 @@ const uninstallClaude = (opts) => {
|
|
|
5221
6318
|
} catch {
|
|
5222
6319
|
obj = {};
|
|
5223
6320
|
}
|
|
5224
|
-
const
|
|
6321
|
+
const afterPostToolUse = removeAislopEntries(obj, "PostToolUse").next;
|
|
6322
|
+
const afterFileChanged = removeAislopEntries(afterPostToolUse, "FileChanged").next;
|
|
6323
|
+
const stripped = removeAislopEntries(afterFileChanged, "Stop").next;
|
|
5225
6324
|
const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
|
|
5226
6325
|
const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
|
|
5227
6326
|
if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
|
|
@@ -5230,7 +6329,7 @@ const uninstallClaude = (opts) => {
|
|
|
5230
6329
|
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
5231
6330
|
else result.skipped.push(paths.aislopMd);
|
|
5232
6331
|
const claudeMd = readIfExists(paths.claudeMd);
|
|
5233
|
-
if (claudeMd
|
|
6332
|
+
if (claudeMd?.includes("@AISLOP.md")) {
|
|
5234
6333
|
const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
5235
6334
|
applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
5236
6335
|
} else result.skipped.push(paths.claudeMd);
|
|
@@ -5791,7 +6890,9 @@ const hookRun = async (agent, flags) => {
|
|
|
5791
6890
|
process.exit(0);
|
|
5792
6891
|
}
|
|
5793
6892
|
let exitCode = 0;
|
|
5794
|
-
if (agent === "claude")
|
|
6893
|
+
if (agent === "claude") if (flags?.onFileChanged) exitCode = await runClaudeFileChangedHook();
|
|
6894
|
+
else if (flags?.stop) exitCode = await runClaudeStopHook();
|
|
6895
|
+
else exitCode = await runClaudeHook();
|
|
5795
6896
|
else if (agent === "cursor") exitCode = await runCursorHook();
|
|
5796
6897
|
else if (agent === "gemini") exitCode = await runGeminiHook();
|
|
5797
6898
|
else {
|
|
@@ -5942,8 +7043,11 @@ const registerCallbacks = (hook) => {
|
|
|
5942
7043
|
hook.command("baseline").description("Capture the current project score as the quality-gate baseline").action(async () => {
|
|
5943
7044
|
await hookBaseline();
|
|
5944
7045
|
});
|
|
5945
|
-
hook.command("claude").description("Internal: Claude Code PostToolUse / Stop callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").action(async (opts) => {
|
|
5946
|
-
await hookRun("claude", {
|
|
7046
|
+
hook.command("claude").description("Internal: Claude Code PostToolUse / Stop / FileChanged callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").option("--on-file-changed", "run in FileChanged mode (refresh baseline on watched file change)").action(async (opts) => {
|
|
7047
|
+
await hookRun("claude", {
|
|
7048
|
+
stop: Boolean(opts.stop),
|
|
7049
|
+
onFileChanged: Boolean(opts.onFileChanged)
|
|
7050
|
+
});
|
|
5947
7051
|
});
|
|
5948
7052
|
hook.command("cursor").description("Internal: Cursor afterFileEdit callback (reads stdin)").action(async () => {
|
|
5949
7053
|
await hookRun("cursor");
|
|
@@ -6452,7 +7556,7 @@ const renderCleanRun = (input, deps = {}) => {
|
|
|
6452
7556
|
|
|
6453
7557
|
//#endregion
|
|
6454
7558
|
//#region src/version.ts
|
|
6455
|
-
const APP_VERSION = "0.
|
|
7559
|
+
const APP_VERSION = "0.8.0";
|
|
6456
7560
|
|
|
6457
7561
|
//#endregion
|
|
6458
7562
|
//#region src/utils/telemetry.ts
|
|
@@ -6607,6 +7711,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
6607
7711
|
const engineConfig = {
|
|
6608
7712
|
quality: config.quality,
|
|
6609
7713
|
security: config.security,
|
|
7714
|
+
lint: config.lint,
|
|
6610
7715
|
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
6611
7716
|
};
|
|
6612
7717
|
const gridRows = ALL_ENGINE_NAMES.filter((engine) => config.engines[engine] !== false).map((engine) => ({
|
|
@@ -6674,7 +7779,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
6674
7779
|
});
|
|
6675
7780
|
}
|
|
6676
7781
|
if (options.json) {
|
|
6677
|
-
const { buildJsonOutput } = await import("./json-
|
|
7782
|
+
const { buildJsonOutput } = await import("./json-BbMwrgyd.js");
|
|
6678
7783
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
6679
7784
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
6680
7785
|
return { exitCode };
|
|
@@ -6885,16 +7990,29 @@ const planFormat = (ctx) => {
|
|
|
6885
7990
|
skipReason: "no supported language"
|
|
6886
7991
|
};
|
|
6887
7992
|
};
|
|
6888
|
-
const
|
|
6889
|
-
const
|
|
6890
|
-
|
|
6891
|
-
|
|
7993
|
+
const findLocalTsc = (root) => {
|
|
7994
|
+
const candidate = path.join(root, "node_modules", ".bin", "tsc");
|
|
7995
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
7996
|
+
};
|
|
7997
|
+
const withTypecheckSuffix = (baseTool, ctx) => {
|
|
7998
|
+
if (!ctx.config.lint?.typecheck) return {
|
|
7999
|
+
tool: baseTool,
|
|
6892
8000
|
status: "ok"
|
|
6893
8001
|
};
|
|
6894
|
-
if (
|
|
6895
|
-
tool:
|
|
8002
|
+
if (findLocalTsc(ctx.rootDirectory)) return {
|
|
8003
|
+
tool: `${baseTool} + tsc`,
|
|
6896
8004
|
status: "ok"
|
|
6897
8005
|
};
|
|
8006
|
+
return {
|
|
8007
|
+
tool: `${baseTool} + tsc not found`,
|
|
8008
|
+
status: "missing",
|
|
8009
|
+
remediation: "Install TypeScript locally (pnpm add -D typescript), or set lint.typecheck: false in .aislop/config.yml."
|
|
8010
|
+
};
|
|
8011
|
+
};
|
|
8012
|
+
const planLint = (ctx) => {
|
|
8013
|
+
const { languages, frameworks, installedTools } = ctx.projectInfo;
|
|
8014
|
+
if (frameworks.includes("expo")) return withTypecheckSuffix("expo-doctor + oxlint (bundled)", ctx);
|
|
8015
|
+
if (hasJsLike(languages)) return withTypecheckSuffix("oxlint (bundled)", ctx);
|
|
6898
8016
|
return firstMatching(languages, installedTools, LINT_SPECS) ?? {
|
|
6899
8017
|
tool: "no linter",
|
|
6900
8018
|
status: "skipped",
|
|
@@ -7489,6 +8607,212 @@ const applyEditsAndCollapse = (lines, linesToRemove, lineReplacements) => {
|
|
|
7489
8607
|
return collapsed.join("\n");
|
|
7490
8608
|
};
|
|
7491
8609
|
|
|
8610
|
+
//#endregion
|
|
8611
|
+
//#region src/engines/ai-slop/duplicate-imports-fix.ts
|
|
8612
|
+
const JS_EXTENSIONS = new Set([
|
|
8613
|
+
".ts",
|
|
8614
|
+
".tsx",
|
|
8615
|
+
".js",
|
|
8616
|
+
".jsx",
|
|
8617
|
+
".mjs",
|
|
8618
|
+
".cjs"
|
|
8619
|
+
]);
|
|
8620
|
+
const IMPORT_FROM_RE = /^\s*import\s+(.*?)\s+from\s+["']([^"']+)["']\s*;?\s*$/;
|
|
8621
|
+
const SIDE_EFFECT_RE = /^\s*import\s+["']([^"']+)["']\s*;?\s*$/;
|
|
8622
|
+
const parseNamedClause = (clause) => {
|
|
8623
|
+
const inner = clause.trim().slice(1, -1).trim();
|
|
8624
|
+
if (inner.length === 0) return [];
|
|
8625
|
+
const items = [];
|
|
8626
|
+
for (const part of inner.split(",")) {
|
|
8627
|
+
const trimmed = part.trim();
|
|
8628
|
+
if (!trimmed) continue;
|
|
8629
|
+
let isType = false;
|
|
8630
|
+
let working = trimmed;
|
|
8631
|
+
if (/^type\s+/.test(working)) {
|
|
8632
|
+
isType = true;
|
|
8633
|
+
working = working.replace(/^type\s+/, "");
|
|
8634
|
+
}
|
|
8635
|
+
const aliasMatch = working.match(/^(\w+)\s+as\s+(\w+)$/);
|
|
8636
|
+
if (aliasMatch) {
|
|
8637
|
+
items.push({
|
|
8638
|
+
name: aliasMatch[1],
|
|
8639
|
+
alias: aliasMatch[2],
|
|
8640
|
+
isType
|
|
8641
|
+
});
|
|
8642
|
+
continue;
|
|
8643
|
+
}
|
|
8644
|
+
if (/^\w+$/.test(working)) items.push({
|
|
8645
|
+
name: working,
|
|
8646
|
+
isType
|
|
8647
|
+
});
|
|
8648
|
+
}
|
|
8649
|
+
return items;
|
|
8650
|
+
};
|
|
8651
|
+
const parseImportClause = (clause) => {
|
|
8652
|
+
let rest = clause.trim();
|
|
8653
|
+
let isTypeOnly = false;
|
|
8654
|
+
if (/^type\s+/.test(rest)) {
|
|
8655
|
+
isTypeOnly = true;
|
|
8656
|
+
rest = rest.replace(/^type\s+/, "");
|
|
8657
|
+
}
|
|
8658
|
+
const out = {
|
|
8659
|
+
named: [],
|
|
8660
|
+
isTypeOnly
|
|
8661
|
+
};
|
|
8662
|
+
const defMatch = rest.match(/^([A-Za-z_$][\w$]*)\s*(?:,\s*(.+))?$/);
|
|
8663
|
+
if (defMatch && !rest.startsWith("{") && !rest.startsWith("*")) {
|
|
8664
|
+
out.default = defMatch[1];
|
|
8665
|
+
rest = defMatch[2]?.trim() ?? "";
|
|
8666
|
+
}
|
|
8667
|
+
if (rest.startsWith("*")) {
|
|
8668
|
+
const nsMatch = rest.match(/^\*\s+as\s+(\w+)/);
|
|
8669
|
+
if (nsMatch) out.namespace = nsMatch[1];
|
|
8670
|
+
return out;
|
|
8671
|
+
}
|
|
8672
|
+
if (rest.startsWith("{")) out.named = parseNamedClause(rest);
|
|
8673
|
+
return out;
|
|
8674
|
+
};
|
|
8675
|
+
const parseImportLine = (line, lineIndex) => {
|
|
8676
|
+
const sideEffect = line.match(SIDE_EFFECT_RE);
|
|
8677
|
+
if (sideEffect) return {
|
|
8678
|
+
lineIndex,
|
|
8679
|
+
module: sideEffect[1],
|
|
8680
|
+
named: [],
|
|
8681
|
+
isTypeOnly: false,
|
|
8682
|
+
isSideEffect: true
|
|
8683
|
+
};
|
|
8684
|
+
const m = line.match(IMPORT_FROM_RE);
|
|
8685
|
+
if (!m) return null;
|
|
8686
|
+
return {
|
|
8687
|
+
lineIndex,
|
|
8688
|
+
module: m[2],
|
|
8689
|
+
isSideEffect: false,
|
|
8690
|
+
...parseImportClause(m[1])
|
|
8691
|
+
};
|
|
8692
|
+
};
|
|
8693
|
+
const formatNamed = (n, stripType) => {
|
|
8694
|
+
const prefix = n.isType && !stripType ? "type " : "";
|
|
8695
|
+
const suffix = n.alias ? ` as ${n.alias}` : "";
|
|
8696
|
+
return `${prefix}${n.name}${suffix}`;
|
|
8697
|
+
};
|
|
8698
|
+
const mergeImports = (group) => {
|
|
8699
|
+
if (group.some((s) => s.isSideEffect)) return null;
|
|
8700
|
+
if (group.some((s) => s.namespace !== void 0)) return null;
|
|
8701
|
+
if (group.some((s) => s.isTypeOnly && s.default !== void 0)) return null;
|
|
8702
|
+
const defaults = group.map((s) => s.default).filter((d) => d !== void 0);
|
|
8703
|
+
const uniqueDefaults = Array.from(new Set(defaults));
|
|
8704
|
+
if (uniqueDefaults.length > 1) return null;
|
|
8705
|
+
const defaultName = uniqueDefaults[0];
|
|
8706
|
+
const merged = /* @__PURE__ */ new Map();
|
|
8707
|
+
for (const stmt of group) for (const nm of stmt.named) {
|
|
8708
|
+
const key = nm.alias ?? nm.name;
|
|
8709
|
+
const isType = nm.isType || stmt.isTypeOnly;
|
|
8710
|
+
const existing = merged.get(key);
|
|
8711
|
+
if (!existing) merged.set(key, {
|
|
8712
|
+
...nm,
|
|
8713
|
+
isType
|
|
8714
|
+
});
|
|
8715
|
+
else existing.isType = existing.isType && isType;
|
|
8716
|
+
}
|
|
8717
|
+
const insertionOrder = Array.from(merged.values());
|
|
8718
|
+
const namedList = [...insertionOrder.filter((n) => !n.isType), ...insertionOrder.filter((n) => n.isType)];
|
|
8719
|
+
const allTypeOnly = namedList.length > 0 && namedList.every((n) => n.isType);
|
|
8720
|
+
const module = group[0].module;
|
|
8721
|
+
if (!defaultName && namedList.length === 0) return null;
|
|
8722
|
+
if (!defaultName && allTypeOnly) return `import type { ${namedList.map((n) => formatNamed(n, true)).join(", ")} } from "${module}";`;
|
|
8723
|
+
const parts = [];
|
|
8724
|
+
if (defaultName) parts.push(defaultName);
|
|
8725
|
+
if (namedList.length > 0) {
|
|
8726
|
+
const items = namedList.map((n) => formatNamed(n, false)).join(", ");
|
|
8727
|
+
parts.push(`{ ${items} }`);
|
|
8728
|
+
}
|
|
8729
|
+
return `import ${parts.join(", ")} from "${module}";`;
|
|
8730
|
+
};
|
|
8731
|
+
const fixDuplicateImports = async (context) => {
|
|
8732
|
+
const files = getSourceFiles(context);
|
|
8733
|
+
for (const filePath of files) {
|
|
8734
|
+
if (!JS_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
8735
|
+
if (isAutoGenerated(filePath)) continue;
|
|
8736
|
+
let content;
|
|
8737
|
+
try {
|
|
8738
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
8739
|
+
} catch {
|
|
8740
|
+
continue;
|
|
8741
|
+
}
|
|
8742
|
+
const lines = content.split("\n");
|
|
8743
|
+
const imports = [];
|
|
8744
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8745
|
+
const stmt = parseImportLine(lines[i], i);
|
|
8746
|
+
if (stmt) imports.push(stmt);
|
|
8747
|
+
}
|
|
8748
|
+
if (imports.length < 2) continue;
|
|
8749
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8750
|
+
for (const stmt of imports) {
|
|
8751
|
+
const list = groups.get(stmt.module) ?? [];
|
|
8752
|
+
list.push(stmt);
|
|
8753
|
+
groups.set(stmt.module, list);
|
|
8754
|
+
}
|
|
8755
|
+
const linesToRemove = /* @__PURE__ */ new Set();
|
|
8756
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
8757
|
+
let modified = false;
|
|
8758
|
+
for (const group of groups.values()) {
|
|
8759
|
+
if (group.length < 2) continue;
|
|
8760
|
+
const merged = mergeImports(group);
|
|
8761
|
+
if (!merged) continue;
|
|
8762
|
+
replacements.set(group[0].lineIndex, merged);
|
|
8763
|
+
for (const stmt of group.slice(1)) linesToRemove.add(stmt.lineIndex);
|
|
8764
|
+
modified = true;
|
|
8765
|
+
}
|
|
8766
|
+
if (!modified) continue;
|
|
8767
|
+
const next = [...lines];
|
|
8768
|
+
for (const [idx, replacement] of replacements) next[idx] = replacement;
|
|
8769
|
+
const sortedRemove = Array.from(linesToRemove).sort((a, b) => b - a);
|
|
8770
|
+
for (const idx of sortedRemove) next.splice(idx, 1);
|
|
8771
|
+
fs.writeFileSync(filePath, next.join("\n"));
|
|
8772
|
+
}
|
|
8773
|
+
};
|
|
8774
|
+
|
|
8775
|
+
//#endregion
|
|
8776
|
+
//#region src/engines/ai-slop/narrative-comments-fix.ts
|
|
8777
|
+
const fixNarrativeComments = async (context) => {
|
|
8778
|
+
const diagnostics = await detectNarrativeComments(context);
|
|
8779
|
+
if (diagnostics.length === 0) return;
|
|
8780
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
8781
|
+
for (const d of diagnostics) {
|
|
8782
|
+
const abs = d.filePath.startsWith("/") ? d.filePath : `${context.rootDirectory}/${d.filePath}`;
|
|
8783
|
+
const list = byFile.get(abs) ?? [];
|
|
8784
|
+
list.push(d);
|
|
8785
|
+
byFile.set(abs, list);
|
|
8786
|
+
}
|
|
8787
|
+
for (const [filePath, diags] of byFile) {
|
|
8788
|
+
const syntax = getCommentSyntax(path.extname(filePath));
|
|
8789
|
+
if (!syntax) continue;
|
|
8790
|
+
let content;
|
|
8791
|
+
try {
|
|
8792
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
8793
|
+
} catch {
|
|
8794
|
+
continue;
|
|
8795
|
+
}
|
|
8796
|
+
const lines = content.split("\n");
|
|
8797
|
+
const blocks = collectBlocks(lines, syntax);
|
|
8798
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
8799
|
+
for (const d of diags) {
|
|
8800
|
+
const block = blocks.find((b) => b.startLine === d.line);
|
|
8801
|
+
if (!block) continue;
|
|
8802
|
+
for (let ln = block.startLine; ln <= block.endLine; ln += 1) toRemove.add(ln);
|
|
8803
|
+
const prev = block.startLine - 1;
|
|
8804
|
+
const next = block.endLine + 1;
|
|
8805
|
+
const prevIsBlank = prev >= 1 && lines[prev - 1]?.trim() === "";
|
|
8806
|
+
const nextIsBlank = next <= lines.length && lines[next - 1]?.trim() === "";
|
|
8807
|
+
if (prevIsBlank && nextIsBlank) toRemove.add(prev);
|
|
8808
|
+
}
|
|
8809
|
+
const kept = [];
|
|
8810
|
+
for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
|
|
8811
|
+
const newContent = kept.join("\n");
|
|
8812
|
+
if (newContent !== content) fs.writeFileSync(filePath, newContent);
|
|
8813
|
+
}
|
|
8814
|
+
};
|
|
8815
|
+
|
|
7492
8816
|
//#endregion
|
|
7493
8817
|
//#region src/engines/ai-slop/unused-imports-fix.ts
|
|
7494
8818
|
const fixUnusedImports = async (context) => {
|
|
@@ -7510,9 +8834,9 @@ const fixUnusedImports = async (context) => {
|
|
|
7510
8834
|
for (const [lineNo, syms] of symbolsByLine) {
|
|
7511
8835
|
const lineIdx = lineNo - 1;
|
|
7512
8836
|
const allUnused = syms.every((s) => unusedNames.has(s.name));
|
|
7513
|
-
const importSpan = JS_EXTENSIONS.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
|
|
8837
|
+
const importSpan = JS_EXTENSIONS$1.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
|
|
7514
8838
|
if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
|
|
7515
|
-
else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
8839
|
+
else if (JS_EXTENSIONS$1.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
7516
8840
|
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
|
|
7517
8841
|
}
|
|
7518
8842
|
if (linesToRemove.size === 0 && unused.length === 0) continue;
|
|
@@ -8245,6 +9569,7 @@ const hasJsOrTs = (projectInfo) => projectInfo.languages.includes("typescript")
|
|
|
8245
9569
|
const runAiSlopSteps = async (deps) => {
|
|
8246
9570
|
if (!deps.config.engines["ai-slop"]) return;
|
|
8247
9571
|
await deps.runStep("Unused imports", () => detectUnusedImports(deps.context), () => fixUnusedImports(deps.context));
|
|
9572
|
+
await deps.runStep("Duplicate imports", () => detectDuplicateImports(deps.context), () => fixDuplicateImports(deps.context));
|
|
8248
9573
|
const detectFixableSlop = async () => {
|
|
8249
9574
|
const [comments, dead, narrative] = await Promise.all([
|
|
8250
9575
|
detectTrivialComments(deps.context),
|
|
@@ -8350,7 +9675,8 @@ const createEngineContext = (rootDirectory, projectInfo, config) => ({
|
|
|
8350
9675
|
installedTools: projectInfo.installedTools,
|
|
8351
9676
|
config: {
|
|
8352
9677
|
quality: config.quality,
|
|
8353
|
-
security: config.security
|
|
9678
|
+
security: config.security,
|
|
9679
|
+
lint: config.lint
|
|
8354
9680
|
}
|
|
8355
9681
|
});
|
|
8356
9682
|
const fixCommand = async (directory, config, options = {
|
|
@@ -8413,6 +9739,7 @@ const fixCommand = async (directory, config, options = {
|
|
|
8413
9739
|
const engineConfig = {
|
|
8414
9740
|
quality: config.quality,
|
|
8415
9741
|
security: config.security,
|
|
9742
|
+
lint: config.lint,
|
|
8416
9743
|
architectureRulesPath: config.engines.architecture ? rulesPath : void 0
|
|
8417
9744
|
};
|
|
8418
9745
|
rail.start("Verifying results");
|
|
@@ -8717,8 +10044,10 @@ const buildRulesRender = (input) => {
|
|
|
8717
10044
|
const AI_SLOP_FIXABLE = new Set([
|
|
8718
10045
|
"ai-slop/trivial-comment",
|
|
8719
10046
|
"ai-slop/unused-import",
|
|
8720
|
-
"ai-slop/narrative-comment"
|
|
10047
|
+
"ai-slop/narrative-comment",
|
|
10048
|
+
"ai-slop/duplicate-import"
|
|
8721
10049
|
]);
|
|
10050
|
+
const AI_SLOP_ERRORS = new Set(["ai-slop/hallucinated-import"]);
|
|
8722
10051
|
const BUILTIN_RULES = [
|
|
8723
10052
|
{
|
|
8724
10053
|
engine: "format",
|
|
@@ -8739,7 +10068,8 @@ const BUILTIN_RULES = [
|
|
|
8739
10068
|
"ruff/*",
|
|
8740
10069
|
"go/*",
|
|
8741
10070
|
"clippy/*",
|
|
8742
|
-
"rubocop/*"
|
|
10071
|
+
"rubocop/*",
|
|
10072
|
+
"typescript/*"
|
|
8743
10073
|
]
|
|
8744
10074
|
},
|
|
8745
10075
|
{
|
|
@@ -8775,7 +10105,16 @@ const BUILTIN_RULES = [
|
|
|
8775
10105
|
"ai-slop/unsafe-type-assertion",
|
|
8776
10106
|
"ai-slop/double-type-assertion",
|
|
8777
10107
|
"ai-slop/ts-directive",
|
|
8778
|
-
"ai-slop/narrative-comment"
|
|
10108
|
+
"ai-slop/narrative-comment",
|
|
10109
|
+
"ai-slop/duplicate-import",
|
|
10110
|
+
"ai-slop/python-bare-except",
|
|
10111
|
+
"ai-slop/python-broad-except",
|
|
10112
|
+
"ai-slop/python-mutable-default",
|
|
10113
|
+
"ai-slop/python-print-debug",
|
|
10114
|
+
"ai-slop/go-library-panic",
|
|
10115
|
+
"ai-slop/rust-non-test-unwrap",
|
|
10116
|
+
"ai-slop/rust-todo-stub",
|
|
10117
|
+
"ai-slop/hallucinated-import"
|
|
8779
10118
|
]
|
|
8780
10119
|
},
|
|
8781
10120
|
{
|
|
@@ -8806,7 +10145,7 @@ const toRuleEntry = (engine, ruleId) => {
|
|
|
8806
10145
|
if (engine === "ai-slop") return {
|
|
8807
10146
|
id: ruleId,
|
|
8808
10147
|
engine,
|
|
8809
|
-
severity: "warning",
|
|
10148
|
+
severity: AI_SLOP_ERRORS.has(ruleId) ? "error" : "warning",
|
|
8810
10149
|
fixable: AI_SLOP_FIXABLE.has(ruleId)
|
|
8811
10150
|
};
|
|
8812
10151
|
return {
|
|
@@ -9120,4 +10459,4 @@ const main = async () => {
|
|
|
9120
10459
|
main();
|
|
9121
10460
|
|
|
9122
10461
|
//#endregion
|
|
9123
|
-
export { ENGINE_INFO as n, APP_VERSION as t };
|
|
10462
|
+
export { ENGINE_INFO as n, runSubprocess as r, APP_VERSION as t };
|