aislop 0.10.0 → 0.10.2
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 +56 -8
- package/dist/cli.js +1145 -631
- package/dist/{expo-doctor-BcIkOte5.js → expo-doctor-c-jE6pR2.js} +1 -1
- package/dist/{generic-D_T4cUaC.js → generic-BsQa13CS.js} +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +3094 -2664
- package/dist/{json-DaFOYHcf.js → json-B01i-GOz.js} +7 -5
- package/dist/{json-OIzja7OM.js → json-CXV4D0Ib.js} +5 -3
- package/dist/mcp.js +656 -470
- package/dist/{sarif-BtSQ92c6.js → sarif-cy5SiDDq.js} +1 -1
- package/dist/{typecheck-DQSzG8fX.js → typecheck-BdQ7uFyK.js} +1 -1
- package/dist/version-BfJVwhN2.js +5 -0
- package/package.json +8 -11
- package/dist/version-DYg_ShBx.js +0 -5
- /package/dist/{engine-info-DCvIfZ0f.js → engine-info-Cpt36DqZ.js} +0 -0
- /package/dist/{subprocess-CQUJDGgn.js → subprocess-0uXz8HdE.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -34,7 +34,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/version.ts
|
|
37
|
-
const APP_VERSION = "0.10.
|
|
37
|
+
const APP_VERSION = "0.10.2";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/telemetry/env.ts
|
|
@@ -183,7 +183,7 @@ const redactProperties = (props) => {
|
|
|
183
183
|
const POSTHOG_HOST = process.env.AISLOP_POSTHOG_HOST ?? "https://eu.i.posthog.com";
|
|
184
184
|
const POSTHOG_KEY = process.env.AISLOP_POSTHOG_KEY ?? "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
185
185
|
const SCHEMA_VERSION = "v2";
|
|
186
|
-
const REQUEST_TIMEOUT_MS = 3e3;
|
|
186
|
+
const REQUEST_TIMEOUT_MS$1 = 3e3;
|
|
187
187
|
const isTelemetryDisabled = (config) => {
|
|
188
188
|
const env = process.env;
|
|
189
189
|
if (env.AISLOP_NO_TELEMETRY === "1" || env.DO_NOT_TRACK === "1") return true;
|
|
@@ -237,16 +237,21 @@ const track = (input) => {
|
|
|
237
237
|
method: "POST",
|
|
238
238
|
headers: { "Content-Type": "application/json" },
|
|
239
239
|
body: JSON.stringify(payload),
|
|
240
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
240
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS$1)
|
|
241
241
|
}).then(() => {}).catch(() => {}).finally(() => {
|
|
242
242
|
pendingRequests.delete(request);
|
|
243
243
|
});
|
|
244
244
|
pendingRequests.add(request);
|
|
245
245
|
return { installCreated };
|
|
246
246
|
};
|
|
247
|
-
const flushTelemetry = async () => {
|
|
247
|
+
const flushTelemetry = async (timeoutMs) => {
|
|
248
248
|
if (pendingRequests.size === 0) return;
|
|
249
|
-
|
|
249
|
+
const all = Promise.all(pendingRequests);
|
|
250
|
+
if (timeoutMs == null) {
|
|
251
|
+
await all;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
await Promise.race([all, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
|
|
250
255
|
};
|
|
251
256
|
|
|
252
257
|
//#endregion
|
|
@@ -361,7 +366,7 @@ const withCommandLifecycle = async (start, run) => {
|
|
|
361
366
|
startProps,
|
|
362
367
|
exitCode: result.exitCode,
|
|
363
368
|
durationMs,
|
|
364
|
-
score: result.score,
|
|
369
|
+
score: result.score ?? void 0,
|
|
365
370
|
findingCount: result.findingCount,
|
|
366
371
|
errorCount: result.errorCount,
|
|
367
372
|
warningCount: result.warningCount,
|
|
@@ -619,12 +624,9 @@ jobs:
|
|
|
619
624
|
runs-on: ubuntu-latest
|
|
620
625
|
steps:
|
|
621
626
|
- uses: actions/checkout@v4
|
|
622
|
-
- uses:
|
|
627
|
+
- uses: scanaislop/aislop@v${APP_VERSION}
|
|
623
628
|
with:
|
|
624
|
-
|
|
625
|
-
# Quality gate: exits 1 when score < ci.failBelow in .aislop/config.yml
|
|
626
|
-
# or when any error-severity diagnostic is present.
|
|
627
|
-
- run: npx aislop@latest ci .
|
|
629
|
+
version: ${APP_VERSION}
|
|
628
630
|
`;
|
|
629
631
|
const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
|
|
630
632
|
# Uncomment and customize to enforce your project's conventions.
|
|
@@ -1012,6 +1014,15 @@ const listProjectFiles = (rootDirectory) => {
|
|
|
1012
1014
|
if (findResult.error || findResult.status !== 0) return [];
|
|
1013
1015
|
return findResult.stdout.split("\n").filter((file) => file.length > 0).map((file) => file.replace(/^\.\//, ""));
|
|
1014
1016
|
};
|
|
1017
|
+
const readAislopIgnorePatterns = (rootDirectory) => {
|
|
1018
|
+
const ignorePath = path.join(rootDirectory, ".aislopignore");
|
|
1019
|
+
if (!fs.existsSync(ignorePath)) return [];
|
|
1020
|
+
try {
|
|
1021
|
+
return fs.readFileSync(ignorePath, "utf-8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
1022
|
+
} catch {
|
|
1023
|
+
return [];
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1015
1026
|
const normalizeExcludePatterns = (patterns) => {
|
|
1016
1027
|
return patterns.flatMap((pattern) => {
|
|
1017
1028
|
const p = pattern.trim();
|
|
@@ -1085,7 +1096,7 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
|
1085
1096
|
};
|
|
1086
1097
|
|
|
1087
1098
|
//#endregion
|
|
1088
|
-
//#region src/
|
|
1099
|
+
//#region src/utils/source-masker.ts
|
|
1089
1100
|
const JS_EXTS$2 = new Set([
|
|
1090
1101
|
".ts",
|
|
1091
1102
|
".tsx",
|
|
@@ -1094,14 +1105,226 @@ const JS_EXTS$2 = new Set([
|
|
|
1094
1105
|
".mjs",
|
|
1095
1106
|
".cjs"
|
|
1096
1107
|
]);
|
|
1108
|
+
const PY_EXTS = new Set([".py"]);
|
|
1109
|
+
const RB_EXTS = new Set([".rb"]);
|
|
1110
|
+
const PHP_EXTS = new Set([".php"]);
|
|
1111
|
+
const familyForExt = (ext) => {
|
|
1112
|
+
if (JS_EXTS$2.has(ext)) return "js";
|
|
1113
|
+
if (PY_EXTS.has(ext)) return "py";
|
|
1114
|
+
if (RB_EXTS.has(ext)) return "rb";
|
|
1115
|
+
if (PHP_EXTS.has(ext)) return "php";
|
|
1116
|
+
return "none";
|
|
1117
|
+
};
|
|
1118
|
+
const maskStringsAndComments = (content, ext) => {
|
|
1119
|
+
const family = familyForExt(ext);
|
|
1120
|
+
if (family === "none") return content;
|
|
1121
|
+
if (family === "js") return maskJs(content, true);
|
|
1122
|
+
return maskSimple(content, family, true);
|
|
1123
|
+
};
|
|
1124
|
+
const maskComments = (content, ext) => {
|
|
1125
|
+
const family = familyForExt(ext);
|
|
1126
|
+
if (family === "none") return content;
|
|
1127
|
+
if (family === "js") return maskJs(content, false);
|
|
1128
|
+
return maskSimple(content, family, false);
|
|
1129
|
+
};
|
|
1130
|
+
const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
|
|
1131
|
+
const len = content.length;
|
|
1132
|
+
const c = content[i];
|
|
1133
|
+
const next = content[i + 1];
|
|
1134
|
+
if (c === "\"" || c === "'") {
|
|
1135
|
+
const strStart = i;
|
|
1136
|
+
const end = consumeQuotedString(content, i, c);
|
|
1137
|
+
if (maskStrings) mask(strStart + 1, end - 1);
|
|
1138
|
+
return {
|
|
1139
|
+
handled: true,
|
|
1140
|
+
nextI: end
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
if (c === "`") {
|
|
1144
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
1145
|
+
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
1146
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
1147
|
+
return {
|
|
1148
|
+
handled: true,
|
|
1149
|
+
nextI: scan.resumeAt
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
if (c === "/" && next === "/") {
|
|
1153
|
+
const strStart = i;
|
|
1154
|
+
let k = i;
|
|
1155
|
+
while (k < len && content[k] !== "\n") k++;
|
|
1156
|
+
mask(strStart, k);
|
|
1157
|
+
return {
|
|
1158
|
+
handled: true,
|
|
1159
|
+
nextI: k
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
if (c === "/" && next === "*") {
|
|
1163
|
+
const strStart = i;
|
|
1164
|
+
let k = i + 2;
|
|
1165
|
+
while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
|
|
1166
|
+
if (k < len - 1) k += 2;
|
|
1167
|
+
mask(strStart, k);
|
|
1168
|
+
return {
|
|
1169
|
+
handled: true,
|
|
1170
|
+
nextI: k
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
return {
|
|
1174
|
+
handled: false,
|
|
1175
|
+
nextI: i
|
|
1176
|
+
};
|
|
1177
|
+
};
|
|
1178
|
+
const maskJs = (content, maskStrings) => {
|
|
1179
|
+
const out = content.split("");
|
|
1180
|
+
const len = content.length;
|
|
1181
|
+
const tplStack = [];
|
|
1182
|
+
let i = 0;
|
|
1183
|
+
const mask = (start, end) => {
|
|
1184
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
1185
|
+
};
|
|
1186
|
+
while (i < len) {
|
|
1187
|
+
const c = content[i];
|
|
1188
|
+
if (tplStack.length > 0) {
|
|
1189
|
+
if (c === "{") {
|
|
1190
|
+
tplStack[tplStack.length - 1]++;
|
|
1191
|
+
i++;
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
if (c === "}") {
|
|
1195
|
+
if (tplStack[tplStack.length - 1] === 0) {
|
|
1196
|
+
tplStack.pop();
|
|
1197
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
1198
|
+
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
1199
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
1200
|
+
i = scan.resumeAt;
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
tplStack[tplStack.length - 1]--;
|
|
1204
|
+
i++;
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
|
|
1209
|
+
if (handled.handled) {
|
|
1210
|
+
i = handled.nextI;
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
i++;
|
|
1214
|
+
}
|
|
1215
|
+
return out.join("");
|
|
1216
|
+
};
|
|
1217
|
+
const consumeQuotedString = (content, start, quote) => {
|
|
1218
|
+
const len = content.length;
|
|
1219
|
+
let i = start + 1;
|
|
1220
|
+
while (i < len) {
|
|
1221
|
+
const c = content[i];
|
|
1222
|
+
if (c === "\\" && i + 1 < len) {
|
|
1223
|
+
i += 2;
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
if (c === quote) return i + 1;
|
|
1227
|
+
if (c === "\n") return i;
|
|
1228
|
+
i++;
|
|
1229
|
+
}
|
|
1230
|
+
return i;
|
|
1231
|
+
};
|
|
1232
|
+
const consumeTemplateString = (content, start) => {
|
|
1233
|
+
const len = content.length;
|
|
1234
|
+
let i = start;
|
|
1235
|
+
while (i < len) {
|
|
1236
|
+
const c = content[i];
|
|
1237
|
+
if (c === "\\" && i + 1 < len) {
|
|
1238
|
+
i += 2;
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (c === "`") return {
|
|
1242
|
+
maskEnd: i,
|
|
1243
|
+
resumeAt: i + 1,
|
|
1244
|
+
openedInterp: false
|
|
1245
|
+
};
|
|
1246
|
+
if (c === "$" && content[i + 1] === "{") return {
|
|
1247
|
+
maskEnd: i,
|
|
1248
|
+
resumeAt: i + 2,
|
|
1249
|
+
openedInterp: true
|
|
1250
|
+
};
|
|
1251
|
+
i++;
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
maskEnd: i,
|
|
1255
|
+
resumeAt: i,
|
|
1256
|
+
openedInterp: false
|
|
1257
|
+
};
|
|
1258
|
+
};
|
|
1259
|
+
const maskSimple = (content, family, maskStrings) => {
|
|
1260
|
+
const out = content.split("");
|
|
1261
|
+
const len = content.length;
|
|
1262
|
+
let i = 0;
|
|
1263
|
+
const mask = (start, end) => {
|
|
1264
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
1265
|
+
};
|
|
1266
|
+
while (i < len) {
|
|
1267
|
+
const c = content[i];
|
|
1268
|
+
const next = content[i + 1];
|
|
1269
|
+
if (family === "py" && (c === "\"" || c === "'")) {
|
|
1270
|
+
if (content[i + 1] === c && content[i + 2] === c) {
|
|
1271
|
+
const triple = c + c + c;
|
|
1272
|
+
const end = content.indexOf(triple, i + 3);
|
|
1273
|
+
const stop = end === -1 ? len : end + 3;
|
|
1274
|
+
if (maskStrings) mask(i + 3, stop - 3);
|
|
1275
|
+
i = stop;
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (c === "\"" || c === "'") {
|
|
1280
|
+
const strStart = i;
|
|
1281
|
+
i = consumeQuotedString(content, i, c);
|
|
1282
|
+
if (maskStrings) mask(strStart + 1, i - 1);
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
1286
|
+
const strStart = i;
|
|
1287
|
+
while (i < len && content[i] !== "\n") i++;
|
|
1288
|
+
mask(strStart, i);
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
if (family === "php" && c === "/" && next === "/") {
|
|
1292
|
+
const strStart = i;
|
|
1293
|
+
while (i < len && content[i] !== "\n") i++;
|
|
1294
|
+
mask(strStart, i);
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if (family === "php" && c === "/" && next === "*") {
|
|
1298
|
+
const strStart = i;
|
|
1299
|
+
i += 2;
|
|
1300
|
+
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
1301
|
+
if (i < len - 1) i += 2;
|
|
1302
|
+
mask(strStart, i);
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
i++;
|
|
1306
|
+
}
|
|
1307
|
+
return out.join("");
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
//#endregion
|
|
1311
|
+
//#region src/engines/ai-slop/abstractions.ts
|
|
1312
|
+
const JS_EXTS$1 = new Set([
|
|
1313
|
+
".ts",
|
|
1314
|
+
".tsx",
|
|
1315
|
+
".js",
|
|
1316
|
+
".jsx",
|
|
1317
|
+
".mjs",
|
|
1318
|
+
".cjs"
|
|
1319
|
+
]);
|
|
1097
1320
|
const THIN_WRAPPER_PATTERNS = [
|
|
1098
1321
|
{
|
|
1099
1322
|
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
1100
|
-
extensions: JS_EXTS$
|
|
1323
|
+
extensions: JS_EXTS$1
|
|
1101
1324
|
},
|
|
1102
1325
|
{
|
|
1103
1326
|
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
1104
|
-
extensions: JS_EXTS$
|
|
1327
|
+
extensions: JS_EXTS$1
|
|
1105
1328
|
},
|
|
1106
1329
|
{
|
|
1107
1330
|
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
@@ -1129,16 +1352,14 @@ const detectThinWrappers = (content, relativePath, ext) => {
|
|
|
1129
1352
|
for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
|
|
1130
1353
|
if (!extensions.has(ext)) continue;
|
|
1131
1354
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1132
|
-
|
|
1133
|
-
while ((match = regex.exec(content)) !== null) {
|
|
1355
|
+
for (const match of content.matchAll(regex)) {
|
|
1134
1356
|
const funcName = match[1];
|
|
1135
1357
|
const matchText = match[0];
|
|
1136
1358
|
const lineNumber = content.slice(0, match.index).split("\n").length;
|
|
1137
1359
|
if (DUNDER_PATTERN.test(funcName)) continue;
|
|
1138
1360
|
if (FRAMEWORK_METHOD_NAMES.test(funcName)) continue;
|
|
1139
1361
|
if (lineNumber >= 2) {
|
|
1140
|
-
|
|
1141
|
-
if (prevLine && prevLine.startsWith("@")) continue;
|
|
1362
|
+
if ((lines[lineNumber - 2]?.trim())?.startsWith("@")) continue;
|
|
1142
1363
|
}
|
|
1143
1364
|
if (!isIdentityForward(matchText)) continue;
|
|
1144
1365
|
if (isUseContextWrapper(matchText)) continue;
|
|
@@ -1194,8 +1415,9 @@ const detectOverAbstraction = async (context) => {
|
|
|
1194
1415
|
}
|
|
1195
1416
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1196
1417
|
const ext = path.extname(filePath);
|
|
1197
|
-
|
|
1198
|
-
diagnostics.push(...
|
|
1418
|
+
const codeOnly = maskComments(content, ext);
|
|
1419
|
+
diagnostics.push(...detectThinWrappers(codeOnly, relativePath, ext));
|
|
1420
|
+
diagnostics.push(...detectAiNaming(codeOnly, relativePath));
|
|
1199
1421
|
}
|
|
1200
1422
|
return diagnostics;
|
|
1201
1423
|
};
|
|
@@ -1514,7 +1736,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1514
1736
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1515
1737
|
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));
|
|
1516
1738
|
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));
|
|
1517
|
-
if (JS_EXTENSIONS$4.has(ext) && /(?:function\s+\w
|
|
1739
|
+
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));
|
|
1518
1740
|
}
|
|
1519
1741
|
return diagnostics;
|
|
1520
1742
|
};
|
|
@@ -1551,9 +1773,10 @@ const detectDeadPatterns = async (context) => {
|
|
|
1551
1773
|
}
|
|
1552
1774
|
const ext = path.extname(filePath);
|
|
1553
1775
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1554
|
-
|
|
1776
|
+
const codeOnly = maskComments(content, ext);
|
|
1777
|
+
diagnostics.push(...detectConsoleLeftovers(codeOnly, relativePath, ext));
|
|
1555
1778
|
diagnostics.push(...detectTodoStubs(content, relativePath));
|
|
1556
|
-
diagnostics.push(...detectDeadCodePatterns(
|
|
1779
|
+
diagnostics.push(...detectDeadCodePatterns(codeOnly, relativePath, ext));
|
|
1557
1780
|
diagnostics.push(...detectUnsafeTypePatterns(content, relativePath, ext));
|
|
1558
1781
|
}
|
|
1559
1782
|
return diagnostics;
|
|
@@ -1743,6 +1966,7 @@ const JS_EXTENSIONS$3 = new Set([
|
|
|
1743
1966
|
const IMPORT_FROM_RE$1 = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
|
|
1744
1967
|
const TYPE_ONLY_RE = /^\s*type\b/;
|
|
1745
1968
|
const VALUE_BINDING_RE = /\{([^}]*)\}/;
|
|
1969
|
+
const NAMESPACE_RE = /\*\s+as\s+/;
|
|
1746
1970
|
const isTypeOnly = (clause) => {
|
|
1747
1971
|
if (TYPE_ONLY_RE.test(clause)) return true;
|
|
1748
1972
|
const braces = VALUE_BINDING_RE.exec(clause);
|
|
@@ -1760,7 +1984,8 @@ const extractImportLines = (content) => {
|
|
|
1760
1984
|
results.push({
|
|
1761
1985
|
spec: match[2],
|
|
1762
1986
|
line: i + 1,
|
|
1763
|
-
typeOnly: isTypeOnly(match[1])
|
|
1987
|
+
typeOnly: isTypeOnly(match[1]),
|
|
1988
|
+
namespace: NAMESPACE_RE.test(match[1])
|
|
1764
1989
|
});
|
|
1765
1990
|
}
|
|
1766
1991
|
return results;
|
|
@@ -1777,11 +2002,11 @@ const detectDuplicateImports = async (context) => {
|
|
|
1777
2002
|
} catch {
|
|
1778
2003
|
continue;
|
|
1779
2004
|
}
|
|
1780
|
-
const imports = extractImportLines(content);
|
|
2005
|
+
const imports = extractImportLines(maskComments(content, path.extname(filePath)));
|
|
1781
2006
|
if (imports.length < 2) continue;
|
|
1782
2007
|
const byBucket = /* @__PURE__ */ new Map();
|
|
1783
2008
|
for (const imp of imports) {
|
|
1784
|
-
const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
2009
|
+
const key = `${imp.namespace ? "ns" : imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
1785
2010
|
const list = byBucket.get(key) ?? [];
|
|
1786
2011
|
list.push(imp);
|
|
1787
2012
|
byBucket.set(key, list);
|
|
@@ -1897,9 +2122,8 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
1897
2122
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1898
2123
|
for (const { pattern, languages, message } of SWALLOWED_EXCEPTION_PATTERNS) {
|
|
1899
2124
|
if (!languages.includes(ext)) continue;
|
|
1900
|
-
let match;
|
|
1901
2125
|
const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
|
|
1902
|
-
|
|
2126
|
+
for (const match of content.matchAll(regex)) {
|
|
1903
2127
|
if (isIntentionalIgnore(match[0], ext)) continue;
|
|
1904
2128
|
const line = content.slice(0, match.index).split("\n").length;
|
|
1905
2129
|
diagnostics.push({
|
|
@@ -1965,190 +2189,30 @@ const flagLibraryPanic = (lines, relPath, pkg, out) => {
|
|
|
1965
2189
|
severity: "warning",
|
|
1966
2190
|
message: `\`panic()\` in package \`${pkg}\` (non-main, non-test). Library code should return errors, not unwind the goroutine.`,
|
|
1967
2191
|
help: "Convert to `return fmt.Errorf(...)` (or a wrapped error) and let the caller decide. Reserve `panic` for genuinely-impossible states (corrupt internal invariants), and mark those with a comment so future readers know it's intentional.",
|
|
1968
|
-
line: i + 1,
|
|
1969
|
-
column: 1,
|
|
1970
|
-
category: "AI Slop",
|
|
1971
|
-
fixable: false
|
|
1972
|
-
});
|
|
1973
|
-
}
|
|
1974
|
-
};
|
|
1975
|
-
const detectGoPatterns = async (context) => {
|
|
1976
|
-
const diagnostics = [];
|
|
1977
|
-
const files = getSourceFiles(context);
|
|
1978
|
-
for (const filePath of files) {
|
|
1979
|
-
if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
1980
|
-
if (isAutoGenerated(filePath)) continue;
|
|
1981
|
-
if (filePath.endsWith("_test.go")) continue;
|
|
1982
|
-
let content;
|
|
1983
|
-
try {
|
|
1984
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
1985
|
-
} catch {
|
|
1986
|
-
continue;
|
|
1987
|
-
}
|
|
1988
|
-
const lines = content.split("\n");
|
|
1989
|
-
const pkg = detectPackageName(lines);
|
|
1990
|
-
if (!pkg) continue;
|
|
1991
|
-
flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
|
|
1992
|
-
}
|
|
1993
|
-
return diagnostics;
|
|
1994
|
-
};
|
|
1995
|
-
|
|
1996
|
-
//#endregion
|
|
1997
|
-
//#region src/engines/ai-slop/hardcoded-config.ts
|
|
1998
|
-
const SOURCE_EXTENSIONS = new Set([
|
|
1999
|
-
".ts",
|
|
2000
|
-
".tsx",
|
|
2001
|
-
".js",
|
|
2002
|
-
".jsx",
|
|
2003
|
-
".mjs",
|
|
2004
|
-
".cjs",
|
|
2005
|
-
".py",
|
|
2006
|
-
".go",
|
|
2007
|
-
".rs",
|
|
2008
|
-
".rb",
|
|
2009
|
-
".java",
|
|
2010
|
-
".php"
|
|
2011
|
-
]);
|
|
2012
|
-
const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
|
|
2013
|
-
const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
|
|
2014
|
-
const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
|
|
2015
|
-
const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
|
|
2016
|
-
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
2017
|
-
const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
|
|
2018
|
-
const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
|
|
2019
|
-
const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
|
|
2020
|
-
const PLACEHOLDER_HOSTS = new Set([
|
|
2021
|
-
"example.com",
|
|
2022
|
-
"example.org",
|
|
2023
|
-
"example.net"
|
|
2024
|
-
]);
|
|
2025
|
-
const LOOPBACK_HOSTS = new Set([
|
|
2026
|
-
"localhost",
|
|
2027
|
-
"127.0.0.1",
|
|
2028
|
-
"0.0.0.0",
|
|
2029
|
-
"::1"
|
|
2030
|
-
]);
|
|
2031
|
-
const VENDOR_API_DOMAINS = [
|
|
2032
|
-
"github.com",
|
|
2033
|
-
"githubusercontent.com",
|
|
2034
|
-
"googleapis.com",
|
|
2035
|
-
"accounts.google.com",
|
|
2036
|
-
"stripe.com",
|
|
2037
|
-
"openai.com",
|
|
2038
|
-
"anthropic.com",
|
|
2039
|
-
"slack.com",
|
|
2040
|
-
"twilio.com",
|
|
2041
|
-
"sendgrid.com",
|
|
2042
|
-
"mailgun.net",
|
|
2043
|
-
"cloudflare.com",
|
|
2044
|
-
"discord.com",
|
|
2045
|
-
"telegram.org",
|
|
2046
|
-
"login.microsoftonline.com",
|
|
2047
|
-
"graph.microsoft.com",
|
|
2048
|
-
"twitter.com",
|
|
2049
|
-
"x.com",
|
|
2050
|
-
"twimg.com",
|
|
2051
|
-
"t.co",
|
|
2052
|
-
"api.telegram.org"
|
|
2053
|
-
];
|
|
2054
|
-
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
2055
|
-
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2056
|
-
const HARDCODED_URL_FINDING = {
|
|
2057
|
-
rule: "ai-slop/hardcoded-url",
|
|
2058
|
-
message: "Hardcoded environment URL in production code",
|
|
2059
|
-
help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
|
|
2060
|
-
};
|
|
2061
|
-
const HARDCODED_ID_FINDING = {
|
|
2062
|
-
rule: "ai-slop/hardcoded-id",
|
|
2063
|
-
message: "Hardcoded provider/project ID in production code",
|
|
2064
|
-
help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
|
|
2065
|
-
};
|
|
2066
|
-
const makeFinding = (filePath, line, spec) => ({
|
|
2067
|
-
filePath,
|
|
2068
|
-
engine: "ai-slop",
|
|
2069
|
-
rule: spec.rule,
|
|
2070
|
-
severity: "warning",
|
|
2071
|
-
message: spec.message,
|
|
2072
|
-
help: spec.help,
|
|
2073
|
-
line,
|
|
2074
|
-
column: 0,
|
|
2075
|
-
category: "AI Slop",
|
|
2076
|
-
fixable: false
|
|
2077
|
-
});
|
|
2078
|
-
const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
2079
|
-
const commentStartsBefore = (line, index, ext) => {
|
|
2080
|
-
const prefix = line.slice(0, index);
|
|
2081
|
-
if (ext === ".py" || ext === ".rb") return prefix.includes("#");
|
|
2082
|
-
if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
|
|
2083
|
-
return prefix.includes("//") || prefix.includes("/*");
|
|
2084
|
-
};
|
|
2085
|
-
const safeUrlHost = (urlText) => {
|
|
2086
|
-
try {
|
|
2087
|
-
return new URL(urlText).hostname.toLowerCase();
|
|
2088
|
-
} catch {
|
|
2089
|
-
return null;
|
|
2090
|
-
}
|
|
2091
|
-
};
|
|
2092
|
-
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
2093
|
-
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
2094
|
-
if (isEnvBackedLine(line)) return false;
|
|
2095
|
-
const host = safeUrlHost(urlText);
|
|
2096
|
-
if (!host) return false;
|
|
2097
|
-
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2098
|
-
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
2099
|
-
if (isVendorApiHost(host)) return false;
|
|
2100
|
-
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2101
|
-
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2102
|
-
};
|
|
2103
|
-
const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
|
|
2104
|
-
const hasUsefulIdShape = (value) => {
|
|
2105
|
-
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
2106
|
-
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
2107
|
-
if (/^https?:\/\//i.test(value)) return false;
|
|
2108
|
-
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
2109
|
-
return /[0-9]/.test(value);
|
|
2110
|
-
};
|
|
2111
|
-
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
2112
|
-
const diagnostics = [];
|
|
2113
|
-
if (isCommentOnlyLine(line.trim())) return diagnostics;
|
|
2114
|
-
URL_LITERAL_RE.lastIndex = 0;
|
|
2115
|
-
let urlMatch;
|
|
2116
|
-
while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
|
|
2117
|
-
const urlText = urlMatch[2];
|
|
2118
|
-
if (commentStartsBefore(line, urlMatch.index, ext)) continue;
|
|
2119
|
-
if (!shouldFlagUrlLiteral(line, urlText)) continue;
|
|
2120
|
-
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
|
|
2121
|
-
}
|
|
2122
|
-
if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
|
|
2123
|
-
ID_LITERAL_RE.lastIndex = 0;
|
|
2124
|
-
let idMatch;
|
|
2125
|
-
while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
|
|
2126
|
-
const value = idMatch[2];
|
|
2127
|
-
if (commentStartsBefore(line, idMatch.index, ext)) continue;
|
|
2128
|
-
if (!hasUsefulIdShape(value)) continue;
|
|
2129
|
-
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
|
|
2130
|
-
}
|
|
2131
|
-
return diagnostics;
|
|
2132
|
-
};
|
|
2133
|
-
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
2134
|
-
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
2135
|
-
if (isNonProductionPath(relativePath)) return [];
|
|
2136
|
-
if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
|
|
2137
|
-
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
2192
|
+
line: i + 1,
|
|
2193
|
+
column: 1,
|
|
2194
|
+
category: "AI Slop",
|
|
2195
|
+
fixable: false
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2138
2198
|
};
|
|
2139
|
-
const
|
|
2199
|
+
const detectGoPatterns = async (context) => {
|
|
2140
2200
|
const diagnostics = [];
|
|
2141
|
-
|
|
2201
|
+
const files = getSourceFiles(context);
|
|
2202
|
+
for (const filePath of files) {
|
|
2203
|
+
if (!GO_EXTENSIONS.has(path.extname(filePath))) continue;
|
|
2142
2204
|
if (isAutoGenerated(filePath)) continue;
|
|
2205
|
+
if (filePath.endsWith("_test.go")) continue;
|
|
2143
2206
|
let content;
|
|
2144
2207
|
try {
|
|
2145
2208
|
content = fs.readFileSync(filePath, "utf-8");
|
|
2146
2209
|
} catch {
|
|
2147
2210
|
continue;
|
|
2148
2211
|
}
|
|
2149
|
-
const
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2212
|
+
const lines = content.split("\n");
|
|
2213
|
+
const pkg = detectPackageName(lines);
|
|
2214
|
+
if (!pkg) continue;
|
|
2215
|
+
flagLibraryPanic(lines, path.relative(context.rootDirectory, filePath), pkg, diagnostics);
|
|
2152
2216
|
}
|
|
2153
2217
|
return diagnostics;
|
|
2154
2218
|
};
|
|
@@ -2250,15 +2314,18 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
|
2250
2314
|
}
|
|
2251
2315
|
return globs;
|
|
2252
2316
|
};
|
|
2317
|
+
const readWorkspaceEntries = (dir) => {
|
|
2318
|
+
try {
|
|
2319
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
2320
|
+
} catch {
|
|
2321
|
+
return [];
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2253
2324
|
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
2254
2325
|
const dirs = [];
|
|
2255
2326
|
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
2256
2327
|
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
2257
|
-
|
|
2258
|
-
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
2259
|
-
} catch {
|
|
2260
|
-
continue;
|
|
2261
|
-
}
|
|
2328
|
+
for (const entry of readWorkspaceEntries(parent)) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
2262
2329
|
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
2263
2330
|
return dirs;
|
|
2264
2331
|
};
|
|
@@ -2891,6 +2958,241 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2891
2958
|
return diagnostics;
|
|
2892
2959
|
};
|
|
2893
2960
|
|
|
2961
|
+
//#endregion
|
|
2962
|
+
//#region src/engines/ai-slop/hardcoded-config.ts
|
|
2963
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
2964
|
+
".ts",
|
|
2965
|
+
".tsx",
|
|
2966
|
+
".js",
|
|
2967
|
+
".jsx",
|
|
2968
|
+
".mjs",
|
|
2969
|
+
".cjs",
|
|
2970
|
+
".py",
|
|
2971
|
+
".go",
|
|
2972
|
+
".rs",
|
|
2973
|
+
".rb",
|
|
2974
|
+
".java",
|
|
2975
|
+
".php"
|
|
2976
|
+
]);
|
|
2977
|
+
const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
|
|
2978
|
+
const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
|
|
2979
|
+
const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
|
|
2980
|
+
const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
|
|
2981
|
+
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
2982
|
+
const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
|
|
2983
|
+
const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
|
|
2984
|
+
const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
|
|
2985
|
+
const PLACEHOLDER_HOSTS = new Set([
|
|
2986
|
+
"example.com",
|
|
2987
|
+
"example.org",
|
|
2988
|
+
"example.net"
|
|
2989
|
+
]);
|
|
2990
|
+
const LOOPBACK_HOSTS = new Set([
|
|
2991
|
+
"localhost",
|
|
2992
|
+
"127.0.0.1",
|
|
2993
|
+
"0.0.0.0",
|
|
2994
|
+
"::1"
|
|
2995
|
+
]);
|
|
2996
|
+
const VENDOR_API_DOMAINS = [
|
|
2997
|
+
"github.com",
|
|
2998
|
+
"githubusercontent.com",
|
|
2999
|
+
"googleapis.com",
|
|
3000
|
+
"accounts.google.com",
|
|
3001
|
+
"stripe.com",
|
|
3002
|
+
"openai.com",
|
|
3003
|
+
"anthropic.com",
|
|
3004
|
+
"slack.com",
|
|
3005
|
+
"twilio.com",
|
|
3006
|
+
"sendgrid.com",
|
|
3007
|
+
"mailgun.net",
|
|
3008
|
+
"cloudflare.com",
|
|
3009
|
+
"discord.com",
|
|
3010
|
+
"telegram.org",
|
|
3011
|
+
"login.microsoftonline.com",
|
|
3012
|
+
"graph.microsoft.com",
|
|
3013
|
+
"twitter.com",
|
|
3014
|
+
"x.com",
|
|
3015
|
+
"twimg.com",
|
|
3016
|
+
"t.co",
|
|
3017
|
+
"api.telegram.org"
|
|
3018
|
+
];
|
|
3019
|
+
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
3020
|
+
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
3021
|
+
const HARDCODED_URL_FINDING = {
|
|
3022
|
+
rule: "ai-slop/hardcoded-url",
|
|
3023
|
+
message: "Hardcoded environment URL in production code",
|
|
3024
|
+
help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
|
|
3025
|
+
};
|
|
3026
|
+
const HARDCODED_ID_FINDING = {
|
|
3027
|
+
rule: "ai-slop/hardcoded-id",
|
|
3028
|
+
message: "Hardcoded provider/project ID in production code",
|
|
3029
|
+
help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
|
|
3030
|
+
};
|
|
3031
|
+
const makeFinding = (filePath, line, spec) => ({
|
|
3032
|
+
filePath,
|
|
3033
|
+
engine: "ai-slop",
|
|
3034
|
+
rule: spec.rule,
|
|
3035
|
+
severity: "warning",
|
|
3036
|
+
message: spec.message,
|
|
3037
|
+
help: spec.help,
|
|
3038
|
+
line,
|
|
3039
|
+
column: 0,
|
|
3040
|
+
category: "AI Slop",
|
|
3041
|
+
fixable: false
|
|
3042
|
+
});
|
|
3043
|
+
const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
3044
|
+
const commentStartsBefore = (line, index, ext) => {
|
|
3045
|
+
const prefix = line.slice(0, index);
|
|
3046
|
+
if (ext === ".py" || ext === ".rb") return prefix.includes("#");
|
|
3047
|
+
if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
|
|
3048
|
+
return prefix.includes("//") || prefix.includes("/*");
|
|
3049
|
+
};
|
|
3050
|
+
const safeUrlHost = (urlText) => {
|
|
3051
|
+
try {
|
|
3052
|
+
return new URL(urlText).hostname.toLowerCase();
|
|
3053
|
+
} catch {
|
|
3054
|
+
return null;
|
|
3055
|
+
}
|
|
3056
|
+
};
|
|
3057
|
+
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
3058
|
+
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
3059
|
+
if (isEnvBackedLine(line)) return false;
|
|
3060
|
+
const host = safeUrlHost(urlText);
|
|
3061
|
+
if (!host) return false;
|
|
3062
|
+
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
3063
|
+
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
3064
|
+
if (isVendorApiHost(host)) return false;
|
|
3065
|
+
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
3066
|
+
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
3067
|
+
};
|
|
3068
|
+
const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
|
|
3069
|
+
const hasUsefulIdShape = (value) => {
|
|
3070
|
+
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
3071
|
+
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
3072
|
+
if (/^https?:\/\//i.test(value)) return false;
|
|
3073
|
+
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
3074
|
+
return /[0-9]/.test(value);
|
|
3075
|
+
};
|
|
3076
|
+
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
3077
|
+
const diagnostics = [];
|
|
3078
|
+
if (isCommentOnlyLine(line.trim())) return diagnostics;
|
|
3079
|
+
for (const urlMatch of line.matchAll(URL_LITERAL_RE)) {
|
|
3080
|
+
const urlText = urlMatch[2];
|
|
3081
|
+
if (commentStartsBefore(line, urlMatch.index, ext)) continue;
|
|
3082
|
+
if (!shouldFlagUrlLiteral(line, urlText)) continue;
|
|
3083
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
|
|
3084
|
+
}
|
|
3085
|
+
if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
|
|
3086
|
+
for (const idMatch of line.matchAll(ID_LITERAL_RE)) {
|
|
3087
|
+
const value = idMatch[2];
|
|
3088
|
+
if (commentStartsBefore(line, idMatch.index, ext)) continue;
|
|
3089
|
+
if (!hasUsefulIdShape(value)) continue;
|
|
3090
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
|
|
3091
|
+
}
|
|
3092
|
+
return diagnostics;
|
|
3093
|
+
};
|
|
3094
|
+
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
3095
|
+
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
3096
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
3097
|
+
if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
|
|
3098
|
+
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
3099
|
+
};
|
|
3100
|
+
const detectHardcodedConfigLiterals = async (context) => {
|
|
3101
|
+
const diagnostics = [];
|
|
3102
|
+
for (const filePath of getSourceFiles(context)) {
|
|
3103
|
+
if (isAutoGenerated(filePath)) continue;
|
|
3104
|
+
let content;
|
|
3105
|
+
try {
|
|
3106
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
3107
|
+
} catch {
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
3111
|
+
const ext = path.extname(filePath);
|
|
3112
|
+
diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
|
|
3113
|
+
}
|
|
3114
|
+
return diagnostics;
|
|
3115
|
+
};
|
|
3116
|
+
|
|
3117
|
+
//#endregion
|
|
3118
|
+
//#region src/utils/suppress.ts
|
|
3119
|
+
const DIRECTIVE_RE = /(?:\/\/|\/\*|#|<!--|\*)\s*aislop-ignore-(next-line|line|file)\b([^\n]*)/;
|
|
3120
|
+
const isAislopDirectiveLine = (line) => DIRECTIVE_RE.test(line);
|
|
3121
|
+
const parseDirective = (rest) => {
|
|
3122
|
+
const tokens = rest.split("--")[0].match(/[A-Za-z0-9@][\w@/.-]*/g) ?? [];
|
|
3123
|
+
if (tokens.length === 0) return {
|
|
3124
|
+
rules: /* @__PURE__ */ new Set(),
|
|
3125
|
+
all: true
|
|
3126
|
+
};
|
|
3127
|
+
return {
|
|
3128
|
+
rules: new Set(tokens),
|
|
3129
|
+
all: false
|
|
3130
|
+
};
|
|
3131
|
+
};
|
|
3132
|
+
const covers = (directive, rule) => directive.all || [...directive.rules].some((r) => r === rule || rule.endsWith(`/${r}`));
|
|
3133
|
+
const parseFileDirectives = (content) => {
|
|
3134
|
+
const lines = content.split(/\r?\n/);
|
|
3135
|
+
const file = [];
|
|
3136
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
3137
|
+
const addLine = (target, directive) => {
|
|
3138
|
+
const list = byLine.get(target) ?? [];
|
|
3139
|
+
list.push(directive);
|
|
3140
|
+
byLine.set(target, list);
|
|
3141
|
+
};
|
|
3142
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3143
|
+
const match = DIRECTIVE_RE.exec(lines[i]);
|
|
3144
|
+
if (!match) continue;
|
|
3145
|
+
const scope = match[1];
|
|
3146
|
+
const directive = parseDirective(match[2] ?? "");
|
|
3147
|
+
if (scope === "file") file.push(directive);
|
|
3148
|
+
else if (scope === "next-line") addLine(i + 2, directive);
|
|
3149
|
+
else addLine(i + 1, directive);
|
|
3150
|
+
}
|
|
3151
|
+
return {
|
|
3152
|
+
file,
|
|
3153
|
+
byLine
|
|
3154
|
+
};
|
|
3155
|
+
};
|
|
3156
|
+
const applySuppressions = (results, rootDirectory) => {
|
|
3157
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3158
|
+
let suppressedCount = 0;
|
|
3159
|
+
const load = (filePath) => {
|
|
3160
|
+
const cached = cache.get(filePath);
|
|
3161
|
+
if (cached !== void 0) return cached;
|
|
3162
|
+
const absolute = path.isAbsolute(filePath) ? filePath : path.join(rootDirectory, filePath);
|
|
3163
|
+
let parsed = null;
|
|
3164
|
+
try {
|
|
3165
|
+
parsed = parseFileDirectives(fs.readFileSync(absolute, "utf-8"));
|
|
3166
|
+
} catch {
|
|
3167
|
+
parsed = null;
|
|
3168
|
+
}
|
|
3169
|
+
cache.set(filePath, parsed);
|
|
3170
|
+
return parsed;
|
|
3171
|
+
};
|
|
3172
|
+
const isSuppressed = (diagnostic) => {
|
|
3173
|
+
const directives = load(diagnostic.filePath);
|
|
3174
|
+
if (!directives) return false;
|
|
3175
|
+
if (directives.file.some((d) => covers(d, diagnostic.rule))) return true;
|
|
3176
|
+
return (directives.byLine.get(diagnostic.line) ?? []).some((d) => covers(d, diagnostic.rule));
|
|
3177
|
+
};
|
|
3178
|
+
return {
|
|
3179
|
+
results: results.map((result) => {
|
|
3180
|
+
const kept = result.diagnostics.filter((diagnostic) => {
|
|
3181
|
+
if (isSuppressed(diagnostic)) {
|
|
3182
|
+
suppressedCount += 1;
|
|
3183
|
+
return false;
|
|
3184
|
+
}
|
|
3185
|
+
return true;
|
|
3186
|
+
});
|
|
3187
|
+
return {
|
|
3188
|
+
...result,
|
|
3189
|
+
diagnostics: kept
|
|
3190
|
+
};
|
|
3191
|
+
}),
|
|
3192
|
+
suppressedCount
|
|
3193
|
+
};
|
|
3194
|
+
};
|
|
3195
|
+
|
|
2894
3196
|
//#endregion
|
|
2895
3197
|
//#region src/engines/ai-slop/comment-blocks.ts
|
|
2896
3198
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
@@ -2914,6 +3216,7 @@ const getCommentSyntax = (ext) => {
|
|
|
2914
3216
|
};
|
|
2915
3217
|
const getMatchedLinePrefix = (line, syntax) => {
|
|
2916
3218
|
const trimmed = line.trimStart();
|
|
3219
|
+
if (isAislopDirectiveLine(trimmed)) return null;
|
|
2917
3220
|
for (const prefix of syntax.linePrefixes) {
|
|
2918
3221
|
if (!trimmed.startsWith(prefix)) continue;
|
|
2919
3222
|
if (prefix === "#" && trimmed.startsWith("#!")) return null;
|
|
@@ -3654,7 +3957,7 @@ const detectRustPatterns = async (context) => {
|
|
|
3654
3957
|
|
|
3655
3958
|
//#endregion
|
|
3656
3959
|
//#region src/engines/ai-slop/silent-recovery.ts
|
|
3657
|
-
const JS_EXTS
|
|
3960
|
+
const JS_EXTS = new Set([
|
|
3658
3961
|
".ts",
|
|
3659
3962
|
".tsx",
|
|
3660
3963
|
".js",
|
|
@@ -3713,9 +4016,7 @@ const isLogOnlyBody = (body) => {
|
|
|
3713
4016
|
};
|
|
3714
4017
|
const detectJsSilentRecovery = (content, relPath) => {
|
|
3715
4018
|
const out = [];
|
|
3716
|
-
CATCH_HEAD_RE
|
|
3717
|
-
let match;
|
|
3718
|
-
while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
|
|
4019
|
+
for (const match of content.matchAll(CATCH_HEAD_RE)) {
|
|
3719
4020
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3720
4021
|
if (body === null) continue;
|
|
3721
4022
|
if (!isLogOnlyBody(body)) continue;
|
|
@@ -3783,7 +4084,7 @@ const detectSilentRecovery = async (context) => {
|
|
|
3783
4084
|
for (const filePath of files) {
|
|
3784
4085
|
if (isAutoGenerated(filePath)) continue;
|
|
3785
4086
|
const ext = path.extname(filePath);
|
|
3786
|
-
const isJs = JS_EXTS
|
|
4087
|
+
const isJs = JS_EXTS.has(ext);
|
|
3787
4088
|
if (!isJs && !(ext === ".py")) continue;
|
|
3788
4089
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3789
4090
|
if (isNonProductionPath(relPath)) continue;
|
|
@@ -3892,18 +4193,22 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3892
4193
|
}
|
|
3893
4194
|
continue;
|
|
3894
4195
|
}
|
|
3895
|
-
const importMatch = trimmed.match(/^import\s+(
|
|
4196
|
+
const importMatch = trimmed.match(/^import\s+(.+)/);
|
|
3896
4197
|
if (importMatch) {
|
|
3897
4198
|
importLines.add(i);
|
|
3898
|
-
const
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
4199
|
+
for (const clause of importMatch[1].replace(/#.*$/, "").split(",")) {
|
|
4200
|
+
const clauseMatch = clause.trim().match(/^([\w.]+)(?:\s+as\s+(\w+))?/);
|
|
4201
|
+
if (!clauseMatch) continue;
|
|
4202
|
+
const alias = clauseMatch[2];
|
|
4203
|
+
if (alias && alias === clauseMatch[1]) continue;
|
|
4204
|
+
const simpleName = (alias ?? clauseMatch[1]).split(".")[0];
|
|
4205
|
+
if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
|
|
4206
|
+
name: simpleName,
|
|
4207
|
+
line: i + 1,
|
|
4208
|
+
isDefault: false,
|
|
4209
|
+
isNamespace: true
|
|
4210
|
+
});
|
|
4211
|
+
}
|
|
3907
4212
|
}
|
|
3908
4213
|
}
|
|
3909
4214
|
return {
|
|
@@ -3913,8 +4218,7 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3913
4218
|
};
|
|
3914
4219
|
const isSymbolUsed = (name, content, importLines, lines) => {
|
|
3915
4220
|
const pattern = new RegExp(`\\b${name}\\b`, "g");
|
|
3916
|
-
|
|
3917
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
4221
|
+
for (const match of content.matchAll(pattern)) {
|
|
3918
4222
|
const lineIndex = content.slice(0, match.index).split("\n").length - 1;
|
|
3919
4223
|
if (!importLines.has(lineIndex)) return true;
|
|
3920
4224
|
}
|
|
@@ -4015,6 +4319,18 @@ const aiSlopEngine = {
|
|
|
4015
4319
|
|
|
4016
4320
|
//#endregion
|
|
4017
4321
|
//#region src/engines/architecture/matchers.ts
|
|
4322
|
+
const REGEX_SPECIAL_CHARS = new Set([
|
|
4323
|
+
".",
|
|
4324
|
+
"+",
|
|
4325
|
+
"^",
|
|
4326
|
+
"$",
|
|
4327
|
+
"{",
|
|
4328
|
+
"}",
|
|
4329
|
+
"(",
|
|
4330
|
+
")",
|
|
4331
|
+
"|",
|
|
4332
|
+
"\\"
|
|
4333
|
+
]);
|
|
4018
4334
|
const minimatch = (filePath, pattern) => {
|
|
4019
4335
|
let regex = "";
|
|
4020
4336
|
let i = 0;
|
|
@@ -4039,7 +4355,7 @@ const minimatch = (filePath, pattern) => {
|
|
|
4039
4355
|
regex += pattern.slice(i, closeIndex + 1);
|
|
4040
4356
|
i = closeIndex + 1;
|
|
4041
4357
|
}
|
|
4042
|
-
} else if (
|
|
4358
|
+
} else if (REGEX_SPECIAL_CHARS.has(ch)) {
|
|
4043
4359
|
regex += `\\${ch}`;
|
|
4044
4360
|
i++;
|
|
4045
4361
|
} else {
|
|
@@ -4059,27 +4375,15 @@ const extractImports = (content, ext) => {
|
|
|
4059
4375
|
".mjs",
|
|
4060
4376
|
".cjs"
|
|
4061
4377
|
].includes(ext)) {
|
|
4062
|
-
const
|
|
4063
|
-
|
|
4064
|
-
while ((match = esPattern.exec(content)) !== null) imports.push(match[1]);
|
|
4065
|
-
const reqPattern = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
4066
|
-
while ((match = reqPattern.exec(content)) !== null) imports.push(match[1]);
|
|
4067
|
-
}
|
|
4068
|
-
if (ext === ".py") {
|
|
4069
|
-
const pyPattern = /(?:from|import)\s+([\w.]+)/g;
|
|
4070
|
-
let match;
|
|
4071
|
-
while ((match = pyPattern.exec(content)) !== null) imports.push(match[1]);
|
|
4378
|
+
for (const match of content.matchAll(/(?:import|from)\s+["']([^"']+)["']/g)) imports.push(match[1]);
|
|
4379
|
+
for (const match of content.matchAll(/require\s*\(\s*["']([^"']+)["']\s*\)/g)) imports.push(match[1]);
|
|
4072
4380
|
}
|
|
4381
|
+
if (ext === ".py") for (const match of content.matchAll(/(?:from|import)\s+([\w.]+)/g)) imports.push(match[1]);
|
|
4073
4382
|
if (ext === ".go") {
|
|
4074
|
-
const
|
|
4075
|
-
|
|
4076
|
-
while ((match = goSingleImport.exec(content)) !== null) imports.push(match[1]);
|
|
4077
|
-
const goMultiImport = /import\s*\(([^)]*)\)/gs;
|
|
4078
|
-
while ((match = goMultiImport.exec(content)) !== null) {
|
|
4383
|
+
for (const match of content.matchAll(/^\s*import\s+"([^"]+)"/gm)) imports.push(match[1]);
|
|
4384
|
+
for (const match of content.matchAll(/import\s*\(([^)]*)\)/gs)) {
|
|
4079
4385
|
const block = match[1];
|
|
4080
|
-
const
|
|
4081
|
-
let pkgMatch;
|
|
4082
|
-
while ((pkgMatch = pkgPattern.exec(block)) !== null) imports.push(pkgMatch[1]);
|
|
4386
|
+
for (const pkgMatch of block.matchAll(/"([^"]+)"/g)) imports.push(pkgMatch[1]);
|
|
4083
4387
|
}
|
|
4084
4388
|
}
|
|
4085
4389
|
return imports;
|
|
@@ -4206,10 +4510,10 @@ const architectureEngine = {
|
|
|
4206
4510
|
//#endregion
|
|
4207
4511
|
//#region src/engines/code-quality/function-boundaries.ts
|
|
4208
4512
|
const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
|
|
4209
|
-
const ARROW_BLOCK_RE =
|
|
4210
|
-
const ARROW_END_RE =
|
|
4211
|
-
const BRACE_START_RE =
|
|
4212
|
-
const NEW_STATEMENT_RE =
|
|
4513
|
+
const ARROW_BLOCK_RE = /=>\s*\{/;
|
|
4514
|
+
const ARROW_END_RE = /=>\s*$/;
|
|
4515
|
+
const BRACE_START_RE = /^\s*\{/;
|
|
4516
|
+
const NEW_STATEMENT_RE = /^(?:export\s+)?(?:const|let|var|function|class)\s/;
|
|
4213
4517
|
const isControlFlowBrace = (lineText, braceIndex) => {
|
|
4214
4518
|
const before = lineText.substring(0, braceIndex).trimEnd();
|
|
4215
4519
|
if (before.endsWith(")")) return true;
|
|
@@ -4263,12 +4567,92 @@ const findBraceFunctionEnd = (lines, startIndex) => {
|
|
|
4263
4567
|
maxNesting
|
|
4264
4568
|
};
|
|
4265
4569
|
};
|
|
4266
|
-
const
|
|
4267
|
-
|
|
4268
|
-
let
|
|
4570
|
+
const extractPythonSignature = (lines, startIndex) => {
|
|
4571
|
+
let depth = 0;
|
|
4572
|
+
let started = false;
|
|
4573
|
+
let params = "";
|
|
4574
|
+
for (let j = startIndex; j < lines.length; j++) {
|
|
4575
|
+
const l = lines[j];
|
|
4576
|
+
for (let ci = 0; ci < l.length; ci++) {
|
|
4577
|
+
const ch = l[ci];
|
|
4578
|
+
if (ch === "(") {
|
|
4579
|
+
depth++;
|
|
4580
|
+
if (depth === 1 && !started) {
|
|
4581
|
+
started = true;
|
|
4582
|
+
continue;
|
|
4583
|
+
}
|
|
4584
|
+
} else if (ch === ")") {
|
|
4585
|
+
depth--;
|
|
4586
|
+
if (depth === 0) return {
|
|
4587
|
+
params,
|
|
4588
|
+
sigEndIndex: j
|
|
4589
|
+
};
|
|
4590
|
+
}
|
|
4591
|
+
if (started) params += ch;
|
|
4592
|
+
}
|
|
4593
|
+
if (started) params += " ";
|
|
4594
|
+
}
|
|
4595
|
+
return {
|
|
4596
|
+
params,
|
|
4597
|
+
sigEndIndex: startIndex
|
|
4598
|
+
};
|
|
4599
|
+
};
|
|
4600
|
+
const countPythonParams = (signature) => {
|
|
4601
|
+
let depth = 0;
|
|
4602
|
+
const parts = [];
|
|
4603
|
+
let current = "";
|
|
4604
|
+
for (const ch of signature) {
|
|
4605
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
4606
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
4607
|
+
if (ch === "," && depth === 0) {
|
|
4608
|
+
parts.push(current);
|
|
4609
|
+
current = "";
|
|
4610
|
+
continue;
|
|
4611
|
+
}
|
|
4612
|
+
current += ch;
|
|
4613
|
+
}
|
|
4614
|
+
parts.push(current);
|
|
4615
|
+
let count = 0;
|
|
4616
|
+
for (const raw of parts) {
|
|
4617
|
+
const p = raw.trim();
|
|
4618
|
+
if (p.length === 0 || p === "*" || p === "/") continue;
|
|
4619
|
+
if (p.startsWith("*")) continue;
|
|
4620
|
+
if (p.includes("=")) continue;
|
|
4621
|
+
const name = p.split(":")[0].trim();
|
|
4622
|
+
if (name === "self" || name === "cls") continue;
|
|
4623
|
+
count++;
|
|
4624
|
+
}
|
|
4625
|
+
return count;
|
|
4626
|
+
};
|
|
4627
|
+
const countPythonBodyCodeLines = (lines, sigEndIndex, endLine) => {
|
|
4628
|
+
let count = 0;
|
|
4629
|
+
let inDoc = false;
|
|
4630
|
+
let delim = "";
|
|
4631
|
+
for (let j = sigEndIndex + 1; j <= endLine && j < lines.length; j++) {
|
|
4632
|
+
const t = lines[j].trim();
|
|
4633
|
+
if (inDoc) {
|
|
4634
|
+
if (t.includes(delim)) inDoc = false;
|
|
4635
|
+
continue;
|
|
4636
|
+
}
|
|
4637
|
+
if (t === "" || t.startsWith("#")) continue;
|
|
4638
|
+
const opener = t.startsWith("\"\"\"") ? "\"\"\"" : t.startsWith("'''") ? "'''" : "";
|
|
4639
|
+
if (opener) {
|
|
4640
|
+
if (!t.slice(3).includes(opener)) {
|
|
4641
|
+
inDoc = true;
|
|
4642
|
+
delim = opener;
|
|
4643
|
+
}
|
|
4644
|
+
continue;
|
|
4645
|
+
}
|
|
4646
|
+
count++;
|
|
4647
|
+
}
|
|
4648
|
+
return count;
|
|
4649
|
+
};
|
|
4650
|
+
const findPythonFunctionEnd = (lines, defIndex, bodyStartIndex) => {
|
|
4651
|
+
const baseIndent = lines[defIndex].match(/^(\s*)/)?.[1].length ?? 0;
|
|
4652
|
+
let endLine = bodyStartIndex;
|
|
4269
4653
|
let maxNesting = 0;
|
|
4270
4654
|
const controlIndentStack = [];
|
|
4271
|
-
for (let j =
|
|
4655
|
+
for (let j = bodyStartIndex + 1; j < lines.length; j++) {
|
|
4272
4656
|
const l = lines[j];
|
|
4273
4657
|
if (l.trim() === "") {
|
|
4274
4658
|
endLine = j;
|
|
@@ -4290,7 +4674,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
|
|
|
4290
4674
|
};
|
|
4291
4675
|
};
|
|
4292
4676
|
const findFunctionEnd = (lines, startIndex, isPython) => {
|
|
4293
|
-
if (isPython)
|
|
4677
|
+
if (isPython) {
|
|
4678
|
+
const { sigEndIndex } = extractPythonSignature(lines, startIndex);
|
|
4679
|
+
return findPythonFunctionEnd(lines, startIndex, sigEndIndex);
|
|
4680
|
+
}
|
|
4294
4681
|
return findBraceFunctionEnd(lines, startIndex);
|
|
4295
4682
|
};
|
|
4296
4683
|
const isBlockArrow = (lines, startIndex) => {
|
|
@@ -4312,14 +4699,14 @@ const countTemplateLines = (bodyLines) => {
|
|
|
4312
4699
|
let templateLineCount = 0;
|
|
4313
4700
|
for (const line of bodyLines) {
|
|
4314
4701
|
const startedInside = insideTemplate;
|
|
4315
|
-
let
|
|
4702
|
+
let escaped = false;
|
|
4316
4703
|
for (const ch of line) {
|
|
4317
|
-
if (
|
|
4318
|
-
|
|
4704
|
+
if (escaped) {
|
|
4705
|
+
escaped = false;
|
|
4319
4706
|
continue;
|
|
4320
4707
|
}
|
|
4321
4708
|
if (ch === "\\") {
|
|
4322
|
-
|
|
4709
|
+
escaped = true;
|
|
4323
4710
|
continue;
|
|
4324
4711
|
}
|
|
4325
4712
|
if (ch === "`") insideTemplate = !insideTemplate;
|
|
@@ -4355,7 +4742,7 @@ const FUNCTION_PATTERNS = [
|
|
|
4355
4742
|
]
|
|
4356
4743
|
},
|
|
4357
4744
|
{
|
|
4358
|
-
regex: /^\s*def\s+(\w+)\s*\(
|
|
4745
|
+
regex: /^\s*(?:async\s+)?def\s+(\w+)\s*\(/,
|
|
4359
4746
|
langFilter: [".py"]
|
|
4360
4747
|
},
|
|
4361
4748
|
{
|
|
@@ -4412,14 +4799,23 @@ const analyzeFunctions = (content, ext) => {
|
|
|
4412
4799
|
const isPython = fnMatch.patternIndex === 2;
|
|
4413
4800
|
if (fnMatch.patternIndex === 1 && !isBlockArrow(lines, i)) continue;
|
|
4414
4801
|
const { endLine, maxNesting } = findFunctionEnd(lines, i, isPython);
|
|
4415
|
-
|
|
4416
|
-
|
|
4802
|
+
let templateLines;
|
|
4803
|
+
let paramCount;
|
|
4804
|
+
if (isPython) {
|
|
4805
|
+
const sig = extractPythonSignature(lines, i);
|
|
4806
|
+
const codeLines = countPythonBodyCodeLines(lines, sig.sigEndIndex, endLine);
|
|
4807
|
+
templateLines = endLine - i + 1 - codeLines;
|
|
4808
|
+
paramCount = countPythonParams(sig.params);
|
|
4809
|
+
} else {
|
|
4810
|
+
templateLines = countTemplateLines(lines.slice(i + 1, endLine));
|
|
4811
|
+
paramCount = countParams(fnMatch.params);
|
|
4812
|
+
}
|
|
4417
4813
|
functions.push({
|
|
4418
4814
|
name: fnMatch.name,
|
|
4419
4815
|
startLine: i + 1,
|
|
4420
4816
|
lineCount: endLine - i + 1,
|
|
4421
4817
|
maxNesting,
|
|
4422
|
-
paramCount
|
|
4818
|
+
paramCount,
|
|
4423
4819
|
templateLines
|
|
4424
4820
|
});
|
|
4425
4821
|
}
|
|
@@ -5354,9 +5750,7 @@ const runRuffFormat = async (context) => {
|
|
|
5354
5750
|
};
|
|
5355
5751
|
const parseRuffFormatOutput = (output, rootDir) => {
|
|
5356
5752
|
const diagnostics = [];
|
|
5357
|
-
const
|
|
5358
|
-
let match;
|
|
5359
|
-
while ((match = filePattern.exec(output)) !== null) {
|
|
5753
|
+
for (const match of output.matchAll(/^--- (.+)$/gm)) {
|
|
5360
5754
|
const filePath = getRuffDiagnosticPath(rootDir, match[1]);
|
|
5361
5755
|
diagnostics.push({
|
|
5362
5756
|
filePath,
|
|
@@ -5390,10 +5784,10 @@ const formatEngine = {
|
|
|
5390
5784
|
const { languages, installedTools } = context;
|
|
5391
5785
|
const promises = [];
|
|
5392
5786
|
if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runBiomeFormat(context));
|
|
5393
|
-
if (languages.includes("python") && installedTools
|
|
5394
|
-
if (languages.includes("go") && installedTools
|
|
5395
|
-
if (languages.includes("rust") && installedTools
|
|
5396
|
-
if (languages.includes("ruby") && installedTools
|
|
5787
|
+
if (languages.includes("python") && installedTools.ruff) promises.push(runRuffFormat(context));
|
|
5788
|
+
if (languages.includes("go") && installedTools.gofmt) promises.push(runGofmt(context));
|
|
5789
|
+
if (languages.includes("rust") && installedTools.rustfmt) promises.push(runGenericFormatter(context, "rust"));
|
|
5790
|
+
if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericFormatter(context, "ruby"));
|
|
5397
5791
|
if (languages.includes("php") && installedTools["php-cs-fixer"]) promises.push(runGenericFormatter(context, "php"));
|
|
5398
5792
|
const results = await Promise.allSettled(promises);
|
|
5399
5793
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
@@ -5829,6 +6223,8 @@ const createOxlintConfig = (options) => {
|
|
|
5829
6223
|
if (options.mode === "fix") {
|
|
5830
6224
|
rules["no-unused-vars"] = "off";
|
|
5831
6225
|
rules["react-hooks/exhaustive-deps"] = "off";
|
|
6226
|
+
rules["jsx-a11y/no-aria-hidden-on-focusable"] = "off";
|
|
6227
|
+
rules["unicorn/no-useless-fallback-in-spread"] = "off";
|
|
5832
6228
|
}
|
|
5833
6229
|
const plugins = [
|
|
5834
6230
|
"import",
|
|
@@ -5993,9 +6389,7 @@ const collectAmbientGlobals = (rootDir) => {
|
|
|
5993
6389
|
if (!relativePath.endsWith(".d.ts")) continue;
|
|
5994
6390
|
const content = readTextFile$1(path.join(rootDir, relativePath));
|
|
5995
6391
|
if (!content) continue;
|
|
5996
|
-
AMBIENT_GLOBAL_RE.
|
|
5997
|
-
let match;
|
|
5998
|
-
while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
|
|
6392
|
+
for (const match of content.matchAll(AMBIENT_GLOBAL_RE)) globals.add(match[1]);
|
|
5999
6393
|
}
|
|
6000
6394
|
const deps = collectPackageNames(rootDir);
|
|
6001
6395
|
if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
|
|
@@ -6346,10 +6740,10 @@ const lintEngine = {
|
|
|
6346
6740
|
if (context.config.lint.typecheck) promises.push(import("./typecheck-wVSohmOX.js").then((mod) => mod.runTypecheck(context)));
|
|
6347
6741
|
}
|
|
6348
6742
|
if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
|
|
6349
|
-
if (languages.includes("python") && installedTools
|
|
6743
|
+
if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
|
|
6350
6744
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
6351
|
-
if (languages.includes("rust") && installedTools
|
|
6352
|
-
if (languages.includes("ruby") && installedTools
|
|
6745
|
+
if (languages.includes("rust") && installedTools.cargo) promises.push(runGenericLinter(context, "rust"));
|
|
6746
|
+
if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericLinter(context, "ruby"));
|
|
6353
6747
|
const results = await Promise.allSettled(promises);
|
|
6354
6748
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
6355
6749
|
return {
|
|
@@ -6379,7 +6773,7 @@ const runDependencyAudit = async (context) => {
|
|
|
6379
6773
|
else if (fs.existsSync(path.join(context.rootDirectory, "package-lock.json")) || fs.existsSync(path.join(context.rootDirectory, "package.json"))) promises.push(runNpmAudit(context.rootDirectory, timeout));
|
|
6380
6774
|
}
|
|
6381
6775
|
if (context.languages.includes("python") && context.installedTools["pip-audit"]) promises.push(runPipAudit(context.rootDirectory, timeout));
|
|
6382
|
-
if (context.languages.includes("go") && context.installedTools
|
|
6776
|
+
if (context.languages.includes("go") && context.installedTools.govulncheck) promises.push(runGovulncheck(context.rootDirectory, timeout));
|
|
6383
6777
|
if (context.languages.includes("rust")) promises.push(runCargoAudit(context.rootDirectory, timeout));
|
|
6384
6778
|
const results = await Promise.allSettled(promises);
|
|
6385
6779
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
@@ -6484,9 +6878,12 @@ const parseLegacyAdvisories = (advisories, source) => {
|
|
|
6484
6878
|
for (const [key, advisory] of Object.entries(advisories)) upsertVuln(bucket, advisory.module_name ?? advisory.name ?? advisory.package ?? key, (advisory.severity ?? "moderate").toLowerCase(), advisory.recommendation ?? advisory.title ?? "");
|
|
6485
6879
|
return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
|
|
6486
6880
|
};
|
|
6881
|
+
const carriesAdvisory = (vulnerability) => Array.isArray(vulnerability.via) && vulnerability.via.some((entry) => entry !== null && typeof entry === "object");
|
|
6487
6882
|
const parseModernVulnerabilities = (vulnerabilities, source) => {
|
|
6488
6883
|
const bucket = /* @__PURE__ */ new Map();
|
|
6884
|
+
const hasRootCauses = Object.values(vulnerabilities).some(carriesAdvisory);
|
|
6489
6885
|
for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
|
|
6886
|
+
if (hasRootCauses && !carriesAdvisory(vulnerability)) continue;
|
|
6490
6887
|
const severity = (vulnerability.severity ?? "moderate").toLowerCase();
|
|
6491
6888
|
const fixAvailable = vulnerability.fixAvailable;
|
|
6492
6889
|
const isDirect = vulnerability.isDirect === true;
|
|
@@ -6568,271 +6965,65 @@ const runGovulncheck = async (rootDir, timeout) => {
|
|
|
6568
6965
|
cwd: rootDir,
|
|
6569
6966
|
timeout
|
|
6570
6967
|
});
|
|
6571
|
-
if (!result.stdout) return [];
|
|
6572
|
-
return parseGovulncheckOutput(result.stdout);
|
|
6573
|
-
} catch {
|
|
6574
|
-
return [];
|
|
6575
|
-
}
|
|
6576
|
-
};
|
|
6577
|
-
const toGovulnDiagnostic = (entry) => {
|
|
6578
|
-
if (!entry.vulnerability) return null;
|
|
6579
|
-
return {
|
|
6580
|
-
filePath: "go.mod",
|
|
6581
|
-
engine: "security",
|
|
6582
|
-
rule: "security/vulnerable-dependency",
|
|
6583
|
-
severity: "error",
|
|
6584
|
-
message: `Go vulnerability: ${entry.vulnerability.id ?? "unknown"}`,
|
|
6585
|
-
help: withFixHint(entry.vulnerability.details ?? ""),
|
|
6586
|
-
line: 0,
|
|
6587
|
-
column: 0,
|
|
6588
|
-
category: "Security",
|
|
6589
|
-
fixable: false
|
|
6590
|
-
};
|
|
6591
|
-
};
|
|
6592
|
-
const parseGovulncheckOutput = (output) => {
|
|
6593
|
-
const diagnostics = [];
|
|
6594
|
-
for (const line of output.split("\n")) {
|
|
6595
|
-
if (!line.startsWith("{")) continue;
|
|
6596
|
-
let parsed = null;
|
|
6597
|
-
try {
|
|
6598
|
-
parsed = JSON.parse(line);
|
|
6599
|
-
} catch {
|
|
6600
|
-
parsed = null;
|
|
6601
|
-
}
|
|
6602
|
-
if (!parsed) continue;
|
|
6603
|
-
const diagnostic = toGovulnDiagnostic(parsed);
|
|
6604
|
-
if (diagnostic) diagnostics.push(diagnostic);
|
|
6605
|
-
}
|
|
6606
|
-
return diagnostics;
|
|
6607
|
-
};
|
|
6608
|
-
const runCargoAudit = async (rootDir, timeout) => {
|
|
6609
|
-
try {
|
|
6610
|
-
const result = await runSubprocess("cargo", ["audit", "--json"], {
|
|
6611
|
-
cwd: rootDir,
|
|
6612
|
-
timeout
|
|
6613
|
-
});
|
|
6614
|
-
if (!result.stdout) return [];
|
|
6615
|
-
return (JSON.parse(result.stdout).vulnerabilities?.list ?? []).map((v) => ({
|
|
6616
|
-
filePath: "Cargo.toml",
|
|
6617
|
-
engine: "security",
|
|
6618
|
-
rule: "security/vulnerable-dependency",
|
|
6619
|
-
severity: "error",
|
|
6620
|
-
message: `Rust vulnerability: ${v.advisory?.id ?? "unknown"}`,
|
|
6621
|
-
help: withFixHint(v.advisory?.title ?? ""),
|
|
6622
|
-
line: 0,
|
|
6623
|
-
column: 0,
|
|
6624
|
-
category: "Security",
|
|
6625
|
-
fixable: false
|
|
6626
|
-
}));
|
|
6627
|
-
} catch {
|
|
6628
|
-
return [];
|
|
6629
|
-
}
|
|
6630
|
-
};
|
|
6631
|
-
|
|
6632
|
-
//#endregion
|
|
6633
|
-
//#region src/utils/source-masker.ts
|
|
6634
|
-
const JS_EXTS = new Set([
|
|
6635
|
-
".ts",
|
|
6636
|
-
".tsx",
|
|
6637
|
-
".js",
|
|
6638
|
-
".jsx",
|
|
6639
|
-
".mjs",
|
|
6640
|
-
".cjs"
|
|
6641
|
-
]);
|
|
6642
|
-
const PY_EXTS = new Set([".py"]);
|
|
6643
|
-
const RB_EXTS = new Set([".rb"]);
|
|
6644
|
-
const PHP_EXTS = new Set([".php"]);
|
|
6645
|
-
const familyForExt = (ext) => {
|
|
6646
|
-
if (JS_EXTS.has(ext)) return "js";
|
|
6647
|
-
if (PY_EXTS.has(ext)) return "py";
|
|
6648
|
-
if (RB_EXTS.has(ext)) return "rb";
|
|
6649
|
-
if (PHP_EXTS.has(ext)) return "php";
|
|
6650
|
-
return "none";
|
|
6651
|
-
};
|
|
6652
|
-
const maskStringsAndComments = (content, ext) => {
|
|
6653
|
-
const family = familyForExt(ext);
|
|
6654
|
-
if (family === "none") return content;
|
|
6655
|
-
if (family === "js") return maskJs(content);
|
|
6656
|
-
return maskSimple(content, family);
|
|
6657
|
-
};
|
|
6658
|
-
const handleQuotesAndComments = (content, i, tplStack, mask) => {
|
|
6659
|
-
const len = content.length;
|
|
6660
|
-
const c = content[i];
|
|
6661
|
-
const next = content[i + 1];
|
|
6662
|
-
if (c === "\"" || c === "'") {
|
|
6663
|
-
const strStart = i;
|
|
6664
|
-
const end = consumeQuotedString(content, i, c);
|
|
6665
|
-
mask(strStart + 1, end - 1);
|
|
6666
|
-
return {
|
|
6667
|
-
handled: true,
|
|
6668
|
-
nextI: end
|
|
6669
|
-
};
|
|
6670
|
-
}
|
|
6671
|
-
if (c === "`") {
|
|
6672
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
6673
|
-
mask(i + 1, scan.maskEnd);
|
|
6674
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
6675
|
-
return {
|
|
6676
|
-
handled: true,
|
|
6677
|
-
nextI: scan.resumeAt
|
|
6678
|
-
};
|
|
6679
|
-
}
|
|
6680
|
-
if (c === "/" && next === "/") {
|
|
6681
|
-
const strStart = i;
|
|
6682
|
-
let k = i;
|
|
6683
|
-
while (k < len && content[k] !== "\n") k++;
|
|
6684
|
-
mask(strStart, k);
|
|
6685
|
-
return {
|
|
6686
|
-
handled: true,
|
|
6687
|
-
nextI: k
|
|
6688
|
-
};
|
|
6689
|
-
}
|
|
6690
|
-
if (c === "/" && next === "*") {
|
|
6691
|
-
const strStart = i;
|
|
6692
|
-
let k = i + 2;
|
|
6693
|
-
while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
|
|
6694
|
-
if (k < len - 1) k += 2;
|
|
6695
|
-
mask(strStart, k);
|
|
6696
|
-
return {
|
|
6697
|
-
handled: true,
|
|
6698
|
-
nextI: k
|
|
6699
|
-
};
|
|
6700
|
-
}
|
|
6701
|
-
return {
|
|
6702
|
-
handled: false,
|
|
6703
|
-
nextI: i
|
|
6704
|
-
};
|
|
6705
|
-
};
|
|
6706
|
-
const maskJs = (content) => {
|
|
6707
|
-
const out = content.split("");
|
|
6708
|
-
const len = content.length;
|
|
6709
|
-
const tplStack = [];
|
|
6710
|
-
let i = 0;
|
|
6711
|
-
const mask = (start, end) => {
|
|
6712
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
6713
|
-
};
|
|
6714
|
-
while (i < len) {
|
|
6715
|
-
const c = content[i];
|
|
6716
|
-
if (tplStack.length > 0) {
|
|
6717
|
-
if (c === "{") {
|
|
6718
|
-
tplStack[tplStack.length - 1]++;
|
|
6719
|
-
i++;
|
|
6720
|
-
continue;
|
|
6721
|
-
}
|
|
6722
|
-
if (c === "}") {
|
|
6723
|
-
if (tplStack[tplStack.length - 1] === 0) {
|
|
6724
|
-
tplStack.pop();
|
|
6725
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
6726
|
-
mask(i + 1, scan.maskEnd);
|
|
6727
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
6728
|
-
i = scan.resumeAt;
|
|
6729
|
-
continue;
|
|
6730
|
-
}
|
|
6731
|
-
tplStack[tplStack.length - 1]--;
|
|
6732
|
-
i++;
|
|
6733
|
-
continue;
|
|
6734
|
-
}
|
|
6735
|
-
}
|
|
6736
|
-
const handled = handleQuotesAndComments(content, i, tplStack, mask);
|
|
6737
|
-
if (handled.handled) {
|
|
6738
|
-
i = handled.nextI;
|
|
6739
|
-
continue;
|
|
6740
|
-
}
|
|
6741
|
-
i++;
|
|
6742
|
-
}
|
|
6743
|
-
return out.join("");
|
|
6744
|
-
};
|
|
6745
|
-
const consumeQuotedString = (content, start, quote) => {
|
|
6746
|
-
const len = content.length;
|
|
6747
|
-
let i = start + 1;
|
|
6748
|
-
while (i < len) {
|
|
6749
|
-
const c = content[i];
|
|
6750
|
-
if (c === "\\" && i + 1 < len) {
|
|
6751
|
-
i += 2;
|
|
6752
|
-
continue;
|
|
6753
|
-
}
|
|
6754
|
-
if (c === quote) return i + 1;
|
|
6755
|
-
if (c === "\n") return i;
|
|
6756
|
-
i++;
|
|
6757
|
-
}
|
|
6758
|
-
return i;
|
|
6759
|
-
};
|
|
6760
|
-
const consumeTemplateString = (content, start) => {
|
|
6761
|
-
const len = content.length;
|
|
6762
|
-
let i = start;
|
|
6763
|
-
while (i < len) {
|
|
6764
|
-
const c = content[i];
|
|
6765
|
-
if (c === "\\" && i + 1 < len) {
|
|
6766
|
-
i += 2;
|
|
6767
|
-
continue;
|
|
6768
|
-
}
|
|
6769
|
-
if (c === "`") return {
|
|
6770
|
-
maskEnd: i,
|
|
6771
|
-
resumeAt: i + 1,
|
|
6772
|
-
openedInterp: false
|
|
6773
|
-
};
|
|
6774
|
-
if (c === "$" && content[i + 1] === "{") return {
|
|
6775
|
-
maskEnd: i,
|
|
6776
|
-
resumeAt: i + 2,
|
|
6777
|
-
openedInterp: true
|
|
6778
|
-
};
|
|
6779
|
-
i++;
|
|
6780
|
-
}
|
|
6781
|
-
return {
|
|
6782
|
-
maskEnd: i,
|
|
6783
|
-
resumeAt: i,
|
|
6784
|
-
openedInterp: false
|
|
6785
|
-
};
|
|
6786
|
-
};
|
|
6787
|
-
const maskSimple = (content, family) => {
|
|
6788
|
-
const out = content.split("");
|
|
6789
|
-
const len = content.length;
|
|
6790
|
-
let i = 0;
|
|
6791
|
-
const mask = (start, end) => {
|
|
6792
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
6793
|
-
};
|
|
6794
|
-
while (i < len) {
|
|
6795
|
-
const c = content[i];
|
|
6796
|
-
const next = content[i + 1];
|
|
6797
|
-
if (family === "py" && (c === "\"" || c === "'")) {
|
|
6798
|
-
if (content[i + 1] === c && content[i + 2] === c) {
|
|
6799
|
-
const triple = c + c + c;
|
|
6800
|
-
const end = content.indexOf(triple, i + 3);
|
|
6801
|
-
const stop = end === -1 ? len : end + 3;
|
|
6802
|
-
mask(i + 3, stop - 3);
|
|
6803
|
-
i = stop;
|
|
6804
|
-
continue;
|
|
6805
|
-
}
|
|
6806
|
-
}
|
|
6807
|
-
if (c === "\"" || c === "'") {
|
|
6808
|
-
const strStart = i;
|
|
6809
|
-
i = consumeQuotedString(content, i, c);
|
|
6810
|
-
mask(strStart + 1, i - 1);
|
|
6811
|
-
continue;
|
|
6812
|
-
}
|
|
6813
|
-
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
6814
|
-
const strStart = i;
|
|
6815
|
-
while (i < len && content[i] !== "\n") i++;
|
|
6816
|
-
mask(strStart, i);
|
|
6817
|
-
continue;
|
|
6818
|
-
}
|
|
6819
|
-
if (family === "php" && c === "/" && next === "/") {
|
|
6820
|
-
const strStart = i;
|
|
6821
|
-
while (i < len && content[i] !== "\n") i++;
|
|
6822
|
-
mask(strStart, i);
|
|
6823
|
-
continue;
|
|
6824
|
-
}
|
|
6825
|
-
if (family === "php" && c === "/" && next === "*") {
|
|
6826
|
-
const strStart = i;
|
|
6827
|
-
i += 2;
|
|
6828
|
-
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
6829
|
-
if (i < len - 1) i += 2;
|
|
6830
|
-
mask(strStart, i);
|
|
6831
|
-
continue;
|
|
6968
|
+
if (!result.stdout) return [];
|
|
6969
|
+
return parseGovulncheckOutput(result.stdout);
|
|
6970
|
+
} catch {
|
|
6971
|
+
return [];
|
|
6972
|
+
}
|
|
6973
|
+
};
|
|
6974
|
+
const toGovulnDiagnostic = (entry) => {
|
|
6975
|
+
if (!entry.vulnerability) return null;
|
|
6976
|
+
return {
|
|
6977
|
+
filePath: "go.mod",
|
|
6978
|
+
engine: "security",
|
|
6979
|
+
rule: "security/vulnerable-dependency",
|
|
6980
|
+
severity: "error",
|
|
6981
|
+
message: `Go vulnerability: ${entry.vulnerability.id ?? "unknown"}`,
|
|
6982
|
+
help: withFixHint(entry.vulnerability.details ?? ""),
|
|
6983
|
+
line: 0,
|
|
6984
|
+
column: 0,
|
|
6985
|
+
category: "Security",
|
|
6986
|
+
fixable: false
|
|
6987
|
+
};
|
|
6988
|
+
};
|
|
6989
|
+
const parseGovulncheckOutput = (output) => {
|
|
6990
|
+
const diagnostics = [];
|
|
6991
|
+
for (const line of output.split("\n")) {
|
|
6992
|
+
if (!line.startsWith("{")) continue;
|
|
6993
|
+
let parsed = null;
|
|
6994
|
+
try {
|
|
6995
|
+
parsed = JSON.parse(line);
|
|
6996
|
+
} catch {
|
|
6997
|
+
parsed = null;
|
|
6832
6998
|
}
|
|
6833
|
-
|
|
6999
|
+
if (!parsed) continue;
|
|
7000
|
+
const diagnostic = toGovulnDiagnostic(parsed);
|
|
7001
|
+
if (diagnostic) diagnostics.push(diagnostic);
|
|
7002
|
+
}
|
|
7003
|
+
return diagnostics;
|
|
7004
|
+
};
|
|
7005
|
+
const runCargoAudit = async (rootDir, timeout) => {
|
|
7006
|
+
try {
|
|
7007
|
+
const result = await runSubprocess("cargo", ["audit", "--json"], {
|
|
7008
|
+
cwd: rootDir,
|
|
7009
|
+
timeout
|
|
7010
|
+
});
|
|
7011
|
+
if (!result.stdout) return [];
|
|
7012
|
+
return (JSON.parse(result.stdout).vulnerabilities?.list ?? []).map((v) => ({
|
|
7013
|
+
filePath: "Cargo.toml",
|
|
7014
|
+
engine: "security",
|
|
7015
|
+
rule: "security/vulnerable-dependency",
|
|
7016
|
+
severity: "error",
|
|
7017
|
+
message: `Rust vulnerability: ${v.advisory?.id ?? "unknown"}`,
|
|
7018
|
+
help: withFixHint(v.advisory?.title ?? ""),
|
|
7019
|
+
line: 0,
|
|
7020
|
+
column: 0,
|
|
7021
|
+
category: "Security",
|
|
7022
|
+
fixable: false
|
|
7023
|
+
}));
|
|
7024
|
+
} catch {
|
|
7025
|
+
return [];
|
|
6834
7026
|
}
|
|
6835
|
-
return out.join("");
|
|
6836
7027
|
};
|
|
6837
7028
|
|
|
6838
7029
|
//#endregion
|
|
@@ -6980,8 +7171,7 @@ const detectRiskyConstructs = async (context) => {
|
|
|
6980
7171
|
if (!extensions.includes(ext)) continue;
|
|
6981
7172
|
if (isMigrationOrSeeder && name === "sql-injection") continue;
|
|
6982
7173
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
6983
|
-
|
|
6984
|
-
while ((match = regex.exec(masked)) !== null) {
|
|
7174
|
+
for (const match of masked.matchAll(regex)) {
|
|
6985
7175
|
const line = content.slice(0, match.index).split("\n").length;
|
|
6986
7176
|
if (name === "innerhtml") {
|
|
6987
7177
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
@@ -7109,11 +7299,11 @@ const scanSecrets = async (context) => {
|
|
|
7109
7299
|
} catch {
|
|
7110
7300
|
continue;
|
|
7111
7301
|
}
|
|
7302
|
+
content = maskComments(content, path.extname(filePath));
|
|
7112
7303
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
7113
7304
|
for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
|
|
7114
7305
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
7115
|
-
|
|
7116
|
-
while ((match = regex.exec(content)) !== null) {
|
|
7306
|
+
for (const match of content.matchAll(regex)) {
|
|
7117
7307
|
if (isPlaceholderValue(match[1] ?? match[0])) continue;
|
|
7118
7308
|
if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
|
|
7119
7309
|
const line = content.slice(0, match.index).split("\n").length;
|
|
@@ -7234,6 +7424,64 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
7234
7424
|
|
|
7235
7425
|
//#endregion
|
|
7236
7426
|
//#region src/utils/discover.ts
|
|
7427
|
+
const UNSUPPORTED_CODE_EXTENSIONS = {
|
|
7428
|
+
".c": "C/C++",
|
|
7429
|
+
".h": "C/C++",
|
|
7430
|
+
".cc": "C/C++",
|
|
7431
|
+
".cpp": "C/C++",
|
|
7432
|
+
".cxx": "C/C++",
|
|
7433
|
+
".hpp": "C/C++",
|
|
7434
|
+
".hh": "C/C++",
|
|
7435
|
+
".hxx": "C/C++",
|
|
7436
|
+
".cs": "C#",
|
|
7437
|
+
".swift": "Swift",
|
|
7438
|
+
".kt": "Kotlin",
|
|
7439
|
+
".kts": "Kotlin",
|
|
7440
|
+
".m": "Objective-C",
|
|
7441
|
+
".mm": "Objective-C",
|
|
7442
|
+
".scala": "Scala",
|
|
7443
|
+
".dart": "Dart",
|
|
7444
|
+
".ex": "Elixir",
|
|
7445
|
+
".exs": "Elixir",
|
|
7446
|
+
".erl": "Erlang",
|
|
7447
|
+
".hs": "Haskell",
|
|
7448
|
+
".clj": "Clojure",
|
|
7449
|
+
".cljs": "Clojure",
|
|
7450
|
+
".lua": "Lua",
|
|
7451
|
+
".jl": "Julia",
|
|
7452
|
+
".zig": "Zig",
|
|
7453
|
+
".nim": "Nim",
|
|
7454
|
+
".ml": "OCaml",
|
|
7455
|
+
".fs": "F#",
|
|
7456
|
+
".sol": "Solidity",
|
|
7457
|
+
".groovy": "Groovy"
|
|
7458
|
+
};
|
|
7459
|
+
const analyzeCoverage = (rootDirectory, excludePatterns = []) => {
|
|
7460
|
+
const allFiles = listProjectFiles(rootDirectory);
|
|
7461
|
+
const supportedFiles = filterProjectFiles(rootDirectory, allFiles, [], excludePatterns).length;
|
|
7462
|
+
const counts = /* @__PURE__ */ new Map();
|
|
7463
|
+
let unsupportedFiles = 0;
|
|
7464
|
+
const candidates = filterProjectFiles(rootDirectory, allFiles, Object.keys(UNSUPPORTED_CODE_EXTENSIONS), excludePatterns);
|
|
7465
|
+
for (const file of candidates) {
|
|
7466
|
+
const lang = UNSUPPORTED_CODE_EXTENSIONS[path.extname(file).toLowerCase()];
|
|
7467
|
+
if (!lang) continue;
|
|
7468
|
+
unsupportedFiles += 1;
|
|
7469
|
+
counts.set(lang, (counts.get(lang) ?? 0) + 1);
|
|
7470
|
+
}
|
|
7471
|
+
let dominantUnsupported = null;
|
|
7472
|
+
let max = 0;
|
|
7473
|
+
for (const [lang, count] of counts) if (count > max) {
|
|
7474
|
+
max = count;
|
|
7475
|
+
dominantUnsupported = lang;
|
|
7476
|
+
}
|
|
7477
|
+
const negligible = supportedFiles === 0 || unsupportedFiles >= 10 && unsupportedFiles > supportedFiles * 3;
|
|
7478
|
+
return {
|
|
7479
|
+
supportedFiles,
|
|
7480
|
+
unsupportedFiles,
|
|
7481
|
+
dominantUnsupported,
|
|
7482
|
+
scoreable: !negligible
|
|
7483
|
+
};
|
|
7484
|
+
};
|
|
7237
7485
|
const LANGUAGE_SIGNALS = {
|
|
7238
7486
|
"tsconfig.json": "typescript",
|
|
7239
7487
|
"go.mod": "go",
|
|
@@ -7353,11 +7601,12 @@ const checkInstalledTools = async () => {
|
|
|
7353
7601
|
}));
|
|
7354
7602
|
return results;
|
|
7355
7603
|
};
|
|
7356
|
-
const discoverProject = async (directory) => {
|
|
7604
|
+
const discoverProject = async (directory, excludePatterns = []) => {
|
|
7357
7605
|
const resolvedDir = path.resolve(directory);
|
|
7358
7606
|
const languages = detectLanguages(resolvedDir);
|
|
7359
7607
|
const frameworks = detectFrameworks(resolvedDir);
|
|
7360
7608
|
const sourceFileCount = countSourceFiles(resolvedDir);
|
|
7609
|
+
const coverage = analyzeCoverage(resolvedDir, excludePatterns);
|
|
7361
7610
|
const installedTools = await checkInstalledTools();
|
|
7362
7611
|
return {
|
|
7363
7612
|
rootDirectory: resolvedDir,
|
|
@@ -7365,6 +7614,7 @@ const discoverProject = async (directory) => {
|
|
|
7365
7614
|
languages,
|
|
7366
7615
|
frameworks,
|
|
7367
7616
|
sourceFileCount,
|
|
7617
|
+
coverage,
|
|
7368
7618
|
installedTools
|
|
7369
7619
|
};
|
|
7370
7620
|
};
|
|
@@ -8071,7 +8321,7 @@ const uninstallRulesOnly = (opts, paths) => {
|
|
|
8071
8321
|
else result.skipped.push(paths.rules);
|
|
8072
8322
|
if (paths.host && paths.marker) {
|
|
8073
8323
|
const host = readIfExists(paths.host);
|
|
8074
|
-
if (host
|
|
8324
|
+
if (host?.includes(paths.marker)) {
|
|
8075
8325
|
const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
8076
8326
|
applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
|
|
8077
8327
|
} else result.skipped.push(paths.host);
|
|
@@ -8487,7 +8737,7 @@ const uninstallGemini = (opts) => {
|
|
|
8487
8737
|
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
8488
8738
|
else result.skipped.push(paths.aislopMd);
|
|
8489
8739
|
const geminiMd = readIfExists(paths.geminiMd);
|
|
8490
|
-
if (geminiMd
|
|
8740
|
+
if (geminiMd?.includes("@AISLOP.md")) {
|
|
8491
8741
|
const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
8492
8742
|
applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
8493
8743
|
} else result.skipped.push(paths.geminiMd);
|
|
@@ -8784,6 +9034,7 @@ const theme = createTheme();
|
|
|
8784
9034
|
|
|
8785
9035
|
//#endregion
|
|
8786
9036
|
//#region src/commands/hook.ts
|
|
9037
|
+
const HOOK_FLUSH_TIMEOUT_MS = 1500;
|
|
8787
9038
|
const AGENT_LABELS = {
|
|
8788
9039
|
claude: {
|
|
8789
9040
|
label: "Claude Code",
|
|
@@ -8912,6 +9163,7 @@ const hookRun = async (agent, flags) => {
|
|
|
8912
9163
|
process.stderr.write(`hook: agent "${agent}" has no runtime adapter (rules-file-only)\n`);
|
|
8913
9164
|
process.exit(0);
|
|
8914
9165
|
}
|
|
9166
|
+
await flushTelemetry(HOOK_FLUSH_TIMEOUT_MS);
|
|
8915
9167
|
process.exit(exitCode);
|
|
8916
9168
|
};
|
|
8917
9169
|
const hookBaseline = async () => {
|
|
@@ -9165,12 +9417,12 @@ const badgeCommand = async (options = {}) => {
|
|
|
9165
9417
|
svgUrl,
|
|
9166
9418
|
pageUrl
|
|
9167
9419
|
});
|
|
9168
|
-
if (options.json) process.stdout.write(JSON.stringify({
|
|
9420
|
+
if (options.json) process.stdout.write(`${JSON.stringify({
|
|
9169
9421
|
owner,
|
|
9170
9422
|
repo,
|
|
9171
9423
|
svgUrl,
|
|
9172
9424
|
pageUrl
|
|
9173
|
-
})
|
|
9425
|
+
})}\n`);
|
|
9174
9426
|
else process.stdout.write(output);
|
|
9175
9427
|
return {
|
|
9176
9428
|
owner,
|
|
@@ -9471,7 +9723,7 @@ const renderHeader = (input, _deps = {}) => {
|
|
|
9471
9723
|
|
|
9472
9724
|
//#endregion
|
|
9473
9725
|
//#region src/ui/width.ts
|
|
9474
|
-
const ANSI_RE = new RegExp(
|
|
9726
|
+
const ANSI_RE = new RegExp(`\\[[0-9;]*m`, "g");
|
|
9475
9727
|
const stripAnsi = (s) => s.replace(ANSI_RE, "");
|
|
9476
9728
|
const stringWidth = (s) => {
|
|
9477
9729
|
const bare = stripAnsi(s);
|
|
@@ -9615,6 +9867,8 @@ var LiveGrid = class {
|
|
|
9615
9867
|
const RULE_LABELS = {
|
|
9616
9868
|
formatting: "Code not formatted",
|
|
9617
9869
|
"code-quality/duplicate-block": "Duplicate code block",
|
|
9870
|
+
"code-quality/repeated-chained-call": "Repeated chained call",
|
|
9871
|
+
"code-quality/unused-declaration": "Unused declaration",
|
|
9618
9872
|
"complexity/file-too-large": "File too large",
|
|
9619
9873
|
"complexity/function-too-long": "Function too long",
|
|
9620
9874
|
"complexity/deep-nesting": "Deeply nested code",
|
|
@@ -9627,6 +9881,7 @@ const RULE_LABELS = {
|
|
|
9627
9881
|
"knip/binaries": "Unused binary",
|
|
9628
9882
|
"knip/exports": "Unused export",
|
|
9629
9883
|
"knip/types": "Unused type",
|
|
9884
|
+
"knip/duplicates": "Duplicate export",
|
|
9630
9885
|
"ai-slop/trivial-comment": "Trivial restating comment",
|
|
9631
9886
|
"ai-slop/swallowed-exception": "Empty catch (swallowed error)",
|
|
9632
9887
|
"ai-slop/silent-recovery": "Catch logs then continues",
|
|
@@ -9663,6 +9918,7 @@ const RULE_LABELS = {
|
|
|
9663
9918
|
"ai-slop/hallucinated-import": "Import not in package.json",
|
|
9664
9919
|
"security/hardcoded-secret": "Possible hardcoded secret",
|
|
9665
9920
|
"security/vulnerable-dependency": "Vulnerable dependency",
|
|
9921
|
+
"security/dependency-audit-skipped": "Dependency audit skipped",
|
|
9666
9922
|
"security/eval": "eval() usage",
|
|
9667
9923
|
"security/innerhtml": "innerHTML assignment",
|
|
9668
9924
|
"security/dangerously-set-innerhtml": "dangerouslySetInnerHTML (XSS risk)",
|
|
@@ -9820,6 +10076,36 @@ const readHistory = (directory) => {
|
|
|
9820
10076
|
return records;
|
|
9821
10077
|
};
|
|
9822
10078
|
|
|
10079
|
+
//#endregion
|
|
10080
|
+
//#region src/commands/scan-coverage.ts
|
|
10081
|
+
const coverageReason = (c) => {
|
|
10082
|
+
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.`;
|
|
10083
|
+
if (c.supportedFiles === 0) return "No files in a language aislop analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Java). Nothing to score.";
|
|
10084
|
+
const lang = c.dominantUnsupported ?? "an unsupported language";
|
|
10085
|
+
const files = `${c.supportedFiles} supported file${c.supportedFiles === 1 ? "" : "s"}`;
|
|
10086
|
+
return `This repository is mostly ${lang} (${c.unsupportedFiles} files); aislop analyzed only ${files}. Score withheld — it would represent a sliver of the codebase.`;
|
|
10087
|
+
};
|
|
10088
|
+
const renderCoverageNotice = (projectInfo, includeHeader) => {
|
|
10089
|
+
const deps = {
|
|
10090
|
+
theme: createTheme(),
|
|
10091
|
+
symbols: createSymbols({ plain: false })
|
|
10092
|
+
};
|
|
10093
|
+
return `${includeHeader === false ? "" : renderHeader({
|
|
10094
|
+
version: APP_VERSION,
|
|
10095
|
+
command: "scan",
|
|
10096
|
+
context: [
|
|
10097
|
+
projectInfo.projectName,
|
|
10098
|
+
projectInfo.languages[0] ?? "unknown",
|
|
10099
|
+
`${projectInfo.sourceFileCount} files`
|
|
10100
|
+
],
|
|
10101
|
+
brand: true
|
|
10102
|
+
}, deps)} ${coverageReason(projectInfo.coverage)}\n\n`;
|
|
10103
|
+
};
|
|
10104
|
+
|
|
10105
|
+
//#endregion
|
|
10106
|
+
//#region src/commands/scan-exit-code.ts
|
|
10107
|
+
const computeScanExitCode = (opts) => opts.hasErrors || opts.scoreable && opts.score < opts.failBelow ? 1 : 0;
|
|
10108
|
+
|
|
9823
10109
|
//#endregion
|
|
9824
10110
|
//#region src/commands/scan.ts
|
|
9825
10111
|
const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
|
|
@@ -9926,7 +10212,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
9926
10212
|
else log.error(msg);
|
|
9927
10213
|
return { exitCode: 1 };
|
|
9928
10214
|
}
|
|
9929
|
-
const projectInfo = await discoverProject(resolvedDir);
|
|
10215
|
+
const projectInfo = await discoverProject(resolvedDir, [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)]);
|
|
9930
10216
|
return withCommandLifecycle({
|
|
9931
10217
|
command: options.command ?? "scan",
|
|
9932
10218
|
config: config.telemetry,
|
|
@@ -9939,15 +10225,16 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
9939
10225
|
const showHeader = options.showHeader !== false;
|
|
9940
10226
|
const machineOutput = isMachineOutput(options);
|
|
9941
10227
|
const useLiveProgress = !machineOutput && shouldUseSpinner();
|
|
10228
|
+
const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
|
|
9942
10229
|
let files;
|
|
9943
10230
|
if (options.staged) {
|
|
9944
|
-
files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [],
|
|
10231
|
+
files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], excludePatterns);
|
|
9945
10232
|
if (!machineOutput) log.muted(`Scope: ${files.length} staged file(s)`);
|
|
9946
10233
|
} else if (options.changes) {
|
|
9947
|
-
files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [],
|
|
10234
|
+
files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], excludePatterns);
|
|
9948
10235
|
if (!machineOutput) log.muted(`Scope: ${files.length} changed file(s)`);
|
|
9949
10236
|
} else {
|
|
9950
|
-
files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [],
|
|
10237
|
+
files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], excludePatterns);
|
|
9951
10238
|
if (!machineOutput) log.muted(`Scope: ${files.length} file(s) after exclusions`);
|
|
9952
10239
|
}
|
|
9953
10240
|
const configDir = findConfigDir(resolvedDir);
|
|
@@ -10001,14 +10288,21 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10001
10288
|
if (!machineOutput && !progressRenderer) printEngineStatus(result);
|
|
10002
10289
|
});
|
|
10003
10290
|
progressRenderer?.stop();
|
|
10004
|
-
const results = rawResults.map((result) => ({
|
|
10291
|
+
const { results, suppressedCount } = applySuppressions(rawResults.map((result) => ({
|
|
10005
10292
|
...result,
|
|
10006
10293
|
diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
|
|
10007
|
-
}));
|
|
10294
|
+
})), resolvedDir);
|
|
10295
|
+
if (suppressedCount > 0 && !machineOutput) log.muted(`Suppressed ${suppressedCount} finding(s) via aislop-ignore directives`);
|
|
10008
10296
|
const allDiagnostics = results.flatMap((r) => r.diagnostics);
|
|
10009
10297
|
const elapsedMs = performance.now() - startTime;
|
|
10010
10298
|
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
10011
|
-
const
|
|
10299
|
+
const scoreable = projectInfo.coverage.scoreable;
|
|
10300
|
+
const exitCode = computeScanExitCode({
|
|
10301
|
+
hasErrors: allDiagnostics.some((d) => d.severity === "error"),
|
|
10302
|
+
scoreable,
|
|
10303
|
+
score: scoreResult.score,
|
|
10304
|
+
failBelow: config.ci.failBelow
|
|
10305
|
+
});
|
|
10012
10306
|
const engineIssues = {};
|
|
10013
10307
|
const engineTimings = {};
|
|
10014
10308
|
for (const r of results) {
|
|
@@ -10017,7 +10311,8 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10017
10311
|
}
|
|
10018
10312
|
const completion = {
|
|
10019
10313
|
exitCode,
|
|
10020
|
-
score: scoreResult.score,
|
|
10314
|
+
score: scoreable ? scoreResult.score : null,
|
|
10315
|
+
scoreable,
|
|
10021
10316
|
findingCount: allDiagnostics.length,
|
|
10022
10317
|
errorCount: allDiagnostics.filter((d) => d.severity === "error").length,
|
|
10023
10318
|
warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
|
|
@@ -10031,11 +10326,18 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10031
10326
|
return completion;
|
|
10032
10327
|
}
|
|
10033
10328
|
if (options.json) {
|
|
10034
|
-
const { buildJsonOutput } = await import("./json-
|
|
10035
|
-
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
10329
|
+
const { buildJsonOutput } = await import("./json-CXV4D0Ib.js");
|
|
10330
|
+
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs, projectInfo.coverage);
|
|
10036
10331
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
10037
10332
|
return completion;
|
|
10038
10333
|
}
|
|
10334
|
+
if (!scoreable) {
|
|
10335
|
+
if (!machineOutput) {
|
|
10336
|
+
process.stdout.write(renderCoverageNotice(projectInfo, showHeader));
|
|
10337
|
+
if (allDiagnostics.length > 0) process.stdout.write(renderDiagnostics(allDiagnostics, options.verbose ?? false));
|
|
10338
|
+
}
|
|
10339
|
+
return completion;
|
|
10340
|
+
}
|
|
10039
10341
|
if (!options.staged && !options.changes && options.command !== "ci" && !isCiEnv()) appendHistory({
|
|
10040
10342
|
directory: resolvedDir,
|
|
10041
10343
|
score: scoreResult.score,
|
|
@@ -10815,7 +11117,7 @@ const ERROR_MESSAGE_PATTERNS = [
|
|
|
10815
11117
|
/**
|
|
10816
11118
|
* Extracts the full text of a console statement spanning multiple lines.
|
|
10817
11119
|
*/
|
|
10818
|
-
const getStatementText = (lines,
|
|
11120
|
+
const getStatementText = (lines, span) => {
|
|
10819
11121
|
const spanLines = [];
|
|
10820
11122
|
for (const lineNo of span) spanLines.push(lines[lineNo - 1]);
|
|
10821
11123
|
return spanLines.join("\n");
|
|
@@ -10827,6 +11129,21 @@ const getStatementText = (lines, startIndex, span) => {
|
|
|
10827
11129
|
const shouldUpgradeToError = (statementText) => {
|
|
10828
11130
|
return ERROR_MESSAGE_PATTERNS.some((pattern) => pattern.test(statementText));
|
|
10829
11131
|
};
|
|
11132
|
+
const DIAGNOSTIC_PATH_RE = /(?:^|\/)(?:tools|scripts|cli|bin)\/|(?:^|\/)test-[^/]*\.[tj]sx?$|[.-](?:test|spec)\.[tj]sx?$/i;
|
|
11133
|
+
const isDiagnosticScriptPath = (filePath) => DIAGNOSTIC_PATH_RE.test(filePath.replace(/\\/g, "/"));
|
|
11134
|
+
const firstNonBlank = (lines, from, step, skip) => {
|
|
11135
|
+
for (let i = from; i >= 0 && i < lines.length; i += step) {
|
|
11136
|
+
if (skip.has(i + 1)) continue;
|
|
11137
|
+
if (lines[i].trim() !== "") return lines[i].trim();
|
|
11138
|
+
}
|
|
11139
|
+
return "";
|
|
11140
|
+
};
|
|
11141
|
+
const wouldEmptyEnclosingBlock = (lines, span, removed) => {
|
|
11142
|
+
const sorted = [...span].sort((a, b) => a - b);
|
|
11143
|
+
const before = firstNonBlank(lines, sorted[0] - 2, -1, removed);
|
|
11144
|
+
const after = firstNonBlank(lines, sorted[sorted.length - 1], 1, removed);
|
|
11145
|
+
return before.endsWith("{") && after.startsWith("}");
|
|
11146
|
+
};
|
|
10830
11147
|
const fixDeadPatterns = async (context) => {
|
|
10831
11148
|
const fixable = [...await detectTrivialComments(context), ...await detectDeadPatterns(context)].filter((d) => d.fixable);
|
|
10832
11149
|
if (fixable.length === 0) return;
|
|
@@ -10840,24 +11157,31 @@ const fixDeadPatterns = async (context) => {
|
|
|
10840
11157
|
});
|
|
10841
11158
|
byFile.set(absolute, entries);
|
|
10842
11159
|
}
|
|
10843
|
-
for (const [filePath, entries] of byFile) fixFileDeadPatterns(filePath, entries);
|
|
11160
|
+
for (const [filePath, entries] of byFile) fixFileDeadPatterns(filePath, entries, context.rootDirectory);
|
|
10844
11161
|
};
|
|
10845
|
-
const fixFileDeadPatterns = (filePath, entries) => {
|
|
11162
|
+
const fixFileDeadPatterns = (filePath, entries, rootDirectory) => {
|
|
10846
11163
|
if (!fs.existsSync(filePath)) return;
|
|
10847
11164
|
const lines = fs.readFileSync(filePath, "utf-8").split("\n");
|
|
10848
11165
|
const linesToRemove = /* @__PURE__ */ new Set();
|
|
10849
11166
|
const lineReplacements = /* @__PURE__ */ new Map();
|
|
11167
|
+
const skipConsole = isDiagnosticScriptPath(path.relative(rootDirectory, filePath));
|
|
11168
|
+
const consoleSpans = [];
|
|
10850
11169
|
for (const entry of entries) {
|
|
10851
11170
|
const index = entry.line - 1;
|
|
10852
11171
|
if (index < 0 || index >= lines.length) continue;
|
|
10853
11172
|
if (entry.rule === "ai-slop/console-leftover") {
|
|
11173
|
+
if (skipConsole) continue;
|
|
10854
11174
|
const span = findStatementSpan(lines, index);
|
|
10855
|
-
if (shouldUpgradeToError(getStatementText(lines,
|
|
10856
|
-
|
|
10857
|
-
lineReplacements.set(entry.line, replaced);
|
|
10858
|
-
} else for (const lineNo of span) linesToRemove.add(lineNo);
|
|
11175
|
+
if (shouldUpgradeToError(getStatementText(lines, span))) lineReplacements.set(entry.line, lines[index].replace(/console\.(?:log|debug|info|trace|dir|table)\s*\(/, "console.error("));
|
|
11176
|
+
else consoleSpans.push(span);
|
|
10859
11177
|
} else linesToRemove.add(entry.line);
|
|
10860
11178
|
}
|
|
11179
|
+
const candidateLines = /* @__PURE__ */ new Set();
|
|
11180
|
+
for (const span of consoleSpans) for (const lineNo of span) candidateLines.add(lineNo);
|
|
11181
|
+
for (const span of consoleSpans) {
|
|
11182
|
+
if (wouldEmptyEnclosingBlock(lines, span, candidateLines)) continue;
|
|
11183
|
+
for (const lineNo of span) linesToRemove.add(lineNo);
|
|
11184
|
+
}
|
|
10861
11185
|
const result = applyEditsAndCollapse(lines, linesToRemove, lineReplacements);
|
|
10862
11186
|
fs.writeFileSync(filePath, result);
|
|
10863
11187
|
};
|
|
@@ -11107,7 +11431,7 @@ const fixUnusedImports = async (context) => {
|
|
|
11107
11431
|
const importSpan = JS_EXTENSIONS$1.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
|
|
11108
11432
|
if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
|
|
11109
11433
|
else if (JS_EXTENSIONS$1.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
11110
|
-
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx,
|
|
11434
|
+
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, unusedNames);
|
|
11111
11435
|
}
|
|
11112
11436
|
if (linesToRemove.size === 0 && unused.length === 0) continue;
|
|
11113
11437
|
const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
|
|
@@ -11171,9 +11495,12 @@ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
|
|
|
11171
11495
|
lines[span[0]] = newImport;
|
|
11172
11496
|
for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
|
|
11173
11497
|
};
|
|
11174
|
-
const rewritePyImportLine = (lines, lineIdx,
|
|
11498
|
+
const rewritePyImportLine = (lines, lineIdx, unusedNames) => {
|
|
11175
11499
|
const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
|
|
11176
|
-
if (!fromMatch)
|
|
11500
|
+
if (!fromMatch) {
|
|
11501
|
+
rewritePlainPyImportLine(lines, lineIdx, unusedNames);
|
|
11502
|
+
return;
|
|
11503
|
+
}
|
|
11177
11504
|
const prefix = fromMatch[1];
|
|
11178
11505
|
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
11179
11506
|
const hasParen = importPart.startsWith("(");
|
|
@@ -11186,6 +11513,19 @@ const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
|
|
|
11186
11513
|
const joined = keptSpecifiers.join(", ");
|
|
11187
11514
|
lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
|
|
11188
11515
|
};
|
|
11516
|
+
const rewritePlainPyImportLine = (lines, lineIdx, unusedNames) => {
|
|
11517
|
+
const match = lines[lineIdx].match(/^(\s*import\s+)(.+)$/);
|
|
11518
|
+
if (!match) return;
|
|
11519
|
+
const prefix = match[1];
|
|
11520
|
+
const specifiers = match[2].replace(/#.*$/, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
11521
|
+
const kept = specifiers.filter((spec) => {
|
|
11522
|
+
const parts = spec.split(/\s+as\s+/);
|
|
11523
|
+
const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim().split(".")[0];
|
|
11524
|
+
return !unusedNames.has(localName);
|
|
11525
|
+
});
|
|
11526
|
+
if (kept.length === 0 || kept.length === specifiers.length) return;
|
|
11527
|
+
lines[lineIdx] = `${prefix}${kept.join(", ")}`;
|
|
11528
|
+
};
|
|
11189
11529
|
|
|
11190
11530
|
//#endregion
|
|
11191
11531
|
//#region src/engines/code-quality/unused-removal-ast.ts
|
|
@@ -11627,6 +11967,61 @@ const runExpoDoctor = async (context) => {
|
|
|
11627
11967
|
return toDiagnostics(parseIssues(output));
|
|
11628
11968
|
};
|
|
11629
11969
|
|
|
11970
|
+
//#endregion
|
|
11971
|
+
//#region src/commands/fix-expo.ts
|
|
11972
|
+
const INSTALL_TIMEOUT$1 = 1800 * 1e3;
|
|
11973
|
+
const fixExpoDependencies = async (context, onProgress) => {
|
|
11974
|
+
await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
|
|
11975
|
+
onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
|
|
11976
|
+
if ((await runSubprocess("npx", [
|
|
11977
|
+
"--yes",
|
|
11978
|
+
"expo",
|
|
11979
|
+
"install",
|
|
11980
|
+
"--fix"
|
|
11981
|
+
], {
|
|
11982
|
+
cwd: context.rootDirectory,
|
|
11983
|
+
timeout: INSTALL_TIMEOUT$1
|
|
11984
|
+
})).exitCode === 0) return;
|
|
11985
|
+
onProgress?.("Expo dependency alignment · checking remaining issues");
|
|
11986
|
+
const checkResult = await runSubprocess("npx", [
|
|
11987
|
+
"--yes",
|
|
11988
|
+
"expo",
|
|
11989
|
+
"install",
|
|
11990
|
+
"--check"
|
|
11991
|
+
], {
|
|
11992
|
+
cwd: context.rootDirectory,
|
|
11993
|
+
timeout: INSTALL_TIMEOUT$1
|
|
11994
|
+
});
|
|
11995
|
+
if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
|
|
11996
|
+
};
|
|
11997
|
+
/**
|
|
11998
|
+
* Run expo-doctor to detect packages that should not be installed directly,
|
|
11999
|
+
* then uninstall them. No hardcoded list — expo-doctor is the source of truth.
|
|
12000
|
+
*/
|
|
12001
|
+
const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
|
|
12002
|
+
try {
|
|
12003
|
+
onProgress?.("Expo dependency alignment · running expo-doctor");
|
|
12004
|
+
const result = await runSubprocess("npx", [
|
|
12005
|
+
"--yes",
|
|
12006
|
+
"expo-doctor",
|
|
12007
|
+
rootDir
|
|
12008
|
+
], {
|
|
12009
|
+
cwd: rootDir,
|
|
12010
|
+
timeout: INSTALL_TIMEOUT$1
|
|
12011
|
+
});
|
|
12012
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
12013
|
+
const packagePattern = /The package "([^"]+)" should not be installed directly/g;
|
|
12014
|
+
const toRemove = [];
|
|
12015
|
+
for (const match of output.matchAll(packagePattern)) toRemove.push(match[1]);
|
|
12016
|
+
if (toRemove.length === 0) return;
|
|
12017
|
+
onProgress?.(`Expo dependency alignment · uninstalling ${toRemove.length} package(s)`);
|
|
12018
|
+
await runSubprocess("npm", ["uninstall", ...toRemove], {
|
|
12019
|
+
cwd: rootDir,
|
|
12020
|
+
timeout: INSTALL_TIMEOUT$1
|
|
12021
|
+
});
|
|
12022
|
+
} catch {}
|
|
12023
|
+
};
|
|
12024
|
+
|
|
11630
12025
|
//#endregion
|
|
11631
12026
|
//#region src/commands/fix-force.ts
|
|
11632
12027
|
const INSTALL_TIMEOUT = 1800 * 1e3;
|
|
@@ -11730,7 +12125,7 @@ const runNpmAuditFix = async (rootDir, onProgress) => {
|
|
|
11730
12125
|
});
|
|
11731
12126
|
if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout || "npm install failed after audit fix");
|
|
11732
12127
|
};
|
|
11733
|
-
const fetchLatestVersion = async (rootDir, pkgName, pm) => {
|
|
12128
|
+
const fetchLatestVersion$1 = async (rootDir, pkgName, pm) => {
|
|
11734
12129
|
try {
|
|
11735
12130
|
const result = await runSubprocess(pm, [
|
|
11736
12131
|
"view",
|
|
@@ -11750,7 +12145,7 @@ const collectOverrides = async (rootDir, vulnerabilities, pm) => {
|
|
|
11750
12145
|
const overrides = {};
|
|
11751
12146
|
for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
|
|
11752
12147
|
if (vuln.fixAvailable !== false || !vuln.range) continue;
|
|
11753
|
-
const latest = await fetchLatestVersion(rootDir, pkgName, pm);
|
|
12148
|
+
const latest = await fetchLatestVersion$1(rootDir, pkgName, pm);
|
|
11754
12149
|
if (latest) overrides[pkgName] = latest;
|
|
11755
12150
|
}
|
|
11756
12151
|
return overrides;
|
|
@@ -11764,7 +12159,9 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
|
|
|
11764
12159
|
if (!auditResult.stdout) return;
|
|
11765
12160
|
const vulnerabilities = JSON.parse(auditResult.stdout).vulnerabilities;
|
|
11766
12161
|
if (!vulnerabilities) return;
|
|
11767
|
-
const
|
|
12162
|
+
const rawOverrides = await collectOverrides(rootDir, vulnerabilities, "npm");
|
|
12163
|
+
if (Object.keys(rawOverrides).length === 0) return;
|
|
12164
|
+
const overrides = guardAndReport(rootDir, rawOverrides, onProgress);
|
|
11768
12165
|
if (Object.keys(overrides).length === 0) return;
|
|
11769
12166
|
const pkgPath = path.join(rootDir, "package.json");
|
|
11770
12167
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -11800,6 +12197,76 @@ const collectPnpmOverrides = (advisories) => {
|
|
|
11800
12197
|
}
|
|
11801
12198
|
return overrides;
|
|
11802
12199
|
};
|
|
12200
|
+
const overrideName = (key) => {
|
|
12201
|
+
const at = key.lastIndexOf("@");
|
|
12202
|
+
return at > 0 ? key.slice(0, at) : key;
|
|
12203
|
+
};
|
|
12204
|
+
const guardOverrides = (overrides, installed) => {
|
|
12205
|
+
const safe = {};
|
|
12206
|
+
const skipped = [];
|
|
12207
|
+
for (const [key, target] of Object.entries(overrides)) {
|
|
12208
|
+
const current = installed.get(overrideName(key));
|
|
12209
|
+
if (current && isDowngrade(current, target)) {
|
|
12210
|
+
skipped.push(`${overrideName(key)} ${current} → ${target}`);
|
|
12211
|
+
continue;
|
|
12212
|
+
}
|
|
12213
|
+
safe[key] = target;
|
|
12214
|
+
}
|
|
12215
|
+
return {
|
|
12216
|
+
safe,
|
|
12217
|
+
skipped
|
|
12218
|
+
};
|
|
12219
|
+
};
|
|
12220
|
+
const readRootNodeModulesVersion = (rootDir, name) => {
|
|
12221
|
+
try {
|
|
12222
|
+
const manifest = path.join(rootDir, "node_modules", name, "package.json");
|
|
12223
|
+
const version = JSON.parse(fs.readFileSync(manifest, "utf-8")).version;
|
|
12224
|
+
return typeof version === "string" ? version : null;
|
|
12225
|
+
} catch {
|
|
12226
|
+
return null;
|
|
12227
|
+
}
|
|
12228
|
+
};
|
|
12229
|
+
const PNPM_STORE_VERSION_RE = /^(\d+\.\d+\.\d+[^_(]*)/;
|
|
12230
|
+
const isHigherVersion = (candidate, current) => {
|
|
12231
|
+
if (!current) return true;
|
|
12232
|
+
const a = parseSemverMin(candidate);
|
|
12233
|
+
const b = parseSemverMin(current);
|
|
12234
|
+
if (!a || !b) return false;
|
|
12235
|
+
for (let i = 0; i < 3; i++) {
|
|
12236
|
+
if ((a[i] ?? 0) > (b[i] ?? 0)) return true;
|
|
12237
|
+
if ((a[i] ?? 0) < (b[i] ?? 0)) return false;
|
|
12238
|
+
}
|
|
12239
|
+
return false;
|
|
12240
|
+
};
|
|
12241
|
+
const readPnpmStoreVersion = (rootDir, name) => {
|
|
12242
|
+
let entries;
|
|
12243
|
+
try {
|
|
12244
|
+
entries = fs.readdirSync(path.join(rootDir, "node_modules", ".pnpm"));
|
|
12245
|
+
} catch {
|
|
12246
|
+
return null;
|
|
12247
|
+
}
|
|
12248
|
+
const prefix = `${name.replace(/\//g, "+")}@`;
|
|
12249
|
+
let best = null;
|
|
12250
|
+
for (const entry of entries) {
|
|
12251
|
+
if (!entry.startsWith(prefix)) continue;
|
|
12252
|
+
const match = PNPM_STORE_VERSION_RE.exec(entry.slice(prefix.length));
|
|
12253
|
+
if (match && isHigherVersion(match[1], best)) best = match[1];
|
|
12254
|
+
}
|
|
12255
|
+
return best;
|
|
12256
|
+
};
|
|
12257
|
+
const readInstalledVersions = (rootDir, names) => {
|
|
12258
|
+
const map = /* @__PURE__ */ new Map();
|
|
12259
|
+
for (const name of names) {
|
|
12260
|
+
const version = readRootNodeModulesVersion(rootDir, name) ?? readPnpmStoreVersion(rootDir, name);
|
|
12261
|
+
if (version) map.set(name, version);
|
|
12262
|
+
}
|
|
12263
|
+
return map;
|
|
12264
|
+
};
|
|
12265
|
+
const guardAndReport = (rootDir, rawOverrides, onProgress) => {
|
|
12266
|
+
const { safe, skipped } = guardOverrides(rawOverrides, readInstalledVersions(rootDir, Object.keys(rawOverrides).map(overrideName)));
|
|
12267
|
+
if (skipped.length > 0) onProgress?.(`Dependency audit fixes · skipped ${skipped.length} downgrade(s), verify intent: ${skipped.join(", ")}`);
|
|
12268
|
+
return safe;
|
|
12269
|
+
};
|
|
11803
12270
|
const isPnpmAuditRetired = (stdout, stderr) => {
|
|
11804
12271
|
const haystack = `${stdout}\n${stderr}`.toLowerCase();
|
|
11805
12272
|
return haystack.includes("410") || haystack.includes("gone") || haystack.includes("retired") || haystack.includes("endpoint") || haystack.includes("err_pnpm_audit") || haystack.includes("audit endpoint");
|
|
@@ -11823,7 +12290,9 @@ const tryPnpmOverrides = async (rootDir, onProgress) => {
|
|
|
11823
12290
|
}
|
|
11824
12291
|
const advisories = parsed.advisories;
|
|
11825
12292
|
if (!advisories || Object.keys(advisories).length === 0) return true;
|
|
11826
|
-
const
|
|
12293
|
+
const rawOverrides = collectPnpmOverrides(advisories);
|
|
12294
|
+
if (Object.keys(rawOverrides).length === 0) return true;
|
|
12295
|
+
const overrides = guardAndReport(rootDir, rawOverrides, onProgress);
|
|
11827
12296
|
if (Object.keys(overrides).length === 0) return true;
|
|
11828
12297
|
const pkgPath = path.join(rootDir, "package.json");
|
|
11829
12298
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -11844,58 +12313,6 @@ const tryPnpmOverrides = async (rootDir, onProgress) => {
|
|
|
11844
12313
|
});
|
|
11845
12314
|
return true;
|
|
11846
12315
|
};
|
|
11847
|
-
const fixExpoDependencies = async (context, onProgress) => {
|
|
11848
|
-
await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
|
|
11849
|
-
onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
|
|
11850
|
-
if ((await runSubprocess("npx", [
|
|
11851
|
-
"--yes",
|
|
11852
|
-
"expo",
|
|
11853
|
-
"install",
|
|
11854
|
-
"--fix"
|
|
11855
|
-
], {
|
|
11856
|
-
cwd: context.rootDirectory,
|
|
11857
|
-
timeout: INSTALL_TIMEOUT
|
|
11858
|
-
})).exitCode === 0) return;
|
|
11859
|
-
onProgress?.("Expo dependency alignment · checking remaining issues");
|
|
11860
|
-
const checkResult = await runSubprocess("npx", [
|
|
11861
|
-
"--yes",
|
|
11862
|
-
"expo",
|
|
11863
|
-
"install",
|
|
11864
|
-
"--check"
|
|
11865
|
-
], {
|
|
11866
|
-
cwd: context.rootDirectory,
|
|
11867
|
-
timeout: INSTALL_TIMEOUT
|
|
11868
|
-
});
|
|
11869
|
-
if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
|
|
11870
|
-
};
|
|
11871
|
-
/**
|
|
11872
|
-
* Run expo-doctor to detect packages that should not be installed directly,
|
|
11873
|
-
* then uninstall them. No hardcoded list — expo-doctor is the source of truth.
|
|
11874
|
-
*/
|
|
11875
|
-
const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
|
|
11876
|
-
try {
|
|
11877
|
-
onProgress?.("Expo dependency alignment · running expo-doctor");
|
|
11878
|
-
const result = await runSubprocess("npx", [
|
|
11879
|
-
"--yes",
|
|
11880
|
-
"expo-doctor",
|
|
11881
|
-
rootDir
|
|
11882
|
-
], {
|
|
11883
|
-
cwd: rootDir,
|
|
11884
|
-
timeout: INSTALL_TIMEOUT
|
|
11885
|
-
});
|
|
11886
|
-
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
11887
|
-
const packagePattern = /The package "([^"]+)" should not be installed directly/g;
|
|
11888
|
-
const toRemove = [];
|
|
11889
|
-
let match;
|
|
11890
|
-
while ((match = packagePattern.exec(output)) !== null) toRemove.push(match[1]);
|
|
11891
|
-
if (toRemove.length === 0) return;
|
|
11892
|
-
onProgress?.(`Expo dependency alignment · uninstalling ${toRemove.length} package(s)`);
|
|
11893
|
-
await runSubprocess("npm", ["uninstall", ...toRemove], {
|
|
11894
|
-
cwd: rootDir,
|
|
11895
|
-
timeout: INSTALL_TIMEOUT
|
|
11896
|
-
});
|
|
11897
|
-
} catch {}
|
|
11898
|
-
};
|
|
11899
12316
|
|
|
11900
12317
|
//#endregion
|
|
11901
12318
|
//#region src/commands/fix-pipeline.ts
|
|
@@ -11904,6 +12321,10 @@ const runAiSlopSteps = async (deps) => {
|
|
|
11904
12321
|
if (!deps.config.engines["ai-slop"]) return;
|
|
11905
12322
|
await deps.runStep("Unused imports", () => detectUnusedImports(deps.context), () => fixUnusedImports(deps.context));
|
|
11906
12323
|
await deps.runStep("Duplicate imports", () => detectDuplicateImports(deps.context), () => fixDuplicateImports(deps.context));
|
|
12324
|
+
if (deps.safe) {
|
|
12325
|
+
await deps.runStep("Narrative comments", async () => (await detectNarrativeComments(deps.context)).filter((d) => d.fixable), () => fixNarrativeComments(deps.context));
|
|
12326
|
+
return;
|
|
12327
|
+
}
|
|
11907
12328
|
const detectFixableSlop = async () => {
|
|
11908
12329
|
const [comments, dead, narrative] = await Promise.all([
|
|
11909
12330
|
detectTrivialComments(deps.context),
|
|
@@ -12054,19 +12475,23 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
12054
12475
|
});
|
|
12055
12476
|
return result;
|
|
12056
12477
|
};
|
|
12478
|
+
const safe = Boolean(options.safe);
|
|
12057
12479
|
const pipelineDeps = {
|
|
12058
12480
|
rail,
|
|
12059
12481
|
context,
|
|
12060
12482
|
config,
|
|
12061
12483
|
resolvedDir,
|
|
12062
12484
|
projectInfo,
|
|
12063
|
-
force: Boolean(options.force),
|
|
12485
|
+
force: safe ? false : Boolean(options.force),
|
|
12486
|
+
safe,
|
|
12064
12487
|
runStep
|
|
12065
12488
|
};
|
|
12066
12489
|
await runAiSlopSteps(pipelineDeps);
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12490
|
+
if (!safe) {
|
|
12491
|
+
await runDeclarationStep(pipelineDeps);
|
|
12492
|
+
await runLintSteps(pipelineDeps);
|
|
12493
|
+
await runDependencyStep(pipelineDeps);
|
|
12494
|
+
}
|
|
12070
12495
|
await runFormattingStep(pipelineDeps);
|
|
12071
12496
|
await runForceSteps(pipelineDeps);
|
|
12072
12497
|
const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
|
|
@@ -12414,6 +12839,7 @@ const AI_SLOP_FIXABLE = new Set([
|
|
|
12414
12839
|
"ai-slop/duplicate-import"
|
|
12415
12840
|
]);
|
|
12416
12841
|
const AI_SLOP_ERRORS = new Set(["ai-slop/hallucinated-import"]);
|
|
12842
|
+
const SECURITY_INFO = new Set(["security/dependency-audit-skipped"]);
|
|
12417
12843
|
const BUILTIN_RULES = [
|
|
12418
12844
|
{
|
|
12419
12845
|
engine: "format",
|
|
@@ -12449,6 +12875,10 @@ const BUILTIN_RULES = [
|
|
|
12449
12875
|
"knip/binaries",
|
|
12450
12876
|
"knip/exports",
|
|
12451
12877
|
"knip/types",
|
|
12878
|
+
"knip/duplicates",
|
|
12879
|
+
"code-quality/duplicate-block",
|
|
12880
|
+
"code-quality/repeated-chained-call",
|
|
12881
|
+
"code-quality/unused-declaration",
|
|
12452
12882
|
"complexity/file-too-large",
|
|
12453
12883
|
"complexity/function-too-long",
|
|
12454
12884
|
"complexity/deep-nesting",
|
|
@@ -12501,8 +12931,10 @@ const BUILTIN_RULES = [
|
|
|
12501
12931
|
"security/vulnerable-dependency",
|
|
12502
12932
|
"security/eval",
|
|
12503
12933
|
"security/innerhtml",
|
|
12934
|
+
"security/dangerously-set-innerhtml",
|
|
12504
12935
|
"security/sql-injection",
|
|
12505
|
-
"security/shell-injection"
|
|
12936
|
+
"security/shell-injection",
|
|
12937
|
+
"security/dependency-audit-skipped"
|
|
12506
12938
|
]
|
|
12507
12939
|
}
|
|
12508
12940
|
];
|
|
@@ -12516,7 +12948,7 @@ const toRuleEntry = (engine, ruleId) => {
|
|
|
12516
12948
|
if (engine === "security") return {
|
|
12517
12949
|
id: ruleId,
|
|
12518
12950
|
engine,
|
|
12519
|
-
severity: "error",
|
|
12951
|
+
severity: SECURITY_INFO.has(ruleId) ? "info" : "error",
|
|
12520
12952
|
fixable: false
|
|
12521
12953
|
};
|
|
12522
12954
|
if (engine === "ai-slop") return {
|
|
@@ -12713,6 +13145,86 @@ const trendCommand = (directory, limit) => {
|
|
|
12713
13145
|
}));
|
|
12714
13146
|
};
|
|
12715
13147
|
|
|
13148
|
+
//#endregion
|
|
13149
|
+
//#region src/update-notifier.ts
|
|
13150
|
+
const REGISTRY_URL = "https://registry.npmjs.org/aislop/latest";
|
|
13151
|
+
const CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
|
|
13152
|
+
const REQUEST_TIMEOUT_MS = 2e3;
|
|
13153
|
+
const CACHE_BASENAME = "update_check.json";
|
|
13154
|
+
const isUpdateNotifierDisabled = (env = process.env) => {
|
|
13155
|
+
if (env.AISLOP_NO_UPDATE_NOTIFIER === "1") return true;
|
|
13156
|
+
if (env.NO_UPDATE_NOTIFIER === "1") return true;
|
|
13157
|
+
if (env.DO_NOT_TRACK === "1") return true;
|
|
13158
|
+
return isCiEnv(env);
|
|
13159
|
+
};
|
|
13160
|
+
const resolveUpdateCachePath = (homedir = os.homedir(), env = process.env) => {
|
|
13161
|
+
if (process.platform === "linux" && env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "aislop", CACHE_BASENAME);
|
|
13162
|
+
return path.join(homedir, ".aislop", CACHE_BASENAME);
|
|
13163
|
+
};
|
|
13164
|
+
const parseVersion = (raw) => {
|
|
13165
|
+
const m = raw.trim().replace(/^v/, "").split(/[-+]/, 1)[0].match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
13166
|
+
if (!m) return null;
|
|
13167
|
+
return {
|
|
13168
|
+
major: Number(m[1]),
|
|
13169
|
+
minor: Number(m[2]),
|
|
13170
|
+
patch: Number(m[3])
|
|
13171
|
+
};
|
|
13172
|
+
};
|
|
13173
|
+
const isOutdated = (current, latest) => {
|
|
13174
|
+
const c = parseVersion(current);
|
|
13175
|
+
const l = parseVersion(latest);
|
|
13176
|
+
if (!c || !l) return false;
|
|
13177
|
+
if (l.major !== c.major) return l.major > c.major;
|
|
13178
|
+
if (l.minor !== c.minor) return l.minor > c.minor;
|
|
13179
|
+
return l.patch > c.patch;
|
|
13180
|
+
};
|
|
13181
|
+
const formatUpdateNotice = (current, latest) => `\nUpdate available: ${current} -> ${latest}. Run npx aislop@latest to upgrade.\n`;
|
|
13182
|
+
const readCache = (cachePath) => {
|
|
13183
|
+
try {
|
|
13184
|
+
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
13185
|
+
if (typeof parsed?.latest === "string" && typeof parsed?.checkedAt === "number") return {
|
|
13186
|
+
latest: parsed.latest,
|
|
13187
|
+
checkedAt: parsed.checkedAt
|
|
13188
|
+
};
|
|
13189
|
+
return null;
|
|
13190
|
+
} catch {
|
|
13191
|
+
return null;
|
|
13192
|
+
}
|
|
13193
|
+
};
|
|
13194
|
+
const writeCache = (cachePath, cache) => {
|
|
13195
|
+
try {
|
|
13196
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
13197
|
+
fs.writeFileSync(cachePath, JSON.stringify(cache));
|
|
13198
|
+
return true;
|
|
13199
|
+
} catch {
|
|
13200
|
+
return false;
|
|
13201
|
+
}
|
|
13202
|
+
};
|
|
13203
|
+
const fetchLatestVersion = async () => {
|
|
13204
|
+
try {
|
|
13205
|
+
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
13206
|
+
if (!res.ok) return null;
|
|
13207
|
+
const data = await res.json();
|
|
13208
|
+
return typeof data.version === "string" ? data.version : null;
|
|
13209
|
+
} catch {
|
|
13210
|
+
return null;
|
|
13211
|
+
}
|
|
13212
|
+
};
|
|
13213
|
+
const maybeNotifyUpdate = async (now = Date.now()) => {
|
|
13214
|
+
if (isUpdateNotifierDisabled()) return;
|
|
13215
|
+
if (!process.stderr.isTTY) return;
|
|
13216
|
+
const cachePath = resolveUpdateCachePath();
|
|
13217
|
+
const cache = readCache(cachePath);
|
|
13218
|
+
if (cache && isOutdated(APP_VERSION, cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
|
|
13219
|
+
if (!cache || now - cache.checkedAt > CHECK_INTERVAL_MS) {
|
|
13220
|
+
const latest = await fetchLatestVersion();
|
|
13221
|
+
if (latest) writeCache(cachePath, {
|
|
13222
|
+
latest,
|
|
13223
|
+
checkedAt: now
|
|
13224
|
+
});
|
|
13225
|
+
}
|
|
13226
|
+
};
|
|
13227
|
+
|
|
12716
13228
|
//#endregion
|
|
12717
13229
|
//#region src/cli.ts
|
|
12718
13230
|
process.on("SIGINT", () => process.exit(0));
|
|
@@ -12880,13 +13392,14 @@ const FIX_AGENT_FLAGS = [
|
|
|
12880
13392
|
const matchFixAgent = (flags) => {
|
|
12881
13393
|
return FIX_AGENT_FLAGS.find((a) => flags[a.name])?.flag;
|
|
12882
13394
|
};
|
|
12883
|
-
const fixProgram = program.command("fix [directory]").description("Auto-fix ai slop in codebase").option("-d, --verbose", "show detailed fix progress").option("-f, --force", "run aggressive fixes (audit and framework dependency alignment)").option("-p, --prompt", "print a prompt for your coding agent to fix remaining issues");
|
|
13395
|
+
const fixProgram = program.command("fix [directory]").description("Auto-fix ai slop in codebase").option("-d, --verbose", "show detailed fix progress").option("-f, --force", "run aggressive fixes (audit and framework dependency alignment)").option("--safe", "only apply reversible fixes (imports, comment removal, formatting); skip anything that deletes code or rewrites behaviour").option("-p, --prompt", "print a prompt for your coding agent to fix remaining issues");
|
|
12884
13396
|
for (const a of FIX_AGENT_FLAGS) fixProgram.option(`--${a.flag}`, a.help);
|
|
12885
13397
|
fixProgram.action(async (directory = ".", _flags, command) => {
|
|
12886
13398
|
const flags = command.optsWithGlobals();
|
|
12887
13399
|
await fixCommand(directory, loadConfig(directory), {
|
|
12888
13400
|
verbose: Boolean(flags.verbose),
|
|
12889
13401
|
force: Boolean(flags.force),
|
|
13402
|
+
safe: Boolean(flags.safe),
|
|
12890
13403
|
prompt: Boolean(flags.prompt),
|
|
12891
13404
|
agent: matchFixAgent(flags)
|
|
12892
13405
|
});
|
|
@@ -12966,6 +13479,7 @@ const main = async () => {
|
|
|
12966
13479
|
fireInstalledOnce();
|
|
12967
13480
|
await program.parseAsync();
|
|
12968
13481
|
await flushTelemetry();
|
|
13482
|
+
await maybeNotifyUpdate();
|
|
12969
13483
|
};
|
|
12970
13484
|
main();
|
|
12971
13485
|
|