aislop 0.10.2 → 0.11.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 +151 -69
- package/dist/cli.js +1803 -374
- package/dist/{expo-doctor-T4DswmX5.js → expo-doctor-BM2JR6f6.js} +1 -1
- package/dist/{expo-doctor-c-jE6pR2.js → expo-doctor-BwLKXF__.js} +2 -2
- package/dist/finding-assessment-PCl1fnok.js +149 -0
- package/dist/{generic-BsQa13CS.js → generic-D_T4cUaC.js} +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +1015 -215
- package/dist/{json-B01i-GOz.js → json-0lJPTrwO.js} +5 -3
- package/dist/{json-CXV4D0Ib.js → json-pHsqtKkz.js} +4 -2
- package/dist/mcp.js +540 -97
- package/dist/{sarif-cy5SiDDq.js → sarif-BXUicqQU.js} +1 -1
- package/dist/{sarif-CZVuavf_.js → sarif-CjxSBcqx.js} +1 -1
- package/dist/{typecheck-BdQ7uFyK.js → typecheck-DQSzG8fX.js} +1 -1
- package/dist/{typecheck-wVSohmOX.js → typecheck-yOGXIIGU.js} +1 -1
- package/dist/version-BJA3AcRM.js +7 -0
- package/package.json +2 -2
- package/dist/engine-info-Cpt36DqZ.js +0 -31
- package/dist/version-BfJVwhN2.js +0 -5
- /package/dist/{subprocess-0uXz8HdE.js → subprocess-CQUJDGgn.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import { n as
|
|
3
|
-
import {
|
|
4
|
-
import { r as runGenericLinter, t as fixRubyLint } from "./generic-
|
|
5
|
-
import { n as runExpoDoctor } from "./expo-doctor-
|
|
1
|
+
import { i as getEngineLabel, r as ENGINE_INFO, t as summarizeFindingAssessments } from "./finding-assessment-PCl1fnok.js";
|
|
2
|
+
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
|
|
3
|
+
import { t as APP_VERSION } from "./version-BJA3AcRM.js";
|
|
4
|
+
import { r as runGenericLinter, t as fixRubyLint } from "./generic-D_T4cUaC.js";
|
|
5
|
+
import { n as runExpoDoctor } from "./expo-doctor-BwLKXF__.js";
|
|
6
6
|
import { createRequire, isBuiltin } from "node:module";
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
@@ -18,6 +18,8 @@ import ts from "typescript";
|
|
|
18
18
|
import os from "node:os";
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
20
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
21
|
+
import * as readline from "node:readline";
|
|
22
|
+
import { Writable } from "node:stream";
|
|
21
23
|
|
|
22
24
|
//#region src/config/defaults.ts
|
|
23
25
|
const DEFAULT_CONFIG = {
|
|
@@ -62,7 +64,8 @@ const DEFAULT_CONFIG = {
|
|
|
62
64
|
good: 75,
|
|
63
65
|
ok: 50
|
|
64
66
|
},
|
|
65
|
-
smoothing: 20
|
|
67
|
+
smoothing: 20,
|
|
68
|
+
maxPerRule: 40
|
|
66
69
|
},
|
|
67
70
|
ci: {
|
|
68
71
|
failBelow: 70,
|
|
@@ -85,9 +88,9 @@ jobs:
|
|
|
85
88
|
runs-on: ubuntu-latest
|
|
86
89
|
steps:
|
|
87
90
|
- uses: actions/checkout@v4
|
|
88
|
-
- uses: scanaislop/aislop@
|
|
91
|
+
- uses: scanaislop/aislop@v1
|
|
89
92
|
with:
|
|
90
|
-
version:
|
|
93
|
+
version: latest
|
|
91
94
|
`;
|
|
92
95
|
const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
|
|
93
96
|
# Uncomment and customize to enforce your project's conventions.
|
|
@@ -186,7 +189,8 @@ const ScoringSchema = z.object({
|
|
|
186
189
|
good: 75,
|
|
187
190
|
ok: 50
|
|
188
191
|
})),
|
|
189
|
-
smoothing: z.number().nonnegative().default(20)
|
|
192
|
+
smoothing: z.number().nonnegative().default(20),
|
|
193
|
+
maxPerRule: z.number().positive().default(40)
|
|
190
194
|
});
|
|
191
195
|
const CiSchema = z.object({
|
|
192
196
|
failBelow: z.number().default(70),
|
|
@@ -226,7 +230,8 @@ const AislopConfigSchema = z.object({
|
|
|
226
230
|
good: 75,
|
|
227
231
|
ok: 50
|
|
228
232
|
},
|
|
229
|
-
smoothing: 20
|
|
233
|
+
smoothing: 20,
|
|
234
|
+
maxPerRule: 40
|
|
230
235
|
})),
|
|
231
236
|
ci: CiSchema.default(() => ({
|
|
232
237
|
failBelow: 70,
|
|
@@ -390,7 +395,7 @@ const renderHeader = (input, _deps = {}) => {
|
|
|
390
395
|
|
|
391
396
|
//#endregion
|
|
392
397
|
//#region src/ui/invocation.ts
|
|
393
|
-
const detectInvocation = () => "
|
|
398
|
+
const detectInvocation = () => "aislop";
|
|
394
399
|
|
|
395
400
|
//#endregion
|
|
396
401
|
//#region src/ui/symbols.ts
|
|
@@ -546,6 +551,19 @@ const padStart = (s, target, fill = " ") => {
|
|
|
546
551
|
if (w >= target) return s;
|
|
547
552
|
return fill.repeat(target - w) + s;
|
|
548
553
|
};
|
|
554
|
+
const truncate = (s, max, ellipsis = "…") => {
|
|
555
|
+
if (stringWidth(s) <= max) return s;
|
|
556
|
+
const limit = Math.max(0, max - stringWidth(ellipsis));
|
|
557
|
+
let out = "";
|
|
558
|
+
let w = 0;
|
|
559
|
+
for (const ch of s) {
|
|
560
|
+
const cw = wcwidth(ch.codePointAt(0) ?? 0);
|
|
561
|
+
if (w + cw > limit) break;
|
|
562
|
+
out += ch;
|
|
563
|
+
w += cw;
|
|
564
|
+
}
|
|
565
|
+
return out + ellipsis;
|
|
566
|
+
};
|
|
549
567
|
|
|
550
568
|
//#endregion
|
|
551
569
|
//#region src/utils/source-files.ts
|
|
@@ -1049,7 +1067,7 @@ const buildDoctorRender = (input) => {
|
|
|
1049
1067
|
};
|
|
1050
1068
|
const header = renderHeader({
|
|
1051
1069
|
version: APP_VERSION,
|
|
1052
|
-
command: "
|
|
1070
|
+
command: "Doctor report",
|
|
1053
1071
|
context: [input.projectName, input.languageLabel].filter((s) => s.length > 0),
|
|
1054
1072
|
brand: input.printBrand !== false
|
|
1055
1073
|
}, deps);
|
|
@@ -1767,8 +1785,8 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
|
|
|
1767
1785
|
|
|
1768
1786
|
//#endregion
|
|
1769
1787
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
1770
|
-
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1771
|
-
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]
|
|
1788
|
+
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|prototypes?|experiments?|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1789
|
+
const BASENAME_PATTERN = /(?:^|\/)(?:(?:prototype|experiment)(?:[-_.][^/]*)?|(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*)\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example|prototype|experiment)\.[mc]?[jt]sx?$/i;
|
|
1772
1790
|
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
1773
1791
|
|
|
1774
1792
|
//#endregion
|
|
@@ -1884,7 +1902,7 @@ const JS_EXTENSIONS$4 = new Set([
|
|
|
1884
1902
|
".mjs",
|
|
1885
1903
|
".cjs"
|
|
1886
1904
|
]);
|
|
1887
|
-
const
|
|
1905
|
+
const CONSOLE_CALL_PATTERN = /\bconsole\.(log|debug|info|trace|dir|table)\s*\(/;
|
|
1888
1906
|
const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
1889
1907
|
filePath,
|
|
1890
1908
|
engine: "ai-slop",
|
|
@@ -1898,20 +1916,35 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
|
1898
1916
|
fixable
|
|
1899
1917
|
});
|
|
1900
1918
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
1919
|
+
const CLI_ENTRYPOINT_PATTERN = /(?:^|\/)(?:cli|cli[-_.][^/]*|[^/]+[-_]cli)\.[mc]?[jt]sx?$/i;
|
|
1920
|
+
const ENTRYPOINT_GUARD_PATTERN = /\b(?:import\.meta\.main|require\.main\s*===\s*module)\b/;
|
|
1921
|
+
const OPERATIONAL_LOG_PATTERN = /\bconsole\.(?:log|info)\s*\(\s*(?:`|["'])\s*\[[^\]\n]{1,48}\]/;
|
|
1922
|
+
const DEBUG_SIGNAL_PATTERN = /\b(?:debug|dbg|trace|dump|inspect|todo|tmp|temp|remove\s+me|leftover|here|checkpoint)\b/i;
|
|
1923
|
+
const shouldFlagConsoleCall = (trimmed) => {
|
|
1924
|
+
const match = CONSOLE_CALL_PATTERN.exec(trimmed);
|
|
1925
|
+
if (!match) return false;
|
|
1926
|
+
const method = match[1];
|
|
1927
|
+
if (method === "trace" || method === "dir" || method === "table") return true;
|
|
1928
|
+
if (method === "debug") return DEBUG_SIGNAL_PATTERN.test(trimmed) || !OPERATIONAL_LOG_PATTERN.test(trimmed);
|
|
1929
|
+
if (method === "info" || method === "log") {
|
|
1930
|
+
if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) return false;
|
|
1931
|
+
if (OPERATIONAL_LOG_PATTERN.test(trimmed)) return false;
|
|
1932
|
+
return true;
|
|
1933
|
+
}
|
|
1934
|
+
return false;
|
|
1935
|
+
};
|
|
1901
1936
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
1902
1937
|
if (!JS_EXTENSIONS$4.has(ext)) return [];
|
|
1903
1938
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
1904
|
-
if (isNonProductionPath(relativePath)) return [];
|
|
1939
|
+
if (isNonProductionPath(relativePath) || CLI_ENTRYPOINT_PATTERN.test(relativePath)) return [];
|
|
1940
|
+
if (content.startsWith("#!")) return [];
|
|
1941
|
+
if (ENTRYPOINT_GUARD_PATTERN.test(content)) return [];
|
|
1905
1942
|
const diagnostics = [];
|
|
1906
1943
|
const lines = content.split("\n");
|
|
1907
1944
|
for (let i = 0; i < lines.length; i++) {
|
|
1908
1945
|
const trimmed = lines[i].trim();
|
|
1909
1946
|
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1910
|
-
if (
|
|
1911
|
-
if (/console\.(?:error|warn)\s*\(/.test(trimmed)) continue;
|
|
1912
|
-
if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) continue;
|
|
1913
|
-
diagnostics.push(slop(relativePath, i + 1, "ai-slop/console-leftover", "warning", "console.log/debug/info statement left in production code", "Remove debugging console statements or replace with a proper logger", true));
|
|
1914
|
-
}
|
|
1947
|
+
if (shouldFlagConsoleCall(trimmed)) diagnostics.push(slop(relativePath, i + 1, "ai-slop/console-leftover", "warning", "console.log/debug/info statement left in production code", "Remove debugging console statements or replace with a proper logger", true));
|
|
1915
1948
|
}
|
|
1916
1949
|
return diagnostics;
|
|
1917
1950
|
};
|
|
@@ -1939,6 +1972,7 @@ const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
|
1939
1972
|
const control = contextLines.join(" ");
|
|
1940
1973
|
return /(?:^|[}\s])(?:if|else\s+if|for|while)\s*\(/.test(control) && !/{\s*$/.test(control);
|
|
1941
1974
|
};
|
|
1975
|
+
const isPropertyNoopAssignment = (trimmed) => /^(?:[\w$]+\.)+[\w$]+\s*=\s*(?:function\s*)?\([^)]*\)\s*(?:=>)?\s*\{\s*\}\s*;?$/.test(trimmed);
|
|
1942
1976
|
const detectTodoStubs = (content, relativePath) => {
|
|
1943
1977
|
const diagnostics = [];
|
|
1944
1978
|
const lines = content.split("\n");
|
|
@@ -1960,7 +1994,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1960
1994
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1961
1995
|
if (JS_EXTENSIONS$4.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !isGuardedSingleLineExit(lines, i) && !isBlockCloserAfterReturn(nextLine) && !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));
|
|
1962
1996
|
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));
|
|
1963
|
-
if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+\s*\([^)]*\)|=>\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));
|
|
1997
|
+
if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w+\s*\([^)]*\)|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ") && !isPropertyNoopAssignment(trimmed)) 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));
|
|
1964
1998
|
}
|
|
1965
1999
|
return diagnostics;
|
|
1966
2000
|
};
|
|
@@ -2458,7 +2492,7 @@ const JS_RESOLUTION_EXTENSIONS = [
|
|
|
2458
2492
|
"/index.js",
|
|
2459
2493
|
"/index.jsx"
|
|
2460
2494
|
];
|
|
2461
|
-
const readJson$
|
|
2495
|
+
const readJson$3 = (filePath) => {
|
|
2462
2496
|
try {
|
|
2463
2497
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2464
2498
|
} catch {
|
|
@@ -2473,7 +2507,7 @@ const buildAliasMatcher = (key) => {
|
|
|
2473
2507
|
return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
|
|
2474
2508
|
};
|
|
2475
2509
|
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
2476
|
-
const opts = readJson$
|
|
2510
|
+
const opts = readJson$3(configPath)?.compilerOptions;
|
|
2477
2511
|
if (!opts || typeof opts !== "object") return;
|
|
2478
2512
|
const configDir = path.dirname(configPath);
|
|
2479
2513
|
const paths = opts.paths;
|
|
@@ -2496,7 +2530,7 @@ const collectTsPathAliases = (rootDir, workspaceDirs) => {
|
|
|
2496
2530
|
|
|
2497
2531
|
//#endregion
|
|
2498
2532
|
//#region src/engines/ai-slop/js-workspaces.ts
|
|
2499
|
-
const readJson$
|
|
2533
|
+
const readJson$2 = (filePath) => {
|
|
2500
2534
|
try {
|
|
2501
2535
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2502
2536
|
} catch {
|
|
@@ -2516,7 +2550,7 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
|
2516
2550
|
}
|
|
2517
2551
|
}
|
|
2518
2552
|
}
|
|
2519
|
-
const lerna = readJson$
|
|
2553
|
+
const lerna = readJson$2(path.join(rootDir, "lerna.json"));
|
|
2520
2554
|
if (lerna && Array.isArray(lerna.packages)) {
|
|
2521
2555
|
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
2522
2556
|
}
|
|
@@ -2917,7 +2951,7 @@ const JS_EXTENSIONS$2 = new Set([
|
|
|
2917
2951
|
".cjs"
|
|
2918
2952
|
]);
|
|
2919
2953
|
const PY_EXTENSIONS$2 = new Set([".py"]);
|
|
2920
|
-
const readJson = (filePath) => {
|
|
2954
|
+
const readJson$1 = (filePath) => {
|
|
2921
2955
|
try {
|
|
2922
2956
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2923
2957
|
} catch {
|
|
@@ -2961,7 +2995,7 @@ const collectNestedManifests = (rootDir, jsDeps) => {
|
|
|
2961
2995
|
const full = path.join(dir, entry.name);
|
|
2962
2996
|
if (entry.isDirectory()) walk(full, depth + 1);
|
|
2963
2997
|
else if (entry.name === "package.json" && depth > 0) {
|
|
2964
|
-
const wsPkg = readJson(full);
|
|
2998
|
+
const wsPkg = readJson$1(full);
|
|
2965
2999
|
if (!wsPkg) continue;
|
|
2966
3000
|
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
2967
3001
|
addDepsFromPkg(wsPkg, jsDeps);
|
|
@@ -2973,13 +3007,13 @@ const collectNestedManifests = (rootDir, jsDeps) => {
|
|
|
2973
3007
|
const collectJsDeps = (rootDir, jsDeps) => {
|
|
2974
3008
|
const pkgPath = path.join(rootDir, "package.json");
|
|
2975
3009
|
if (!fs.existsSync(pkgPath)) return false;
|
|
2976
|
-
const pkg = readJson(pkgPath);
|
|
3010
|
+
const pkg = readJson$1(pkgPath);
|
|
2977
3011
|
if (!pkg || typeof pkg !== "object") return false;
|
|
2978
3012
|
addDepsFromPkg(pkg, jsDeps);
|
|
2979
3013
|
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
2980
3014
|
const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
|
|
2981
3015
|
for (const wsDir of workspaceDirs) {
|
|
2982
|
-
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
3016
|
+
const wsPkg = readJson$1(path.join(wsDir, "package.json"));
|
|
2983
3017
|
if (!wsPkg) continue;
|
|
2984
3018
|
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
2985
3019
|
addDepsFromPkg(wsPkg, jsDeps);
|
|
@@ -3008,7 +3042,11 @@ const VIRTUAL_MODULE_PREFIXES = [
|
|
|
3008
3042
|
"astro:",
|
|
3009
3043
|
"virtual:",
|
|
3010
3044
|
"bun:",
|
|
3011
|
-
"file:"
|
|
3045
|
+
"file:",
|
|
3046
|
+
"http:",
|
|
3047
|
+
"https:",
|
|
3048
|
+
"jsr:",
|
|
3049
|
+
"npm:"
|
|
3012
3050
|
];
|
|
3013
3051
|
const isJsVirtualModule = (spec, manifest) => {
|
|
3014
3052
|
if (VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p))) return true;
|
|
@@ -3137,7 +3175,7 @@ const checkPyImport = (spec, manifest) => {
|
|
|
3137
3175
|
return root;
|
|
3138
3176
|
};
|
|
3139
3177
|
const detectHallucinatedImports = async (context) => {
|
|
3140
|
-
const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
|
|
3178
|
+
const rootPkg = readJson$1(path.join(context.rootDirectory, "package.json"));
|
|
3141
3179
|
const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
|
|
3142
3180
|
const manifest = loadManifest(context.rootDirectory);
|
|
3143
3181
|
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
@@ -3242,6 +3280,9 @@ const VENDOR_API_DOMAINS = [
|
|
|
3242
3280
|
];
|
|
3243
3281
|
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
3244
3282
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
3283
|
+
const PROVIDER_ID_RE = /^(?:price|prod|cus|sub|acct|org|app|tenant|workspace|project|client|key|tok|token|sk|pk)_[A-Za-z0-9][A-Za-z0-9_-]{7,}$/i;
|
|
3284
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3285
|
+
const READABLE_KEY_RE = /^[a-z][a-z0-9]*(?:[_-][a-z0-9]+){2,}$/;
|
|
3245
3286
|
const HARDCODED_URL_FINDING = {
|
|
3246
3287
|
rule: "ai-slop/hardcoded-url",
|
|
3247
3288
|
message: "Hardcoded environment URL in production code",
|
|
@@ -3279,8 +3320,10 @@ const safeUrlHost = (urlText) => {
|
|
|
3279
3320
|
}
|
|
3280
3321
|
};
|
|
3281
3322
|
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
3323
|
+
const TEMPLATE_INTERPOLATION_START = "${";
|
|
3282
3324
|
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
3283
3325
|
if (isEnvBackedLine(line)) return false;
|
|
3326
|
+
if (urlText.includes(TEMPLATE_INTERPOLATION_START) && /\bnew\s+URL\s*\(/.test(line)) return false;
|
|
3284
3327
|
const host = safeUrlHost(urlText);
|
|
3285
3328
|
if (!host) return false;
|
|
3286
3329
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
@@ -3295,7 +3338,11 @@ const hasUsefulIdShape = (value) => {
|
|
|
3295
3338
|
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
3296
3339
|
if (/^https?:\/\//i.test(value)) return false;
|
|
3297
3340
|
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
3298
|
-
|
|
3341
|
+
if (READABLE_KEY_RE.test(value) && !PROVIDER_ID_RE.test(value)) return false;
|
|
3342
|
+
if (PROVIDER_ID_RE.test(value)) return true;
|
|
3343
|
+
if (UUID_RE.test(value)) return true;
|
|
3344
|
+
if (!/[0-9]/.test(value)) return false;
|
|
3345
|
+
return value.length >= 24 && !/[_-]/.test(value) && /[a-z]/.test(value) && /[A-Z]/.test(value);
|
|
3299
3346
|
};
|
|
3300
3347
|
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
3301
3348
|
const diagnostics = [];
|
|
@@ -5309,8 +5356,8 @@ const shouldIncludeIssue = (issueType, filePath) => {
|
|
|
5309
5356
|
return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
|
|
5310
5357
|
};
|
|
5311
5358
|
const DEPENDENCY_HELP = {
|
|
5312
|
-
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `
|
|
5313
|
-
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `
|
|
5359
|
+
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
5360
|
+
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
5314
5361
|
unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
|
|
5315
5362
|
unresolved: "This import cannot be resolved. Check for typos or missing packages.",
|
|
5316
5363
|
binaries: "This binary is used but its package is not in package.json."
|
|
@@ -5659,7 +5706,7 @@ const parseBiomeJsonOutput = (output, rootDir) => {
|
|
|
5659
5706
|
rule: "formatting",
|
|
5660
5707
|
severity,
|
|
5661
5708
|
message,
|
|
5662
|
-
help: "Run `
|
|
5709
|
+
help: "Run `aislop fix` to auto-format",
|
|
5663
5710
|
line: entry.location?.start?.line ?? 0,
|
|
5664
5711
|
column: entry.location?.start?.column ?? 0,
|
|
5665
5712
|
category: "Format",
|
|
@@ -5698,7 +5745,7 @@ const FORMATTERS = {
|
|
|
5698
5745
|
rule: "rust-formatting",
|
|
5699
5746
|
severity: "warning",
|
|
5700
5747
|
message: "Rust file is not formatted correctly",
|
|
5701
|
-
help: "Run `
|
|
5748
|
+
help: "Run `aislop fix` to auto-format with rustfmt",
|
|
5702
5749
|
line: parseInt(match[2], 10),
|
|
5703
5750
|
column: 0,
|
|
5704
5751
|
category: "Format",
|
|
@@ -5731,7 +5778,7 @@ const FORMATTERS = {
|
|
|
5731
5778
|
rule: offense.cop_name ?? "ruby-formatting",
|
|
5732
5779
|
severity: "warning",
|
|
5733
5780
|
message: offense.message ?? "Ruby formatting issue",
|
|
5734
|
-
help: "Run `
|
|
5781
|
+
help: "Run `aislop fix` to auto-format",
|
|
5735
5782
|
line: offense.location?.start_line ?? 0,
|
|
5736
5783
|
column: offense.location?.start_column ?? 0,
|
|
5737
5784
|
category: "Format",
|
|
@@ -5762,7 +5809,7 @@ const FORMATTERS = {
|
|
|
5762
5809
|
rule: "php-formatting",
|
|
5763
5810
|
severity: "warning",
|
|
5764
5811
|
message: "PHP file is not formatted correctly",
|
|
5765
|
-
help: "Run `
|
|
5812
|
+
help: "Run `aislop fix` to auto-format",
|
|
5766
5813
|
line: 0,
|
|
5767
5814
|
column: 0,
|
|
5768
5815
|
category: "Format",
|
|
@@ -5815,7 +5862,7 @@ const runGofmt = async (context) => {
|
|
|
5815
5862
|
rule: "go-formatting",
|
|
5816
5863
|
severity: "warning",
|
|
5817
5864
|
message: "Go file is not formatted correctly",
|
|
5818
|
-
help: "Run `
|
|
5865
|
+
help: "Run `aislop fix` to auto-format with gofmt",
|
|
5819
5866
|
line: 0,
|
|
5820
5867
|
column: 0,
|
|
5821
5868
|
category: "Format",
|
|
@@ -5881,7 +5928,7 @@ const parseRuffFormatOutput = (output, rootDir) => {
|
|
|
5881
5928
|
rule: "python-formatting",
|
|
5882
5929
|
severity: "warning",
|
|
5883
5930
|
message: "Python file is not formatted correctly",
|
|
5884
|
-
help: "Run `
|
|
5931
|
+
help: "Run `aislop fix` to auto-format with ruff",
|
|
5885
5932
|
line: 0,
|
|
5886
5933
|
column: 0,
|
|
5887
5934
|
category: "Format",
|
|
@@ -6293,6 +6340,7 @@ const AMBIENT_GLOBAL_DEPS = [
|
|
|
6293
6340
|
const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
|
|
6294
6341
|
const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
|
|
6295
6342
|
const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
|
|
6343
|
+
const SUPABASE_FUNCTION_PATH_RE = /(?:^|\/)supabase\/functions\/[^/]+\/.+\.[cm]?[jt]sx?$/;
|
|
6296
6344
|
const detectAmbientSources = (rootDir) => {
|
|
6297
6345
|
const found = /* @__PURE__ */ new Set();
|
|
6298
6346
|
const skipDirs = new Set([
|
|
@@ -6337,6 +6385,36 @@ const detectAmbientSources = (rootDir) => {
|
|
|
6337
6385
|
const extractNoUndefIdentifier = (message) => {
|
|
6338
6386
|
return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
|
|
6339
6387
|
};
|
|
6388
|
+
const looksLikeChromeExtensionManifest = (filePath) => {
|
|
6389
|
+
try {
|
|
6390
|
+
const manifest = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
6391
|
+
return typeof manifest.manifest_version === "number" && ("background" in manifest || "content_scripts" in manifest || "permissions" in manifest);
|
|
6392
|
+
} catch {
|
|
6393
|
+
return false;
|
|
6394
|
+
}
|
|
6395
|
+
};
|
|
6396
|
+
const chromeExtensionFileCache = /* @__PURE__ */ new Map();
|
|
6397
|
+
const isChromeExtensionFile = (rootDir, relativeFilePath) => {
|
|
6398
|
+
const cacheKey = `${rootDir}:${relativeFilePath.split(path.sep).join("/")}`;
|
|
6399
|
+
const cached = chromeExtensionFileCache.get(cacheKey);
|
|
6400
|
+
if (cached !== void 0) return cached;
|
|
6401
|
+
const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
|
|
6402
|
+
const root = path.resolve(rootDir);
|
|
6403
|
+
let dir = path.dirname(path.resolve(absolute));
|
|
6404
|
+
let matched = false;
|
|
6405
|
+
while (true) {
|
|
6406
|
+
const relativeToRoot = path.relative(root, dir);
|
|
6407
|
+
if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) break;
|
|
6408
|
+
if (looksLikeChromeExtensionManifest(path.join(dir, "manifest.json"))) {
|
|
6409
|
+
matched = true;
|
|
6410
|
+
break;
|
|
6411
|
+
}
|
|
6412
|
+
if (dir === root) break;
|
|
6413
|
+
dir = path.dirname(dir);
|
|
6414
|
+
}
|
|
6415
|
+
chromeExtensionFileCache.set(cacheKey, matched);
|
|
6416
|
+
return matched;
|
|
6417
|
+
};
|
|
6340
6418
|
const isAmbientFalsePositive = (rule, message, sources) => {
|
|
6341
6419
|
if (rule !== "eslint/no-undef") return false;
|
|
6342
6420
|
const ident = extractNoUndefIdentifier(message);
|
|
@@ -6345,9 +6423,19 @@ const isAmbientFalsePositive = (rule, message, sources) => {
|
|
|
6345
6423
|
if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
|
|
6346
6424
|
return false;
|
|
6347
6425
|
};
|
|
6426
|
+
const isRuntimeGlobalFalsePositive = (rule, message, rootDir, relativeFilePath) => {
|
|
6427
|
+
if (rule !== "eslint/no-undef") return false;
|
|
6428
|
+
const ident = extractNoUndefIdentifier(message);
|
|
6429
|
+
if (!ident) return false;
|
|
6430
|
+
const normalized = relativeFilePath.split(path.sep).join("/");
|
|
6431
|
+
if (ident === "Deno" && SUPABASE_FUNCTION_PATH_RE.test(normalized)) return true;
|
|
6432
|
+
if (ident === "chrome" && isChromeExtensionFile(rootDir, relativeFilePath)) return true;
|
|
6433
|
+
return false;
|
|
6434
|
+
};
|
|
6348
6435
|
const sstReferencedFiles = /* @__PURE__ */ new Map();
|
|
6349
6436
|
const clearSstReferenceCache = () => {
|
|
6350
6437
|
sstReferencedFiles.clear();
|
|
6438
|
+
chromeExtensionFileCache.clear();
|
|
6351
6439
|
};
|
|
6352
6440
|
const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
6353
6441
|
const cached = sstReferencedFiles.get(relativeFilePath);
|
|
@@ -6399,6 +6487,32 @@ const collectPackageNames = (dir) => {
|
|
|
6399
6487
|
}
|
|
6400
6488
|
return names;
|
|
6401
6489
|
};
|
|
6490
|
+
const readJson = (filePath) => {
|
|
6491
|
+
const raw = readTextFile$1(filePath);
|
|
6492
|
+
if (!raw) return null;
|
|
6493
|
+
try {
|
|
6494
|
+
const parsed = JSON.parse(raw);
|
|
6495
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
6496
|
+
} catch {
|
|
6497
|
+
return null;
|
|
6498
|
+
}
|
|
6499
|
+
};
|
|
6500
|
+
const hasBunRuntime = (rootDir, projectFiles) => {
|
|
6501
|
+
if (fs.existsSync(path.join(rootDir, "bun.lock")) || fs.existsSync(path.join(rootDir, "bun.lockb")) || fs.existsSync(path.join(rootDir, "bunfig.toml"))) return true;
|
|
6502
|
+
const hasBunFiles = projectFiles.some((filePath) => /(?:^|\/)bunfig\.toml$|(?:^|\/)bun\.lockb?$/.test(filePath));
|
|
6503
|
+
const pkg = readJson(path.join(rootDir, "package.json"));
|
|
6504
|
+
if (!pkg) return hasBunFiles;
|
|
6505
|
+
if (typeof pkg.packageManager === "string" && /^bun@/i.test(pkg.packageManager)) return true;
|
|
6506
|
+
const scripts = pkg.scripts;
|
|
6507
|
+
if (scripts && typeof scripts === "object") {
|
|
6508
|
+
for (const command of Object.values(scripts)) if (typeof command === "string" && /(?:^|[;&|()\s])bunx?\s/.test(command)) return true;
|
|
6509
|
+
}
|
|
6510
|
+
return hasBunFiles;
|
|
6511
|
+
};
|
|
6512
|
+
const hasDenoRuntime = (rootDir, projectFiles) => {
|
|
6513
|
+
if (fs.existsSync(path.join(rootDir, "deno.json")) || fs.existsSync(path.join(rootDir, "deno.jsonc"))) return true;
|
|
6514
|
+
return projectFiles.some((filePath) => /(?:^|\/)deno\.jsonc?$/.test(filePath));
|
|
6515
|
+
};
|
|
6402
6516
|
const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
6403
6517
|
const collectAmbientGlobals = (rootDir) => {
|
|
6404
6518
|
const globals = /* @__PURE__ */ new Set();
|
|
@@ -6410,7 +6524,8 @@ const collectAmbientGlobals = (rootDir) => {
|
|
|
6410
6524
|
for (const match of content.matchAll(AMBIENT_GLOBAL_RE)) globals.add(match[1]);
|
|
6411
6525
|
}
|
|
6412
6526
|
const deps = collectPackageNames(rootDir);
|
|
6413
|
-
if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
|
|
6527
|
+
if (deps.has("@types/bun") || deps.has("bun-types") || hasBunRuntime(rootDir, projectFiles)) globals.add("Bun");
|
|
6528
|
+
if (hasDenoRuntime(rootDir, projectFiles)) globals.add("Deno");
|
|
6414
6529
|
if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
|
|
6415
6530
|
"$app",
|
|
6416
6531
|
"$config",
|
|
@@ -6566,6 +6681,37 @@ const removeDuplicateKeyLines = (rootDirectory, diagnostics) => {
|
|
|
6566
6681
|
fs.writeFileSync(filePath, filtered.join("\n"));
|
|
6567
6682
|
}
|
|
6568
6683
|
};
|
|
6684
|
+
const toDiagnostic = (d) => {
|
|
6685
|
+
const { plugin, rule } = parseRuleCode(d.code);
|
|
6686
|
+
const label = d.labels[0];
|
|
6687
|
+
return {
|
|
6688
|
+
filePath: d.filename,
|
|
6689
|
+
engine: "lint",
|
|
6690
|
+
rule: `${plugin}/${rule}`,
|
|
6691
|
+
severity: d.severity,
|
|
6692
|
+
message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
|
|
6693
|
+
help: d.help || "",
|
|
6694
|
+
line: label?.span.line ?? 0,
|
|
6695
|
+
column: label?.span.column ?? 0,
|
|
6696
|
+
category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
|
|
6697
|
+
fixable: false
|
|
6698
|
+
};
|
|
6699
|
+
};
|
|
6700
|
+
const shouldKeepOxlintDiagnostic = (context, ambientSources, seen, d) => {
|
|
6701
|
+
const relativePath = path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath;
|
|
6702
|
+
if (isExcludedFromScan(relativePath)) return false;
|
|
6703
|
+
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
6704
|
+
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
6705
|
+
if (isRuntimeGlobalFalsePositive(d.rule, d.message, context.rootDirectory, relativePath)) return false;
|
|
6706
|
+
if (isSolidRefFalsePositive(context, d)) return false;
|
|
6707
|
+
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
6708
|
+
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
6709
|
+
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
6710
|
+
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
6711
|
+
if (seen.has(key)) return false;
|
|
6712
|
+
seen.add(key);
|
|
6713
|
+
return true;
|
|
6714
|
+
};
|
|
6569
6715
|
const runOxlint = async (context) => {
|
|
6570
6716
|
const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
|
|
6571
6717
|
const framework = context.frameworks.find((f) => f !== "none");
|
|
@@ -6602,34 +6748,7 @@ const runOxlint = async (context) => {
|
|
|
6602
6748
|
return [];
|
|
6603
6749
|
}
|
|
6604
6750
|
const seen = /* @__PURE__ */ new Set();
|
|
6605
|
-
return output.diagnostics.map((d) =>
|
|
6606
|
-
const { plugin, rule } = parseRuleCode(d.code);
|
|
6607
|
-
const label = d.labels[0];
|
|
6608
|
-
return {
|
|
6609
|
-
filePath: d.filename,
|
|
6610
|
-
engine: "lint",
|
|
6611
|
-
rule: `${plugin}/${rule}`,
|
|
6612
|
-
severity: d.severity,
|
|
6613
|
-
message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
|
|
6614
|
-
help: d.help || "",
|
|
6615
|
-
line: label?.span.line ?? 0,
|
|
6616
|
-
column: label?.span.column ?? 0,
|
|
6617
|
-
category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
|
|
6618
|
-
fixable: false
|
|
6619
|
-
};
|
|
6620
|
-
}).filter((d) => {
|
|
6621
|
-
if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
|
|
6622
|
-
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
6623
|
-
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
6624
|
-
if (isSolidRefFalsePositive(context, d)) return false;
|
|
6625
|
-
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
6626
|
-
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
6627
|
-
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
6628
|
-
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
6629
|
-
if (seen.has(key)) return false;
|
|
6630
|
-
seen.add(key);
|
|
6631
|
-
return true;
|
|
6632
|
-
});
|
|
6751
|
+
return output.diagnostics.map(toDiagnostic).filter((d) => shouldKeepOxlintDiagnostic(context, ambientSources, seen, d));
|
|
6633
6752
|
} finally {
|
|
6634
6753
|
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
6635
6754
|
}
|
|
@@ -6755,9 +6874,9 @@ const lintEngine = {
|
|
|
6755
6874
|
const promises = [];
|
|
6756
6875
|
if (languages.includes("typescript") || languages.includes("javascript")) {
|
|
6757
6876
|
promises.push(runOxlint(context));
|
|
6758
|
-
if (context.config.lint.typecheck) promises.push(import("./typecheck-
|
|
6877
|
+
if (context.config.lint.typecheck) promises.push(import("./typecheck-DQSzG8fX.js").then((mod) => mod.runTypecheck(context)));
|
|
6759
6878
|
}
|
|
6760
|
-
if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-
|
|
6879
|
+
if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-BwLKXF__.js").then((n) => n.t).then((mod) => mod.runExpoDoctor(context)));
|
|
6761
6880
|
if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
|
|
6762
6881
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
6763
6882
|
if (languages.includes("rust") && installedTools.cargo) promises.push(runGenericLinter(context, "rust"));
|
|
@@ -6923,7 +7042,7 @@ const parseJsAudit = (output, source) => {
|
|
|
6923
7042
|
rule: "security/dependency-audit-skipped",
|
|
6924
7043
|
severity: "info",
|
|
6925
7044
|
message: `Dependency audit skipped (${source}): lockfile is missing`,
|
|
6926
|
-
help: error.detail ?? "Generate a lockfile, then re-run `
|
|
7045
|
+
help: error.detail ?? "Generate a lockfile, then re-run `aislop scan` for dependency vulnerability checks.",
|
|
6927
7046
|
line: 0,
|
|
6928
7047
|
column: 0,
|
|
6929
7048
|
category: "Security",
|
|
@@ -7040,6 +7159,194 @@ const runCargoAudit = async (rootDir, timeout) => {
|
|
|
7040
7159
|
}
|
|
7041
7160
|
};
|
|
7042
7161
|
|
|
7162
|
+
//#endregion
|
|
7163
|
+
//#region src/engines/security/html-safety.ts
|
|
7164
|
+
const SAFE_EMPTY_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:""|''|``)\s*;?/;
|
|
7165
|
+
const SAFE_SANITIZED_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:escapeHtml|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)\s*;?(?:\n|$)/;
|
|
7166
|
+
const SANITIZER_EXPR_RE = /^(?:escapeHtml|escapeHTML|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)$/;
|
|
7167
|
+
const IDENT_RE = /^[A-Za-z_$][\w$]*$/;
|
|
7168
|
+
const STATIC_STRING_RE = /^(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\$])*`)$/;
|
|
7169
|
+
const NUMERICISH_EXPR_RE = /^(?:[-+]?\d+(?:\.\d+)?|[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*(?:\s*\|\|\s*[-+]?\d+(?:\.\d+)?)?)$/;
|
|
7170
|
+
const NUMERICISH_NAME_RE = /(?:^|\.)(?:count|length|size|width|height|top|right|bottom|left|duration|elapsed|timestamp|time|ms|port|pid|attempt|attempts|index|total|x|y)$|(?:count|length|size|width|height|duration|elapsed|timestamp|time|port|pid|attempt|index|total)$/i;
|
|
7171
|
+
const SAFE_FORMAT_CALL_RE = /^(?:format[A-Z]\w*|fmt[A-Z]?\w*)\s*\((.*)\)$/;
|
|
7172
|
+
const consumeQuotedLiteral = (content, startIndex, quote) => {
|
|
7173
|
+
let i = startIndex + 1;
|
|
7174
|
+
while (i < content.length) {
|
|
7175
|
+
const char = content[i];
|
|
7176
|
+
if (char === "\\") {
|
|
7177
|
+
i += 2;
|
|
7178
|
+
continue;
|
|
7179
|
+
}
|
|
7180
|
+
if (char === quote) return { endIndex: i };
|
|
7181
|
+
if (char === "\n") return null;
|
|
7182
|
+
i++;
|
|
7183
|
+
}
|
|
7184
|
+
return null;
|
|
7185
|
+
};
|
|
7186
|
+
const consumeTemplateLiteral = (content, startIndex) => {
|
|
7187
|
+
const openIndex = content.indexOf("`", startIndex);
|
|
7188
|
+
if (openIndex === -1) return null;
|
|
7189
|
+
let i = openIndex + 1;
|
|
7190
|
+
while (i < content.length) {
|
|
7191
|
+
const char = content[i];
|
|
7192
|
+
if (char === "\\") {
|
|
7193
|
+
i += 2;
|
|
7194
|
+
continue;
|
|
7195
|
+
}
|
|
7196
|
+
if (char === "`") return {
|
|
7197
|
+
body: content.slice(openIndex + 1, i),
|
|
7198
|
+
endIndex: i
|
|
7199
|
+
};
|
|
7200
|
+
i++;
|
|
7201
|
+
}
|
|
7202
|
+
return null;
|
|
7203
|
+
};
|
|
7204
|
+
const assignmentTailIsClosed = (content, endIndex) => /^\s*(?:;[^\n]*)?(?:\n|$)/.test(content.slice(endIndex + 1));
|
|
7205
|
+
const assignmentRhsStart = (content, matchIndex) => {
|
|
7206
|
+
const match = /^\.innerHTML\s*=\s*/.exec(content.slice(matchIndex));
|
|
7207
|
+
return match ? matchIndex + match[0].length : null;
|
|
7208
|
+
};
|
|
7209
|
+
const templateExpressions = (templateBody) => [...templateBody.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
|
|
7210
|
+
const staticTernaryRe = /^\s*[^?]+\?\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*:\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*$/;
|
|
7211
|
+
const splitTopLevelTernary = (expr) => {
|
|
7212
|
+
let quote = null;
|
|
7213
|
+
let depth = 0;
|
|
7214
|
+
let question = -1;
|
|
7215
|
+
let colon = -1;
|
|
7216
|
+
for (let i = 0; i < expr.length; i++) {
|
|
7217
|
+
const char = expr[i];
|
|
7218
|
+
if (char === "\\") {
|
|
7219
|
+
i++;
|
|
7220
|
+
continue;
|
|
7221
|
+
}
|
|
7222
|
+
if ((char === "'" || char === "\"" || char === "`") && quote === null) {
|
|
7223
|
+
quote = char;
|
|
7224
|
+
continue;
|
|
7225
|
+
}
|
|
7226
|
+
if (char === quote) {
|
|
7227
|
+
quote = null;
|
|
7228
|
+
continue;
|
|
7229
|
+
}
|
|
7230
|
+
if (quote) continue;
|
|
7231
|
+
if (char === "(" || char === "[" || char === "{") depth++;
|
|
7232
|
+
else if (char === ")" || char === "]" || char === "}") depth = Math.max(0, depth - 1);
|
|
7233
|
+
else if (char === "?" && depth === 0 && question === -1) question = i;
|
|
7234
|
+
else if (char === ":" && depth === 0 && question !== -1) {
|
|
7235
|
+
colon = i;
|
|
7236
|
+
break;
|
|
7237
|
+
}
|
|
7238
|
+
}
|
|
7239
|
+
if (question === -1 || colon === -1) return null;
|
|
7240
|
+
return {
|
|
7241
|
+
whenTrue: expr.slice(question + 1, colon).trim(),
|
|
7242
|
+
whenFalse: expr.slice(colon + 1).trim()
|
|
7243
|
+
};
|
|
7244
|
+
};
|
|
7245
|
+
const isNumericishExpression = (expr) => {
|
|
7246
|
+
const normalized = expr.trim();
|
|
7247
|
+
if (/^(?:Math\.\w+|Number|parseInt|parseFloat)\s*\(/.test(normalized)) return true;
|
|
7248
|
+
if (!NUMERICISH_EXPR_RE.test(normalized)) return false;
|
|
7249
|
+
return /\d/.test(normalized) || NUMERICISH_NAME_RE.test(normalized);
|
|
7250
|
+
};
|
|
7251
|
+
const isSafeTemplateLiteralExpression = (expr, safeNames) => {
|
|
7252
|
+
if (!expr.startsWith("`") || !expr.endsWith("`")) return false;
|
|
7253
|
+
return templateExpressions(expr.slice(1, -1)).every((part) => isSafeHtmlExpression(part, safeNames));
|
|
7254
|
+
};
|
|
7255
|
+
const collectSafeHtmlNames = (content, matchIndex) => {
|
|
7256
|
+
const safeNames = /* @__PURE__ */ new Set();
|
|
7257
|
+
const prefix = content.slice(Math.max(0, matchIndex - 8e3), matchIndex);
|
|
7258
|
+
for (const rawLine of prefix.split("\n")) {
|
|
7259
|
+
const line = rawLine.trim();
|
|
7260
|
+
let match = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
|
|
7261
|
+
if (match) {
|
|
7262
|
+
const [, name, expr] = match;
|
|
7263
|
+
if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
7264
|
+
else safeNames.delete(name);
|
|
7265
|
+
continue;
|
|
7266
|
+
}
|
|
7267
|
+
match = /^([A-Za-z_$][\w$]*)\s*\+=\s*(.+?)\s*;?$/.exec(line);
|
|
7268
|
+
if (match) {
|
|
7269
|
+
const [, name, expr] = match;
|
|
7270
|
+
if (safeNames.has(name) && isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
7271
|
+
else safeNames.delete(name);
|
|
7272
|
+
continue;
|
|
7273
|
+
}
|
|
7274
|
+
match = /^([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
|
|
7275
|
+
if (match) {
|
|
7276
|
+
const [, name, expr] = match;
|
|
7277
|
+
if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
7278
|
+
else safeNames.delete(name);
|
|
7279
|
+
}
|
|
7280
|
+
}
|
|
7281
|
+
return safeNames;
|
|
7282
|
+
};
|
|
7283
|
+
const isSafeHtmlExpression = (expr, safeNames) => {
|
|
7284
|
+
const normalized = expr.trim();
|
|
7285
|
+
if (SANITIZER_EXPR_RE.test(normalized)) return true;
|
|
7286
|
+
if (STATIC_STRING_RE.test(normalized)) return true;
|
|
7287
|
+
if (staticTernaryRe.test(expr)) return true;
|
|
7288
|
+
if (isNumericishExpression(normalized)) return true;
|
|
7289
|
+
if (IDENT_RE.test(normalized) && safeNames.has(normalized)) return true;
|
|
7290
|
+
if (isSafeTemplateLiteralExpression(normalized, safeNames)) return true;
|
|
7291
|
+
const ternary = splitTopLevelTernary(normalized);
|
|
7292
|
+
if (ternary && isSafeHtmlExpression(ternary.whenTrue, safeNames) && isSafeHtmlExpression(ternary.whenFalse, safeNames)) return true;
|
|
7293
|
+
const formatCall = SAFE_FORMAT_CALL_RE.exec(normalized);
|
|
7294
|
+
if (formatCall) return formatCall[1].split(",").map((arg) => arg.trim()).filter((arg) => arg.length > 0).every((arg) => isNumericishExpression(arg) || IDENT_RE.test(arg) && safeNames.has(arg));
|
|
7295
|
+
return false;
|
|
7296
|
+
};
|
|
7297
|
+
const readSingleLineRhs = (content, rhsStart) => {
|
|
7298
|
+
const lineEnd = content.indexOf("\n", rhsStart);
|
|
7299
|
+
const line = content.slice(rhsStart, lineEnd === -1 ? content.length : lineEnd);
|
|
7300
|
+
let quote = null;
|
|
7301
|
+
for (let i = 0; i < line.length; i++) {
|
|
7302
|
+
const char = line[i];
|
|
7303
|
+
if (char === "\\") {
|
|
7304
|
+
i++;
|
|
7305
|
+
continue;
|
|
7306
|
+
}
|
|
7307
|
+
if ((char === "'" || char === "\"" || char === "`") && quote === null) {
|
|
7308
|
+
quote = char;
|
|
7309
|
+
continue;
|
|
7310
|
+
}
|
|
7311
|
+
if (char === quote) {
|
|
7312
|
+
quote = null;
|
|
7313
|
+
continue;
|
|
7314
|
+
}
|
|
7315
|
+
if (char === ";" && quote === null) return line.slice(0, i).trim();
|
|
7316
|
+
}
|
|
7317
|
+
return line.trim();
|
|
7318
|
+
};
|
|
7319
|
+
const isSafeMapJoinHtmlAssignment = (content, rhsStart) => {
|
|
7320
|
+
const head = content.slice(rhsStart);
|
|
7321
|
+
const mapMatch = /^[A-Za-z_$][\w$.]*\.map\(\s*[A-Za-z_$][\w$]*\s*=>\s*`/.exec(head);
|
|
7322
|
+
if (!mapMatch) return false;
|
|
7323
|
+
const template = consumeTemplateLiteral(content, rhsStart + mapMatch[0].length - 1);
|
|
7324
|
+
if (!template) return false;
|
|
7325
|
+
if (!/^\s*\)\.join\(\s*(?:""|'')\s*\)/.test(content.slice(template.endIndex + 1))) return false;
|
|
7326
|
+
const safeNames = collectSafeHtmlNames(content, rhsStart);
|
|
7327
|
+
return templateExpressions(template.body).every((expr) => isSafeHtmlExpression(expr, safeNames));
|
|
7328
|
+
};
|
|
7329
|
+
const isSafeInnerHtmlAssignment = (content, matchIndex) => {
|
|
7330
|
+
const tail = content.slice(matchIndex);
|
|
7331
|
+
if (SAFE_EMPTY_INNER_HTML_RE.test(tail) || SAFE_SANITIZED_INNER_HTML_RE.test(tail)) return true;
|
|
7332
|
+
const rhsStart = assignmentRhsStart(content, matchIndex);
|
|
7333
|
+
if (rhsStart === null) return false;
|
|
7334
|
+
const first = content[rhsStart];
|
|
7335
|
+
const safeNames = collectSafeHtmlNames(content, matchIndex);
|
|
7336
|
+
if (isSafeHtmlExpression(readSingleLineRhs(content, rhsStart), safeNames)) return true;
|
|
7337
|
+
if (isSafeMapJoinHtmlAssignment(content, rhsStart)) return true;
|
|
7338
|
+
if (first === "'" || first === "\"") {
|
|
7339
|
+
const quoted = consumeQuotedLiteral(content, rhsStart, first);
|
|
7340
|
+
return Boolean(quoted && assignmentTailIsClosed(content, quoted.endIndex));
|
|
7341
|
+
}
|
|
7342
|
+
if (first !== "`") return false;
|
|
7343
|
+
const template = consumeTemplateLiteral(content, rhsStart);
|
|
7344
|
+
if (!template || !assignmentTailIsClosed(content, template.endIndex)) return false;
|
|
7345
|
+
const expressions = templateExpressions(template.body);
|
|
7346
|
+
if (expressions.length === 0) return true;
|
|
7347
|
+
return expressions.every((expr) => isSafeHtmlExpression(expr, safeNames));
|
|
7348
|
+
};
|
|
7349
|
+
|
|
7043
7350
|
//#endregion
|
|
7044
7351
|
//#region src/engines/security/risky.ts
|
|
7045
7352
|
const ev = "eval";
|
|
@@ -7165,6 +7472,30 @@ const isStructuredDataScript = (content, matchIndex) => {
|
|
|
7165
7472
|
const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
|
|
7166
7473
|
return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
|
|
7167
7474
|
};
|
|
7475
|
+
const isSafeShellSpawnArray = (content, matchIndex) => /^spawn\s*\(\s*\[/.test(content.slice(matchIndex)) && !/^\s*spawn\s*\(\s*\[\s*["'](?:sh|bash|zsh|cmd|cmd\.exe|powershell|pwsh)["']\s*,\s*["'](?:-c|\/c|\/C)["']/i.test(content.slice(matchIndex)) && !/shell\s*:\s*true\b/.test(content.slice(matchIndex, matchIndex + 500));
|
|
7476
|
+
const PLACEHOLDER_EXPR_RE = /^(?:placeholders?|placeholderList|bindMarkers?|bindingMarkers?|bindPlaceholders?|bindingPlaceholders?|parameterPlaceholders?|sqlPlaceholders?)(?:\.\w+\([^)]*\))?$/i;
|
|
7477
|
+
const SQL_PLACEHOLDER_LITERAL_RE = /["'](?:\?|\$\d+|\$\{[^}]+\})["']/;
|
|
7478
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7479
|
+
const isGeneratedPlaceholderList = (content, matchIndex, placeholderExpr) => {
|
|
7480
|
+
const name = placeholderExpr.match(/^([A-Za-z_$][\w$]*)/)?.[1];
|
|
7481
|
+
if (!name) return false;
|
|
7482
|
+
const prefix = content.slice(Math.max(0, matchIndex - 4e3), matchIndex);
|
|
7483
|
+
const declarationRe = new RegExp(`\\b(?:const|let|var)\\s+${escapeRegExp(name)}\\s*=\\s*([^;\\n]+)`, "g");
|
|
7484
|
+
const declaration = [...prefix.matchAll(declarationRe)].at(-1);
|
|
7485
|
+
if (!declaration) return false;
|
|
7486
|
+
const expr = declaration[1];
|
|
7487
|
+
if (!/\.join\s*\(/.test(expr)) return false;
|
|
7488
|
+
return /\.map\s*\(/.test(expr) && /=>/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr) || /\.fill\s*\(/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr);
|
|
7489
|
+
};
|
|
7490
|
+
const isSafeSqlPlaceholderTemplate = (content, matchIndex) => {
|
|
7491
|
+
const template = consumeTemplateLiteral(content, matchIndex);
|
|
7492
|
+
if (!template) return false;
|
|
7493
|
+
const afterTemplate = content.slice(template.endIndex + 1);
|
|
7494
|
+
if (!(/^\s*,/.test(afterTemplate) || /^\s*\)\s*\.(?:all|get|run|values)\s*\(/.test(afterTemplate))) return false;
|
|
7495
|
+
const expressions = [...template.body.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
|
|
7496
|
+
if (expressions.length === 0) return false;
|
|
7497
|
+
return expressions.every((expr) => PLACEHOLDER_EXPR_RE.test(expr) && isGeneratedPlaceholderList(content, matchIndex, expr));
|
|
7498
|
+
};
|
|
7168
7499
|
const detectRiskyConstructs = async (context) => {
|
|
7169
7500
|
const files = getSourceFiles(context);
|
|
7170
7501
|
const diagnostics = [];
|
|
@@ -7189,8 +7520,11 @@ const detectRiskyConstructs = async (context) => {
|
|
|
7189
7520
|
const line = content.slice(0, match.index).split("\n").length;
|
|
7190
7521
|
if (name === "innerhtml") {
|
|
7191
7522
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
7523
|
+
if (isSafeInnerHtmlAssignment(content, match.index)) continue;
|
|
7192
7524
|
if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
|
|
7193
7525
|
}
|
|
7526
|
+
if (name === "sql-injection" && isSafeSqlPlaceholderTemplate(content, match.index)) continue;
|
|
7527
|
+
if (name === "shell-injection" && isSafeShellSpawnArray(content, match.index)) continue;
|
|
7194
7528
|
if (name === "dangerously-set-innerhtml") {
|
|
7195
7529
|
if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
|
|
7196
7530
|
if (isStructuredDataScript(content, match.index)) continue;
|
|
@@ -7287,7 +7621,28 @@ const PLACEHOLDER_EXACT = new Set([
|
|
|
7287
7621
|
"todo",
|
|
7288
7622
|
"replace_me"
|
|
7289
7623
|
]);
|
|
7624
|
+
const PLACEHOLDER_URL_PARTS = new Set([
|
|
7625
|
+
"example",
|
|
7626
|
+
"host",
|
|
7627
|
+
"localhost",
|
|
7628
|
+
"pass",
|
|
7629
|
+
"password",
|
|
7630
|
+
"pw",
|
|
7631
|
+
"user",
|
|
7632
|
+
"username"
|
|
7633
|
+
]);
|
|
7634
|
+
const isPlaceholderCredentialUrl = (matchedText) => {
|
|
7635
|
+
const credentialMatch = matchedText.match(/^[a-z]+:\/\/([^:@/\s]+):([^@/\s]+)@/i);
|
|
7636
|
+
if (credentialMatch) return PLACEHOLDER_URL_PARTS.has(credentialMatch[1].toLowerCase()) && PLACEHOLDER_URL_PARTS.has(credentialMatch[2].toLowerCase());
|
|
7637
|
+
try {
|
|
7638
|
+
const parsed = new URL(matchedText);
|
|
7639
|
+
return PLACEHOLDER_URL_PARTS.has(parsed.username.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.password.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.hostname.toLowerCase());
|
|
7640
|
+
} catch {
|
|
7641
|
+
return false;
|
|
7642
|
+
}
|
|
7643
|
+
};
|
|
7290
7644
|
const isPlaceholderValue = (matchedText) => {
|
|
7645
|
+
if (isPlaceholderCredentialUrl(matchedText)) return true;
|
|
7291
7646
|
if (/env\(/i.test(matchedText)) return true;
|
|
7292
7647
|
if (matchedText.includes("process.env")) return true;
|
|
7293
7648
|
if (matchedText.includes("os.environ")) return true;
|
|
@@ -7408,23 +7763,36 @@ const STYLE_RULES = new Set([
|
|
|
7408
7763
|
"complexity/function-too-long"
|
|
7409
7764
|
]);
|
|
7410
7765
|
const STYLE_WEIGHT = .5;
|
|
7766
|
+
const COMMENT_STYLE_RULE_CAP = 12;
|
|
7767
|
+
const COMMENT_STYLE_RULES = new Set(["ai-slop/trivial-comment", "ai-slop/narrative-comment"]);
|
|
7411
7768
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
7412
7769
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
7413
7770
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
7414
7771
|
return Math.max(1, filesWithDiagnostics);
|
|
7415
7772
|
};
|
|
7416
|
-
const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing) => {
|
|
7773
|
+
const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing, maxPerRule) => {
|
|
7417
7774
|
if (diagnostics.length === 0) return {
|
|
7418
7775
|
score: PERFECT_SCORE,
|
|
7419
7776
|
label: "Healthy"
|
|
7420
7777
|
};
|
|
7421
|
-
|
|
7778
|
+
const deductionsByRule = /* @__PURE__ */ new Map();
|
|
7422
7779
|
for (const d of diagnostics) {
|
|
7423
7780
|
const engineWeight = weights[d.engine] ?? 1;
|
|
7424
7781
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
7425
7782
|
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
7426
|
-
|
|
7427
|
-
|
|
7783
|
+
const key = `${d.engine}:${d.rule}`;
|
|
7784
|
+
deductionsByRule.set(key, (deductionsByRule.get(key) ?? 0) + severityPenalty * engineWeight * styleFactor);
|
|
7785
|
+
}
|
|
7786
|
+
const defaultRuleCap = typeof maxPerRule === "number" && maxPerRule > 0 ? maxPerRule : null;
|
|
7787
|
+
const capForRule = (key) => {
|
|
7788
|
+
const rule = key.slice(key.indexOf(":") + 1);
|
|
7789
|
+
if (COMMENT_STYLE_RULES.has(rule)) return defaultRuleCap ? Math.min(defaultRuleCap, COMMENT_STYLE_RULE_CAP) : COMMENT_STYLE_RULE_CAP;
|
|
7790
|
+
return defaultRuleCap;
|
|
7791
|
+
};
|
|
7792
|
+
const deductions = [...deductionsByRule.entries()].reduce((total, [key, value]) => {
|
|
7793
|
+
const cap = capForRule(key);
|
|
7794
|
+
return total + (cap ? Math.min(value, cap) : value);
|
|
7795
|
+
}, 0);
|
|
7428
7796
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
7429
7797
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
7430
7798
|
const issueDensity = Math.min(1, diagnostics.length / (effectiveFileCount + smoothingConstant));
|
|
@@ -8067,7 +8435,7 @@ const buildAgentPrompt = (rootDirectory, diagnostics, score) => {
|
|
|
8067
8435
|
}
|
|
8068
8436
|
lines.push("---");
|
|
8069
8437
|
lines.push("Fix each issue following the guidance above. Prioritize errors over warnings.");
|
|
8070
|
-
lines.push("After making changes, run `
|
|
8438
|
+
lines.push("After making changes, run `aislop scan` to verify all issues are resolved and the score improves.");
|
|
8071
8439
|
return lines.join("\n");
|
|
8072
8440
|
};
|
|
8073
8441
|
const SUPPORTED_AGENT_NAMES = Object.keys(AGENT_CONFIGS);
|
|
@@ -9274,7 +9642,7 @@ const runLintSteps = async (deps) => {
|
|
|
9274
9642
|
if (hasJsOrTs(deps.projectInfo)) await deps.runStep("Lint fixes (js/ts)", () => runOxlint(deps.context), () => fixOxlint(deps.context, { force: deps.force }));
|
|
9275
9643
|
if (deps.projectInfo.languages.includes("python") && deps.projectInfo.installedTools.ruff) await deps.runStep("Lint fixes (python)", () => runRuffLint(deps.context), () => deps.force ? fixRuffLintForce(deps.resolvedDir) : fixRuffLint(deps.resolvedDir));
|
|
9276
9644
|
else if (deps.projectInfo.languages.includes("python")) log.warn("Python detected but ruff is not installed; skipping Python lint fixes.");
|
|
9277
|
-
if (deps.projectInfo.languages.includes("ruby") && deps.projectInfo.installedTools.rubocop) await deps.runStep("Lint fixes (ruby)", () => import("./generic-
|
|
9645
|
+
if (deps.projectInfo.languages.includes("ruby") && deps.projectInfo.installedTools.rubocop) await deps.runStep("Lint fixes (ruby)", () => import("./generic-D_T4cUaC.js").then((n) => n.n).then((mod) => mod.runGenericLinter(deps.context, "ruby")), () => fixRubyLint(deps.resolvedDir));
|
|
9278
9646
|
else if (deps.projectInfo.languages.includes("ruby")) log.warn("Ruby detected but rubocop is not installed; skipping Ruby lint fixes.");
|
|
9279
9647
|
};
|
|
9280
9648
|
const runDependencyStep = async (deps) => {
|
|
@@ -9620,6 +9988,121 @@ var LiveGrid = class {
|
|
|
9620
9988
|
}
|
|
9621
9989
|
};
|
|
9622
9990
|
|
|
9991
|
+
//#endregion
|
|
9992
|
+
//#region src/utils/git.ts
|
|
9993
|
+
const MAX_BUFFER = 50 * 1024 * 1024;
|
|
9994
|
+
const baseRefExists = (cwd, ref) => {
|
|
9995
|
+
const result = spawnSync("git", [
|
|
9996
|
+
"rev-parse",
|
|
9997
|
+
"--verify",
|
|
9998
|
+
"--quiet",
|
|
9999
|
+
`${ref}^{commit}`
|
|
10000
|
+
], {
|
|
10001
|
+
cwd,
|
|
10002
|
+
encoding: "utf-8",
|
|
10003
|
+
maxBuffer: MAX_BUFFER
|
|
10004
|
+
});
|
|
10005
|
+
return !result.error && result.status === 0;
|
|
10006
|
+
};
|
|
10007
|
+
const getChangedFiles = (cwd, base) => {
|
|
10008
|
+
const diff = spawnSync("git", [
|
|
10009
|
+
"diff",
|
|
10010
|
+
"--name-only",
|
|
10011
|
+
"--diff-filter=ACMR",
|
|
10012
|
+
base ?? "HEAD"
|
|
10013
|
+
], {
|
|
10014
|
+
cwd,
|
|
10015
|
+
encoding: "utf-8",
|
|
10016
|
+
maxBuffer: MAX_BUFFER
|
|
10017
|
+
});
|
|
10018
|
+
if (diff.error || diff.status !== 0) return [];
|
|
10019
|
+
const untracked = spawnSync("git", [
|
|
10020
|
+
"ls-files",
|
|
10021
|
+
"--others",
|
|
10022
|
+
"--exclude-standard"
|
|
10023
|
+
], {
|
|
10024
|
+
cwd,
|
|
10025
|
+
encoding: "utf-8",
|
|
10026
|
+
maxBuffer: MAX_BUFFER
|
|
10027
|
+
});
|
|
10028
|
+
const names = /* @__PURE__ */ new Set();
|
|
10029
|
+
for (const line of diff.stdout.split("\n")) if (line.length > 0) names.add(line);
|
|
10030
|
+
if (!untracked.error && untracked.status === 0) {
|
|
10031
|
+
for (const line of untracked.stdout.split("\n")) if (line.length > 0) names.add(line);
|
|
10032
|
+
}
|
|
10033
|
+
return Array.from(names).map((f) => path.resolve(cwd, f));
|
|
10034
|
+
};
|
|
10035
|
+
const getStagedFiles = (cwd) => {
|
|
10036
|
+
const result = spawnSync("git", [
|
|
10037
|
+
"diff",
|
|
10038
|
+
"--cached",
|
|
10039
|
+
"--name-only",
|
|
10040
|
+
"--diff-filter=ACMR"
|
|
10041
|
+
], {
|
|
10042
|
+
cwd,
|
|
10043
|
+
encoding: "utf-8",
|
|
10044
|
+
maxBuffer: MAX_BUFFER
|
|
10045
|
+
});
|
|
10046
|
+
if (result.error || result.status !== 0) return [];
|
|
10047
|
+
return result.stdout.split("\n").filter((f) => f.length > 0).map((f) => path.resolve(cwd, f));
|
|
10048
|
+
};
|
|
10049
|
+
|
|
10050
|
+
//#endregion
|
|
10051
|
+
//#region src/utils/history.ts
|
|
10052
|
+
const HISTORY_FILE = "history.jsonl";
|
|
10053
|
+
const isHistoryDisabled = (env = process.env) => env.AISLOP_NO_HISTORY === "1";
|
|
10054
|
+
const historyPath = (directory) => path.join(path.resolve(directory), CONFIG_DIR, HISTORY_FILE);
|
|
10055
|
+
/**
|
|
10056
|
+
* Append a compact scan record to .aislop/history.jsonl. Best-effort: never
|
|
10057
|
+
* throws, so a read-only checkout or missing config dir can't break a scan.
|
|
10058
|
+
*/
|
|
10059
|
+
const appendHistory = (input) => {
|
|
10060
|
+
if (isHistoryDisabled()) return;
|
|
10061
|
+
const configDir = path.join(path.resolve(input.directory), CONFIG_DIR);
|
|
10062
|
+
if (!fs.existsSync(configDir)) return;
|
|
10063
|
+
const record = {
|
|
10064
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10065
|
+
score: input.score,
|
|
10066
|
+
errors: input.errors,
|
|
10067
|
+
warnings: input.warnings,
|
|
10068
|
+
files: input.files,
|
|
10069
|
+
cliVersion: APP_VERSION
|
|
10070
|
+
};
|
|
10071
|
+
try {
|
|
10072
|
+
fs.appendFileSync(historyPath(input.directory), `${JSON.stringify(record)}\n`);
|
|
10073
|
+
} catch {}
|
|
10074
|
+
};
|
|
10075
|
+
|
|
10076
|
+
//#endregion
|
|
10077
|
+
//#region src/commands/scan-coverage.ts
|
|
10078
|
+
const coverageReason = (c) => {
|
|
10079
|
+
if (c.supportedFiles === 0 && c.dominantUnsupported) return `This repository is ${c.dominantUnsupported} (${c.unsupportedFiles} files), which aislop does not analyze. No score — it would not reflect this code.`;
|
|
10080
|
+
if (c.supportedFiles === 0) return "No files in a language aislop analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Java). Nothing to score.";
|
|
10081
|
+
const lang = c.dominantUnsupported ?? "an unsupported language";
|
|
10082
|
+
const files = `${c.supportedFiles} supported file${c.supportedFiles === 1 ? "" : "s"}`;
|
|
10083
|
+
return `This repository is mostly ${lang} (${c.unsupportedFiles} files); aislop analyzed only ${files}. Score withheld — it would represent a sliver of the codebase.`;
|
|
10084
|
+
};
|
|
10085
|
+
const renderCoverageNotice = (projectInfo, includeHeader) => {
|
|
10086
|
+
const deps = {
|
|
10087
|
+
theme: createTheme(),
|
|
10088
|
+
symbols: createSymbols({ plain: false })
|
|
10089
|
+
};
|
|
10090
|
+
return `${includeHeader === false ? "" : renderHeader({
|
|
10091
|
+
version: APP_VERSION,
|
|
10092
|
+
command: "Scan result",
|
|
10093
|
+
context: [
|
|
10094
|
+
projectInfo.projectName,
|
|
10095
|
+
projectInfo.languages[0] ?? "unknown",
|
|
10096
|
+
`${projectInfo.sourceFileCount} files`
|
|
10097
|
+
],
|
|
10098
|
+
brand: true
|
|
10099
|
+
}, deps)} ${coverageReason(projectInfo.coverage)}\n\n`;
|
|
10100
|
+
};
|
|
10101
|
+
|
|
10102
|
+
//#endregion
|
|
10103
|
+
//#region src/commands/scan-exit-code.ts
|
|
10104
|
+
const computeScanExitCode = (opts) => opts.hasErrors || opts.scoreable && opts.score < opts.failBelow ? 1 : 0;
|
|
10105
|
+
|
|
9623
10106
|
//#endregion
|
|
9624
10107
|
//#region src/output/rule-labels.ts
|
|
9625
10108
|
const RULE_LABELS = {
|
|
@@ -9704,11 +10187,85 @@ const RULE_LABELS = {
|
|
|
9704
10187
|
"unicorn/no-useless-spread": "Useless spread",
|
|
9705
10188
|
"unicorn/no-single-promise-in-promise-methods": "Single-element Promise.all"
|
|
9706
10189
|
};
|
|
10190
|
+
const RULE_DESCRIPTIONS = {
|
|
10191
|
+
formatting: "File needs standard formatter output.",
|
|
10192
|
+
"code-quality/duplicate-block": "Large repeated code block should be shared or simplified.",
|
|
10193
|
+
"code-quality/repeated-chained-call": "Same chained call is repeated instead of stored once.",
|
|
10194
|
+
"code-quality/unused-declaration": "Declared symbol is not referenced.",
|
|
10195
|
+
"complexity/file-too-large": "File is large enough to be hard to review safely.",
|
|
10196
|
+
"complexity/function-too-long": "Function is doing too much in one body.",
|
|
10197
|
+
"complexity/deep-nesting": "Nested branches make the path hard to follow.",
|
|
10198
|
+
"complexity/too-many-params": "Function takes more arguments than readers can track.",
|
|
10199
|
+
"knip/files": "Source file is not imported or referenced.",
|
|
10200
|
+
"knip/dependencies": "Production dependency is listed but unused.",
|
|
10201
|
+
"knip/devDependencies": "Dev dependency is listed but unused.",
|
|
10202
|
+
"knip/unlisted": "Code imports a package missing from package.json.",
|
|
10203
|
+
"knip/unresolved": "Import cannot be resolved from the project.",
|
|
10204
|
+
"knip/binaries": "Package binary is listed but unused.",
|
|
10205
|
+
"knip/exports": "Exported value is not imported anywhere.",
|
|
10206
|
+
"knip/types": "Exported type is not imported anywhere.",
|
|
10207
|
+
"knip/duplicates": "Same export is declared more than once.",
|
|
10208
|
+
"ai-slop/trivial-comment": "Comment repeats obvious code instead of explaining intent.",
|
|
10209
|
+
"ai-slop/swallowed-exception": "Catch block hides an error without handling it.",
|
|
10210
|
+
"ai-slop/silent-recovery": "Error path logs or defaults, then continues as if safe.",
|
|
10211
|
+
"ai-slop/meta-comment": "Comment describes editing steps, plans, or generated-code process.",
|
|
10212
|
+
"ai-slop/redundant-try-catch": "try/catch only rethrows or adds no useful handling.",
|
|
10213
|
+
"ai-slop/redundant-type-coercion": "Conversion does not change the value meaningfully.",
|
|
10214
|
+
"ai-slop/duplicate-type-declaration": "Same exported type shape appears more than once.",
|
|
10215
|
+
"ai-slop/thin-wrapper": "Wrapper function adds no behavior or clearer contract.",
|
|
10216
|
+
"ai-slop/generic-naming": "Name is too vague to explain its role.",
|
|
10217
|
+
"ai-slop/unused-import": "Imported symbol is never used.",
|
|
10218
|
+
"ai-slop/console-leftover": "console/debug output was left in application code.",
|
|
10219
|
+
"ai-slop/todo-stub": "TODO/FIXME/stub marks unfinished behavior.",
|
|
10220
|
+
"ai-slop/unreachable-code": "Code path cannot execute.",
|
|
10221
|
+
"ai-slop/constant-condition": "Condition is always true or always false.",
|
|
10222
|
+
"ai-slop/empty-function": "Function body is empty or placeholder-only.",
|
|
10223
|
+
"ai-slop/unsafe-type-assertion": "Type assertion bypasses useful checking.",
|
|
10224
|
+
"ai-slop/double-type-assertion": "Value is cast through unknown/any to force a type.",
|
|
10225
|
+
"ai-slop/ts-directive": "TypeScript error is suppressed with a directive.",
|
|
10226
|
+
"ai-slop/narrative-comment": "Comment narrates implementation instead of adding context.",
|
|
10227
|
+
"ai-slop/duplicate-import": "Same module is imported more than once.",
|
|
10228
|
+
"ai-slop/hardcoded-url": "URL-like value is embedded directly in code.",
|
|
10229
|
+
"ai-slop/hardcoded-id": "Provider/account/test ID is embedded directly in code.",
|
|
10230
|
+
"ai-slop/python-bare-except": "Bare except catches everything, including system exits.",
|
|
10231
|
+
"ai-slop/python-broad-except": "Broad exception catch hides specific failure modes.",
|
|
10232
|
+
"ai-slop/python-mutable-default": "Mutable default argument is shared across calls.",
|
|
10233
|
+
"ai-slop/python-print-debug": "print/debug output was left in Python source.",
|
|
10234
|
+
"ai-slop/python-range-len-loop": "Loop uses indexes where direct iteration is clearer.",
|
|
10235
|
+
"ai-slop/python-chained-dict-get": "Nested get chain hides shape assumptions.",
|
|
10236
|
+
"ai-slop/python-repetitive-dispatch": "Repeated if/elif dispatch should be table-driven.",
|
|
10237
|
+
"ai-slop/python-isinstance-ladder": "Long isinstance ladder is brittle polymorphism.",
|
|
10238
|
+
"ai-slop/go-library-panic": "Library code panics instead of returning an error.",
|
|
10239
|
+
"ai-slop/rust-non-test-unwrap": "Production Rust uses unwrap instead of handling failure.",
|
|
10240
|
+
"ai-slop/rust-todo-stub": "Rust todo!/unimplemented! leaves behavior unfinished.",
|
|
10241
|
+
"ai-slop/hallucinated-import": "Import names a package not declared by the project.",
|
|
10242
|
+
"security/hardcoded-secret": "Secret-looking token is embedded in source.",
|
|
10243
|
+
"security/vulnerable-dependency": "Dependency audit reported a known vulnerability.",
|
|
10244
|
+
"security/dependency-audit-skipped": "Audit could not run because inputs/tools are missing.",
|
|
10245
|
+
"security/eval": "Dynamic code execution can run attacker-controlled input.",
|
|
10246
|
+
"security/innerhtml": "Raw HTML assignment can introduce XSS.",
|
|
10247
|
+
"security/dangerously-set-innerhtml": "React raw HTML escape hatch can introduce XSS.",
|
|
10248
|
+
"security/sql-injection": "SQL is built from interpolated or concatenated input.",
|
|
10249
|
+
"security/shell-injection": "Shell command is built from unsanitized input.",
|
|
10250
|
+
"oxlint/*": "JavaScript/TypeScript lint finding from oxlint.",
|
|
10251
|
+
"ruff/*": "Python lint finding from ruff.",
|
|
10252
|
+
"go/*": "Go lint finding from bundled checks.",
|
|
10253
|
+
"clippy/*": "Rust lint finding from clippy.",
|
|
10254
|
+
"rubocop/*": "Ruby lint finding from rubocop.",
|
|
10255
|
+
"typescript/*": "TypeScript compiler finding.",
|
|
10256
|
+
"import-order": "Imports need deterministic ordering.",
|
|
10257
|
+
"python-formatting": "Python file needs ruff formatting.",
|
|
10258
|
+
"go-formatting": "Go file needs gofmt.",
|
|
10259
|
+
"rust-formatting": "Rust file needs rustfmt.",
|
|
10260
|
+
"ruby-formatting": "Ruby file needs rubocop formatting.",
|
|
10261
|
+
"php-formatting": "PHP file needs php-cs-fixer formatting."
|
|
10262
|
+
};
|
|
9707
10263
|
const prettifyFallback = (ruleId) => {
|
|
9708
10264
|
const spaced = (ruleId.includes("/") ? ruleId.slice(ruleId.indexOf("/") + 1) : ruleId).replace(/[-_]/g, " ").replace(/\//g, " · ");
|
|
9709
10265
|
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
9710
10266
|
};
|
|
9711
10267
|
const labelForRule = (ruleId) => RULE_LABELS[ruleId] ?? prettifyFallback(ruleId);
|
|
10268
|
+
const descriptionForRule = (ruleId) => RULE_DESCRIPTIONS[ruleId] ?? labelForRule(ruleId);
|
|
9712
10269
|
|
|
9713
10270
|
//#endregion
|
|
9714
10271
|
//#region src/ui/summary.ts
|
|
@@ -9718,6 +10275,18 @@ const scoreToken = (score, thresholds) => {
|
|
|
9718
10275
|
if (score >= thresholds.ok) return "warn";
|
|
9719
10276
|
return "danger";
|
|
9720
10277
|
};
|
|
10278
|
+
const renderFindingAssessment = (assessment, t, sep) => {
|
|
10279
|
+
if (assessment.rows.length === 0) return [];
|
|
10280
|
+
const parts = assessment.rows.filter((row) => row.count > 0).map((row) => `${row.count} ${row.label}`);
|
|
10281
|
+
if (parts.length === 0) return [];
|
|
10282
|
+
const high = assessment.byConfidence.high;
|
|
10283
|
+
const medium = assessment.byConfidence.medium;
|
|
10284
|
+
const confidenceParts = [];
|
|
10285
|
+
if (high > 0) confidenceParts.push(`${high} high-confidence`);
|
|
10286
|
+
if (medium > 0) confidenceParts.push(`${medium} medium-confidence`);
|
|
10287
|
+
const confidence = confidenceParts.length > 0 ? ` ${sep} ${style(t, "muted", confidenceParts.join(", "))}` : "";
|
|
10288
|
+
return [` ${style(t, "muted", "Verdict mix:")} ${parts.join(` ${sep} `)}${confidence}`];
|
|
10289
|
+
};
|
|
9721
10290
|
const renderSummary = (input, deps = {}) => {
|
|
9722
10291
|
const t = deps.theme ?? theme;
|
|
9723
10292
|
const s = deps.symbols ?? symbols;
|
|
@@ -9736,6 +10305,10 @@ const renderSummary = (input, deps = {}) => {
|
|
|
9736
10305
|
` ${style(t, "muted", `${input.files} files`)} ${sep} ${style(t, "muted", `${input.engines} engines`)} ${sep} ${style(t, "muted", elapsed(input.elapsedMs))}`,
|
|
9737
10306
|
""
|
|
9738
10307
|
];
|
|
10308
|
+
if (input.findingAssessment) {
|
|
10309
|
+
lines.push(...renderFindingAssessment(input.findingAssessment, t, sep));
|
|
10310
|
+
lines.push("");
|
|
10311
|
+
}
|
|
9739
10312
|
if (input.breakdown && input.breakdown.rows.length > 0) {
|
|
9740
10313
|
lines.push(` ${style(t, "bold", "Top findings")}`);
|
|
9741
10314
|
const maxCountWidth = input.breakdown.rows.reduce((w, r) => Math.max(w, String(r.errors + r.warnings + r.info).length), 0);
|
|
@@ -9790,112 +10363,7 @@ const renderCleanRun = (input, deps = {}) => {
|
|
|
9790
10363
|
};
|
|
9791
10364
|
|
|
9792
10365
|
//#endregion
|
|
9793
|
-
//#region src/
|
|
9794
|
-
const MAX_BUFFER = 50 * 1024 * 1024;
|
|
9795
|
-
const getChangedFiles = (cwd, base) => {
|
|
9796
|
-
const diff = spawnSync("git", [
|
|
9797
|
-
"diff",
|
|
9798
|
-
"--name-only",
|
|
9799
|
-
"--diff-filter=ACMR",
|
|
9800
|
-
base ?? "HEAD"
|
|
9801
|
-
], {
|
|
9802
|
-
cwd,
|
|
9803
|
-
encoding: "utf-8",
|
|
9804
|
-
maxBuffer: MAX_BUFFER
|
|
9805
|
-
});
|
|
9806
|
-
if (diff.error || diff.status !== 0) return [];
|
|
9807
|
-
const untracked = spawnSync("git", [
|
|
9808
|
-
"ls-files",
|
|
9809
|
-
"--others",
|
|
9810
|
-
"--exclude-standard"
|
|
9811
|
-
], {
|
|
9812
|
-
cwd,
|
|
9813
|
-
encoding: "utf-8",
|
|
9814
|
-
maxBuffer: MAX_BUFFER
|
|
9815
|
-
});
|
|
9816
|
-
const names = /* @__PURE__ */ new Set();
|
|
9817
|
-
for (const line of diff.stdout.split("\n")) if (line.length > 0) names.add(line);
|
|
9818
|
-
if (!untracked.error && untracked.status === 0) {
|
|
9819
|
-
for (const line of untracked.stdout.split("\n")) if (line.length > 0) names.add(line);
|
|
9820
|
-
}
|
|
9821
|
-
return Array.from(names).map((f) => path.resolve(cwd, f));
|
|
9822
|
-
};
|
|
9823
|
-
const getStagedFiles = (cwd) => {
|
|
9824
|
-
const result = spawnSync("git", [
|
|
9825
|
-
"diff",
|
|
9826
|
-
"--cached",
|
|
9827
|
-
"--name-only",
|
|
9828
|
-
"--diff-filter=ACMR"
|
|
9829
|
-
], {
|
|
9830
|
-
cwd,
|
|
9831
|
-
encoding: "utf-8",
|
|
9832
|
-
maxBuffer: MAX_BUFFER
|
|
9833
|
-
});
|
|
9834
|
-
if (result.error || result.status !== 0) return [];
|
|
9835
|
-
return result.stdout.split("\n").filter((f) => f.length > 0).map((f) => path.resolve(cwd, f));
|
|
9836
|
-
};
|
|
9837
|
-
|
|
9838
|
-
//#endregion
|
|
9839
|
-
//#region src/utils/history.ts
|
|
9840
|
-
const HISTORY_FILE = "history.jsonl";
|
|
9841
|
-
const isHistoryDisabled = (env = process.env) => env.AISLOP_NO_HISTORY === "1";
|
|
9842
|
-
const historyPath = (directory) => path.join(path.resolve(directory), CONFIG_DIR, HISTORY_FILE);
|
|
9843
|
-
/**
|
|
9844
|
-
* Append a compact scan record to .aislop/history.jsonl. Best-effort: never
|
|
9845
|
-
* throws, so a read-only checkout or missing config dir can't break a scan.
|
|
9846
|
-
*/
|
|
9847
|
-
const appendHistory = (input) => {
|
|
9848
|
-
if (isHistoryDisabled()) return;
|
|
9849
|
-
const configDir = path.join(path.resolve(input.directory), CONFIG_DIR);
|
|
9850
|
-
if (!fs.existsSync(configDir)) return;
|
|
9851
|
-
const record = {
|
|
9852
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9853
|
-
score: input.score,
|
|
9854
|
-
errors: input.errors,
|
|
9855
|
-
warnings: input.warnings,
|
|
9856
|
-
files: input.files,
|
|
9857
|
-
cliVersion: APP_VERSION
|
|
9858
|
-
};
|
|
9859
|
-
try {
|
|
9860
|
-
fs.appendFileSync(historyPath(input.directory), `${JSON.stringify(record)}\n`);
|
|
9861
|
-
} catch {}
|
|
9862
|
-
};
|
|
9863
|
-
|
|
9864
|
-
//#endregion
|
|
9865
|
-
//#region src/commands/scan-coverage.ts
|
|
9866
|
-
const coverageReason = (c) => {
|
|
9867
|
-
if (c.supportedFiles === 0 && c.dominantUnsupported) return `This repository is ${c.dominantUnsupported} (${c.unsupportedFiles} files), which aislop does not analyze. No score — it would not reflect this code.`;
|
|
9868
|
-
if (c.supportedFiles === 0) return "No files in a language aislop analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Java). Nothing to score.";
|
|
9869
|
-
const lang = c.dominantUnsupported ?? "an unsupported language";
|
|
9870
|
-
const files = `${c.supportedFiles} supported file${c.supportedFiles === 1 ? "" : "s"}`;
|
|
9871
|
-
return `This repository is mostly ${lang} (${c.unsupportedFiles} files); aislop analyzed only ${files}. Score withheld — it would represent a sliver of the codebase.`;
|
|
9872
|
-
};
|
|
9873
|
-
const renderCoverageNotice = (projectInfo, includeHeader) => {
|
|
9874
|
-
const deps = {
|
|
9875
|
-
theme: createTheme(),
|
|
9876
|
-
symbols: createSymbols({ plain: false })
|
|
9877
|
-
};
|
|
9878
|
-
return `${includeHeader === false ? "" : renderHeader({
|
|
9879
|
-
version: APP_VERSION,
|
|
9880
|
-
command: "scan",
|
|
9881
|
-
context: [
|
|
9882
|
-
projectInfo.projectName,
|
|
9883
|
-
projectInfo.languages[0] ?? "unknown",
|
|
9884
|
-
`${projectInfo.sourceFileCount} files`
|
|
9885
|
-
],
|
|
9886
|
-
brand: true
|
|
9887
|
-
}, deps)} ${coverageReason(projectInfo.coverage)}\n\n`;
|
|
9888
|
-
};
|
|
9889
|
-
|
|
9890
|
-
//#endregion
|
|
9891
|
-
//#region src/commands/scan-exit-code.ts
|
|
9892
|
-
const computeScanExitCode = (opts) => opts.hasErrors || opts.scoreable && opts.score < opts.failBelow ? 1 : 0;
|
|
9893
|
-
|
|
9894
|
-
//#endregion
|
|
9895
|
-
//#region src/commands/scan.ts
|
|
9896
|
-
const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
|
|
9897
|
-
const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
|
|
9898
|
-
const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
|
|
10366
|
+
//#region src/commands/scan-render.ts
|
|
9899
10367
|
const BREAKDOWN_TOP_N = 10;
|
|
9900
10368
|
const computeBreakdown = (diagnostics) => {
|
|
9901
10369
|
const byRule = /* @__PURE__ */ new Map();
|
|
@@ -9937,7 +10405,7 @@ const buildScanRender = (input) => {
|
|
|
9937
10405
|
const invocation = detectInvocation();
|
|
9938
10406
|
const header = input.includeHeader === false ? "" : renderHeader({
|
|
9939
10407
|
version: APP_VERSION,
|
|
9940
|
-
command: "
|
|
10408
|
+
command: "Scan result",
|
|
9941
10409
|
context: [
|
|
9942
10410
|
input.projectName,
|
|
9943
10411
|
input.language,
|
|
@@ -9980,9 +10448,16 @@ const buildScanRender = (input) => {
|
|
|
9980
10448
|
elapsedMs: input.elapsedMs,
|
|
9981
10449
|
nextSteps,
|
|
9982
10450
|
breakdown: computeBreakdown(input.diagnostics),
|
|
10451
|
+
findingAssessment: summarizeFindingAssessments(input.diagnostics),
|
|
9983
10452
|
thresholds: input.thresholds
|
|
9984
10453
|
}, deps)}${starCta}`;
|
|
9985
10454
|
};
|
|
10455
|
+
|
|
10456
|
+
//#endregion
|
|
10457
|
+
//#region src/commands/scan.ts
|
|
10458
|
+
const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
|
|
10459
|
+
const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
|
|
10460
|
+
const ALL_ENGINE_NAMES = Object.keys(ENGINE_INFO);
|
|
9986
10461
|
const scanCommand = async (directory, config, options) => {
|
|
9987
10462
|
const resolvedDir = path.resolve(directory);
|
|
9988
10463
|
if (!fs.existsSync(resolvedDir)) {
|
|
@@ -9997,6 +10472,12 @@ const scanCommand = async (directory, config, options) => {
|
|
|
9997
10472
|
else log.error(msg);
|
|
9998
10473
|
return { exitCode: 1 };
|
|
9999
10474
|
}
|
|
10475
|
+
if (options.changes && options.base && !baseRefExists(resolvedDir, options.base)) {
|
|
10476
|
+
const msg = `Could not resolve base ref "${options.base}". Make sure it exists and was fetched (e.g. \`git fetch origin ${options.base}\`).`;
|
|
10477
|
+
if (options.json) console.log(JSON.stringify({ error: msg }, null, 2));
|
|
10478
|
+
else log.error(msg);
|
|
10479
|
+
return { exitCode: 1 };
|
|
10480
|
+
}
|
|
10000
10481
|
const projectInfo = await discoverProject(resolvedDir, [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)]);
|
|
10001
10482
|
return withCommandLifecycle({
|
|
10002
10483
|
command: options.command ?? "scan",
|
|
@@ -10010,14 +10491,30 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10010
10491
|
const showHeader = options.showHeader !== false;
|
|
10011
10492
|
const machineOutput = isMachineOutput(options);
|
|
10012
10493
|
const useLiveProgress = !machineOutput && shouldUseSpinner();
|
|
10494
|
+
const projectName = projectInfo.projectName ?? "project";
|
|
10495
|
+
const language = projectInfo.languages[0] ?? "unknown";
|
|
10496
|
+
const printedHumanHeader = !machineOutput && showHeader;
|
|
10497
|
+
if (printedHumanHeader) process.stdout.write(renderHeader({
|
|
10498
|
+
version: APP_VERSION,
|
|
10499
|
+
command: "Scan result",
|
|
10500
|
+
context: [
|
|
10501
|
+
projectName,
|
|
10502
|
+
language,
|
|
10503
|
+
`${projectInfo.sourceFileCount} files`
|
|
10504
|
+
],
|
|
10505
|
+
brand: options.printBrand !== false
|
|
10506
|
+
}));
|
|
10013
10507
|
const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
|
|
10014
10508
|
let files;
|
|
10015
10509
|
if (options.staged) {
|
|
10016
10510
|
files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], excludePatterns);
|
|
10017
10511
|
if (!machineOutput) log.muted(`Scope: ${files.length} staged file(s)`);
|
|
10018
10512
|
} else if (options.changes) {
|
|
10019
|
-
files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], excludePatterns);
|
|
10020
|
-
if (!machineOutput)
|
|
10513
|
+
files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir, options.base), [], excludePatterns);
|
|
10514
|
+
if (!machineOutput) {
|
|
10515
|
+
const scope = options.base ? `changed vs ${options.base}` : "changed";
|
|
10516
|
+
log.muted(`Scope: ${files.length} ${scope} file(s)`);
|
|
10517
|
+
}
|
|
10021
10518
|
} else {
|
|
10022
10519
|
files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], excludePatterns);
|
|
10023
10520
|
if (!machineOutput) log.muted(`Scope: ${files.length} file(s) after exclusions`);
|
|
@@ -10080,7 +10577,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10080
10577
|
if (suppressedCount > 0 && !machineOutput) log.muted(`Suppressed ${suppressedCount} finding(s) via aislop-ignore directives`);
|
|
10081
10578
|
const allDiagnostics = results.flatMap((r) => r.diagnostics);
|
|
10082
10579
|
const elapsedMs = performance.now() - startTime;
|
|
10083
|
-
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
10580
|
+
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
|
|
10084
10581
|
const scoreable = projectInfo.coverage.scoreable;
|
|
10085
10582
|
const exitCode = computeScanExitCode({
|
|
10086
10583
|
hasErrors: allDiagnostics.some((d) => d.severity === "error"),
|
|
@@ -10106,19 +10603,19 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10106
10603
|
engineTimings
|
|
10107
10604
|
};
|
|
10108
10605
|
if (options.sarif) {
|
|
10109
|
-
const { buildSarifLog } = await import("./sarif-
|
|
10606
|
+
const { buildSarifLog } = await import("./sarif-BXUicqQU.js");
|
|
10110
10607
|
console.log(JSON.stringify(buildSarifLog(results), null, 2));
|
|
10111
10608
|
return completion;
|
|
10112
10609
|
}
|
|
10113
10610
|
if (options.json) {
|
|
10114
|
-
const { buildJsonOutput } = await import("./json-
|
|
10611
|
+
const { buildJsonOutput } = await import("./json-0lJPTrwO.js");
|
|
10115
10612
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs, projectInfo.coverage);
|
|
10116
10613
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
10117
10614
|
return completion;
|
|
10118
10615
|
}
|
|
10119
10616
|
if (!scoreable) {
|
|
10120
10617
|
if (!machineOutput) {
|
|
10121
|
-
process.stdout.write(renderCoverageNotice(projectInfo, showHeader));
|
|
10618
|
+
process.stdout.write(renderCoverageNotice(projectInfo, !printedHumanHeader && showHeader));
|
|
10122
10619
|
if (allDiagnostics.length > 0) process.stdout.write(renderDiagnostics(allDiagnostics, options.verbose ?? false));
|
|
10123
10620
|
}
|
|
10124
10621
|
return completion;
|
|
@@ -10130,8 +10627,6 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10130
10627
|
warnings: completion.warningCount,
|
|
10131
10628
|
files: projectInfo.sourceFileCount
|
|
10132
10629
|
});
|
|
10133
|
-
const projectName = projectInfo.projectName ?? "project";
|
|
10134
|
-
const language = projectInfo.languages[0] ?? "unknown";
|
|
10135
10630
|
process.stdout.write(buildScanRender({
|
|
10136
10631
|
projectName,
|
|
10137
10632
|
language,
|
|
@@ -10142,7 +10637,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10142
10637
|
elapsedMs,
|
|
10143
10638
|
thresholds: config.scoring.thresholds,
|
|
10144
10639
|
verbose: options.verbose,
|
|
10145
|
-
includeHeader: showHeader,
|
|
10640
|
+
includeHeader: !printedHumanHeader && showHeader,
|
|
10146
10641
|
printBrand: options.printBrand
|
|
10147
10642
|
}));
|
|
10148
10643
|
return completion;
|
|
@@ -10185,7 +10680,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10185
10680
|
const projectName = projectInfo.projectName ?? "project";
|
|
10186
10681
|
if (showHeader) process.stdout.write(renderHeader({
|
|
10187
10682
|
version: APP_VERSION,
|
|
10188
|
-
command: "
|
|
10683
|
+
command: "Fix run",
|
|
10189
10684
|
context: [projectName],
|
|
10190
10685
|
brand: options.printBrand !== false
|
|
10191
10686
|
}));
|
|
@@ -10243,10 +10738,11 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10243
10738
|
label: "Verification complete"
|
|
10244
10739
|
});
|
|
10245
10740
|
const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
|
|
10246
|
-
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
10741
|
+
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
|
|
10247
10742
|
const errors = allDiagnostics.filter((d) => d.severity === "error").length;
|
|
10248
10743
|
const warnings = allDiagnostics.filter((d) => d.severity === "warning").length;
|
|
10249
10744
|
const remaining = errors + warnings;
|
|
10745
|
+
const actionableDiagnostics = allDiagnostics.filter((d) => d.severity !== "info");
|
|
10250
10746
|
if (steps.length === 0) rail.complete({
|
|
10251
10747
|
status: "skipped",
|
|
10252
10748
|
label: "No applicable auto-fixers found"
|
|
@@ -10264,7 +10760,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10264
10760
|
language,
|
|
10265
10761
|
fileCount: projectInfo.sourceFileCount,
|
|
10266
10762
|
results: scanResults,
|
|
10267
|
-
diagnostics:
|
|
10763
|
+
diagnostics: actionableDiagnostics,
|
|
10268
10764
|
score: scoreResult,
|
|
10269
10765
|
elapsedMs: performance.now() - startTime,
|
|
10270
10766
|
thresholds: config.scoring.thresholds,
|
|
@@ -10274,7 +10770,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10274
10770
|
}));
|
|
10275
10771
|
}
|
|
10276
10772
|
if (options.agent) {
|
|
10277
|
-
launchAgent(options.agent, resolvedDir,
|
|
10773
|
+
launchAgent(options.agent, resolvedDir, actionableDiagnostics, scoreResult.score);
|
|
10278
10774
|
return {
|
|
10279
10775
|
exitCode: 0,
|
|
10280
10776
|
score: scoreResult.score,
|
|
@@ -10283,7 +10779,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10283
10779
|
};
|
|
10284
10780
|
}
|
|
10285
10781
|
if (options.prompt) {
|
|
10286
|
-
printPrompt(resolvedDir,
|
|
10782
|
+
printPrompt(resolvedDir, actionableDiagnostics, scoreResult.score);
|
|
10287
10783
|
return {
|
|
10288
10784
|
exitCode: 0,
|
|
10289
10785
|
score: scoreResult.score,
|
|
@@ -10311,7 +10807,7 @@ const buildInitSuccessRender = (input) => {
|
|
|
10311
10807
|
};
|
|
10312
10808
|
const header = input.includeHeader === false ? "" : renderHeader({
|
|
10313
10809
|
version: APP_VERSION,
|
|
10314
|
-
command: "
|
|
10810
|
+
command: "Setup",
|
|
10315
10811
|
context: [],
|
|
10316
10812
|
brand: input.printBrand !== false
|
|
10317
10813
|
}, deps);
|
|
@@ -10446,7 +10942,8 @@ const writeAislopConfig = (configDir, configPath, choices) => {
|
|
|
10446
10942
|
scoring: {
|
|
10447
10943
|
weights: { ...DEFAULT_CONFIG.scoring.weights },
|
|
10448
10944
|
thresholds: { ...DEFAULT_CONFIG.scoring.thresholds },
|
|
10449
|
-
smoothing: DEFAULT_CONFIG.scoring.smoothing
|
|
10945
|
+
smoothing: DEFAULT_CONFIG.scoring.smoothing,
|
|
10946
|
+
maxPerRule: DEFAULT_CONFIG.scoring.maxPerRule
|
|
10450
10947
|
},
|
|
10451
10948
|
ci: {
|
|
10452
10949
|
failBelow: choices.failBelow,
|
|
@@ -10462,7 +10959,7 @@ const initCommand = async (directory, options = {}) => {
|
|
|
10462
10959
|
const printBrand = options.printBrand !== false;
|
|
10463
10960
|
process.stdout.write(renderHeader({
|
|
10464
10961
|
version: APP_VERSION,
|
|
10465
|
-
command: "
|
|
10962
|
+
command: "Setup",
|
|
10466
10963
|
context: [],
|
|
10467
10964
|
brand: printBrand
|
|
10468
10965
|
}));
|
|
@@ -10527,13 +11024,254 @@ const initCommand = async (directory, options = {}) => {
|
|
|
10527
11024
|
}));
|
|
10528
11025
|
};
|
|
10529
11026
|
|
|
11027
|
+
//#endregion
|
|
11028
|
+
//#region src/ui/search-select.ts
|
|
11029
|
+
const silentOutput = new Writable({ write(_chunk, _encoding, callback) {
|
|
11030
|
+
callback();
|
|
11031
|
+
} });
|
|
11032
|
+
const filterSearchItems = (items, query) => {
|
|
11033
|
+
const q = query.trim().toLowerCase();
|
|
11034
|
+
if (q.length === 0) return items;
|
|
11035
|
+
return items.map((item, index) => {
|
|
11036
|
+
const label = item.label.toLowerCase();
|
|
11037
|
+
const value = String(item.value).toLowerCase();
|
|
11038
|
+
const hint = item.hint?.toLowerCase() ?? "";
|
|
11039
|
+
const keywords = (item.keywords ?? []).join(" ").toLowerCase();
|
|
11040
|
+
const haystack = [
|
|
11041
|
+
label,
|
|
11042
|
+
value,
|
|
11043
|
+
hint,
|
|
11044
|
+
keywords
|
|
11045
|
+
].filter((v) => v.length > 0).join(" ");
|
|
11046
|
+
if (!q.split(/\s+/).every((part) => haystack.includes(part))) return null;
|
|
11047
|
+
let rank = 80;
|
|
11048
|
+
if (label === q || value === q) rank = 0;
|
|
11049
|
+
else if (label.startsWith(q) || value.startsWith(q)) rank = 10;
|
|
11050
|
+
else if (label.includes(q) || value.includes(q)) rank = 20;
|
|
11051
|
+
else if (keywords.includes(q)) rank = 40;
|
|
11052
|
+
else if (hint.includes(q)) rank = 60;
|
|
11053
|
+
return {
|
|
11054
|
+
item,
|
|
11055
|
+
index,
|
|
11056
|
+
rank
|
|
11057
|
+
};
|
|
11058
|
+
}).filter((entry) => entry !== null).sort((a, b) => {
|
|
11059
|
+
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
11060
|
+
return a.index - b.index;
|
|
11061
|
+
}).map((entry) => entry.item);
|
|
11062
|
+
};
|
|
11063
|
+
const countRows = (lines, columns) => {
|
|
11064
|
+
const width = columns && columns > 0 ? columns : 80;
|
|
11065
|
+
return lines.reduce((sum, line) => sum + Math.max(1, Math.ceil(stringWidth(line) / width)), 0);
|
|
11066
|
+
};
|
|
11067
|
+
const renderSearchLines = (options) => {
|
|
11068
|
+
const maxVisible = options.maxVisible ?? 8;
|
|
11069
|
+
const filtered = filterSearchItems(options.items, options.query);
|
|
11070
|
+
const cursor = Math.max(0, Math.min(options.cursor, Math.max(0, filtered.length - 1)));
|
|
11071
|
+
const start = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible));
|
|
11072
|
+
const visible = filtered.slice(start, start + maxVisible);
|
|
11073
|
+
const lines = [];
|
|
11074
|
+
const marker = options.state === "cancel" ? style(theme, "danger", symbols.fail) : options.state === "submit" ? style(theme, "success", symbols.stepDone) : style(theme, "accent", symbols.stepActive);
|
|
11075
|
+
lines.push(` ${marker} ${style(theme, "bold", options.message)}`);
|
|
11076
|
+
if (options.state === "cancel") {
|
|
11077
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", "Cancelled")}`);
|
|
11078
|
+
return lines;
|
|
11079
|
+
}
|
|
11080
|
+
if (options.state === "submit") {
|
|
11081
|
+
const selected = options.items.filter((item) => options.selected.has(item.value));
|
|
11082
|
+
const label = selected.length > 0 ? selected.map((item) => item.label).join(", ") : "No selection";
|
|
11083
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", label)}`);
|
|
11084
|
+
return lines;
|
|
11085
|
+
}
|
|
11086
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", "Search:")} ${options.query}${style(theme, "dim", "_")}`);
|
|
11087
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", options.mode === "multi" ? "type to filter, arrows move, space toggles, enter confirms" : "type to filter, arrows move, enter selects")}`);
|
|
11088
|
+
lines.push(` ${style(theme, "muted", symbols.rail)}`);
|
|
11089
|
+
if (visible.length === 0) lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", "No matches")}`);
|
|
11090
|
+
else for (const [offset, item] of visible.entries()) {
|
|
11091
|
+
const active = start + offset === cursor;
|
|
11092
|
+
const selected = options.selected.has(item.value);
|
|
11093
|
+
const pointer = active ? style(theme, "info", symbols.engineActive) : " ";
|
|
11094
|
+
const radio = options.mode === "multi" ? selected ? style(theme, "success", symbols.pass) : style(theme, "muted", symbols.pending) : active ? style(theme, "accent", symbols.bullet) : style(theme, "muted", symbols.pending);
|
|
11095
|
+
const label = active ? style(theme, "bold", item.label) : item.label;
|
|
11096
|
+
const hint = item.hint ? ` ${style(theme, "muted", truncate(item.hint, 72))}` : "";
|
|
11097
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${pointer} ${radio} ${label}${hint}`);
|
|
11098
|
+
}
|
|
11099
|
+
const hiddenBefore = start;
|
|
11100
|
+
const hiddenAfter = Math.max(0, filtered.length - (start + visible.length));
|
|
11101
|
+
if (hiddenBefore > 0 || hiddenAfter > 0) {
|
|
11102
|
+
const parts = [];
|
|
11103
|
+
if (hiddenBefore > 0) parts.push(`up ${hiddenBefore} more`);
|
|
11104
|
+
if (hiddenAfter > 0) parts.push(`down ${hiddenAfter} more`);
|
|
11105
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "muted", parts.join(" · "))}`);
|
|
11106
|
+
}
|
|
11107
|
+
if (options.mode === "multi") {
|
|
11108
|
+
const picked = options.items.filter((item) => options.selected.has(item.value));
|
|
11109
|
+
const summary = picked.length === 0 ? "Selected: none" : picked.length <= 3 ? `Selected: ${picked.map((item) => item.label).join(", ")}` : `Selected: ${picked.slice(0, 3).map((item) => item.label).join(", ")} +${picked.length - 3} more`;
|
|
11110
|
+
lines.push(` ${style(theme, "muted", symbols.rail)}`);
|
|
11111
|
+
lines.push(` ${style(theme, "muted", symbols.rail)} ${style(theme, "success", summary)}`);
|
|
11112
|
+
}
|
|
11113
|
+
lines.push(` ${style(theme, "muted", symbols.railEnd)}`);
|
|
11114
|
+
return lines;
|
|
11115
|
+
};
|
|
11116
|
+
const runSearchPrompt = async (options) => {
|
|
11117
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return options.mode === "multi" ? options.initialSelected ?? [] : null;
|
|
11118
|
+
return new Promise((resolve) => {
|
|
11119
|
+
const rl = readline.createInterface({
|
|
11120
|
+
input: process.stdin,
|
|
11121
|
+
output: silentOutput,
|
|
11122
|
+
terminal: false
|
|
11123
|
+
});
|
|
11124
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
11125
|
+
process.stdin.setRawMode(true);
|
|
11126
|
+
let query = "";
|
|
11127
|
+
let cursor = 0;
|
|
11128
|
+
let lastRows = 0;
|
|
11129
|
+
const selected = new Set(options.initialSelected ?? []);
|
|
11130
|
+
const clear = () => {
|
|
11131
|
+
if (lastRows === 0) return;
|
|
11132
|
+
process.stdout.write(`\x1b[${lastRows}A`);
|
|
11133
|
+
for (let i = 0; i < lastRows; i++) process.stdout.write("\x1B[2K\x1B[1B");
|
|
11134
|
+
process.stdout.write(`\x1b[${lastRows}A`);
|
|
11135
|
+
};
|
|
11136
|
+
const render = (state = "active") => {
|
|
11137
|
+
clear();
|
|
11138
|
+
const lines = renderSearchLines({
|
|
11139
|
+
...options,
|
|
11140
|
+
query,
|
|
11141
|
+
cursor,
|
|
11142
|
+
selected,
|
|
11143
|
+
state
|
|
11144
|
+
});
|
|
11145
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
11146
|
+
lastRows = countRows(lines, process.stdout.columns);
|
|
11147
|
+
};
|
|
11148
|
+
const cleanup = () => {
|
|
11149
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
11150
|
+
process.stdin.setRawMode(false);
|
|
11151
|
+
rl.close();
|
|
11152
|
+
};
|
|
11153
|
+
const submit = () => {
|
|
11154
|
+
const item = filterSearchItems(options.items, query)[cursor];
|
|
11155
|
+
if (options.mode === "single") {
|
|
11156
|
+
if (!item) {
|
|
11157
|
+
if (options.required) return;
|
|
11158
|
+
render("cancel");
|
|
11159
|
+
cleanup();
|
|
11160
|
+
resolve(null);
|
|
11161
|
+
return;
|
|
11162
|
+
}
|
|
11163
|
+
selected.clear();
|
|
11164
|
+
selected.add(item.value);
|
|
11165
|
+
render("submit");
|
|
11166
|
+
cleanup();
|
|
11167
|
+
resolve(item.value);
|
|
11168
|
+
return;
|
|
11169
|
+
}
|
|
11170
|
+
if (options.required && selected.size === 0) return;
|
|
11171
|
+
render("submit");
|
|
11172
|
+
cleanup();
|
|
11173
|
+
resolve([...selected]);
|
|
11174
|
+
};
|
|
11175
|
+
const cancel = () => {
|
|
11176
|
+
render("cancel");
|
|
11177
|
+
cleanup();
|
|
11178
|
+
resolve(null);
|
|
11179
|
+
};
|
|
11180
|
+
const onKeypress = (_str, key) => {
|
|
11181
|
+
if (!key) return;
|
|
11182
|
+
const filtered = filterSearchItems(options.items, query);
|
|
11183
|
+
if (key.name === "return") {
|
|
11184
|
+
submit();
|
|
11185
|
+
return;
|
|
11186
|
+
}
|
|
11187
|
+
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
11188
|
+
cancel();
|
|
11189
|
+
return;
|
|
11190
|
+
}
|
|
11191
|
+
if (key.name === "up") {
|
|
11192
|
+
cursor = Math.max(0, cursor - 1);
|
|
11193
|
+
render();
|
|
11194
|
+
return;
|
|
11195
|
+
}
|
|
11196
|
+
if (key.name === "down") {
|
|
11197
|
+
cursor = Math.min(Math.max(0, filtered.length - 1), cursor + 1);
|
|
11198
|
+
render();
|
|
11199
|
+
return;
|
|
11200
|
+
}
|
|
11201
|
+
if (key.name === "space" && options.mode === "multi") {
|
|
11202
|
+
const item = filtered[cursor];
|
|
11203
|
+
if (item) if (selected.has(item.value)) selected.delete(item.value);
|
|
11204
|
+
else selected.add(item.value);
|
|
11205
|
+
render();
|
|
11206
|
+
return;
|
|
11207
|
+
}
|
|
11208
|
+
if (key.name === "backspace") {
|
|
11209
|
+
query = query.slice(0, -1);
|
|
11210
|
+
cursor = 0;
|
|
11211
|
+
render();
|
|
11212
|
+
return;
|
|
11213
|
+
}
|
|
11214
|
+
if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
11215
|
+
query += key.sequence;
|
|
11216
|
+
cursor = 0;
|
|
11217
|
+
render();
|
|
11218
|
+
}
|
|
11219
|
+
};
|
|
11220
|
+
process.stdin.on("keypress", onKeypress);
|
|
11221
|
+
render();
|
|
11222
|
+
});
|
|
11223
|
+
};
|
|
11224
|
+
const searchSelect = async (options) => await runSearchPrompt({
|
|
11225
|
+
...options,
|
|
11226
|
+
mode: "single"
|
|
11227
|
+
});
|
|
11228
|
+
|
|
10530
11229
|
//#endregion
|
|
10531
11230
|
//#region src/commands/rules.ts
|
|
11231
|
+
const ENGINE_PRESENTATION = {
|
|
11232
|
+
"ai-slop": {
|
|
11233
|
+
label: "AI Slop",
|
|
11234
|
+
summary: "Generated-code leftovers: vague comments, unsafe casts, stubs, swallowed errors.",
|
|
11235
|
+
order: 10
|
|
11236
|
+
},
|
|
11237
|
+
security: {
|
|
11238
|
+
label: "Security",
|
|
11239
|
+
summary: "Secrets, injection, XSS, shell execution, and vulnerable dependencies.",
|
|
11240
|
+
order: 20
|
|
11241
|
+
},
|
|
11242
|
+
"code-quality": {
|
|
11243
|
+
label: "Code Quality",
|
|
11244
|
+
summary: "Dead code, duplicate code, complexity, and dependency hygiene.",
|
|
11245
|
+
order: 30
|
|
11246
|
+
},
|
|
11247
|
+
format: {
|
|
11248
|
+
label: "Format",
|
|
11249
|
+
summary: "Formatter and import-order checks that aislop can usually fix.",
|
|
11250
|
+
order: 40
|
|
11251
|
+
},
|
|
11252
|
+
lint: {
|
|
11253
|
+
label: "Lint",
|
|
11254
|
+
summary: "Language linter and compiler findings from bundled or system tools.",
|
|
11255
|
+
order: 50
|
|
11256
|
+
},
|
|
11257
|
+
architecture: {
|
|
11258
|
+
label: "Architecture",
|
|
11259
|
+
summary: "Project-specific import and layering rules from .aislop/rules.yml.",
|
|
11260
|
+
order: 60
|
|
11261
|
+
}
|
|
11262
|
+
};
|
|
11263
|
+
const presentationFor = (engine) => ENGINE_PRESENTATION[engine] ?? {
|
|
11264
|
+
label: engine,
|
|
11265
|
+
summary: "Project-specific rules.",
|
|
11266
|
+
order: 100
|
|
11267
|
+
};
|
|
11268
|
+
const severityLabel = (severity) => severity === "warning" ? "warn" : severity;
|
|
11269
|
+
const fixModeLabel = (fixable) => fixable ? "auto" : "review";
|
|
10532
11270
|
const buildRulesRender = (input) => {
|
|
10533
|
-
const header = renderHeader({
|
|
11271
|
+
const header = input.includeHeader === false ? "" : renderHeader({
|
|
10534
11272
|
version: APP_VERSION,
|
|
10535
|
-
command: "
|
|
10536
|
-
context: [],
|
|
11273
|
+
command: "Rules catalog",
|
|
11274
|
+
context: [`${input.rules.length} checks`],
|
|
10537
11275
|
brand: input.printBrand !== false
|
|
10538
11276
|
});
|
|
10539
11277
|
const byEngine = /* @__PURE__ */ new Map();
|
|
@@ -10542,23 +11280,50 @@ const buildRulesRender = (input) => {
|
|
|
10542
11280
|
list.push(r);
|
|
10543
11281
|
byEngine.set(r.engine, list);
|
|
10544
11282
|
}
|
|
10545
|
-
const engines = [...byEngine.keys()].sort()
|
|
11283
|
+
const engines = [...byEngine.keys()].sort((a, b) => {
|
|
11284
|
+
const pa = presentationFor(a);
|
|
11285
|
+
const pb = presentationFor(b);
|
|
11286
|
+
if (pa.order !== pb.order) return pa.order - pb.order;
|
|
11287
|
+
return pa.label.localeCompare(pb.label);
|
|
11288
|
+
});
|
|
10546
11289
|
const idWidth = Math.max(20, ...input.rules.map((r) => r.id.length));
|
|
10547
|
-
const lines = [];
|
|
11290
|
+
const lines = [` ${style(theme, "muted", "auto = aislop fix can change it; review = inspect and fix with a developer or agent.")}`, ""];
|
|
10548
11291
|
for (const engine of engines) {
|
|
10549
|
-
|
|
11292
|
+
const presentation = presentationFor(engine);
|
|
11293
|
+
lines.push(` ${style(theme, "accent", presentation.label)}`);
|
|
11294
|
+
lines.push(` ${style(theme, "muted", presentation.summary)}`);
|
|
11295
|
+
lines.push(` ${style(theme, "dim", padEnd("Rule ID", idWidth))} ${style(theme, "dim", "Sev")} ${style(theme, "dim", "Fix")} ${style(theme, "dim", "Meaning")}`);
|
|
10550
11296
|
const rules = (byEngine.get(engine) ?? []).sort((a, b) => a.id.localeCompare(b.id));
|
|
10551
11297
|
for (const r of rules) {
|
|
10552
|
-
const
|
|
10553
|
-
const
|
|
10554
|
-
|
|
11298
|
+
const severityText = severityLabel(r.severity);
|
|
11299
|
+
const severity = style(theme, r.severity === "error" ? "danger" : "warn", padEnd(severityText, 5));
|
|
11300
|
+
const fixable = r.fixable ? style(theme, "accent", padEnd("auto", 6)) : style(theme, "muted", padEnd("review", 6));
|
|
11301
|
+
lines.push(` ${padEnd(r.id, idWidth)} ${severity} ${fixable} ${descriptionForRule(r.id)}`);
|
|
10555
11302
|
}
|
|
10556
11303
|
lines.push("");
|
|
10557
11304
|
}
|
|
10558
11305
|
const invocation = input.invocation ?? detectInvocation();
|
|
10559
|
-
const tail = renderHintLine(`Run ${invocation} scan to
|
|
11306
|
+
const tail = renderHintLine(`Run ${invocation} scan to check your project against these rules`) + renderHintLine(`Run ${invocation} init to choose engines and CI settings`);
|
|
10560
11307
|
return `${header}${lines.join("\n")}\n${tail}`;
|
|
10561
11308
|
};
|
|
11309
|
+
const buildRuleDetailRender = (rule, input = {}) => {
|
|
11310
|
+
const presentation = presentationFor(rule.engine);
|
|
11311
|
+
const header = input.includeHeader === false ? "" : renderHeader({
|
|
11312
|
+
version: APP_VERSION,
|
|
11313
|
+
command: "Rule detail",
|
|
11314
|
+
context: [presentation.label],
|
|
11315
|
+
brand: input.printBrand !== false
|
|
11316
|
+
});
|
|
11317
|
+
const rows = [
|
|
11318
|
+
["Rule", rule.id],
|
|
11319
|
+
["Engine", `${presentation.label} — ${presentation.summary}`],
|
|
11320
|
+
["Severity", severityLabel(rule.severity)],
|
|
11321
|
+
["Fix", `${fixModeLabel(rule.fixable)}${rule.fixable ? " (aislop fix can change it)" : " (review and fix intentionally)"}`],
|
|
11322
|
+
["Meaning", descriptionForRule(rule.id)]
|
|
11323
|
+
];
|
|
11324
|
+
const labelWidth = Math.max(...rows.map(([label]) => label.length));
|
|
11325
|
+
return `${header}${rows.map(([label, value]) => ` ${style(theme, "muted", padEnd(label, labelWidth))} ${style(theme, label === "Severity" && rule.severity === "error" ? "danger" : "fg", value)}`).join("\n")}\n\n${renderHintLine(rule.fixable ? "Run aislop fix to apply the automatic fix" : "Use the meaning above to fix or review the finding")}`;
|
|
11326
|
+
};
|
|
10562
11327
|
const AI_SLOP_FIXABLE = new Set([
|
|
10563
11328
|
"ai-slop/trivial-comment",
|
|
10564
11329
|
"ai-slop/unused-import",
|
|
@@ -10691,7 +11456,7 @@ const toRuleEntry = (engine, ruleId) => {
|
|
|
10691
11456
|
fixable: false
|
|
10692
11457
|
};
|
|
10693
11458
|
};
|
|
10694
|
-
const
|
|
11459
|
+
const collectRuleEntries = (directory) => {
|
|
10695
11460
|
const resolvedDir = path.resolve(directory);
|
|
10696
11461
|
const entries = [];
|
|
10697
11462
|
for (const { engine, rules } of BUILTIN_RULES) for (const rule of rules) entries.push(toRuleEntry(engine, rule));
|
|
@@ -10705,6 +11470,41 @@ const rulesCommand = async (directory, options = {}) => {
|
|
|
10705
11470
|
fixable: false
|
|
10706
11471
|
});
|
|
10707
11472
|
}
|
|
11473
|
+
return entries;
|
|
11474
|
+
};
|
|
11475
|
+
const runRulesExplorer = async (entries, options) => {
|
|
11476
|
+
const selected = await searchSelect({
|
|
11477
|
+
message: "Search rules",
|
|
11478
|
+
items: entries.map((rule) => {
|
|
11479
|
+
const presentation = presentationFor(rule.engine);
|
|
11480
|
+
return {
|
|
11481
|
+
value: rule,
|
|
11482
|
+
label: rule.id,
|
|
11483
|
+
hint: `${presentation.label} · ${severityLabel(rule.severity)} · ${descriptionForRule(rule.id)}`,
|
|
11484
|
+
keywords: [
|
|
11485
|
+
presentation.label,
|
|
11486
|
+
rule.engine,
|
|
11487
|
+
rule.severity,
|
|
11488
|
+
fixModeLabel(rule.fixable),
|
|
11489
|
+
descriptionForRule(rule.id)
|
|
11490
|
+
]
|
|
11491
|
+
};
|
|
11492
|
+
}),
|
|
11493
|
+
maxVisible: 10,
|
|
11494
|
+
required: true
|
|
11495
|
+
});
|
|
11496
|
+
if (selected === null) return;
|
|
11497
|
+
process.stdout.write(`${buildRuleDetailRender(selected, {
|
|
11498
|
+
printBrand: options.printBrand,
|
|
11499
|
+
includeHeader: true
|
|
11500
|
+
})}\n`);
|
|
11501
|
+
};
|
|
11502
|
+
const rulesCommand = async (directory, options = {}) => {
|
|
11503
|
+
const entries = collectRuleEntries(directory);
|
|
11504
|
+
if (options.interactive && process.stdin.isTTY && process.stdout.isTTY) {
|
|
11505
|
+
await runRulesExplorer(entries, options);
|
|
11506
|
+
return;
|
|
11507
|
+
}
|
|
10708
11508
|
process.stdout.write(`${buildRulesRender({
|
|
10709
11509
|
rules: entries,
|
|
10710
11510
|
invocation: detectInvocation(),
|
|
@@ -10713,4 +11513,4 @@ const rulesCommand = async (directory, options = {}) => {
|
|
|
10713
11513
|
};
|
|
10714
11514
|
|
|
10715
11515
|
//#endregion
|
|
10716
|
-
export { buildDoctorRender, buildInitSuccessRender, buildRulesRender, calculateScore, discoverProject, doctorCommand, fixCommand, initCommand, loadConfig, rulesCommand, scanCommand };
|
|
11516
|
+
export { buildDoctorRender, buildInitSuccessRender, buildRuleDetailRender, buildRulesRender, calculateScore, discoverProject, doctorCommand, fixCommand, initCommand, loadConfig, rulesCommand, scanCommand };
|