aislop 0.10.1 → 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 +942 -612
- 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 +2673 -2346
- package/dist/{json-Bqkcl1DF.js → json-B01i-GOz.js} +7 -5
- package/dist/{json-OIzja7OM.js → json-CXV4D0Ib.js} +5 -3
- package/dist/mcp.js +584 -501
- package/dist/{sarif-C-vh4wcC.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-rlhQD8Qh.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
|
|
@@ -244,9 +244,14 @@ const track = (input) => {
|
|
|
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.
|
|
@@ -836,218 +838,6 @@ const loadConfig = (directory) => {
|
|
|
836
838
|
}
|
|
837
839
|
};
|
|
838
840
|
|
|
839
|
-
//#endregion
|
|
840
|
-
//#region src/utils/source-masker.ts
|
|
841
|
-
const JS_EXTS$2 = new Set([
|
|
842
|
-
".ts",
|
|
843
|
-
".tsx",
|
|
844
|
-
".js",
|
|
845
|
-
".jsx",
|
|
846
|
-
".mjs",
|
|
847
|
-
".cjs"
|
|
848
|
-
]);
|
|
849
|
-
const PY_EXTS = new Set([".py"]);
|
|
850
|
-
const RB_EXTS = new Set([".rb"]);
|
|
851
|
-
const PHP_EXTS = new Set([".php"]);
|
|
852
|
-
const familyForExt = (ext) => {
|
|
853
|
-
if (JS_EXTS$2.has(ext)) return "js";
|
|
854
|
-
if (PY_EXTS.has(ext)) return "py";
|
|
855
|
-
if (RB_EXTS.has(ext)) return "rb";
|
|
856
|
-
if (PHP_EXTS.has(ext)) return "php";
|
|
857
|
-
return "none";
|
|
858
|
-
};
|
|
859
|
-
const maskStringsAndComments = (content, ext) => {
|
|
860
|
-
const family = familyForExt(ext);
|
|
861
|
-
if (family === "none") return content;
|
|
862
|
-
if (family === "js") return maskJs(content, true);
|
|
863
|
-
return maskSimple(content, family, true);
|
|
864
|
-
};
|
|
865
|
-
const maskComments = (content, ext) => {
|
|
866
|
-
const family = familyForExt(ext);
|
|
867
|
-
if (family === "none") return content;
|
|
868
|
-
if (family === "js") return maskJs(content, false);
|
|
869
|
-
return maskSimple(content, family, false);
|
|
870
|
-
};
|
|
871
|
-
const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
|
|
872
|
-
const len = content.length;
|
|
873
|
-
const c = content[i];
|
|
874
|
-
const next = content[i + 1];
|
|
875
|
-
if (c === "\"" || c === "'") {
|
|
876
|
-
const strStart = i;
|
|
877
|
-
const end = consumeQuotedString(content, i, c);
|
|
878
|
-
if (maskStrings) mask(strStart + 1, end - 1);
|
|
879
|
-
return {
|
|
880
|
-
handled: true,
|
|
881
|
-
nextI: end
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
if (c === "`") {
|
|
885
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
886
|
-
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
887
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
888
|
-
return {
|
|
889
|
-
handled: true,
|
|
890
|
-
nextI: scan.resumeAt
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
if (c === "/" && next === "/") {
|
|
894
|
-
const strStart = i;
|
|
895
|
-
let k = i;
|
|
896
|
-
while (k < len && content[k] !== "\n") k++;
|
|
897
|
-
mask(strStart, k);
|
|
898
|
-
return {
|
|
899
|
-
handled: true,
|
|
900
|
-
nextI: k
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
if (c === "/" && next === "*") {
|
|
904
|
-
const strStart = i;
|
|
905
|
-
let k = i + 2;
|
|
906
|
-
while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
|
|
907
|
-
if (k < len - 1) k += 2;
|
|
908
|
-
mask(strStart, k);
|
|
909
|
-
return {
|
|
910
|
-
handled: true,
|
|
911
|
-
nextI: k
|
|
912
|
-
};
|
|
913
|
-
}
|
|
914
|
-
return {
|
|
915
|
-
handled: false,
|
|
916
|
-
nextI: i
|
|
917
|
-
};
|
|
918
|
-
};
|
|
919
|
-
const maskJs = (content, maskStrings) => {
|
|
920
|
-
const out = content.split("");
|
|
921
|
-
const len = content.length;
|
|
922
|
-
const tplStack = [];
|
|
923
|
-
let i = 0;
|
|
924
|
-
const mask = (start, end) => {
|
|
925
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
926
|
-
};
|
|
927
|
-
while (i < len) {
|
|
928
|
-
const c = content[i];
|
|
929
|
-
if (tplStack.length > 0) {
|
|
930
|
-
if (c === "{") {
|
|
931
|
-
tplStack[tplStack.length - 1]++;
|
|
932
|
-
i++;
|
|
933
|
-
continue;
|
|
934
|
-
}
|
|
935
|
-
if (c === "}") {
|
|
936
|
-
if (tplStack[tplStack.length - 1] === 0) {
|
|
937
|
-
tplStack.pop();
|
|
938
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
939
|
-
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
940
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
941
|
-
i = scan.resumeAt;
|
|
942
|
-
continue;
|
|
943
|
-
}
|
|
944
|
-
tplStack[tplStack.length - 1]--;
|
|
945
|
-
i++;
|
|
946
|
-
continue;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
|
|
950
|
-
if (handled.handled) {
|
|
951
|
-
i = handled.nextI;
|
|
952
|
-
continue;
|
|
953
|
-
}
|
|
954
|
-
i++;
|
|
955
|
-
}
|
|
956
|
-
return out.join("");
|
|
957
|
-
};
|
|
958
|
-
const consumeQuotedString = (content, start, quote) => {
|
|
959
|
-
const len = content.length;
|
|
960
|
-
let i = start + 1;
|
|
961
|
-
while (i < len) {
|
|
962
|
-
const c = content[i];
|
|
963
|
-
if (c === "\\" && i + 1 < len) {
|
|
964
|
-
i += 2;
|
|
965
|
-
continue;
|
|
966
|
-
}
|
|
967
|
-
if (c === quote) return i + 1;
|
|
968
|
-
if (c === "\n") return i;
|
|
969
|
-
i++;
|
|
970
|
-
}
|
|
971
|
-
return i;
|
|
972
|
-
};
|
|
973
|
-
const consumeTemplateString = (content, start) => {
|
|
974
|
-
const len = content.length;
|
|
975
|
-
let i = start;
|
|
976
|
-
while (i < len) {
|
|
977
|
-
const c = content[i];
|
|
978
|
-
if (c === "\\" && i + 1 < len) {
|
|
979
|
-
i += 2;
|
|
980
|
-
continue;
|
|
981
|
-
}
|
|
982
|
-
if (c === "`") return {
|
|
983
|
-
maskEnd: i,
|
|
984
|
-
resumeAt: i + 1,
|
|
985
|
-
openedInterp: false
|
|
986
|
-
};
|
|
987
|
-
if (c === "$" && content[i + 1] === "{") return {
|
|
988
|
-
maskEnd: i,
|
|
989
|
-
resumeAt: i + 2,
|
|
990
|
-
openedInterp: true
|
|
991
|
-
};
|
|
992
|
-
i++;
|
|
993
|
-
}
|
|
994
|
-
return {
|
|
995
|
-
maskEnd: i,
|
|
996
|
-
resumeAt: i,
|
|
997
|
-
openedInterp: false
|
|
998
|
-
};
|
|
999
|
-
};
|
|
1000
|
-
const maskSimple = (content, family, maskStrings) => {
|
|
1001
|
-
const out = content.split("");
|
|
1002
|
-
const len = content.length;
|
|
1003
|
-
let i = 0;
|
|
1004
|
-
const mask = (start, end) => {
|
|
1005
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
1006
|
-
};
|
|
1007
|
-
while (i < len) {
|
|
1008
|
-
const c = content[i];
|
|
1009
|
-
const next = content[i + 1];
|
|
1010
|
-
if (family === "py" && (c === "\"" || c === "'")) {
|
|
1011
|
-
if (content[i + 1] === c && content[i + 2] === c) {
|
|
1012
|
-
const triple = c + c + c;
|
|
1013
|
-
const end = content.indexOf(triple, i + 3);
|
|
1014
|
-
const stop = end === -1 ? len : end + 3;
|
|
1015
|
-
if (maskStrings) mask(i + 3, stop - 3);
|
|
1016
|
-
i = stop;
|
|
1017
|
-
continue;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
if (c === "\"" || c === "'") {
|
|
1021
|
-
const strStart = i;
|
|
1022
|
-
i = consumeQuotedString(content, i, c);
|
|
1023
|
-
if (maskStrings) mask(strStart + 1, i - 1);
|
|
1024
|
-
continue;
|
|
1025
|
-
}
|
|
1026
|
-
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
1027
|
-
const strStart = i;
|
|
1028
|
-
while (i < len && content[i] !== "\n") i++;
|
|
1029
|
-
mask(strStart, i);
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
if (family === "php" && c === "/" && next === "/") {
|
|
1033
|
-
const strStart = i;
|
|
1034
|
-
while (i < len && content[i] !== "\n") i++;
|
|
1035
|
-
mask(strStart, i);
|
|
1036
|
-
continue;
|
|
1037
|
-
}
|
|
1038
|
-
if (family === "php" && c === "/" && next === "*") {
|
|
1039
|
-
const strStart = i;
|
|
1040
|
-
i += 2;
|
|
1041
|
-
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
1042
|
-
if (i < len - 1) i += 2;
|
|
1043
|
-
mask(strStart, i);
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
i++;
|
|
1047
|
-
}
|
|
1048
|
-
return out.join("");
|
|
1049
|
-
};
|
|
1050
|
-
|
|
1051
841
|
//#endregion
|
|
1052
842
|
//#region src/utils/source-files.ts
|
|
1053
843
|
const MAX_BUFFER$1 = 50 * 1024 * 1024;
|
|
@@ -1224,6 +1014,15 @@ const listProjectFiles = (rootDirectory) => {
|
|
|
1224
1014
|
if (findResult.error || findResult.status !== 0) return [];
|
|
1225
1015
|
return findResult.stdout.split("\n").filter((file) => file.length > 0).map((file) => file.replace(/^\.\//, ""));
|
|
1226
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
|
+
};
|
|
1227
1026
|
const normalizeExcludePatterns = (patterns) => {
|
|
1228
1027
|
return patterns.flatMap((pattern) => {
|
|
1229
1028
|
const p = pattern.trim();
|
|
@@ -1297,8 +1096,8 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
|
1297
1096
|
};
|
|
1298
1097
|
|
|
1299
1098
|
//#endregion
|
|
1300
|
-
//#region src/
|
|
1301
|
-
const JS_EXTS$
|
|
1099
|
+
//#region src/utils/source-masker.ts
|
|
1100
|
+
const JS_EXTS$2 = new Set([
|
|
1302
1101
|
".ts",
|
|
1303
1102
|
".tsx",
|
|
1304
1103
|
".js",
|
|
@@ -1306,51 +1105,261 @@ const JS_EXTS$1 = new Set([
|
|
|
1306
1105
|
".mjs",
|
|
1307
1106
|
".cjs"
|
|
1308
1107
|
]);
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
const
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
const
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
+
]);
|
|
1320
|
+
const THIN_WRAPPER_PATTERNS = [
|
|
1321
|
+
{
|
|
1322
|
+
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
1323
|
+
extensions: JS_EXTS$1
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
1327
|
+
extensions: JS_EXTS$1
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
1331
|
+
extensions: new Set([".py"])
|
|
1332
|
+
}
|
|
1333
|
+
];
|
|
1334
|
+
const AI_NAMING_PATTERNS = [/(?:helper|util|handler|process|do|handle|execute|perform)_?\d+/i, /(?:data|temp|result|value|item|obj|arr|str|num|val)\d+/];
|
|
1335
|
+
const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
|
|
1336
|
+
const DUNDER_PATTERN = /^__\w+__$/;
|
|
1337
|
+
const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
|
|
1338
|
+
const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
|
|
1339
|
+
const isIdentityForward = (matchText) => {
|
|
1340
|
+
const paramsMatch = matchText.match(/\(([^)]*)\)/);
|
|
1341
|
+
const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
|
|
1342
|
+
if (!paramsMatch || !innerMatch) return false;
|
|
1343
|
+
const params = paramNames(paramsMatch[1]);
|
|
1344
|
+
const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
|
|
1345
|
+
if (args.length === 0) return false;
|
|
1346
|
+
return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
|
|
1347
|
+
};
|
|
1348
|
+
const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
|
|
1349
|
+
const detectThinWrappers = (content, relativePath, ext) => {
|
|
1350
|
+
const diagnostics = [];
|
|
1351
|
+
const lines = content.split("\n");
|
|
1341
1352
|
for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
|
|
1342
1353
|
if (!extensions.has(ext)) continue;
|
|
1343
1354
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1344
|
-
|
|
1345
|
-
while ((match = regex.exec(content)) !== null) {
|
|
1355
|
+
for (const match of content.matchAll(regex)) {
|
|
1346
1356
|
const funcName = match[1];
|
|
1347
1357
|
const matchText = match[0];
|
|
1348
1358
|
const lineNumber = content.slice(0, match.index).split("\n").length;
|
|
1349
1359
|
if (DUNDER_PATTERN.test(funcName)) continue;
|
|
1350
1360
|
if (FRAMEWORK_METHOD_NAMES.test(funcName)) continue;
|
|
1351
1361
|
if (lineNumber >= 2) {
|
|
1352
|
-
|
|
1353
|
-
if (prevLine && prevLine.startsWith("@")) continue;
|
|
1362
|
+
if ((lines[lineNumber - 2]?.trim())?.startsWith("@")) continue;
|
|
1354
1363
|
}
|
|
1355
1364
|
if (!isIdentityForward(matchText)) continue;
|
|
1356
1365
|
if (isUseContextWrapper(matchText)) continue;
|
|
@@ -1727,7 +1736,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
1727
1736
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
1728
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));
|
|
1729
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));
|
|
1730
|
-
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));
|
|
1731
1740
|
}
|
|
1732
1741
|
return diagnostics;
|
|
1733
1742
|
};
|
|
@@ -2113,9 +2122,8 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
2113
2122
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2114
2123
|
for (const { pattern, languages, message } of SWALLOWED_EXCEPTION_PATTERNS) {
|
|
2115
2124
|
if (!languages.includes(ext)) continue;
|
|
2116
|
-
let match;
|
|
2117
2125
|
const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
|
|
2118
|
-
|
|
2126
|
+
for (const match of content.matchAll(regex)) {
|
|
2119
2127
|
if (isIntentionalIgnore(match[0], ext)) continue;
|
|
2120
2128
|
const line = content.slice(0, match.index).split("\n").length;
|
|
2121
2129
|
diagnostics.push({
|
|
@@ -2210,170 +2218,10 @@ const detectGoPatterns = async (context) => {
|
|
|
2210
2218
|
};
|
|
2211
2219
|
|
|
2212
2220
|
//#endregion
|
|
2213
|
-
//#region src/engines/ai-slop/
|
|
2214
|
-
const
|
|
2215
|
-
|
|
2216
|
-
"
|
|
2217
|
-
".js",
|
|
2218
|
-
".jsx",
|
|
2219
|
-
".mjs",
|
|
2220
|
-
".cjs",
|
|
2221
|
-
".py",
|
|
2222
|
-
".go",
|
|
2223
|
-
".rs",
|
|
2224
|
-
".rb",
|
|
2225
|
-
".java",
|
|
2226
|
-
".php"
|
|
2227
|
-
]);
|
|
2228
|
-
const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
|
|
2229
|
-
const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
|
|
2230
|
-
const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
|
|
2231
|
-
const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
|
|
2232
|
-
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
2233
|
-
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;
|
|
2234
|
-
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;
|
|
2235
|
-
const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
|
|
2236
|
-
const PLACEHOLDER_HOSTS = new Set([
|
|
2237
|
-
"example.com",
|
|
2238
|
-
"example.org",
|
|
2239
|
-
"example.net"
|
|
2240
|
-
]);
|
|
2241
|
-
const LOOPBACK_HOSTS = new Set([
|
|
2242
|
-
"localhost",
|
|
2243
|
-
"127.0.0.1",
|
|
2244
|
-
"0.0.0.0",
|
|
2245
|
-
"::1"
|
|
2246
|
-
]);
|
|
2247
|
-
const VENDOR_API_DOMAINS = [
|
|
2248
|
-
"github.com",
|
|
2249
|
-
"githubusercontent.com",
|
|
2250
|
-
"googleapis.com",
|
|
2251
|
-
"accounts.google.com",
|
|
2252
|
-
"stripe.com",
|
|
2253
|
-
"openai.com",
|
|
2254
|
-
"anthropic.com",
|
|
2255
|
-
"slack.com",
|
|
2256
|
-
"twilio.com",
|
|
2257
|
-
"sendgrid.com",
|
|
2258
|
-
"mailgun.net",
|
|
2259
|
-
"cloudflare.com",
|
|
2260
|
-
"discord.com",
|
|
2261
|
-
"telegram.org",
|
|
2262
|
-
"login.microsoftonline.com",
|
|
2263
|
-
"graph.microsoft.com",
|
|
2264
|
-
"twitter.com",
|
|
2265
|
-
"x.com",
|
|
2266
|
-
"twimg.com",
|
|
2267
|
-
"t.co",
|
|
2268
|
-
"api.telegram.org"
|
|
2269
|
-
];
|
|
2270
|
-
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
2271
|
-
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2272
|
-
const HARDCODED_URL_FINDING = {
|
|
2273
|
-
rule: "ai-slop/hardcoded-url",
|
|
2274
|
-
message: "Hardcoded environment URL in production code",
|
|
2275
|
-
help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
|
|
2276
|
-
};
|
|
2277
|
-
const HARDCODED_ID_FINDING = {
|
|
2278
|
-
rule: "ai-slop/hardcoded-id",
|
|
2279
|
-
message: "Hardcoded provider/project ID in production code",
|
|
2280
|
-
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."
|
|
2281
|
-
};
|
|
2282
|
-
const makeFinding = (filePath, line, spec) => ({
|
|
2283
|
-
filePath,
|
|
2284
|
-
engine: "ai-slop",
|
|
2285
|
-
rule: spec.rule,
|
|
2286
|
-
severity: "warning",
|
|
2287
|
-
message: spec.message,
|
|
2288
|
-
help: spec.help,
|
|
2289
|
-
line,
|
|
2290
|
-
column: 0,
|
|
2291
|
-
category: "AI Slop",
|
|
2292
|
-
fixable: false
|
|
2293
|
-
});
|
|
2294
|
-
const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
2295
|
-
const commentStartsBefore = (line, index, ext) => {
|
|
2296
|
-
const prefix = line.slice(0, index);
|
|
2297
|
-
if (ext === ".py" || ext === ".rb") return prefix.includes("#");
|
|
2298
|
-
if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
|
|
2299
|
-
return prefix.includes("//") || prefix.includes("/*");
|
|
2300
|
-
};
|
|
2301
|
-
const safeUrlHost = (urlText) => {
|
|
2302
|
-
try {
|
|
2303
|
-
return new URL(urlText).hostname.toLowerCase();
|
|
2304
|
-
} catch {
|
|
2305
|
-
return null;
|
|
2306
|
-
}
|
|
2307
|
-
};
|
|
2308
|
-
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
2309
|
-
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
2310
|
-
if (isEnvBackedLine(line)) return false;
|
|
2311
|
-
const host = safeUrlHost(urlText);
|
|
2312
|
-
if (!host) return false;
|
|
2313
|
-
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2314
|
-
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
2315
|
-
if (isVendorApiHost(host)) return false;
|
|
2316
|
-
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2317
|
-
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2318
|
-
};
|
|
2319
|
-
const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
|
|
2320
|
-
const hasUsefulIdShape = (value) => {
|
|
2321
|
-
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
2322
|
-
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
2323
|
-
if (/^https?:\/\//i.test(value)) return false;
|
|
2324
|
-
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
2325
|
-
return /[0-9]/.test(value);
|
|
2326
|
-
};
|
|
2327
|
-
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
2328
|
-
const diagnostics = [];
|
|
2329
|
-
if (isCommentOnlyLine(line.trim())) return diagnostics;
|
|
2330
|
-
URL_LITERAL_RE.lastIndex = 0;
|
|
2331
|
-
let urlMatch;
|
|
2332
|
-
while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
|
|
2333
|
-
const urlText = urlMatch[2];
|
|
2334
|
-
if (commentStartsBefore(line, urlMatch.index, ext)) continue;
|
|
2335
|
-
if (!shouldFlagUrlLiteral(line, urlText)) continue;
|
|
2336
|
-
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
|
|
2337
|
-
}
|
|
2338
|
-
if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
|
|
2339
|
-
ID_LITERAL_RE.lastIndex = 0;
|
|
2340
|
-
let idMatch;
|
|
2341
|
-
while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
|
|
2342
|
-
const value = idMatch[2];
|
|
2343
|
-
if (commentStartsBefore(line, idMatch.index, ext)) continue;
|
|
2344
|
-
if (!hasUsefulIdShape(value)) continue;
|
|
2345
|
-
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
|
|
2346
|
-
}
|
|
2347
|
-
return diagnostics;
|
|
2348
|
-
};
|
|
2349
|
-
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
2350
|
-
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
2351
|
-
if (isNonProductionPath(relativePath)) return [];
|
|
2352
|
-
if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
|
|
2353
|
-
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
2354
|
-
};
|
|
2355
|
-
const detectHardcodedConfigLiterals = async (context) => {
|
|
2356
|
-
const diagnostics = [];
|
|
2357
|
-
for (const filePath of getSourceFiles(context)) {
|
|
2358
|
-
if (isAutoGenerated(filePath)) continue;
|
|
2359
|
-
let content;
|
|
2360
|
-
try {
|
|
2361
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
2362
|
-
} catch {
|
|
2363
|
-
continue;
|
|
2364
|
-
}
|
|
2365
|
-
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2366
|
-
const ext = path.extname(filePath);
|
|
2367
|
-
diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
|
|
2368
|
-
}
|
|
2369
|
-
return diagnostics;
|
|
2370
|
-
};
|
|
2371
|
-
|
|
2372
|
-
//#endregion
|
|
2373
|
-
//#region src/engines/ai-slop/js-import-aliases.ts
|
|
2374
|
-
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
2375
|
-
const JS_RESOLUTION_EXTENSIONS = [
|
|
2376
|
-
"",
|
|
2221
|
+
//#region src/engines/ai-slop/js-import-aliases.ts
|
|
2222
|
+
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
2223
|
+
const JS_RESOLUTION_EXTENSIONS = [
|
|
2224
|
+
"",
|
|
2377
2225
|
".ts",
|
|
2378
2226
|
".tsx",
|
|
2379
2227
|
".js",
|
|
@@ -2466,15 +2314,18 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
|
2466
2314
|
}
|
|
2467
2315
|
return globs;
|
|
2468
2316
|
};
|
|
2317
|
+
const readWorkspaceEntries = (dir) => {
|
|
2318
|
+
try {
|
|
2319
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
2320
|
+
} catch {
|
|
2321
|
+
return [];
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2469
2324
|
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
2470
2325
|
const dirs = [];
|
|
2471
2326
|
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
2472
2327
|
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
2473
|
-
|
|
2474
|
-
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
2475
|
-
} catch {
|
|
2476
|
-
continue;
|
|
2477
|
-
}
|
|
2328
|
+
for (const entry of readWorkspaceEntries(parent)) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
2478
2329
|
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
2479
2330
|
return dirs;
|
|
2480
2331
|
};
|
|
@@ -3061,21 +2912,194 @@ const checkPyImport = (spec, manifest) => {
|
|
|
3061
2912
|
if ((PYTHON_IMPORT_TO_PIP[root] ?? PYTHON_IMPORT_TO_PIP[normalized])?.some((dist) => manifest.pyDeps.has(normalizePyName(dist)))) return null;
|
|
3062
2913
|
return root;
|
|
3063
2914
|
};
|
|
3064
|
-
const detectHallucinatedImports = async (context) => {
|
|
3065
|
-
const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
|
|
3066
|
-
const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
|
|
3067
|
-
const manifest = loadManifest(context.rootDirectory);
|
|
3068
|
-
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
3069
|
-
const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
|
|
2915
|
+
const detectHallucinatedImports = async (context) => {
|
|
2916
|
+
const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
|
|
2917
|
+
const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
|
|
2918
|
+
const manifest = loadManifest(context.rootDirectory);
|
|
2919
|
+
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
2920
|
+
const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
|
|
2921
|
+
const diagnostics = [];
|
|
2922
|
+
const files = getSourceFiles(context);
|
|
2923
|
+
for (const filePath of files) {
|
|
2924
|
+
const ext = path.extname(filePath);
|
|
2925
|
+
const isJs = JS_EXTENSIONS$2.has(ext);
|
|
2926
|
+
const isPy = PY_EXTENSIONS$2.has(ext);
|
|
2927
|
+
if (!isJs && !isPy) continue;
|
|
2928
|
+
if (isJs && !manifest.hasJsManifest) continue;
|
|
2929
|
+
if (isPy && !manifest.hasPyManifest) continue;
|
|
2930
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2931
|
+
let content;
|
|
2932
|
+
try {
|
|
2933
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2934
|
+
} catch {
|
|
2935
|
+
continue;
|
|
2936
|
+
}
|
|
2937
|
+
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2938
|
+
if (isNonProductionPath(relPath)) continue;
|
|
2939
|
+
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2940
|
+
for (const { spec, line } of imports) {
|
|
2941
|
+
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
2942
|
+
if (!hallucinated) continue;
|
|
2943
|
+
const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
|
|
2944
|
+
diagnostics.push({
|
|
2945
|
+
filePath: relPath,
|
|
2946
|
+
engine: "ai-slop",
|
|
2947
|
+
rule: "ai-slop/hallucinated-import",
|
|
2948
|
+
severity: "error",
|
|
2949
|
+
message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
|
|
2950
|
+
help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
|
|
2951
|
+
line,
|
|
2952
|
+
column: 1,
|
|
2953
|
+
category: "AI Slop",
|
|
2954
|
+
fixable: false
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
return diagnostics;
|
|
2959
|
+
};
|
|
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) => {
|
|
3070
3101
|
const diagnostics = [];
|
|
3071
|
-
const
|
|
3072
|
-
for (const filePath of files) {
|
|
3073
|
-
const ext = path.extname(filePath);
|
|
3074
|
-
const isJs = JS_EXTENSIONS$2.has(ext);
|
|
3075
|
-
const isPy = PY_EXTENSIONS$2.has(ext);
|
|
3076
|
-
if (!isJs && !isPy) continue;
|
|
3077
|
-
if (isJs && !manifest.hasJsManifest) continue;
|
|
3078
|
-
if (isPy && !manifest.hasPyManifest) continue;
|
|
3102
|
+
for (const filePath of getSourceFiles(context)) {
|
|
3079
3103
|
if (isAutoGenerated(filePath)) continue;
|
|
3080
3104
|
let content;
|
|
3081
3105
|
try {
|
|
@@ -3083,30 +3107,92 @@ const detectHallucinatedImports = async (context) => {
|
|
|
3083
3107
|
} catch {
|
|
3084
3108
|
continue;
|
|
3085
3109
|
}
|
|
3086
|
-
const
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
for (const { spec, line } of imports) {
|
|
3090
|
-
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
3091
|
-
if (!hallucinated) continue;
|
|
3092
|
-
const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
|
|
3093
|
-
diagnostics.push({
|
|
3094
|
-
filePath: relPath,
|
|
3095
|
-
engine: "ai-slop",
|
|
3096
|
-
rule: "ai-slop/hallucinated-import",
|
|
3097
|
-
severity: "error",
|
|
3098
|
-
message: `Imports "${hallucinated}" but it's not declared in ${manifestLabel}${isPy ? " and isn't Python stdlib" : ""}`,
|
|
3099
|
-
help: "Most often this is an LLM hallucinating a plausible-sounding package name. Either add the package to your manifest, or correct the import.",
|
|
3100
|
-
line,
|
|
3101
|
-
column: 1,
|
|
3102
|
-
category: "AI Slop",
|
|
3103
|
-
fixable: false
|
|
3104
|
-
});
|
|
3105
|
-
}
|
|
3110
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
3111
|
+
const ext = path.extname(filePath);
|
|
3112
|
+
diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
|
|
3106
3113
|
}
|
|
3107
3114
|
return diagnostics;
|
|
3108
3115
|
};
|
|
3109
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
|
+
|
|
3110
3196
|
//#endregion
|
|
3111
3197
|
//#region src/engines/ai-slop/comment-blocks.ts
|
|
3112
3198
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
@@ -3130,6 +3216,7 @@ const getCommentSyntax = (ext) => {
|
|
|
3130
3216
|
};
|
|
3131
3217
|
const getMatchedLinePrefix = (line, syntax) => {
|
|
3132
3218
|
const trimmed = line.trimStart();
|
|
3219
|
+
if (isAislopDirectiveLine(trimmed)) return null;
|
|
3133
3220
|
for (const prefix of syntax.linePrefixes) {
|
|
3134
3221
|
if (!trimmed.startsWith(prefix)) continue;
|
|
3135
3222
|
if (prefix === "#" && trimmed.startsWith("#!")) return null;
|
|
@@ -3929,9 +4016,7 @@ const isLogOnlyBody = (body) => {
|
|
|
3929
4016
|
};
|
|
3930
4017
|
const detectJsSilentRecovery = (content, relPath) => {
|
|
3931
4018
|
const out = [];
|
|
3932
|
-
CATCH_HEAD_RE
|
|
3933
|
-
let match;
|
|
3934
|
-
while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
|
|
4019
|
+
for (const match of content.matchAll(CATCH_HEAD_RE)) {
|
|
3935
4020
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3936
4021
|
if (body === null) continue;
|
|
3937
4022
|
if (!isLogOnlyBody(body)) continue;
|
|
@@ -4108,18 +4193,22 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
4108
4193
|
}
|
|
4109
4194
|
continue;
|
|
4110
4195
|
}
|
|
4111
|
-
const importMatch = trimmed.match(/^import\s+(
|
|
4196
|
+
const importMatch = trimmed.match(/^import\s+(.+)/);
|
|
4112
4197
|
if (importMatch) {
|
|
4113
4198
|
importLines.add(i);
|
|
4114
|
-
const
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
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
|
+
}
|
|
4123
4212
|
}
|
|
4124
4213
|
}
|
|
4125
4214
|
return {
|
|
@@ -4129,8 +4218,7 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
4129
4218
|
};
|
|
4130
4219
|
const isSymbolUsed = (name, content, importLines, lines) => {
|
|
4131
4220
|
const pattern = new RegExp(`\\b${name}\\b`, "g");
|
|
4132
|
-
|
|
4133
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
4221
|
+
for (const match of content.matchAll(pattern)) {
|
|
4134
4222
|
const lineIndex = content.slice(0, match.index).split("\n").length - 1;
|
|
4135
4223
|
if (!importLines.has(lineIndex)) return true;
|
|
4136
4224
|
}
|
|
@@ -4231,6 +4319,18 @@ const aiSlopEngine = {
|
|
|
4231
4319
|
|
|
4232
4320
|
//#endregion
|
|
4233
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
|
+
]);
|
|
4234
4334
|
const minimatch = (filePath, pattern) => {
|
|
4235
4335
|
let regex = "";
|
|
4236
4336
|
let i = 0;
|
|
@@ -4255,7 +4355,7 @@ const minimatch = (filePath, pattern) => {
|
|
|
4255
4355
|
regex += pattern.slice(i, closeIndex + 1);
|
|
4256
4356
|
i = closeIndex + 1;
|
|
4257
4357
|
}
|
|
4258
|
-
} else if (
|
|
4358
|
+
} else if (REGEX_SPECIAL_CHARS.has(ch)) {
|
|
4259
4359
|
regex += `\\${ch}`;
|
|
4260
4360
|
i++;
|
|
4261
4361
|
} else {
|
|
@@ -4275,27 +4375,15 @@ const extractImports = (content, ext) => {
|
|
|
4275
4375
|
".mjs",
|
|
4276
4376
|
".cjs"
|
|
4277
4377
|
].includes(ext)) {
|
|
4278
|
-
const
|
|
4279
|
-
|
|
4280
|
-
while ((match = esPattern.exec(content)) !== null) imports.push(match[1]);
|
|
4281
|
-
const reqPattern = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
4282
|
-
while ((match = reqPattern.exec(content)) !== null) imports.push(match[1]);
|
|
4283
|
-
}
|
|
4284
|
-
if (ext === ".py") {
|
|
4285
|
-
const pyPattern = /(?:from|import)\s+([\w.]+)/g;
|
|
4286
|
-
let match;
|
|
4287
|
-
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]);
|
|
4288
4380
|
}
|
|
4381
|
+
if (ext === ".py") for (const match of content.matchAll(/(?:from|import)\s+([\w.]+)/g)) imports.push(match[1]);
|
|
4289
4382
|
if (ext === ".go") {
|
|
4290
|
-
const
|
|
4291
|
-
|
|
4292
|
-
while ((match = goSingleImport.exec(content)) !== null) imports.push(match[1]);
|
|
4293
|
-
const goMultiImport = /import\s*\(([^)]*)\)/gs;
|
|
4294
|
-
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)) {
|
|
4295
4385
|
const block = match[1];
|
|
4296
|
-
const
|
|
4297
|
-
let pkgMatch;
|
|
4298
|
-
while ((pkgMatch = pkgPattern.exec(block)) !== null) imports.push(pkgMatch[1]);
|
|
4386
|
+
for (const pkgMatch of block.matchAll(/"([^"]+)"/g)) imports.push(pkgMatch[1]);
|
|
4299
4387
|
}
|
|
4300
4388
|
}
|
|
4301
4389
|
return imports;
|
|
@@ -4422,10 +4510,10 @@ const architectureEngine = {
|
|
|
4422
4510
|
//#endregion
|
|
4423
4511
|
//#region src/engines/code-quality/function-boundaries.ts
|
|
4424
4512
|
const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
|
|
4425
|
-
const ARROW_BLOCK_RE =
|
|
4426
|
-
const ARROW_END_RE =
|
|
4427
|
-
const BRACE_START_RE =
|
|
4428
|
-
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/;
|
|
4429
4517
|
const isControlFlowBrace = (lineText, braceIndex) => {
|
|
4430
4518
|
const before = lineText.substring(0, braceIndex).trimEnd();
|
|
4431
4519
|
if (before.endsWith(")")) return true;
|
|
@@ -4611,14 +4699,14 @@ const countTemplateLines = (bodyLines) => {
|
|
|
4611
4699
|
let templateLineCount = 0;
|
|
4612
4700
|
for (const line of bodyLines) {
|
|
4613
4701
|
const startedInside = insideTemplate;
|
|
4614
|
-
let
|
|
4702
|
+
let escaped = false;
|
|
4615
4703
|
for (const ch of line) {
|
|
4616
|
-
if (
|
|
4617
|
-
|
|
4704
|
+
if (escaped) {
|
|
4705
|
+
escaped = false;
|
|
4618
4706
|
continue;
|
|
4619
4707
|
}
|
|
4620
4708
|
if (ch === "\\") {
|
|
4621
|
-
|
|
4709
|
+
escaped = true;
|
|
4622
4710
|
continue;
|
|
4623
4711
|
}
|
|
4624
4712
|
if (ch === "`") insideTemplate = !insideTemplate;
|
|
@@ -5662,9 +5750,7 @@ const runRuffFormat = async (context) => {
|
|
|
5662
5750
|
};
|
|
5663
5751
|
const parseRuffFormatOutput = (output, rootDir) => {
|
|
5664
5752
|
const diagnostics = [];
|
|
5665
|
-
const
|
|
5666
|
-
let match;
|
|
5667
|
-
while ((match = filePattern.exec(output)) !== null) {
|
|
5753
|
+
for (const match of output.matchAll(/^--- (.+)$/gm)) {
|
|
5668
5754
|
const filePath = getRuffDiagnosticPath(rootDir, match[1]);
|
|
5669
5755
|
diagnostics.push({
|
|
5670
5756
|
filePath,
|
|
@@ -5698,10 +5784,10 @@ const formatEngine = {
|
|
|
5698
5784
|
const { languages, installedTools } = context;
|
|
5699
5785
|
const promises = [];
|
|
5700
5786
|
if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runBiomeFormat(context));
|
|
5701
|
-
if (languages.includes("python") && installedTools
|
|
5702
|
-
if (languages.includes("go") && installedTools
|
|
5703
|
-
if (languages.includes("rust") && installedTools
|
|
5704
|
-
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"));
|
|
5705
5791
|
if (languages.includes("php") && installedTools["php-cs-fixer"]) promises.push(runGenericFormatter(context, "php"));
|
|
5706
5792
|
const results = await Promise.allSettled(promises);
|
|
5707
5793
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
@@ -6137,6 +6223,8 @@ const createOxlintConfig = (options) => {
|
|
|
6137
6223
|
if (options.mode === "fix") {
|
|
6138
6224
|
rules["no-unused-vars"] = "off";
|
|
6139
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";
|
|
6140
6228
|
}
|
|
6141
6229
|
const plugins = [
|
|
6142
6230
|
"import",
|
|
@@ -6301,9 +6389,7 @@ const collectAmbientGlobals = (rootDir) => {
|
|
|
6301
6389
|
if (!relativePath.endsWith(".d.ts")) continue;
|
|
6302
6390
|
const content = readTextFile$1(path.join(rootDir, relativePath));
|
|
6303
6391
|
if (!content) continue;
|
|
6304
|
-
AMBIENT_GLOBAL_RE.
|
|
6305
|
-
let match;
|
|
6306
|
-
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]);
|
|
6307
6393
|
}
|
|
6308
6394
|
const deps = collectPackageNames(rootDir);
|
|
6309
6395
|
if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
|
|
@@ -6654,10 +6740,10 @@ const lintEngine = {
|
|
|
6654
6740
|
if (context.config.lint.typecheck) promises.push(import("./typecheck-wVSohmOX.js").then((mod) => mod.runTypecheck(context)));
|
|
6655
6741
|
}
|
|
6656
6742
|
if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
|
|
6657
|
-
if (languages.includes("python") && installedTools
|
|
6743
|
+
if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
|
|
6658
6744
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
6659
|
-
if (languages.includes("rust") && installedTools
|
|
6660
|
-
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"));
|
|
6661
6747
|
const results = await Promise.allSettled(promises);
|
|
6662
6748
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
6663
6749
|
return {
|
|
@@ -6687,7 +6773,7 @@ const runDependencyAudit = async (context) => {
|
|
|
6687
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));
|
|
6688
6774
|
}
|
|
6689
6775
|
if (context.languages.includes("python") && context.installedTools["pip-audit"]) promises.push(runPipAudit(context.rootDirectory, timeout));
|
|
6690
|
-
if (context.languages.includes("go") && context.installedTools
|
|
6776
|
+
if (context.languages.includes("go") && context.installedTools.govulncheck) promises.push(runGovulncheck(context.rootDirectory, timeout));
|
|
6691
6777
|
if (context.languages.includes("rust")) promises.push(runCargoAudit(context.rootDirectory, timeout));
|
|
6692
6778
|
const results = await Promise.allSettled(promises);
|
|
6693
6779
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
@@ -6792,9 +6878,12 @@ const parseLegacyAdvisories = (advisories, source) => {
|
|
|
6792
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 ?? "");
|
|
6793
6879
|
return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
|
|
6794
6880
|
};
|
|
6881
|
+
const carriesAdvisory = (vulnerability) => Array.isArray(vulnerability.via) && vulnerability.via.some((entry) => entry !== null && typeof entry === "object");
|
|
6795
6882
|
const parseModernVulnerabilities = (vulnerabilities, source) => {
|
|
6796
6883
|
const bucket = /* @__PURE__ */ new Map();
|
|
6884
|
+
const hasRootCauses = Object.values(vulnerabilities).some(carriesAdvisory);
|
|
6797
6885
|
for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
|
|
6886
|
+
if (hasRootCauses && !carriesAdvisory(vulnerability)) continue;
|
|
6798
6887
|
const severity = (vulnerability.severity ?? "moderate").toLowerCase();
|
|
6799
6888
|
const fixAvailable = vulnerability.fixAvailable;
|
|
6800
6889
|
const isDirect = vulnerability.isDirect === true;
|
|
@@ -7082,8 +7171,7 @@ const detectRiskyConstructs = async (context) => {
|
|
|
7082
7171
|
if (!extensions.includes(ext)) continue;
|
|
7083
7172
|
if (isMigrationOrSeeder && name === "sql-injection") continue;
|
|
7084
7173
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
7085
|
-
|
|
7086
|
-
while ((match = regex.exec(masked)) !== null) {
|
|
7174
|
+
for (const match of masked.matchAll(regex)) {
|
|
7087
7175
|
const line = content.slice(0, match.index).split("\n").length;
|
|
7088
7176
|
if (name === "innerhtml") {
|
|
7089
7177
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
@@ -7215,8 +7303,7 @@ const scanSecrets = async (context) => {
|
|
|
7215
7303
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
7216
7304
|
for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
|
|
7217
7305
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
7218
|
-
|
|
7219
|
-
while ((match = regex.exec(content)) !== null) {
|
|
7306
|
+
for (const match of content.matchAll(regex)) {
|
|
7220
7307
|
if (isPlaceholderValue(match[1] ?? match[0])) continue;
|
|
7221
7308
|
if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
|
|
7222
7309
|
const line = content.slice(0, match.index).split("\n").length;
|
|
@@ -7337,6 +7424,64 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
7337
7424
|
|
|
7338
7425
|
//#endregion
|
|
7339
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
|
+
};
|
|
7340
7485
|
const LANGUAGE_SIGNALS = {
|
|
7341
7486
|
"tsconfig.json": "typescript",
|
|
7342
7487
|
"go.mod": "go",
|
|
@@ -7456,11 +7601,12 @@ const checkInstalledTools = async () => {
|
|
|
7456
7601
|
}));
|
|
7457
7602
|
return results;
|
|
7458
7603
|
};
|
|
7459
|
-
const discoverProject = async (directory) => {
|
|
7604
|
+
const discoverProject = async (directory, excludePatterns = []) => {
|
|
7460
7605
|
const resolvedDir = path.resolve(directory);
|
|
7461
7606
|
const languages = detectLanguages(resolvedDir);
|
|
7462
7607
|
const frameworks = detectFrameworks(resolvedDir);
|
|
7463
7608
|
const sourceFileCount = countSourceFiles(resolvedDir);
|
|
7609
|
+
const coverage = analyzeCoverage(resolvedDir, excludePatterns);
|
|
7464
7610
|
const installedTools = await checkInstalledTools();
|
|
7465
7611
|
return {
|
|
7466
7612
|
rootDirectory: resolvedDir,
|
|
@@ -7468,6 +7614,7 @@ const discoverProject = async (directory) => {
|
|
|
7468
7614
|
languages,
|
|
7469
7615
|
frameworks,
|
|
7470
7616
|
sourceFileCount,
|
|
7617
|
+
coverage,
|
|
7471
7618
|
installedTools
|
|
7472
7619
|
};
|
|
7473
7620
|
};
|
|
@@ -8174,7 +8321,7 @@ const uninstallRulesOnly = (opts, paths) => {
|
|
|
8174
8321
|
else result.skipped.push(paths.rules);
|
|
8175
8322
|
if (paths.host && paths.marker) {
|
|
8176
8323
|
const host = readIfExists(paths.host);
|
|
8177
|
-
if (host
|
|
8324
|
+
if (host?.includes(paths.marker)) {
|
|
8178
8325
|
const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
8179
8326
|
applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
|
|
8180
8327
|
} else result.skipped.push(paths.host);
|
|
@@ -8590,7 +8737,7 @@ const uninstallGemini = (opts) => {
|
|
|
8590
8737
|
if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
|
|
8591
8738
|
else result.skipped.push(paths.aislopMd);
|
|
8592
8739
|
const geminiMd = readIfExists(paths.geminiMd);
|
|
8593
|
-
if (geminiMd
|
|
8740
|
+
if (geminiMd?.includes("@AISLOP.md")) {
|
|
8594
8741
|
const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
8595
8742
|
applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
|
|
8596
8743
|
} else result.skipped.push(paths.geminiMd);
|
|
@@ -8887,6 +9034,7 @@ const theme = createTheme();
|
|
|
8887
9034
|
|
|
8888
9035
|
//#endregion
|
|
8889
9036
|
//#region src/commands/hook.ts
|
|
9037
|
+
const HOOK_FLUSH_TIMEOUT_MS = 1500;
|
|
8890
9038
|
const AGENT_LABELS = {
|
|
8891
9039
|
claude: {
|
|
8892
9040
|
label: "Claude Code",
|
|
@@ -9015,6 +9163,7 @@ const hookRun = async (agent, flags) => {
|
|
|
9015
9163
|
process.stderr.write(`hook: agent "${agent}" has no runtime adapter (rules-file-only)\n`);
|
|
9016
9164
|
process.exit(0);
|
|
9017
9165
|
}
|
|
9166
|
+
await flushTelemetry(HOOK_FLUSH_TIMEOUT_MS);
|
|
9018
9167
|
process.exit(exitCode);
|
|
9019
9168
|
};
|
|
9020
9169
|
const hookBaseline = async () => {
|
|
@@ -9268,12 +9417,12 @@ const badgeCommand = async (options = {}) => {
|
|
|
9268
9417
|
svgUrl,
|
|
9269
9418
|
pageUrl
|
|
9270
9419
|
});
|
|
9271
|
-
if (options.json) process.stdout.write(JSON.stringify({
|
|
9420
|
+
if (options.json) process.stdout.write(`${JSON.stringify({
|
|
9272
9421
|
owner,
|
|
9273
9422
|
repo,
|
|
9274
9423
|
svgUrl,
|
|
9275
9424
|
pageUrl
|
|
9276
|
-
})
|
|
9425
|
+
})}\n`);
|
|
9277
9426
|
else process.stdout.write(output);
|
|
9278
9427
|
return {
|
|
9279
9428
|
owner,
|
|
@@ -9574,7 +9723,7 @@ const renderHeader = (input, _deps = {}) => {
|
|
|
9574
9723
|
|
|
9575
9724
|
//#endregion
|
|
9576
9725
|
//#region src/ui/width.ts
|
|
9577
|
-
const ANSI_RE = new RegExp(
|
|
9726
|
+
const ANSI_RE = new RegExp(`\\[[0-9;]*m`, "g");
|
|
9578
9727
|
const stripAnsi = (s) => s.replace(ANSI_RE, "");
|
|
9579
9728
|
const stringWidth = (s) => {
|
|
9580
9729
|
const bare = stripAnsi(s);
|
|
@@ -9718,6 +9867,8 @@ var LiveGrid = class {
|
|
|
9718
9867
|
const RULE_LABELS = {
|
|
9719
9868
|
formatting: "Code not formatted",
|
|
9720
9869
|
"code-quality/duplicate-block": "Duplicate code block",
|
|
9870
|
+
"code-quality/repeated-chained-call": "Repeated chained call",
|
|
9871
|
+
"code-quality/unused-declaration": "Unused declaration",
|
|
9721
9872
|
"complexity/file-too-large": "File too large",
|
|
9722
9873
|
"complexity/function-too-long": "Function too long",
|
|
9723
9874
|
"complexity/deep-nesting": "Deeply nested code",
|
|
@@ -9730,6 +9881,7 @@ const RULE_LABELS = {
|
|
|
9730
9881
|
"knip/binaries": "Unused binary",
|
|
9731
9882
|
"knip/exports": "Unused export",
|
|
9732
9883
|
"knip/types": "Unused type",
|
|
9884
|
+
"knip/duplicates": "Duplicate export",
|
|
9733
9885
|
"ai-slop/trivial-comment": "Trivial restating comment",
|
|
9734
9886
|
"ai-slop/swallowed-exception": "Empty catch (swallowed error)",
|
|
9735
9887
|
"ai-slop/silent-recovery": "Catch logs then continues",
|
|
@@ -9766,6 +9918,7 @@ const RULE_LABELS = {
|
|
|
9766
9918
|
"ai-slop/hallucinated-import": "Import not in package.json",
|
|
9767
9919
|
"security/hardcoded-secret": "Possible hardcoded secret",
|
|
9768
9920
|
"security/vulnerable-dependency": "Vulnerable dependency",
|
|
9921
|
+
"security/dependency-audit-skipped": "Dependency audit skipped",
|
|
9769
9922
|
"security/eval": "eval() usage",
|
|
9770
9923
|
"security/innerhtml": "innerHTML assignment",
|
|
9771
9924
|
"security/dangerously-set-innerhtml": "dangerouslySetInnerHTML (XSS risk)",
|
|
@@ -9923,6 +10076,36 @@ const readHistory = (directory) => {
|
|
|
9923
10076
|
return records;
|
|
9924
10077
|
};
|
|
9925
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
|
+
|
|
9926
10109
|
//#endregion
|
|
9927
10110
|
//#region src/commands/scan.ts
|
|
9928
10111
|
const isMachineOutput = (options) => Boolean(options.json) || Boolean(options.sarif);
|
|
@@ -10029,7 +10212,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
10029
10212
|
else log.error(msg);
|
|
10030
10213
|
return { exitCode: 1 };
|
|
10031
10214
|
}
|
|
10032
|
-
const projectInfo = await discoverProject(resolvedDir);
|
|
10215
|
+
const projectInfo = await discoverProject(resolvedDir, [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)]);
|
|
10033
10216
|
return withCommandLifecycle({
|
|
10034
10217
|
command: options.command ?? "scan",
|
|
10035
10218
|
config: config.telemetry,
|
|
@@ -10042,15 +10225,16 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10042
10225
|
const showHeader = options.showHeader !== false;
|
|
10043
10226
|
const machineOutput = isMachineOutput(options);
|
|
10044
10227
|
const useLiveProgress = !machineOutput && shouldUseSpinner();
|
|
10228
|
+
const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
|
|
10045
10229
|
let files;
|
|
10046
10230
|
if (options.staged) {
|
|
10047
|
-
files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [],
|
|
10231
|
+
files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], excludePatterns);
|
|
10048
10232
|
if (!machineOutput) log.muted(`Scope: ${files.length} staged file(s)`);
|
|
10049
10233
|
} else if (options.changes) {
|
|
10050
|
-
files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [],
|
|
10234
|
+
files = filterProjectFiles(resolvedDir, getChangedFiles(resolvedDir), [], excludePatterns);
|
|
10051
10235
|
if (!machineOutput) log.muted(`Scope: ${files.length} changed file(s)`);
|
|
10052
10236
|
} else {
|
|
10053
|
-
files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [],
|
|
10237
|
+
files = filterProjectFiles(resolvedDir, listProjectFiles(resolvedDir), [], excludePatterns);
|
|
10054
10238
|
if (!machineOutput) log.muted(`Scope: ${files.length} file(s) after exclusions`);
|
|
10055
10239
|
}
|
|
10056
10240
|
const configDir = findConfigDir(resolvedDir);
|
|
@@ -10104,14 +10288,21 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10104
10288
|
if (!machineOutput && !progressRenderer) printEngineStatus(result);
|
|
10105
10289
|
});
|
|
10106
10290
|
progressRenderer?.stop();
|
|
10107
|
-
const results = rawResults.map((result) => ({
|
|
10291
|
+
const { results, suppressedCount } = applySuppressions(rawResults.map((result) => ({
|
|
10108
10292
|
...result,
|
|
10109
10293
|
diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
|
|
10110
|
-
}));
|
|
10294
|
+
})), resolvedDir);
|
|
10295
|
+
if (suppressedCount > 0 && !machineOutput) log.muted(`Suppressed ${suppressedCount} finding(s) via aislop-ignore directives`);
|
|
10111
10296
|
const allDiagnostics = results.flatMap((r) => r.diagnostics);
|
|
10112
10297
|
const elapsedMs = performance.now() - startTime;
|
|
10113
10298
|
const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
|
|
10114
|
-
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
|
+
});
|
|
10115
10306
|
const engineIssues = {};
|
|
10116
10307
|
const engineTimings = {};
|
|
10117
10308
|
for (const r of results) {
|
|
@@ -10120,7 +10311,8 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10120
10311
|
}
|
|
10121
10312
|
const completion = {
|
|
10122
10313
|
exitCode,
|
|
10123
|
-
score: scoreResult.score,
|
|
10314
|
+
score: scoreable ? scoreResult.score : null,
|
|
10315
|
+
scoreable,
|
|
10124
10316
|
findingCount: allDiagnostics.length,
|
|
10125
10317
|
errorCount: allDiagnostics.filter((d) => d.severity === "error").length,
|
|
10126
10318
|
warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
|
|
@@ -10134,11 +10326,18 @@ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
10134
10326
|
return completion;
|
|
10135
10327
|
}
|
|
10136
10328
|
if (options.json) {
|
|
10137
|
-
const { buildJsonOutput } = await import("./json-
|
|
10138
|
-
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);
|
|
10139
10331
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
10140
10332
|
return completion;
|
|
10141
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
|
+
}
|
|
10142
10341
|
if (!options.staged && !options.changes && options.command !== "ci" && !isCiEnv()) appendHistory({
|
|
10143
10342
|
directory: resolvedDir,
|
|
10144
10343
|
score: scoreResult.score,
|
|
@@ -10918,7 +11117,7 @@ const ERROR_MESSAGE_PATTERNS = [
|
|
|
10918
11117
|
/**
|
|
10919
11118
|
* Extracts the full text of a console statement spanning multiple lines.
|
|
10920
11119
|
*/
|
|
10921
|
-
const getStatementText = (lines,
|
|
11120
|
+
const getStatementText = (lines, span) => {
|
|
10922
11121
|
const spanLines = [];
|
|
10923
11122
|
for (const lineNo of span) spanLines.push(lines[lineNo - 1]);
|
|
10924
11123
|
return spanLines.join("\n");
|
|
@@ -10930,6 +11129,21 @@ const getStatementText = (lines, startIndex, span) => {
|
|
|
10930
11129
|
const shouldUpgradeToError = (statementText) => {
|
|
10931
11130
|
return ERROR_MESSAGE_PATTERNS.some((pattern) => pattern.test(statementText));
|
|
10932
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
|
+
};
|
|
10933
11147
|
const fixDeadPatterns = async (context) => {
|
|
10934
11148
|
const fixable = [...await detectTrivialComments(context), ...await detectDeadPatterns(context)].filter((d) => d.fixable);
|
|
10935
11149
|
if (fixable.length === 0) return;
|
|
@@ -10943,24 +11157,31 @@ const fixDeadPatterns = async (context) => {
|
|
|
10943
11157
|
});
|
|
10944
11158
|
byFile.set(absolute, entries);
|
|
10945
11159
|
}
|
|
10946
|
-
for (const [filePath, entries] of byFile) fixFileDeadPatterns(filePath, entries);
|
|
11160
|
+
for (const [filePath, entries] of byFile) fixFileDeadPatterns(filePath, entries, context.rootDirectory);
|
|
10947
11161
|
};
|
|
10948
|
-
const fixFileDeadPatterns = (filePath, entries) => {
|
|
11162
|
+
const fixFileDeadPatterns = (filePath, entries, rootDirectory) => {
|
|
10949
11163
|
if (!fs.existsSync(filePath)) return;
|
|
10950
11164
|
const lines = fs.readFileSync(filePath, "utf-8").split("\n");
|
|
10951
11165
|
const linesToRemove = /* @__PURE__ */ new Set();
|
|
10952
11166
|
const lineReplacements = /* @__PURE__ */ new Map();
|
|
11167
|
+
const skipConsole = isDiagnosticScriptPath(path.relative(rootDirectory, filePath));
|
|
11168
|
+
const consoleSpans = [];
|
|
10953
11169
|
for (const entry of entries) {
|
|
10954
11170
|
const index = entry.line - 1;
|
|
10955
11171
|
if (index < 0 || index >= lines.length) continue;
|
|
10956
11172
|
if (entry.rule === "ai-slop/console-leftover") {
|
|
11173
|
+
if (skipConsole) continue;
|
|
10957
11174
|
const span = findStatementSpan(lines, index);
|
|
10958
|
-
if (shouldUpgradeToError(getStatementText(lines,
|
|
10959
|
-
|
|
10960
|
-
lineReplacements.set(entry.line, replaced);
|
|
10961
|
-
} 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);
|
|
10962
11177
|
} else linesToRemove.add(entry.line);
|
|
10963
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
|
+
}
|
|
10964
11185
|
const result = applyEditsAndCollapse(lines, linesToRemove, lineReplacements);
|
|
10965
11186
|
fs.writeFileSync(filePath, result);
|
|
10966
11187
|
};
|
|
@@ -11210,7 +11431,7 @@ const fixUnusedImports = async (context) => {
|
|
|
11210
11431
|
const importSpan = JS_EXTENSIONS$1.has(analysis.ext) ? getJsImportSpan(lines, lineIdx) : [lineIdx];
|
|
11211
11432
|
if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
|
|
11212
11433
|
else if (JS_EXTENSIONS$1.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
11213
|
-
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx,
|
|
11434
|
+
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, unusedNames);
|
|
11214
11435
|
}
|
|
11215
11436
|
if (linesToRemove.size === 0 && unused.length === 0) continue;
|
|
11216
11437
|
const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
|
|
@@ -11274,9 +11495,12 @@ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
|
|
|
11274
11495
|
lines[span[0]] = newImport;
|
|
11275
11496
|
for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
|
|
11276
11497
|
};
|
|
11277
|
-
const rewritePyImportLine = (lines, lineIdx,
|
|
11498
|
+
const rewritePyImportLine = (lines, lineIdx, unusedNames) => {
|
|
11278
11499
|
const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
|
|
11279
|
-
if (!fromMatch)
|
|
11500
|
+
if (!fromMatch) {
|
|
11501
|
+
rewritePlainPyImportLine(lines, lineIdx, unusedNames);
|
|
11502
|
+
return;
|
|
11503
|
+
}
|
|
11280
11504
|
const prefix = fromMatch[1];
|
|
11281
11505
|
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
11282
11506
|
const hasParen = importPart.startsWith("(");
|
|
@@ -11289,6 +11513,19 @@ const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
|
|
|
11289
11513
|
const joined = keptSpecifiers.join(", ");
|
|
11290
11514
|
lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
|
|
11291
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
|
+
};
|
|
11292
11529
|
|
|
11293
11530
|
//#endregion
|
|
11294
11531
|
//#region src/engines/code-quality/unused-removal-ast.ts
|
|
@@ -11730,6 +11967,61 @@ const runExpoDoctor = async (context) => {
|
|
|
11730
11967
|
return toDiagnostics(parseIssues(output));
|
|
11731
11968
|
};
|
|
11732
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
|
+
|
|
11733
12025
|
//#endregion
|
|
11734
12026
|
//#region src/commands/fix-force.ts
|
|
11735
12027
|
const INSTALL_TIMEOUT = 1800 * 1e3;
|
|
@@ -11867,7 +12159,9 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
|
|
|
11867
12159
|
if (!auditResult.stdout) return;
|
|
11868
12160
|
const vulnerabilities = JSON.parse(auditResult.stdout).vulnerabilities;
|
|
11869
12161
|
if (!vulnerabilities) return;
|
|
11870
|
-
const
|
|
12162
|
+
const rawOverrides = await collectOverrides(rootDir, vulnerabilities, "npm");
|
|
12163
|
+
if (Object.keys(rawOverrides).length === 0) return;
|
|
12164
|
+
const overrides = guardAndReport(rootDir, rawOverrides, onProgress);
|
|
11871
12165
|
if (Object.keys(overrides).length === 0) return;
|
|
11872
12166
|
const pkgPath = path.join(rootDir, "package.json");
|
|
11873
12167
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -11903,6 +12197,76 @@ const collectPnpmOverrides = (advisories) => {
|
|
|
11903
12197
|
}
|
|
11904
12198
|
return overrides;
|
|
11905
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
|
+
};
|
|
11906
12270
|
const isPnpmAuditRetired = (stdout, stderr) => {
|
|
11907
12271
|
const haystack = `${stdout}\n${stderr}`.toLowerCase();
|
|
11908
12272
|
return haystack.includes("410") || haystack.includes("gone") || haystack.includes("retired") || haystack.includes("endpoint") || haystack.includes("err_pnpm_audit") || haystack.includes("audit endpoint");
|
|
@@ -11926,7 +12290,9 @@ const tryPnpmOverrides = async (rootDir, onProgress) => {
|
|
|
11926
12290
|
}
|
|
11927
12291
|
const advisories = parsed.advisories;
|
|
11928
12292
|
if (!advisories || Object.keys(advisories).length === 0) return true;
|
|
11929
|
-
const
|
|
12293
|
+
const rawOverrides = collectPnpmOverrides(advisories);
|
|
12294
|
+
if (Object.keys(rawOverrides).length === 0) return true;
|
|
12295
|
+
const overrides = guardAndReport(rootDir, rawOverrides, onProgress);
|
|
11930
12296
|
if (Object.keys(overrides).length === 0) return true;
|
|
11931
12297
|
const pkgPath = path.join(rootDir, "package.json");
|
|
11932
12298
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -11947,58 +12313,6 @@ const tryPnpmOverrides = async (rootDir, onProgress) => {
|
|
|
11947
12313
|
});
|
|
11948
12314
|
return true;
|
|
11949
12315
|
};
|
|
11950
|
-
const fixExpoDependencies = async (context, onProgress) => {
|
|
11951
|
-
await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
|
|
11952
|
-
onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
|
|
11953
|
-
if ((await runSubprocess("npx", [
|
|
11954
|
-
"--yes",
|
|
11955
|
-
"expo",
|
|
11956
|
-
"install",
|
|
11957
|
-
"--fix"
|
|
11958
|
-
], {
|
|
11959
|
-
cwd: context.rootDirectory,
|
|
11960
|
-
timeout: INSTALL_TIMEOUT
|
|
11961
|
-
})).exitCode === 0) return;
|
|
11962
|
-
onProgress?.("Expo dependency alignment · checking remaining issues");
|
|
11963
|
-
const checkResult = await runSubprocess("npx", [
|
|
11964
|
-
"--yes",
|
|
11965
|
-
"expo",
|
|
11966
|
-
"install",
|
|
11967
|
-
"--check"
|
|
11968
|
-
], {
|
|
11969
|
-
cwd: context.rootDirectory,
|
|
11970
|
-
timeout: INSTALL_TIMEOUT
|
|
11971
|
-
});
|
|
11972
|
-
if (checkResult.exitCode !== 0) throw new Error(checkResult.stderr || checkResult.stdout || "expo dependency check failed");
|
|
11973
|
-
};
|
|
11974
|
-
/**
|
|
11975
|
-
* Run expo-doctor to detect packages that should not be installed directly,
|
|
11976
|
-
* then uninstall them. No hardcoded list — expo-doctor is the source of truth.
|
|
11977
|
-
*/
|
|
11978
|
-
const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
|
|
11979
|
-
try {
|
|
11980
|
-
onProgress?.("Expo dependency alignment · running expo-doctor");
|
|
11981
|
-
const result = await runSubprocess("npx", [
|
|
11982
|
-
"--yes",
|
|
11983
|
-
"expo-doctor",
|
|
11984
|
-
rootDir
|
|
11985
|
-
], {
|
|
11986
|
-
cwd: rootDir,
|
|
11987
|
-
timeout: INSTALL_TIMEOUT
|
|
11988
|
-
});
|
|
11989
|
-
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
11990
|
-
const packagePattern = /The package "([^"]+)" should not be installed directly/g;
|
|
11991
|
-
const toRemove = [];
|
|
11992
|
-
let match;
|
|
11993
|
-
while ((match = packagePattern.exec(output)) !== null) toRemove.push(match[1]);
|
|
11994
|
-
if (toRemove.length === 0) return;
|
|
11995
|
-
onProgress?.(`Expo dependency alignment · uninstalling ${toRemove.length} package(s)`);
|
|
11996
|
-
await runSubprocess("npm", ["uninstall", ...toRemove], {
|
|
11997
|
-
cwd: rootDir,
|
|
11998
|
-
timeout: INSTALL_TIMEOUT
|
|
11999
|
-
});
|
|
12000
|
-
} catch {}
|
|
12001
|
-
};
|
|
12002
12316
|
|
|
12003
12317
|
//#endregion
|
|
12004
12318
|
//#region src/commands/fix-pipeline.ts
|
|
@@ -12007,6 +12321,10 @@ const runAiSlopSteps = async (deps) => {
|
|
|
12007
12321
|
if (!deps.config.engines["ai-slop"]) return;
|
|
12008
12322
|
await deps.runStep("Unused imports", () => detectUnusedImports(deps.context), () => fixUnusedImports(deps.context));
|
|
12009
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
|
+
}
|
|
12010
12328
|
const detectFixableSlop = async () => {
|
|
12011
12329
|
const [comments, dead, narrative] = await Promise.all([
|
|
12012
12330
|
detectTrivialComments(deps.context),
|
|
@@ -12157,19 +12475,23 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
|
|
|
12157
12475
|
});
|
|
12158
12476
|
return result;
|
|
12159
12477
|
};
|
|
12478
|
+
const safe = Boolean(options.safe);
|
|
12160
12479
|
const pipelineDeps = {
|
|
12161
12480
|
rail,
|
|
12162
12481
|
context,
|
|
12163
12482
|
config,
|
|
12164
12483
|
resolvedDir,
|
|
12165
12484
|
projectInfo,
|
|
12166
|
-
force: Boolean(options.force),
|
|
12485
|
+
force: safe ? false : Boolean(options.force),
|
|
12486
|
+
safe,
|
|
12167
12487
|
runStep
|
|
12168
12488
|
};
|
|
12169
12489
|
await runAiSlopSteps(pipelineDeps);
|
|
12170
|
-
|
|
12171
|
-
|
|
12172
|
-
|
|
12490
|
+
if (!safe) {
|
|
12491
|
+
await runDeclarationStep(pipelineDeps);
|
|
12492
|
+
await runLintSteps(pipelineDeps);
|
|
12493
|
+
await runDependencyStep(pipelineDeps);
|
|
12494
|
+
}
|
|
12173
12495
|
await runFormattingStep(pipelineDeps);
|
|
12174
12496
|
await runForceSteps(pipelineDeps);
|
|
12175
12497
|
const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
|
|
@@ -12517,6 +12839,7 @@ const AI_SLOP_FIXABLE = new Set([
|
|
|
12517
12839
|
"ai-slop/duplicate-import"
|
|
12518
12840
|
]);
|
|
12519
12841
|
const AI_SLOP_ERRORS = new Set(["ai-slop/hallucinated-import"]);
|
|
12842
|
+
const SECURITY_INFO = new Set(["security/dependency-audit-skipped"]);
|
|
12520
12843
|
const BUILTIN_RULES = [
|
|
12521
12844
|
{
|
|
12522
12845
|
engine: "format",
|
|
@@ -12552,6 +12875,10 @@ const BUILTIN_RULES = [
|
|
|
12552
12875
|
"knip/binaries",
|
|
12553
12876
|
"knip/exports",
|
|
12554
12877
|
"knip/types",
|
|
12878
|
+
"knip/duplicates",
|
|
12879
|
+
"code-quality/duplicate-block",
|
|
12880
|
+
"code-quality/repeated-chained-call",
|
|
12881
|
+
"code-quality/unused-declaration",
|
|
12555
12882
|
"complexity/file-too-large",
|
|
12556
12883
|
"complexity/function-too-long",
|
|
12557
12884
|
"complexity/deep-nesting",
|
|
@@ -12604,8 +12931,10 @@ const BUILTIN_RULES = [
|
|
|
12604
12931
|
"security/vulnerable-dependency",
|
|
12605
12932
|
"security/eval",
|
|
12606
12933
|
"security/innerhtml",
|
|
12934
|
+
"security/dangerously-set-innerhtml",
|
|
12607
12935
|
"security/sql-injection",
|
|
12608
|
-
"security/shell-injection"
|
|
12936
|
+
"security/shell-injection",
|
|
12937
|
+
"security/dependency-audit-skipped"
|
|
12609
12938
|
]
|
|
12610
12939
|
}
|
|
12611
12940
|
];
|
|
@@ -12619,7 +12948,7 @@ const toRuleEntry = (engine, ruleId) => {
|
|
|
12619
12948
|
if (engine === "security") return {
|
|
12620
12949
|
id: ruleId,
|
|
12621
12950
|
engine,
|
|
12622
|
-
severity: "error",
|
|
12951
|
+
severity: SECURITY_INFO.has(ruleId) ? "info" : "error",
|
|
12623
12952
|
fixable: false
|
|
12624
12953
|
};
|
|
12625
12954
|
if (engine === "ai-slop") return {
|
|
@@ -13063,13 +13392,14 @@ const FIX_AGENT_FLAGS = [
|
|
|
13063
13392
|
const matchFixAgent = (flags) => {
|
|
13064
13393
|
return FIX_AGENT_FLAGS.find((a) => flags[a.name])?.flag;
|
|
13065
13394
|
};
|
|
13066
|
-
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");
|
|
13067
13396
|
for (const a of FIX_AGENT_FLAGS) fixProgram.option(`--${a.flag}`, a.help);
|
|
13068
13397
|
fixProgram.action(async (directory = ".", _flags, command) => {
|
|
13069
13398
|
const flags = command.optsWithGlobals();
|
|
13070
13399
|
await fixCommand(directory, loadConfig(directory), {
|
|
13071
13400
|
verbose: Boolean(flags.verbose),
|
|
13072
13401
|
force: Boolean(flags.force),
|
|
13402
|
+
safe: Boolean(flags.safe),
|
|
13073
13403
|
prompt: Boolean(flags.prompt),
|
|
13074
13404
|
agent: matchFixAgent(flags)
|
|
13075
13405
|
});
|