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/mcp.js
CHANGED
|
@@ -16,10 +16,6 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
18
|
|
|
19
|
-
//#region src/version.ts
|
|
20
|
-
const APP_VERSION = "0.10.2";
|
|
21
|
-
|
|
22
|
-
//#endregion
|
|
23
19
|
//#region src/config/defaults.ts
|
|
24
20
|
const DEFAULT_CONFIG = {
|
|
25
21
|
version: 1,
|
|
@@ -63,7 +59,8 @@ const DEFAULT_CONFIG = {
|
|
|
63
59
|
good: 75,
|
|
64
60
|
ok: 50
|
|
65
61
|
},
|
|
66
|
-
smoothing: 20
|
|
62
|
+
smoothing: 20,
|
|
63
|
+
maxPerRule: 40
|
|
67
64
|
},
|
|
68
65
|
ci: {
|
|
69
66
|
failBelow: 70,
|
|
@@ -72,22 +69,6 @@ const DEFAULT_CONFIG = {
|
|
|
72
69
|
telemetry: { enabled: true },
|
|
73
70
|
rules: {}
|
|
74
71
|
};
|
|
75
|
-
const DEFAULT_GITHUB_WORKFLOW_YAML = `name: aislop
|
|
76
|
-
|
|
77
|
-
on:
|
|
78
|
-
push:
|
|
79
|
-
branches: [main]
|
|
80
|
-
pull_request:
|
|
81
|
-
|
|
82
|
-
jobs:
|
|
83
|
-
quality-gate:
|
|
84
|
-
runs-on: ubuntu-latest
|
|
85
|
-
steps:
|
|
86
|
-
- uses: actions/checkout@v4
|
|
87
|
-
- uses: scanaislop/aislop@v${APP_VERSION}
|
|
88
|
-
with:
|
|
89
|
-
version: ${APP_VERSION}
|
|
90
|
-
`;
|
|
91
72
|
|
|
92
73
|
//#endregion
|
|
93
74
|
//#region src/config/extends.ts
|
|
@@ -170,7 +151,8 @@ const ScoringSchema = z$1.object({
|
|
|
170
151
|
good: 75,
|
|
171
152
|
ok: 50
|
|
172
153
|
})),
|
|
173
|
-
smoothing: z$1.number().nonnegative().default(20)
|
|
154
|
+
smoothing: z$1.number().nonnegative().default(20),
|
|
155
|
+
maxPerRule: z$1.number().positive().default(40)
|
|
174
156
|
});
|
|
175
157
|
const CiSchema = z$1.object({
|
|
176
158
|
failBelow: z$1.number().default(70),
|
|
@@ -210,7 +192,8 @@ const AislopConfigSchema = z$1.object({
|
|
|
210
192
|
good: 75,
|
|
211
193
|
ok: 50
|
|
212
194
|
},
|
|
213
|
-
smoothing: 20
|
|
195
|
+
smoothing: 20,
|
|
196
|
+
maxPerRule: 40
|
|
214
197
|
})),
|
|
215
198
|
ci: CiSchema.default(() => ({
|
|
216
199
|
failBelow: 70,
|
|
@@ -979,8 +962,8 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
|
|
|
979
962
|
|
|
980
963
|
//#endregion
|
|
981
964
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
982
|
-
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;
|
|
983
|
-
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]
|
|
965
|
+
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;
|
|
966
|
+
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;
|
|
984
967
|
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
985
968
|
|
|
986
969
|
//#endregion
|
|
@@ -1096,7 +1079,7 @@ const JS_EXTENSIONS$3 = new Set([
|
|
|
1096
1079
|
".mjs",
|
|
1097
1080
|
".cjs"
|
|
1098
1081
|
]);
|
|
1099
|
-
const
|
|
1082
|
+
const CONSOLE_CALL_PATTERN = /\bconsole\.(log|debug|info|trace|dir|table)\s*\(/;
|
|
1100
1083
|
const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
1101
1084
|
filePath,
|
|
1102
1085
|
engine: "ai-slop",
|
|
@@ -1110,20 +1093,35 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
|
1110
1093
|
fixable
|
|
1111
1094
|
});
|
|
1112
1095
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
1096
|
+
const CLI_ENTRYPOINT_PATTERN = /(?:^|\/)(?:cli|cli[-_.][^/]*|[^/]+[-_]cli)\.[mc]?[jt]sx?$/i;
|
|
1097
|
+
const ENTRYPOINT_GUARD_PATTERN = /\b(?:import\.meta\.main|require\.main\s*===\s*module)\b/;
|
|
1098
|
+
const OPERATIONAL_LOG_PATTERN = /\bconsole\.(?:log|info)\s*\(\s*(?:`|["'])\s*\[[^\]\n]{1,48}\]/;
|
|
1099
|
+
const DEBUG_SIGNAL_PATTERN = /\b(?:debug|dbg|trace|dump|inspect|todo|tmp|temp|remove\s+me|leftover|here|checkpoint)\b/i;
|
|
1100
|
+
const shouldFlagConsoleCall = (trimmed) => {
|
|
1101
|
+
const match = CONSOLE_CALL_PATTERN.exec(trimmed);
|
|
1102
|
+
if (!match) return false;
|
|
1103
|
+
const method = match[1];
|
|
1104
|
+
if (method === "trace" || method === "dir" || method === "table") return true;
|
|
1105
|
+
if (method === "debug") return DEBUG_SIGNAL_PATTERN.test(trimmed) || !OPERATIONAL_LOG_PATTERN.test(trimmed);
|
|
1106
|
+
if (method === "info" || method === "log") {
|
|
1107
|
+
if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) return false;
|
|
1108
|
+
if (OPERATIONAL_LOG_PATTERN.test(trimmed)) return false;
|
|
1109
|
+
return true;
|
|
1110
|
+
}
|
|
1111
|
+
return false;
|
|
1112
|
+
};
|
|
1113
1113
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
1114
1114
|
if (!JS_EXTENSIONS$3.has(ext)) return [];
|
|
1115
1115
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
1116
|
-
if (isNonProductionPath(relativePath)) return [];
|
|
1116
|
+
if (isNonProductionPath(relativePath) || CLI_ENTRYPOINT_PATTERN.test(relativePath)) return [];
|
|
1117
|
+
if (content.startsWith("#!")) return [];
|
|
1118
|
+
if (ENTRYPOINT_GUARD_PATTERN.test(content)) return [];
|
|
1117
1119
|
const diagnostics = [];
|
|
1118
1120
|
const lines = content.split("\n");
|
|
1119
1121
|
for (let i = 0; i < lines.length; i++) {
|
|
1120
1122
|
const trimmed = lines[i].trim();
|
|
1121
1123
|
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
1122
|
-
if (
|
|
1123
|
-
if (/console\.(?:error|warn)\s*\(/.test(trimmed)) continue;
|
|
1124
|
-
if (/console\.log\(\s*JSON\.stringify\b/.test(trimmed)) continue;
|
|
1125
|
-
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));
|
|
1126
|
-
}
|
|
1124
|
+
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));
|
|
1127
1125
|
}
|
|
1128
1126
|
return diagnostics;
|
|
1129
1127
|
};
|
|
@@ -1151,6 +1149,7 @@ const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
|
1151
1149
|
const control = contextLines.join(" ");
|
|
1152
1150
|
return /(?:^|[}\s])(?:if|else\s+if|for|while)\s*\(/.test(control) && !/{\s*$/.test(control);
|
|
1153
1151
|
};
|
|
1152
|
+
const isPropertyNoopAssignment = (trimmed) => /^(?:[\w$]+\.)+[\w$]+\s*=\s*(?:function\s*)?\([^)]*\)\s*(?:=>)?\s*\{\s*\}\s*;?$/.test(trimmed);
|
|
1154
1153
|
const detectTodoStubs = (content, relativePath) => {
|
|
1155
1154
|
const diagnostics = [];
|
|
1156
1155
|
const lines = content.split("\n");
|
|
@@ -1172,7 +1171,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1172
1171
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1173
1172
|
if (JS_EXTENSIONS$3.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));
|
|
1174
1173
|
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));
|
|
1175
|
-
if (JS_EXTENSIONS$3.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));
|
|
1174
|
+
if (JS_EXTENSIONS$3.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));
|
|
1176
1175
|
}
|
|
1177
1176
|
return diagnostics;
|
|
1178
1177
|
};
|
|
@@ -1670,7 +1669,7 @@ const JS_RESOLUTION_EXTENSIONS = [
|
|
|
1670
1669
|
"/index.js",
|
|
1671
1670
|
"/index.jsx"
|
|
1672
1671
|
];
|
|
1673
|
-
const readJson$
|
|
1672
|
+
const readJson$3 = (filePath) => {
|
|
1674
1673
|
try {
|
|
1675
1674
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1676
1675
|
} catch {
|
|
@@ -1685,7 +1684,7 @@ const buildAliasMatcher = (key) => {
|
|
|
1685
1684
|
return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
|
|
1686
1685
|
};
|
|
1687
1686
|
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
1688
|
-
const opts = readJson$
|
|
1687
|
+
const opts = readJson$3(configPath)?.compilerOptions;
|
|
1689
1688
|
if (!opts || typeof opts !== "object") return;
|
|
1690
1689
|
const configDir = path.dirname(configPath);
|
|
1691
1690
|
const paths = opts.paths;
|
|
@@ -1708,7 +1707,7 @@ const collectTsPathAliases = (rootDir, workspaceDirs) => {
|
|
|
1708
1707
|
|
|
1709
1708
|
//#endregion
|
|
1710
1709
|
//#region src/engines/ai-slop/js-workspaces.ts
|
|
1711
|
-
const readJson$
|
|
1710
|
+
const readJson$2 = (filePath) => {
|
|
1712
1711
|
try {
|
|
1713
1712
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1714
1713
|
} catch {
|
|
@@ -1728,7 +1727,7 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
|
1728
1727
|
}
|
|
1729
1728
|
}
|
|
1730
1729
|
}
|
|
1731
|
-
const lerna = readJson$
|
|
1730
|
+
const lerna = readJson$2(path.join(rootDir, "lerna.json"));
|
|
1732
1731
|
if (lerna && Array.isArray(lerna.packages)) {
|
|
1733
1732
|
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
1734
1733
|
}
|
|
@@ -2129,7 +2128,7 @@ const JS_EXTENSIONS$1 = new Set([
|
|
|
2129
2128
|
".cjs"
|
|
2130
2129
|
]);
|
|
2131
2130
|
const PY_EXTENSIONS$2 = new Set([".py"]);
|
|
2132
|
-
const readJson = (filePath) => {
|
|
2131
|
+
const readJson$1 = (filePath) => {
|
|
2133
2132
|
try {
|
|
2134
2133
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
2135
2134
|
} catch {
|
|
@@ -2173,7 +2172,7 @@ const collectNestedManifests = (rootDir, jsDeps) => {
|
|
|
2173
2172
|
const full = path.join(dir, entry.name);
|
|
2174
2173
|
if (entry.isDirectory()) walk(full, depth + 1);
|
|
2175
2174
|
else if (entry.name === "package.json" && depth > 0) {
|
|
2176
|
-
const wsPkg = readJson(full);
|
|
2175
|
+
const wsPkg = readJson$1(full);
|
|
2177
2176
|
if (!wsPkg) continue;
|
|
2178
2177
|
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
2179
2178
|
addDepsFromPkg(wsPkg, jsDeps);
|
|
@@ -2185,13 +2184,13 @@ const collectNestedManifests = (rootDir, jsDeps) => {
|
|
|
2185
2184
|
const collectJsDeps = (rootDir, jsDeps) => {
|
|
2186
2185
|
const pkgPath = path.join(rootDir, "package.json");
|
|
2187
2186
|
if (!fs.existsSync(pkgPath)) return false;
|
|
2188
|
-
const pkg = readJson(pkgPath);
|
|
2187
|
+
const pkg = readJson$1(pkgPath);
|
|
2189
2188
|
if (!pkg || typeof pkg !== "object") return false;
|
|
2190
2189
|
addDepsFromPkg(pkg, jsDeps);
|
|
2191
2190
|
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
2192
2191
|
const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
|
|
2193
2192
|
for (const wsDir of workspaceDirs) {
|
|
2194
|
-
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
2193
|
+
const wsPkg = readJson$1(path.join(wsDir, "package.json"));
|
|
2195
2194
|
if (!wsPkg) continue;
|
|
2196
2195
|
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
2197
2196
|
addDepsFromPkg(wsPkg, jsDeps);
|
|
@@ -2220,7 +2219,11 @@ const VIRTUAL_MODULE_PREFIXES = [
|
|
|
2220
2219
|
"astro:",
|
|
2221
2220
|
"virtual:",
|
|
2222
2221
|
"bun:",
|
|
2223
|
-
"file:"
|
|
2222
|
+
"file:",
|
|
2223
|
+
"http:",
|
|
2224
|
+
"https:",
|
|
2225
|
+
"jsr:",
|
|
2226
|
+
"npm:"
|
|
2224
2227
|
];
|
|
2225
2228
|
const isJsVirtualModule = (spec, manifest) => {
|
|
2226
2229
|
if (VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p))) return true;
|
|
@@ -2349,7 +2352,7 @@ const checkPyImport = (spec, manifest) => {
|
|
|
2349
2352
|
return root;
|
|
2350
2353
|
};
|
|
2351
2354
|
const detectHallucinatedImports = async (context) => {
|
|
2352
|
-
const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
|
|
2355
|
+
const rootPkg = readJson$1(path.join(context.rootDirectory, "package.json"));
|
|
2353
2356
|
const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
|
|
2354
2357
|
const manifest = loadManifest(context.rootDirectory);
|
|
2355
2358
|
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
@@ -2454,6 +2457,9 @@ const VENDOR_API_DOMAINS = [
|
|
|
2454
2457
|
];
|
|
2455
2458
|
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
2456
2459
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2460
|
+
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;
|
|
2461
|
+
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;
|
|
2462
|
+
const READABLE_KEY_RE = /^[a-z][a-z0-9]*(?:[_-][a-z0-9]+){2,}$/;
|
|
2457
2463
|
const HARDCODED_URL_FINDING = {
|
|
2458
2464
|
rule: "ai-slop/hardcoded-url",
|
|
2459
2465
|
message: "Hardcoded environment URL in production code",
|
|
@@ -2491,8 +2497,10 @@ const safeUrlHost = (urlText) => {
|
|
|
2491
2497
|
}
|
|
2492
2498
|
};
|
|
2493
2499
|
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
2500
|
+
const TEMPLATE_INTERPOLATION_START = "${";
|
|
2494
2501
|
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
2495
2502
|
if (isEnvBackedLine(line)) return false;
|
|
2503
|
+
if (urlText.includes(TEMPLATE_INTERPOLATION_START) && /\bnew\s+URL\s*\(/.test(line)) return false;
|
|
2496
2504
|
const host = safeUrlHost(urlText);
|
|
2497
2505
|
if (!host) return false;
|
|
2498
2506
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
@@ -2507,7 +2515,11 @@ const hasUsefulIdShape = (value) => {
|
|
|
2507
2515
|
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
2508
2516
|
if (/^https?:\/\//i.test(value)) return false;
|
|
2509
2517
|
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
2510
|
-
|
|
2518
|
+
if (READABLE_KEY_RE.test(value) && !PROVIDER_ID_RE.test(value)) return false;
|
|
2519
|
+
if (PROVIDER_ID_RE.test(value)) return true;
|
|
2520
|
+
if (UUID_RE.test(value)) return true;
|
|
2521
|
+
if (!/[0-9]/.test(value)) return false;
|
|
2522
|
+
return value.length >= 24 && !/[_-]/.test(value) && /[a-z]/.test(value) && /[A-Z]/.test(value);
|
|
2511
2523
|
};
|
|
2512
2524
|
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
2513
2525
|
const diagnostics = [];
|
|
@@ -4458,8 +4470,8 @@ const shouldIncludeIssue = (issueType, filePath) => {
|
|
|
4458
4470
|
return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
|
|
4459
4471
|
};
|
|
4460
4472
|
const DEPENDENCY_HELP = {
|
|
4461
|
-
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `
|
|
4462
|
-
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `
|
|
4473
|
+
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
4474
|
+
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
4463
4475
|
unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
|
|
4464
4476
|
unresolved: "This import cannot be resolved. Check for typos or missing packages.",
|
|
4465
4477
|
binaries: "This binary is used but its package is not in package.json."
|
|
@@ -4766,7 +4778,7 @@ const parseBiomeJsonOutput = (output, rootDir) => {
|
|
|
4766
4778
|
rule: "formatting",
|
|
4767
4779
|
severity,
|
|
4768
4780
|
message,
|
|
4769
|
-
help: "Run `
|
|
4781
|
+
help: "Run `aislop fix` to auto-format",
|
|
4770
4782
|
line: entry.location?.start?.line ?? 0,
|
|
4771
4783
|
column: entry.location?.start?.column ?? 0,
|
|
4772
4784
|
category: "Format",
|
|
@@ -4795,7 +4807,7 @@ const FORMATTERS = {
|
|
|
4795
4807
|
rule: "rust-formatting",
|
|
4796
4808
|
severity: "warning",
|
|
4797
4809
|
message: "Rust file is not formatted correctly",
|
|
4798
|
-
help: "Run `
|
|
4810
|
+
help: "Run `aislop fix` to auto-format with rustfmt",
|
|
4799
4811
|
line: parseInt(match[2], 10),
|
|
4800
4812
|
column: 0,
|
|
4801
4813
|
category: "Format",
|
|
@@ -4828,7 +4840,7 @@ const FORMATTERS = {
|
|
|
4828
4840
|
rule: offense.cop_name ?? "ruby-formatting",
|
|
4829
4841
|
severity: "warning",
|
|
4830
4842
|
message: offense.message ?? "Ruby formatting issue",
|
|
4831
|
-
help: "Run `
|
|
4843
|
+
help: "Run `aislop fix` to auto-format",
|
|
4832
4844
|
line: offense.location?.start_line ?? 0,
|
|
4833
4845
|
column: offense.location?.start_column ?? 0,
|
|
4834
4846
|
category: "Format",
|
|
@@ -4859,7 +4871,7 @@ const FORMATTERS = {
|
|
|
4859
4871
|
rule: "php-formatting",
|
|
4860
4872
|
severity: "warning",
|
|
4861
4873
|
message: "PHP file is not formatted correctly",
|
|
4862
|
-
help: "Run `
|
|
4874
|
+
help: "Run `aislop fix` to auto-format",
|
|
4863
4875
|
line: 0,
|
|
4864
4876
|
column: 0,
|
|
4865
4877
|
category: "Format",
|
|
@@ -4903,7 +4915,7 @@ const runGofmt = async (context) => {
|
|
|
4903
4915
|
rule: "go-formatting",
|
|
4904
4916
|
severity: "warning",
|
|
4905
4917
|
message: "Go file is not formatted correctly",
|
|
4906
|
-
help: "Run `
|
|
4918
|
+
help: "Run `aislop fix` to auto-format with gofmt",
|
|
4907
4919
|
line: 0,
|
|
4908
4920
|
column: 0,
|
|
4909
4921
|
category: "Format",
|
|
@@ -4995,7 +5007,7 @@ const parseRuffFormatOutput = (output, rootDir) => {
|
|
|
4995
5007
|
rule: "python-formatting",
|
|
4996
5008
|
severity: "warning",
|
|
4997
5009
|
message: "Python file is not formatted correctly",
|
|
4998
|
-
help: "Run `
|
|
5010
|
+
help: "Run `aislop fix` to auto-format with ruff",
|
|
4999
5011
|
line: 0,
|
|
5000
5012
|
column: 0,
|
|
5001
5013
|
category: "Format",
|
|
@@ -5273,6 +5285,7 @@ const AMBIENT_GLOBAL_DEPS = [
|
|
|
5273
5285
|
const SST_PLATFORM_REF_RE = /\/\/\/\s*<reference\s+path=["'][^"']*sst[\\/]+platform[\\/]+config\.d\.ts["']/;
|
|
5274
5286
|
const ICON_AUTOIMPORT_RE = /^Icon[A-Z]/;
|
|
5275
5287
|
const NO_UNDEF_IDENT_RE = /^['‘"`]([^'’"`]+)['’"`]/;
|
|
5288
|
+
const SUPABASE_FUNCTION_PATH_RE = /(?:^|\/)supabase\/functions\/[^/]+\/.+\.[cm]?[jt]sx?$/;
|
|
5276
5289
|
const detectAmbientSources = (rootDir) => {
|
|
5277
5290
|
const found = /* @__PURE__ */ new Set();
|
|
5278
5291
|
const skipDirs = new Set([
|
|
@@ -5317,6 +5330,36 @@ const detectAmbientSources = (rootDir) => {
|
|
|
5317
5330
|
const extractNoUndefIdentifier = (message) => {
|
|
5318
5331
|
return NO_UNDEF_IDENT_RE.exec(message)?.[1] ?? null;
|
|
5319
5332
|
};
|
|
5333
|
+
const looksLikeChromeExtensionManifest = (filePath) => {
|
|
5334
|
+
try {
|
|
5335
|
+
const manifest = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
5336
|
+
return typeof manifest.manifest_version === "number" && ("background" in manifest || "content_scripts" in manifest || "permissions" in manifest);
|
|
5337
|
+
} catch {
|
|
5338
|
+
return false;
|
|
5339
|
+
}
|
|
5340
|
+
};
|
|
5341
|
+
const chromeExtensionFileCache = /* @__PURE__ */ new Map();
|
|
5342
|
+
const isChromeExtensionFile = (rootDir, relativeFilePath) => {
|
|
5343
|
+
const cacheKey = `${rootDir}:${relativeFilePath.split(path.sep).join("/")}`;
|
|
5344
|
+
const cached = chromeExtensionFileCache.get(cacheKey);
|
|
5345
|
+
if (cached !== void 0) return cached;
|
|
5346
|
+
const absolute = path.isAbsolute(relativeFilePath) ? relativeFilePath : path.join(rootDir, relativeFilePath);
|
|
5347
|
+
const root = path.resolve(rootDir);
|
|
5348
|
+
let dir = path.dirname(path.resolve(absolute));
|
|
5349
|
+
let matched = false;
|
|
5350
|
+
while (true) {
|
|
5351
|
+
const relativeToRoot = path.relative(root, dir);
|
|
5352
|
+
if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) break;
|
|
5353
|
+
if (looksLikeChromeExtensionManifest(path.join(dir, "manifest.json"))) {
|
|
5354
|
+
matched = true;
|
|
5355
|
+
break;
|
|
5356
|
+
}
|
|
5357
|
+
if (dir === root) break;
|
|
5358
|
+
dir = path.dirname(dir);
|
|
5359
|
+
}
|
|
5360
|
+
chromeExtensionFileCache.set(cacheKey, matched);
|
|
5361
|
+
return matched;
|
|
5362
|
+
};
|
|
5320
5363
|
const isAmbientFalsePositive = (rule, message, sources) => {
|
|
5321
5364
|
if (rule !== "eslint/no-undef") return false;
|
|
5322
5365
|
const ident = extractNoUndefIdentifier(message);
|
|
@@ -5325,9 +5368,19 @@ const isAmbientFalsePositive = (rule, message, sources) => {
|
|
|
5325
5368
|
if ((sources.has("@types/bun") || sources.has("bun-types")) && ident === "Bun") return true;
|
|
5326
5369
|
return false;
|
|
5327
5370
|
};
|
|
5371
|
+
const isRuntimeGlobalFalsePositive = (rule, message, rootDir, relativeFilePath) => {
|
|
5372
|
+
if (rule !== "eslint/no-undef") return false;
|
|
5373
|
+
const ident = extractNoUndefIdentifier(message);
|
|
5374
|
+
if (!ident) return false;
|
|
5375
|
+
const normalized = relativeFilePath.split(path.sep).join("/");
|
|
5376
|
+
if (ident === "Deno" && SUPABASE_FUNCTION_PATH_RE.test(normalized)) return true;
|
|
5377
|
+
if (ident === "chrome" && isChromeExtensionFile(rootDir, relativeFilePath)) return true;
|
|
5378
|
+
return false;
|
|
5379
|
+
};
|
|
5328
5380
|
const sstReferencedFiles = /* @__PURE__ */ new Map();
|
|
5329
5381
|
const clearSstReferenceCache = () => {
|
|
5330
5382
|
sstReferencedFiles.clear();
|
|
5383
|
+
chromeExtensionFileCache.clear();
|
|
5331
5384
|
};
|
|
5332
5385
|
const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
5333
5386
|
const cached = sstReferencedFiles.get(relativeFilePath);
|
|
@@ -5379,6 +5432,32 @@ const collectPackageNames = (dir) => {
|
|
|
5379
5432
|
}
|
|
5380
5433
|
return names;
|
|
5381
5434
|
};
|
|
5435
|
+
const readJson = (filePath) => {
|
|
5436
|
+
const raw = readTextFile$1(filePath);
|
|
5437
|
+
if (!raw) return null;
|
|
5438
|
+
try {
|
|
5439
|
+
const parsed = JSON.parse(raw);
|
|
5440
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
5441
|
+
} catch {
|
|
5442
|
+
return null;
|
|
5443
|
+
}
|
|
5444
|
+
};
|
|
5445
|
+
const hasBunRuntime = (rootDir, projectFiles) => {
|
|
5446
|
+
if (fs.existsSync(path.join(rootDir, "bun.lock")) || fs.existsSync(path.join(rootDir, "bun.lockb")) || fs.existsSync(path.join(rootDir, "bunfig.toml"))) return true;
|
|
5447
|
+
const hasBunFiles = projectFiles.some((filePath) => /(?:^|\/)bunfig\.toml$|(?:^|\/)bun\.lockb?$/.test(filePath));
|
|
5448
|
+
const pkg = readJson(path.join(rootDir, "package.json"));
|
|
5449
|
+
if (!pkg) return hasBunFiles;
|
|
5450
|
+
if (typeof pkg.packageManager === "string" && /^bun@/i.test(pkg.packageManager)) return true;
|
|
5451
|
+
const scripts = pkg.scripts;
|
|
5452
|
+
if (scripts && typeof scripts === "object") {
|
|
5453
|
+
for (const command of Object.values(scripts)) if (typeof command === "string" && /(?:^|[;&|()\s])bunx?\s/.test(command)) return true;
|
|
5454
|
+
}
|
|
5455
|
+
return hasBunFiles;
|
|
5456
|
+
};
|
|
5457
|
+
const hasDenoRuntime = (rootDir, projectFiles) => {
|
|
5458
|
+
if (fs.existsSync(path.join(rootDir, "deno.json")) || fs.existsSync(path.join(rootDir, "deno.jsonc"))) return true;
|
|
5459
|
+
return projectFiles.some((filePath) => /(?:^|\/)deno\.jsonc?$/.test(filePath));
|
|
5460
|
+
};
|
|
5382
5461
|
const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
5383
5462
|
const collectAmbientGlobals = (rootDir) => {
|
|
5384
5463
|
const globals = /* @__PURE__ */ new Set();
|
|
@@ -5390,7 +5469,8 @@ const collectAmbientGlobals = (rootDir) => {
|
|
|
5390
5469
|
for (const match of content.matchAll(AMBIENT_GLOBAL_RE)) globals.add(match[1]);
|
|
5391
5470
|
}
|
|
5392
5471
|
const deps = collectPackageNames(rootDir);
|
|
5393
|
-
if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
|
|
5472
|
+
if (deps.has("@types/bun") || deps.has("bun-types") || hasBunRuntime(rootDir, projectFiles)) globals.add("Bun");
|
|
5473
|
+
if (hasDenoRuntime(rootDir, projectFiles)) globals.add("Deno");
|
|
5394
5474
|
if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
|
|
5395
5475
|
"$app",
|
|
5396
5476
|
"$config",
|
|
@@ -5488,6 +5568,37 @@ const detectTestFramework = (rootDir) => {
|
|
|
5488
5568
|
return null;
|
|
5489
5569
|
};
|
|
5490
5570
|
const getOxlintTargets = (context) => getSourceFiles(context).filter((filePath) => OXLINT_EXTENSIONS.has(path.extname(filePath).toLowerCase())).filter((filePath) => !isAutoGenerated(filePath)).map((filePath) => path.relative(context.rootDirectory, filePath).split(path.sep).join("/"));
|
|
5571
|
+
const toDiagnostic = (d) => {
|
|
5572
|
+
const { plugin, rule } = parseRuleCode(d.code);
|
|
5573
|
+
const label = d.labels[0];
|
|
5574
|
+
return {
|
|
5575
|
+
filePath: d.filename,
|
|
5576
|
+
engine: "lint",
|
|
5577
|
+
rule: `${plugin}/${rule}`,
|
|
5578
|
+
severity: d.severity,
|
|
5579
|
+
message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
|
|
5580
|
+
help: d.help || "",
|
|
5581
|
+
line: label?.span.line ?? 0,
|
|
5582
|
+
column: label?.span.column ?? 0,
|
|
5583
|
+
category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
|
|
5584
|
+
fixable: false
|
|
5585
|
+
};
|
|
5586
|
+
};
|
|
5587
|
+
const shouldKeepOxlintDiagnostic = (context, ambientSources, seen, d) => {
|
|
5588
|
+
const relativePath = path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath;
|
|
5589
|
+
if (isExcludedFromScan(relativePath)) return false;
|
|
5590
|
+
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
5591
|
+
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
5592
|
+
if (isRuntimeGlobalFalsePositive(d.rule, d.message, context.rootDirectory, relativePath)) return false;
|
|
5593
|
+
if (isSolidRefFalsePositive(context, d)) return false;
|
|
5594
|
+
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
5595
|
+
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
5596
|
+
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
5597
|
+
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
5598
|
+
if (seen.has(key)) return false;
|
|
5599
|
+
seen.add(key);
|
|
5600
|
+
return true;
|
|
5601
|
+
};
|
|
5491
5602
|
const runOxlint = async (context) => {
|
|
5492
5603
|
const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
|
|
5493
5604
|
const framework = context.frameworks.find((f) => f !== "none");
|
|
@@ -5524,34 +5635,7 @@ const runOxlint = async (context) => {
|
|
|
5524
5635
|
return [];
|
|
5525
5636
|
}
|
|
5526
5637
|
const seen = /* @__PURE__ */ new Set();
|
|
5527
|
-
return output.diagnostics.map((d) =>
|
|
5528
|
-
const { plugin, rule } = parseRuleCode(d.code);
|
|
5529
|
-
const label = d.labels[0];
|
|
5530
|
-
return {
|
|
5531
|
-
filePath: d.filename,
|
|
5532
|
-
engine: "lint",
|
|
5533
|
-
rule: `${plugin}/${rule}`,
|
|
5534
|
-
severity: d.severity,
|
|
5535
|
-
message: d.message.replace(/\S+\.\w+:\d+:\d+[\s\S]*$/, "").trim() || d.message,
|
|
5536
|
-
help: d.help || "",
|
|
5537
|
-
line: label?.span.line ?? 0,
|
|
5538
|
-
column: label?.span.column ?? 0,
|
|
5539
|
-
category: plugin === "react" ? "React" : plugin === "import" ? "Imports" : "Lint",
|
|
5540
|
-
fixable: false
|
|
5541
|
-
};
|
|
5542
|
-
}).filter((d) => {
|
|
5543
|
-
if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
|
|
5544
|
-
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
5545
|
-
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
5546
|
-
if (isSolidRefFalsePositive(context, d)) return false;
|
|
5547
|
-
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
5548
|
-
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
5549
|
-
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
5550
|
-
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
5551
|
-
if (seen.has(key)) return false;
|
|
5552
|
-
seen.add(key);
|
|
5553
|
-
return true;
|
|
5554
|
-
});
|
|
5638
|
+
return output.diagnostics.map(toDiagnostic).filter((d) => shouldKeepOxlintDiagnostic(context, ambientSources, seen, d));
|
|
5555
5639
|
} finally {
|
|
5556
5640
|
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
5557
5641
|
}
|
|
@@ -5602,7 +5686,7 @@ const lintEngine = {
|
|
|
5602
5686
|
promises.push(runOxlint(context));
|
|
5603
5687
|
if (context.config.lint.typecheck) promises.push(import("./typecheck-By967nny.js").then((mod) => mod.runTypecheck(context)));
|
|
5604
5688
|
}
|
|
5605
|
-
if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-
|
|
5689
|
+
if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-BM2JR6f6.js").then((mod) => mod.runExpoDoctor(context)));
|
|
5606
5690
|
if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
|
|
5607
5691
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
5608
5692
|
if (languages.includes("rust") && installedTools.cargo) promises.push(runGenericLinter(context, "rust"));
|
|
@@ -5620,7 +5704,7 @@ const lintEngine = {
|
|
|
5620
5704
|
|
|
5621
5705
|
//#endregion
|
|
5622
5706
|
//#region src/ui/invocation.ts
|
|
5623
|
-
const detectInvocation = () => "
|
|
5707
|
+
const detectInvocation = () => "aislop";
|
|
5624
5708
|
|
|
5625
5709
|
//#endregion
|
|
5626
5710
|
//#region src/engines/security/audit.ts
|
|
@@ -5772,7 +5856,7 @@ const parseJsAudit = (output, source) => {
|
|
|
5772
5856
|
rule: "security/dependency-audit-skipped",
|
|
5773
5857
|
severity: "info",
|
|
5774
5858
|
message: `Dependency audit skipped (${source}): lockfile is missing`,
|
|
5775
|
-
help: error.detail ?? "Generate a lockfile, then re-run `
|
|
5859
|
+
help: error.detail ?? "Generate a lockfile, then re-run `aislop scan` for dependency vulnerability checks.",
|
|
5776
5860
|
line: 0,
|
|
5777
5861
|
column: 0,
|
|
5778
5862
|
category: "Security",
|
|
@@ -5889,6 +5973,194 @@ const runCargoAudit = async (rootDir, timeout) => {
|
|
|
5889
5973
|
}
|
|
5890
5974
|
};
|
|
5891
5975
|
|
|
5976
|
+
//#endregion
|
|
5977
|
+
//#region src/engines/security/html-safety.ts
|
|
5978
|
+
const SAFE_EMPTY_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:""|''|``)\s*;?/;
|
|
5979
|
+
const SAFE_SANITIZED_INNER_HTML_RE = /^\.innerHTML\s*=\s*(?:escapeHtml|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)\s*;?(?:\n|$)/;
|
|
5980
|
+
const SANITIZER_EXPR_RE = /^(?:escapeHtml|escapeHTML|sanitizeHtml|sanitizeHTML|DOMPurify\.sanitize)\s*\([^;\n]*\)$/;
|
|
5981
|
+
const IDENT_RE = /^[A-Za-z_$][\w$]*$/;
|
|
5982
|
+
const STATIC_STRING_RE = /^(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\$])*`)$/;
|
|
5983
|
+
const NUMERICISH_EXPR_RE = /^(?:[-+]?\d+(?:\.\d+)?|[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*(?:\s*\|\|\s*[-+]?\d+(?:\.\d+)?)?)$/;
|
|
5984
|
+
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;
|
|
5985
|
+
const SAFE_FORMAT_CALL_RE = /^(?:format[A-Z]\w*|fmt[A-Z]?\w*)\s*\((.*)\)$/;
|
|
5986
|
+
const consumeQuotedLiteral = (content, startIndex, quote) => {
|
|
5987
|
+
let i = startIndex + 1;
|
|
5988
|
+
while (i < content.length) {
|
|
5989
|
+
const char = content[i];
|
|
5990
|
+
if (char === "\\") {
|
|
5991
|
+
i += 2;
|
|
5992
|
+
continue;
|
|
5993
|
+
}
|
|
5994
|
+
if (char === quote) return { endIndex: i };
|
|
5995
|
+
if (char === "\n") return null;
|
|
5996
|
+
i++;
|
|
5997
|
+
}
|
|
5998
|
+
return null;
|
|
5999
|
+
};
|
|
6000
|
+
const consumeTemplateLiteral = (content, startIndex) => {
|
|
6001
|
+
const openIndex = content.indexOf("`", startIndex);
|
|
6002
|
+
if (openIndex === -1) return null;
|
|
6003
|
+
let i = openIndex + 1;
|
|
6004
|
+
while (i < content.length) {
|
|
6005
|
+
const char = content[i];
|
|
6006
|
+
if (char === "\\") {
|
|
6007
|
+
i += 2;
|
|
6008
|
+
continue;
|
|
6009
|
+
}
|
|
6010
|
+
if (char === "`") return {
|
|
6011
|
+
body: content.slice(openIndex + 1, i),
|
|
6012
|
+
endIndex: i
|
|
6013
|
+
};
|
|
6014
|
+
i++;
|
|
6015
|
+
}
|
|
6016
|
+
return null;
|
|
6017
|
+
};
|
|
6018
|
+
const assignmentTailIsClosed = (content, endIndex) => /^\s*(?:;[^\n]*)?(?:\n|$)/.test(content.slice(endIndex + 1));
|
|
6019
|
+
const assignmentRhsStart = (content, matchIndex) => {
|
|
6020
|
+
const match = /^\.innerHTML\s*=\s*/.exec(content.slice(matchIndex));
|
|
6021
|
+
return match ? matchIndex + match[0].length : null;
|
|
6022
|
+
};
|
|
6023
|
+
const templateExpressions = (templateBody) => [...templateBody.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
|
|
6024
|
+
const staticTernaryRe = /^\s*[^?]+\?\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*:\s*(?:"[^"]*"|'[^']*'|`[^`$]*`)\s*$/;
|
|
6025
|
+
const splitTopLevelTernary = (expr) => {
|
|
6026
|
+
let quote = null;
|
|
6027
|
+
let depth = 0;
|
|
6028
|
+
let question = -1;
|
|
6029
|
+
let colon = -1;
|
|
6030
|
+
for (let i = 0; i < expr.length; i++) {
|
|
6031
|
+
const char = expr[i];
|
|
6032
|
+
if (char === "\\") {
|
|
6033
|
+
i++;
|
|
6034
|
+
continue;
|
|
6035
|
+
}
|
|
6036
|
+
if ((char === "'" || char === "\"" || char === "`") && quote === null) {
|
|
6037
|
+
quote = char;
|
|
6038
|
+
continue;
|
|
6039
|
+
}
|
|
6040
|
+
if (char === quote) {
|
|
6041
|
+
quote = null;
|
|
6042
|
+
continue;
|
|
6043
|
+
}
|
|
6044
|
+
if (quote) continue;
|
|
6045
|
+
if (char === "(" || char === "[" || char === "{") depth++;
|
|
6046
|
+
else if (char === ")" || char === "]" || char === "}") depth = Math.max(0, depth - 1);
|
|
6047
|
+
else if (char === "?" && depth === 0 && question === -1) question = i;
|
|
6048
|
+
else if (char === ":" && depth === 0 && question !== -1) {
|
|
6049
|
+
colon = i;
|
|
6050
|
+
break;
|
|
6051
|
+
}
|
|
6052
|
+
}
|
|
6053
|
+
if (question === -1 || colon === -1) return null;
|
|
6054
|
+
return {
|
|
6055
|
+
whenTrue: expr.slice(question + 1, colon).trim(),
|
|
6056
|
+
whenFalse: expr.slice(colon + 1).trim()
|
|
6057
|
+
};
|
|
6058
|
+
};
|
|
6059
|
+
const isNumericishExpression = (expr) => {
|
|
6060
|
+
const normalized = expr.trim();
|
|
6061
|
+
if (/^(?:Math\.\w+|Number|parseInt|parseFloat)\s*\(/.test(normalized)) return true;
|
|
6062
|
+
if (!NUMERICISH_EXPR_RE.test(normalized)) return false;
|
|
6063
|
+
return /\d/.test(normalized) || NUMERICISH_NAME_RE.test(normalized);
|
|
6064
|
+
};
|
|
6065
|
+
const isSafeTemplateLiteralExpression = (expr, safeNames) => {
|
|
6066
|
+
if (!expr.startsWith("`") || !expr.endsWith("`")) return false;
|
|
6067
|
+
return templateExpressions(expr.slice(1, -1)).every((part) => isSafeHtmlExpression(part, safeNames));
|
|
6068
|
+
};
|
|
6069
|
+
const collectSafeHtmlNames = (content, matchIndex) => {
|
|
6070
|
+
const safeNames = /* @__PURE__ */ new Set();
|
|
6071
|
+
const prefix = content.slice(Math.max(0, matchIndex - 8e3), matchIndex);
|
|
6072
|
+
for (const rawLine of prefix.split("\n")) {
|
|
6073
|
+
const line = rawLine.trim();
|
|
6074
|
+
let match = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
|
|
6075
|
+
if (match) {
|
|
6076
|
+
const [, name, expr] = match;
|
|
6077
|
+
if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
6078
|
+
else safeNames.delete(name);
|
|
6079
|
+
continue;
|
|
6080
|
+
}
|
|
6081
|
+
match = /^([A-Za-z_$][\w$]*)\s*\+=\s*(.+?)\s*;?$/.exec(line);
|
|
6082
|
+
if (match) {
|
|
6083
|
+
const [, name, expr] = match;
|
|
6084
|
+
if (safeNames.has(name) && isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
6085
|
+
else safeNames.delete(name);
|
|
6086
|
+
continue;
|
|
6087
|
+
}
|
|
6088
|
+
match = /^([A-Za-z_$][\w$]*)\s*=\s*(.+?)\s*;?$/.exec(line);
|
|
6089
|
+
if (match) {
|
|
6090
|
+
const [, name, expr] = match;
|
|
6091
|
+
if (isSafeHtmlExpression(expr.trim(), safeNames)) safeNames.add(name);
|
|
6092
|
+
else safeNames.delete(name);
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
return safeNames;
|
|
6096
|
+
};
|
|
6097
|
+
const isSafeHtmlExpression = (expr, safeNames) => {
|
|
6098
|
+
const normalized = expr.trim();
|
|
6099
|
+
if (SANITIZER_EXPR_RE.test(normalized)) return true;
|
|
6100
|
+
if (STATIC_STRING_RE.test(normalized)) return true;
|
|
6101
|
+
if (staticTernaryRe.test(expr)) return true;
|
|
6102
|
+
if (isNumericishExpression(normalized)) return true;
|
|
6103
|
+
if (IDENT_RE.test(normalized) && safeNames.has(normalized)) return true;
|
|
6104
|
+
if (isSafeTemplateLiteralExpression(normalized, safeNames)) return true;
|
|
6105
|
+
const ternary = splitTopLevelTernary(normalized);
|
|
6106
|
+
if (ternary && isSafeHtmlExpression(ternary.whenTrue, safeNames) && isSafeHtmlExpression(ternary.whenFalse, safeNames)) return true;
|
|
6107
|
+
const formatCall = SAFE_FORMAT_CALL_RE.exec(normalized);
|
|
6108
|
+
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));
|
|
6109
|
+
return false;
|
|
6110
|
+
};
|
|
6111
|
+
const readSingleLineRhs = (content, rhsStart) => {
|
|
6112
|
+
const lineEnd = content.indexOf("\n", rhsStart);
|
|
6113
|
+
const line = content.slice(rhsStart, lineEnd === -1 ? content.length : lineEnd);
|
|
6114
|
+
let quote = null;
|
|
6115
|
+
for (let i = 0; i < line.length; i++) {
|
|
6116
|
+
const char = line[i];
|
|
6117
|
+
if (char === "\\") {
|
|
6118
|
+
i++;
|
|
6119
|
+
continue;
|
|
6120
|
+
}
|
|
6121
|
+
if ((char === "'" || char === "\"" || char === "`") && quote === null) {
|
|
6122
|
+
quote = char;
|
|
6123
|
+
continue;
|
|
6124
|
+
}
|
|
6125
|
+
if (char === quote) {
|
|
6126
|
+
quote = null;
|
|
6127
|
+
continue;
|
|
6128
|
+
}
|
|
6129
|
+
if (char === ";" && quote === null) return line.slice(0, i).trim();
|
|
6130
|
+
}
|
|
6131
|
+
return line.trim();
|
|
6132
|
+
};
|
|
6133
|
+
const isSafeMapJoinHtmlAssignment = (content, rhsStart) => {
|
|
6134
|
+
const head = content.slice(rhsStart);
|
|
6135
|
+
const mapMatch = /^[A-Za-z_$][\w$.]*\.map\(\s*[A-Za-z_$][\w$]*\s*=>\s*`/.exec(head);
|
|
6136
|
+
if (!mapMatch) return false;
|
|
6137
|
+
const template = consumeTemplateLiteral(content, rhsStart + mapMatch[0].length - 1);
|
|
6138
|
+
if (!template) return false;
|
|
6139
|
+
if (!/^\s*\)\.join\(\s*(?:""|'')\s*\)/.test(content.slice(template.endIndex + 1))) return false;
|
|
6140
|
+
const safeNames = collectSafeHtmlNames(content, rhsStart);
|
|
6141
|
+
return templateExpressions(template.body).every((expr) => isSafeHtmlExpression(expr, safeNames));
|
|
6142
|
+
};
|
|
6143
|
+
const isSafeInnerHtmlAssignment = (content, matchIndex) => {
|
|
6144
|
+
const tail = content.slice(matchIndex);
|
|
6145
|
+
if (SAFE_EMPTY_INNER_HTML_RE.test(tail) || SAFE_SANITIZED_INNER_HTML_RE.test(tail)) return true;
|
|
6146
|
+
const rhsStart = assignmentRhsStart(content, matchIndex);
|
|
6147
|
+
if (rhsStart === null) return false;
|
|
6148
|
+
const first = content[rhsStart];
|
|
6149
|
+
const safeNames = collectSafeHtmlNames(content, matchIndex);
|
|
6150
|
+
if (isSafeHtmlExpression(readSingleLineRhs(content, rhsStart), safeNames)) return true;
|
|
6151
|
+
if (isSafeMapJoinHtmlAssignment(content, rhsStart)) return true;
|
|
6152
|
+
if (first === "'" || first === "\"") {
|
|
6153
|
+
const quoted = consumeQuotedLiteral(content, rhsStart, first);
|
|
6154
|
+
return Boolean(quoted && assignmentTailIsClosed(content, quoted.endIndex));
|
|
6155
|
+
}
|
|
6156
|
+
if (first !== "`") return false;
|
|
6157
|
+
const template = consumeTemplateLiteral(content, rhsStart);
|
|
6158
|
+
if (!template || !assignmentTailIsClosed(content, template.endIndex)) return false;
|
|
6159
|
+
const expressions = templateExpressions(template.body);
|
|
6160
|
+
if (expressions.length === 0) return true;
|
|
6161
|
+
return expressions.every((expr) => isSafeHtmlExpression(expr, safeNames));
|
|
6162
|
+
};
|
|
6163
|
+
|
|
5892
6164
|
//#endregion
|
|
5893
6165
|
//#region src/engines/security/risky.ts
|
|
5894
6166
|
const ev = "eval";
|
|
@@ -6014,6 +6286,30 @@ const isStructuredDataScript = (content, matchIndex) => {
|
|
|
6014
6286
|
const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
|
|
6015
6287
|
return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
|
|
6016
6288
|
};
|
|
6289
|
+
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));
|
|
6290
|
+
const PLACEHOLDER_EXPR_RE = /^(?:placeholders?|placeholderList|bindMarkers?|bindingMarkers?|bindPlaceholders?|bindingPlaceholders?|parameterPlaceholders?|sqlPlaceholders?)(?:\.\w+\([^)]*\))?$/i;
|
|
6291
|
+
const SQL_PLACEHOLDER_LITERAL_RE = /["'](?:\?|\$\d+|\$\{[^}]+\})["']/;
|
|
6292
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6293
|
+
const isGeneratedPlaceholderList = (content, matchIndex, placeholderExpr) => {
|
|
6294
|
+
const name = placeholderExpr.match(/^([A-Za-z_$][\w$]*)/)?.[1];
|
|
6295
|
+
if (!name) return false;
|
|
6296
|
+
const prefix = content.slice(Math.max(0, matchIndex - 4e3), matchIndex);
|
|
6297
|
+
const declarationRe = new RegExp(`\\b(?:const|let|var)\\s+${escapeRegExp(name)}\\s*=\\s*([^;\\n]+)`, "g");
|
|
6298
|
+
const declaration = [...prefix.matchAll(declarationRe)].at(-1);
|
|
6299
|
+
if (!declaration) return false;
|
|
6300
|
+
const expr = declaration[1];
|
|
6301
|
+
if (!/\.join\s*\(/.test(expr)) return false;
|
|
6302
|
+
return /\.map\s*\(/.test(expr) && /=>/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr) || /\.fill\s*\(/.test(expr) && SQL_PLACEHOLDER_LITERAL_RE.test(expr);
|
|
6303
|
+
};
|
|
6304
|
+
const isSafeSqlPlaceholderTemplate = (content, matchIndex) => {
|
|
6305
|
+
const template = consumeTemplateLiteral(content, matchIndex);
|
|
6306
|
+
if (!template) return false;
|
|
6307
|
+
const afterTemplate = content.slice(template.endIndex + 1);
|
|
6308
|
+
if (!(/^\s*,/.test(afterTemplate) || /^\s*\)\s*\.(?:all|get|run|values)\s*\(/.test(afterTemplate))) return false;
|
|
6309
|
+
const expressions = [...template.body.matchAll(/\$\{\s*([^}]+?)\s*\}/g)].map((match) => match[1].trim());
|
|
6310
|
+
if (expressions.length === 0) return false;
|
|
6311
|
+
return expressions.every((expr) => PLACEHOLDER_EXPR_RE.test(expr) && isGeneratedPlaceholderList(content, matchIndex, expr));
|
|
6312
|
+
};
|
|
6017
6313
|
const detectRiskyConstructs = async (context) => {
|
|
6018
6314
|
const files = getSourceFiles(context);
|
|
6019
6315
|
const diagnostics = [];
|
|
@@ -6038,8 +6334,11 @@ const detectRiskyConstructs = async (context) => {
|
|
|
6038
6334
|
const line = content.slice(0, match.index).split("\n").length;
|
|
6039
6335
|
if (name === "innerhtml") {
|
|
6040
6336
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
6337
|
+
if (isSafeInnerHtmlAssignment(content, match.index)) continue;
|
|
6041
6338
|
if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
|
|
6042
6339
|
}
|
|
6340
|
+
if (name === "sql-injection" && isSafeSqlPlaceholderTemplate(content, match.index)) continue;
|
|
6341
|
+
if (name === "shell-injection" && isSafeShellSpawnArray(content, match.index)) continue;
|
|
6043
6342
|
if (name === "dangerously-set-innerhtml") {
|
|
6044
6343
|
if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
|
|
6045
6344
|
if (isStructuredDataScript(content, match.index)) continue;
|
|
@@ -6136,7 +6435,28 @@ const PLACEHOLDER_EXACT = new Set([
|
|
|
6136
6435
|
"todo",
|
|
6137
6436
|
"replace_me"
|
|
6138
6437
|
]);
|
|
6438
|
+
const PLACEHOLDER_URL_PARTS = new Set([
|
|
6439
|
+
"example",
|
|
6440
|
+
"host",
|
|
6441
|
+
"localhost",
|
|
6442
|
+
"pass",
|
|
6443
|
+
"password",
|
|
6444
|
+
"pw",
|
|
6445
|
+
"user",
|
|
6446
|
+
"username"
|
|
6447
|
+
]);
|
|
6448
|
+
const isPlaceholderCredentialUrl = (matchedText) => {
|
|
6449
|
+
const credentialMatch = matchedText.match(/^[a-z]+:\/\/([^:@/\s]+):([^@/\s]+)@/i);
|
|
6450
|
+
if (credentialMatch) return PLACEHOLDER_URL_PARTS.has(credentialMatch[1].toLowerCase()) && PLACEHOLDER_URL_PARTS.has(credentialMatch[2].toLowerCase());
|
|
6451
|
+
try {
|
|
6452
|
+
const parsed = new URL(matchedText);
|
|
6453
|
+
return PLACEHOLDER_URL_PARTS.has(parsed.username.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.password.toLowerCase()) && PLACEHOLDER_URL_PARTS.has(parsed.hostname.toLowerCase());
|
|
6454
|
+
} catch {
|
|
6455
|
+
return false;
|
|
6456
|
+
}
|
|
6457
|
+
};
|
|
6139
6458
|
const isPlaceholderValue = (matchedText) => {
|
|
6459
|
+
if (isPlaceholderCredentialUrl(matchedText)) return true;
|
|
6140
6460
|
if (/env\(/i.test(matchedText)) return true;
|
|
6141
6461
|
if (matchedText.includes("process.env")) return true;
|
|
6142
6462
|
if (matchedText.includes("os.environ")) return true;
|
|
@@ -6257,23 +6577,36 @@ const STYLE_RULES = new Set([
|
|
|
6257
6577
|
"complexity/function-too-long"
|
|
6258
6578
|
]);
|
|
6259
6579
|
const STYLE_WEIGHT = .5;
|
|
6580
|
+
const COMMENT_STYLE_RULE_CAP = 12;
|
|
6581
|
+
const COMMENT_STYLE_RULES = new Set(["ai-slop/trivial-comment", "ai-slop/narrative-comment"]);
|
|
6260
6582
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
6261
6583
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
6262
6584
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
6263
6585
|
return Math.max(1, filesWithDiagnostics);
|
|
6264
6586
|
};
|
|
6265
|
-
const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing) => {
|
|
6587
|
+
const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoothing, maxPerRule) => {
|
|
6266
6588
|
if (diagnostics.length === 0) return {
|
|
6267
6589
|
score: PERFECT_SCORE,
|
|
6268
6590
|
label: "Healthy"
|
|
6269
6591
|
};
|
|
6270
|
-
|
|
6592
|
+
const deductionsByRule = /* @__PURE__ */ new Map();
|
|
6271
6593
|
for (const d of diagnostics) {
|
|
6272
6594
|
const engineWeight = weights[d.engine] ?? 1;
|
|
6273
6595
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
6274
6596
|
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
6275
|
-
|
|
6276
|
-
|
|
6597
|
+
const key = `${d.engine}:${d.rule}`;
|
|
6598
|
+
deductionsByRule.set(key, (deductionsByRule.get(key) ?? 0) + severityPenalty * engineWeight * styleFactor);
|
|
6599
|
+
}
|
|
6600
|
+
const defaultRuleCap = typeof maxPerRule === "number" && maxPerRule > 0 ? maxPerRule : null;
|
|
6601
|
+
const capForRule = (key) => {
|
|
6602
|
+
const rule = key.slice(key.indexOf(":") + 1);
|
|
6603
|
+
if (COMMENT_STYLE_RULES.has(rule)) return defaultRuleCap ? Math.min(defaultRuleCap, COMMENT_STYLE_RULE_CAP) : COMMENT_STYLE_RULE_CAP;
|
|
6604
|
+
return defaultRuleCap;
|
|
6605
|
+
};
|
|
6606
|
+
const deductions = [...deductionsByRule.entries()].reduce((total, [key, value]) => {
|
|
6607
|
+
const cap = capForRule(key);
|
|
6608
|
+
return total + (cap ? Math.min(value, cap) : value);
|
|
6609
|
+
}, 0);
|
|
6277
6610
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
6278
6611
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
6279
6612
|
const issueDensity = Math.min(1, diagnostics.length / (effectiveFileCount + smoothingConstant));
|
|
@@ -6516,6 +6849,107 @@ const readBaseline = (cwd) => {
|
|
|
6516
6849
|
}
|
|
6517
6850
|
};
|
|
6518
6851
|
|
|
6852
|
+
//#endregion
|
|
6853
|
+
//#region src/output/finding-assessment.ts
|
|
6854
|
+
const FINDING_KIND_LABELS = {
|
|
6855
|
+
"confirmed-defect": "confirmed defects",
|
|
6856
|
+
"conservative-security": "conservative security",
|
|
6857
|
+
"style-policy": "style/policy",
|
|
6858
|
+
"ai-slop-indicator": "AI-slop indicators"
|
|
6859
|
+
};
|
|
6860
|
+
const STYLE_POLICY_RULES = new Set([
|
|
6861
|
+
"ai-slop/trivial-comment",
|
|
6862
|
+
"ai-slop/narrative-comment",
|
|
6863
|
+
"ai-slop/meta-comment",
|
|
6864
|
+
"ai-slop/console-leftover",
|
|
6865
|
+
"ai-slop/ts-directive",
|
|
6866
|
+
"complexity/file-too-large",
|
|
6867
|
+
"complexity/function-too-long",
|
|
6868
|
+
"complexity/deep-nesting",
|
|
6869
|
+
"complexity/too-many-params",
|
|
6870
|
+
"code-quality/duplicate-block",
|
|
6871
|
+
"eslint/no-empty",
|
|
6872
|
+
"eslint/no-unused-vars",
|
|
6873
|
+
"eslint/no-useless-escape",
|
|
6874
|
+
"eslint/no-unused-expressions",
|
|
6875
|
+
"unicorn/no-useless-fallback-in-spread",
|
|
6876
|
+
"unicorn/prefer-string-starts-ends-with",
|
|
6877
|
+
"unicorn/no-new-array",
|
|
6878
|
+
"unicorn/no-useless-spread"
|
|
6879
|
+
]);
|
|
6880
|
+
const CONFIRMED_DEFECT_RULES = new Set([
|
|
6881
|
+
"ai-slop/hallucinated-import",
|
|
6882
|
+
"eslint/no-undef",
|
|
6883
|
+
"eslint/no-unreachable",
|
|
6884
|
+
"security/vulnerable-dependency"
|
|
6885
|
+
]);
|
|
6886
|
+
const LOW_CONFIDENCE_SECURITY_RULES = new Set(["security/innerhtml", "security/dangerously-set-innerhtml"]);
|
|
6887
|
+
const confidenceFor = (diagnostic, kind) => {
|
|
6888
|
+
if (kind === "confirmed-defect") return "high";
|
|
6889
|
+
if (kind === "style-policy") return "medium";
|
|
6890
|
+
if (kind === "conservative-security") {
|
|
6891
|
+
if (LOW_CONFIDENCE_SECURITY_RULES.has(diagnostic.rule)) return "medium";
|
|
6892
|
+
return diagnostic.severity === "error" ? "high" : "medium";
|
|
6893
|
+
}
|
|
6894
|
+
return diagnostic.severity === "error" ? "high" : "medium";
|
|
6895
|
+
};
|
|
6896
|
+
const classifyKind = (diagnostic) => {
|
|
6897
|
+
if (CONFIRMED_DEFECT_RULES.has(diagnostic.rule)) return "confirmed-defect";
|
|
6898
|
+
if (diagnostic.engine === "security") return "conservative-security";
|
|
6899
|
+
if (STYLE_POLICY_RULES.has(diagnostic.rule)) return "style-policy";
|
|
6900
|
+
if (diagnostic.engine === "format" || diagnostic.engine === "code-quality") return "style-policy";
|
|
6901
|
+
if (diagnostic.engine === "ai-slop") return "ai-slop-indicator";
|
|
6902
|
+
if (diagnostic.severity === "error") return "confirmed-defect";
|
|
6903
|
+
return "style-policy";
|
|
6904
|
+
};
|
|
6905
|
+
const assessDiagnostic = (diagnostic) => {
|
|
6906
|
+
const kind = classifyKind(diagnostic);
|
|
6907
|
+
return {
|
|
6908
|
+
kind,
|
|
6909
|
+
confidence: confidenceFor(diagnostic, kind),
|
|
6910
|
+
label: FINDING_KIND_LABELS[kind]
|
|
6911
|
+
};
|
|
6912
|
+
};
|
|
6913
|
+
const summarizeFindingAssessments = (diagnostics) => {
|
|
6914
|
+
const byKind = {
|
|
6915
|
+
"confirmed-defect": 0,
|
|
6916
|
+
"conservative-security": 0,
|
|
6917
|
+
"style-policy": 0,
|
|
6918
|
+
"ai-slop-indicator": 0
|
|
6919
|
+
};
|
|
6920
|
+
const byConfidence = {
|
|
6921
|
+
high: 0,
|
|
6922
|
+
medium: 0,
|
|
6923
|
+
low: 0
|
|
6924
|
+
};
|
|
6925
|
+
const rows = /* @__PURE__ */ new Map();
|
|
6926
|
+
for (const diagnostic of diagnostics) {
|
|
6927
|
+
const assessment = assessDiagnostic(diagnostic);
|
|
6928
|
+
byKind[assessment.kind]++;
|
|
6929
|
+
byConfidence[assessment.confidence]++;
|
|
6930
|
+
const row = rows.get(assessment.kind) ?? {
|
|
6931
|
+
kind: assessment.kind,
|
|
6932
|
+
label: assessment.label,
|
|
6933
|
+
count: 0,
|
|
6934
|
+
errors: 0,
|
|
6935
|
+
warnings: 0,
|
|
6936
|
+
info: 0,
|
|
6937
|
+
fixable: 0
|
|
6938
|
+
};
|
|
6939
|
+
row.count++;
|
|
6940
|
+
if (diagnostic.severity === "error") row.errors++;
|
|
6941
|
+
else if (diagnostic.severity === "warning") row.warnings++;
|
|
6942
|
+
else row.info++;
|
|
6943
|
+
if (diagnostic.fixable) row.fixable++;
|
|
6944
|
+
rows.set(assessment.kind, row);
|
|
6945
|
+
}
|
|
6946
|
+
return {
|
|
6947
|
+
rows: [...rows.values()].sort((a, b) => b.count - a.count),
|
|
6948
|
+
byKind,
|
|
6949
|
+
byConfidence
|
|
6950
|
+
};
|
|
6951
|
+
};
|
|
6952
|
+
|
|
6519
6953
|
//#endregion
|
|
6520
6954
|
//#region src/mcp/tools.ts
|
|
6521
6955
|
const MAX_FINDINGS = 25;
|
|
@@ -6553,27 +6987,32 @@ const summariseDiagnostic = (d, rootDirectory) => ({
|
|
|
6553
6987
|
column: d.column,
|
|
6554
6988
|
rule: d.rule,
|
|
6555
6989
|
severity: d.severity,
|
|
6990
|
+
assessment: assessDiagnostic(d),
|
|
6556
6991
|
message: d.message,
|
|
6557
6992
|
fixable: d.fixable,
|
|
6558
6993
|
help: d.help || void 0
|
|
6559
6994
|
});
|
|
6560
6995
|
const summariseDiagnostics = (diagnostics, rootDirectory) => {
|
|
6996
|
+
const counts = {
|
|
6997
|
+
error: diagnostics.filter((d) => d.severity === "error").length,
|
|
6998
|
+
warning: diagnostics.filter((d) => d.severity === "warning").length,
|
|
6999
|
+
fixable: diagnostics.filter((d) => d.fixable).length,
|
|
7000
|
+
total: diagnostics.length
|
|
7001
|
+
};
|
|
7002
|
+
const findings = diagnostics.slice(0, MAX_FINDINGS).map((d) => summariseDiagnostic(d, rootDirectory));
|
|
7003
|
+
const elided = diagnostics.length > MAX_FINDINGS ? diagnostics.length - MAX_FINDINGS : 0;
|
|
6561
7004
|
return {
|
|
6562
|
-
counts
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
total: diagnostics.length
|
|
6567
|
-
},
|
|
6568
|
-
findings: diagnostics.slice(0, MAX_FINDINGS).map((d) => summariseDiagnostic(d, rootDirectory)),
|
|
6569
|
-
elided: diagnostics.length > MAX_FINDINGS ? diagnostics.length - MAX_FINDINGS : 0
|
|
7005
|
+
counts,
|
|
7006
|
+
findingAssessment: summarizeFindingAssessments(diagnostics),
|
|
7007
|
+
findings,
|
|
7008
|
+
elided
|
|
6570
7009
|
};
|
|
6571
7010
|
};
|
|
6572
7011
|
const runScan = async (cwd) => {
|
|
6573
7012
|
const project = await discoverProject(cwd);
|
|
6574
7013
|
const config = loadConfig(cwd);
|
|
6575
7014
|
const diagnostics = (await runEngines(buildEngineContext(project.rootDirectory, project, config), enabledEnginesFromConfig(config))).flatMap((r) => r.diagnostics);
|
|
6576
|
-
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
|
|
7015
|
+
const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
|
|
6577
7016
|
const errorCount = diagnostics.filter((d) => d.severity === "error").length;
|
|
6578
7017
|
const failBelow = config.ci.failBelow;
|
|
6579
7018
|
return {
|
|
@@ -6697,6 +7136,10 @@ const handleAislopBaseline = (input) => {
|
|
|
6697
7136
|
};
|
|
6698
7137
|
};
|
|
6699
7138
|
|
|
7139
|
+
//#endregion
|
|
7140
|
+
//#region src/version.ts
|
|
7141
|
+
const APP_VERSION = "0.11.0";
|
|
7142
|
+
|
|
6700
7143
|
//#endregion
|
|
6701
7144
|
//#region src/telemetry/env.ts
|
|
6702
7145
|
const detectPackageManager = (env = process.env) => {
|