aislop 0.9.6 → 0.10.1
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/dist/cli.js +523 -293
- package/dist/index.js +440 -291
- package/dist/{json-CxiErSgX.js → json-Bqkcl1DF.js} +1 -1
- package/dist/mcp.js +438 -289
- package/dist/{sarif-CLVijBAO.js → sarif-C-vh4wcC.js} +1 -1
- package/dist/version-rlhQD8Qh.js +5 -0
- package/package.json +1 -1
- package/dist/version-CPpO6jbj.js +0 -5
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.
|
|
37
|
+
const APP_VERSION = "0.10.1";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/telemetry/env.ts
|
|
@@ -183,7 +183,7 @@ const redactProperties = (props) => {
|
|
|
183
183
|
const POSTHOG_HOST = process.env.AISLOP_POSTHOG_HOST ?? "https://eu.i.posthog.com";
|
|
184
184
|
const POSTHOG_KEY = process.env.AISLOP_POSTHOG_KEY ?? "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
185
185
|
const SCHEMA_VERSION = "v2";
|
|
186
|
-
const REQUEST_TIMEOUT_MS = 3e3;
|
|
186
|
+
const REQUEST_TIMEOUT_MS$1 = 3e3;
|
|
187
187
|
const isTelemetryDisabled = (config) => {
|
|
188
188
|
const env = process.env;
|
|
189
189
|
if (env.AISLOP_NO_TELEMETRY === "1" || env.DO_NOT_TRACK === "1") return true;
|
|
@@ -237,7 +237,7 @@ const track = (input) => {
|
|
|
237
237
|
method: "POST",
|
|
238
238
|
headers: { "Content-Type": "application/json" },
|
|
239
239
|
body: JSON.stringify(payload),
|
|
240
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
240
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS$1)
|
|
241
241
|
}).then(() => {}).catch(() => {}).finally(() => {
|
|
242
242
|
pendingRequests.delete(request);
|
|
243
243
|
});
|
|
@@ -836,6 +836,218 @@ const loadConfig = (directory) => {
|
|
|
836
836
|
}
|
|
837
837
|
};
|
|
838
838
|
|
|
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
|
+
|
|
839
1051
|
//#endregion
|
|
840
1052
|
//#region src/utils/source-files.ts
|
|
841
1053
|
const MAX_BUFFER$1 = 50 * 1024 * 1024;
|
|
@@ -1086,7 +1298,7 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
|
1086
1298
|
|
|
1087
1299
|
//#endregion
|
|
1088
1300
|
//#region src/engines/ai-slop/abstractions.ts
|
|
1089
|
-
const JS_EXTS$
|
|
1301
|
+
const JS_EXTS$1 = new Set([
|
|
1090
1302
|
".ts",
|
|
1091
1303
|
".tsx",
|
|
1092
1304
|
".js",
|
|
@@ -1097,11 +1309,11 @@ const JS_EXTS$2 = new Set([
|
|
|
1097
1309
|
const THIN_WRAPPER_PATTERNS = [
|
|
1098
1310
|
{
|
|
1099
1311
|
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
1100
|
-
extensions: JS_EXTS$
|
|
1312
|
+
extensions: JS_EXTS$1
|
|
1101
1313
|
},
|
|
1102
1314
|
{
|
|
1103
1315
|
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
1104
|
-
extensions: JS_EXTS$
|
|
1316
|
+
extensions: JS_EXTS$1
|
|
1105
1317
|
},
|
|
1106
1318
|
{
|
|
1107
1319
|
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
@@ -1111,14 +1323,16 @@ const THIN_WRAPPER_PATTERNS = [
|
|
|
1111
1323
|
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+/];
|
|
1112
1324
|
const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
|
|
1113
1325
|
const DUNDER_PATTERN = /^__\w+__$/;
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1326
|
+
const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
|
|
1327
|
+
const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
|
|
1328
|
+
const isIdentityForward = (matchText) => {
|
|
1329
|
+
const paramsMatch = matchText.match(/\(([^)]*)\)/);
|
|
1330
|
+
const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
|
|
1331
|
+
if (!paramsMatch || !innerMatch) return false;
|
|
1332
|
+
const params = paramNames(paramsMatch[1]);
|
|
1333
|
+
const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
|
|
1334
|
+
if (args.length === 0) return false;
|
|
1335
|
+
return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
|
|
1122
1336
|
};
|
|
1123
1337
|
const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
|
|
1124
1338
|
const detectThinWrappers = (content, relativePath, ext) => {
|
|
@@ -1138,7 +1352,7 @@ const detectThinWrappers = (content, relativePath, ext) => {
|
|
|
1138
1352
|
const prevLine = lines[lineNumber - 2]?.trim();
|
|
1139
1353
|
if (prevLine && prevLine.startsWith("@")) continue;
|
|
1140
1354
|
}
|
|
1141
|
-
if (
|
|
1355
|
+
if (!isIdentityForward(matchText)) continue;
|
|
1142
1356
|
if (isUseContextWrapper(matchText)) continue;
|
|
1143
1357
|
diagnostics.push({
|
|
1144
1358
|
filePath: relativePath,
|
|
@@ -1192,8 +1406,9 @@ const detectOverAbstraction = async (context) => {
|
|
|
1192
1406
|
}
|
|
1193
1407
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1194
1408
|
const ext = path.extname(filePath);
|
|
1195
|
-
|
|
1196
|
-
diagnostics.push(...
|
|
1409
|
+
const codeOnly = maskComments(content, ext);
|
|
1410
|
+
diagnostics.push(...detectThinWrappers(codeOnly, relativePath, ext));
|
|
1411
|
+
diagnostics.push(...detectAiNaming(codeOnly, relativePath));
|
|
1197
1412
|
}
|
|
1198
1413
|
return diagnostics;
|
|
1199
1414
|
};
|
|
@@ -1223,8 +1438,7 @@ const JUSTIFICATION_OPENERS = [
|
|
|
1223
1438
|
/^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
|
|
1224
1439
|
];
|
|
1225
1440
|
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
1226
|
-
const
|
|
1227
|
-
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design)\b/i;
|
|
1441
|
+
const EXPLANATORY_WHY_MARKERS = /\b(?:because|since|otherwise|workaround|caveat|warning|important|assumes?|note:|bug|issue|see\s+(?:issue|above|below)|in\s+prod|in\s+production|breaks?\s+when|fails?\s+when|must\s+run|must\s+be|has\s+to\s+be|hack\s+for|fix\s+for|reason:|to\s+avoid|to\s+ensure|to\s+prevent|in\s+order\s+to|necessary|guarantee[sd]?|prevents?|regardless\s+of|required\s+(?:for|to|by)|for\s+example|e\.g\.|i\.e\.|useful\s+(?:for|when)|intended\s+to|on\s+purpose|by\s+design|ideally|however|although|even\s+though|despite|whereas|unfortunately|trade-?off|first\s+need)\b/i;
|
|
1228
1442
|
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
1229
1443
|
"deprecated",
|
|
1230
1444
|
"see",
|
|
@@ -1320,7 +1534,7 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
|
|
|
1320
1534
|
|
|
1321
1535
|
//#endregion
|
|
1322
1536
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
1323
|
-
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1537
|
+
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|docs?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1324
1538
|
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
|
|
1325
1539
|
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
1326
1540
|
|
|
@@ -1329,7 +1543,7 @@ const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) ||
|
|
|
1329
1543
|
const TRIVIAL_VERB_STEMS = "Import|Defin|Initializ|Setting|Set\\s+up|Setup|Return|Check|Loop|Iterat|Creat|Updat|Delet|Remov|Handl|Get|Fetch|Increment|Decrement|Writ|Runn|Run|Pars|Execut|Extract|Sav|Load|Build|Start|Stopp|Stop|Clean(?:up|\\s+up)?|Configur|Validat|Process|Queue|Fire|Emit|Dispatch|Log|Print|Render";
|
|
1330
1544
|
const TRIVIAL_JS_COMMENT_PATTERNS = [/\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i, new RegExp(`\\/\\/\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
1331
1545
|
const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, new RegExp(`^#\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
1332
|
-
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
|
|
1546
|
+
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?|if|when|unless|until|only|except|otherwise|needs?|must|should|ensure|avoid|prevent|requires?)\b/i;
|
|
1333
1547
|
const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
1334
1548
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
1335
1549
|
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
@@ -1478,6 +1692,7 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
|
|
|
1478
1692
|
"PLACEHOLDER",
|
|
1479
1693
|
"STUB"
|
|
1480
1694
|
].join("|")})[:\\s]`);
|
|
1695
|
+
const TODO_TRACKING_RE = /https?:\/\/|#\d+|\bgh-\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|\b(?:issue|ticket|jira)\b/i;
|
|
1481
1696
|
const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
|
|
1482
1697
|
const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
1483
1698
|
const contextLines = [];
|
|
@@ -1497,7 +1712,10 @@ const detectTodoStubs = (content, relativePath) => {
|
|
|
1497
1712
|
for (let i = 0; i < lines.length; i++) {
|
|
1498
1713
|
const trimmed = lines[i].trim();
|
|
1499
1714
|
if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
|
|
1500
|
-
if (TODO_PATTERN.test(trimmed))
|
|
1715
|
+
if (TODO_PATTERN.test(trimmed)) {
|
|
1716
|
+
if (TODO_TRACKING_RE.test(trimmed)) continue;
|
|
1717
|
+
diagnostics.push(slop(relativePath, i + 1, "ai-slop/todo-stub", "info", "Unresolved TODO/FIXME/HACK comment indicates incomplete code", "Resolve the TODO or create a tracked issue for it", false));
|
|
1718
|
+
}
|
|
1501
1719
|
}
|
|
1502
1720
|
return diagnostics;
|
|
1503
1721
|
};
|
|
@@ -1546,9 +1764,10 @@ const detectDeadPatterns = async (context) => {
|
|
|
1546
1764
|
}
|
|
1547
1765
|
const ext = path.extname(filePath);
|
|
1548
1766
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1549
|
-
|
|
1767
|
+
const codeOnly = maskComments(content, ext);
|
|
1768
|
+
diagnostics.push(...detectConsoleLeftovers(codeOnly, relativePath, ext));
|
|
1550
1769
|
diagnostics.push(...detectTodoStubs(content, relativePath));
|
|
1551
|
-
diagnostics.push(...detectDeadCodePatterns(
|
|
1770
|
+
diagnostics.push(...detectDeadCodePatterns(codeOnly, relativePath, ext));
|
|
1552
1771
|
diagnostics.push(...detectUnsafeTypePatterns(content, relativePath, ext));
|
|
1553
1772
|
}
|
|
1554
1773
|
return diagnostics;
|
|
@@ -1738,6 +1957,7 @@ const JS_EXTENSIONS$3 = new Set([
|
|
|
1738
1957
|
const IMPORT_FROM_RE$1 = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
|
|
1739
1958
|
const TYPE_ONLY_RE = /^\s*type\b/;
|
|
1740
1959
|
const VALUE_BINDING_RE = /\{([^}]*)\}/;
|
|
1960
|
+
const NAMESPACE_RE = /\*\s+as\s+/;
|
|
1741
1961
|
const isTypeOnly = (clause) => {
|
|
1742
1962
|
if (TYPE_ONLY_RE.test(clause)) return true;
|
|
1743
1963
|
const braces = VALUE_BINDING_RE.exec(clause);
|
|
@@ -1755,7 +1975,8 @@ const extractImportLines = (content) => {
|
|
|
1755
1975
|
results.push({
|
|
1756
1976
|
spec: match[2],
|
|
1757
1977
|
line: i + 1,
|
|
1758
|
-
typeOnly: isTypeOnly(match[1])
|
|
1978
|
+
typeOnly: isTypeOnly(match[1]),
|
|
1979
|
+
namespace: NAMESPACE_RE.test(match[1])
|
|
1759
1980
|
});
|
|
1760
1981
|
}
|
|
1761
1982
|
return results;
|
|
@@ -1772,11 +1993,11 @@ const detectDuplicateImports = async (context) => {
|
|
|
1772
1993
|
} catch {
|
|
1773
1994
|
continue;
|
|
1774
1995
|
}
|
|
1775
|
-
const imports = extractImportLines(content);
|
|
1996
|
+
const imports = extractImportLines(maskComments(content, path.extname(filePath)));
|
|
1776
1997
|
if (imports.length < 2) continue;
|
|
1777
1998
|
const byBucket = /* @__PURE__ */ new Map();
|
|
1778
1999
|
for (const imp of imports) {
|
|
1779
|
-
const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
2000
|
+
const key = `${imp.namespace ? "ns" : imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
1780
2001
|
const list = byBucket.get(key) ?? [];
|
|
1781
2002
|
list.push(imp);
|
|
1782
2003
|
byBucket.set(key, list);
|
|
@@ -2023,6 +2244,30 @@ const LOOPBACK_HOSTS = new Set([
|
|
|
2023
2244
|
"0.0.0.0",
|
|
2024
2245
|
"::1"
|
|
2025
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}`));
|
|
2026
2271
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2027
2272
|
const HARDCODED_URL_FINDING = {
|
|
2028
2273
|
rule: "ai-slop/hardcoded-url",
|
|
@@ -2067,6 +2312,7 @@ const shouldFlagUrlLiteral = (line, urlText) => {
|
|
|
2067
2312
|
if (!host) return false;
|
|
2068
2313
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2069
2314
|
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
2315
|
+
if (isVendorApiHost(host)) return false;
|
|
2070
2316
|
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2071
2317
|
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2072
2318
|
};
|
|
@@ -2118,7 +2364,7 @@ const detectHardcodedConfigLiterals = async (context) => {
|
|
|
2118
2364
|
}
|
|
2119
2365
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2120
2366
|
const ext = path.extname(filePath);
|
|
2121
|
-
diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
|
|
2367
|
+
diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
|
|
2122
2368
|
}
|
|
2123
2369
|
return diagnostics;
|
|
2124
2370
|
};
|
|
@@ -2251,7 +2497,9 @@ const PYTHON_STDLIB = new Set([
|
|
|
2251
2497
|
"builtins",
|
|
2252
2498
|
"bz2",
|
|
2253
2499
|
"calendar",
|
|
2500
|
+
"code",
|
|
2254
2501
|
"codecs",
|
|
2502
|
+
"codeop",
|
|
2255
2503
|
"collections",
|
|
2256
2504
|
"concurrent",
|
|
2257
2505
|
"configparser",
|
|
@@ -2324,6 +2572,7 @@ const PYTHON_STDLIB = new Set([
|
|
|
2324
2572
|
"readline",
|
|
2325
2573
|
"reprlib",
|
|
2326
2574
|
"resource",
|
|
2575
|
+
"rlcompleter",
|
|
2327
2576
|
"secrets",
|
|
2328
2577
|
"select",
|
|
2329
2578
|
"selectors",
|
|
@@ -2512,6 +2761,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
2512
2761
|
}
|
|
2513
2762
|
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
2514
2763
|
if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2764
|
+
const groups = content.match(/\[dependency-groups\]([\s\S]*?)(?=\n\[[^[]|$)/);
|
|
2765
|
+
if (groups) for (const m of groups[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2515
2766
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
2516
2767
|
let match = poetryRe.exec(content);
|
|
2517
2768
|
while (match !== null) {
|
|
@@ -2744,9 +2995,28 @@ const extractJsImports = (content) => {
|
|
|
2744
2995
|
const extractPyImports = (content) => {
|
|
2745
2996
|
const lines = content.split("\n");
|
|
2746
2997
|
const results = [];
|
|
2998
|
+
let inDoc = null;
|
|
2999
|
+
let typeCheckIndent = -1;
|
|
2747
3000
|
for (let i = 0; i < lines.length; i++) {
|
|
2748
|
-
const
|
|
2749
|
-
|
|
3001
|
+
const raw = lines[i];
|
|
3002
|
+
const line = raw.trim();
|
|
3003
|
+
if (inDoc) {
|
|
3004
|
+
if (line.includes(inDoc)) inDoc = null;
|
|
3005
|
+
continue;
|
|
3006
|
+
}
|
|
3007
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
3008
|
+
const triples = line.match(/"""|'''/g);
|
|
3009
|
+
if (triples) {
|
|
3010
|
+
if (triples.length % 2 === 1) inDoc = triples[triples.length - 1];
|
|
3011
|
+
continue;
|
|
3012
|
+
}
|
|
3013
|
+
const indent = raw.length - raw.trimStart().length;
|
|
3014
|
+
if (typeCheckIndent >= 0 && indent <= typeCheckIndent) typeCheckIndent = -1;
|
|
3015
|
+
if (/^if\s+(?:[\w.]+\.)?TYPE_CHECKING\b/.test(line)) {
|
|
3016
|
+
typeCheckIndent = indent;
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
if (typeCheckIndent >= 0) continue;
|
|
2750
3020
|
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
2751
3021
|
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
2752
3022
|
results.push({
|
|
@@ -2756,8 +3026,8 @@ const extractPyImports = (content) => {
|
|
|
2756
3026
|
continue;
|
|
2757
3027
|
}
|
|
2758
3028
|
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
2759
|
-
if (importMatch) for (const
|
|
2760
|
-
const cleaned =
|
|
3029
|
+
if (importMatch) for (const part of importMatch[1].split(",")) {
|
|
3030
|
+
const cleaned = part.trim().split(/\s+as\s+/)[0];
|
|
2761
3031
|
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
2762
3032
|
spec: cleaned,
|
|
2763
3033
|
line: i + 1
|
|
@@ -2814,6 +3084,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2814
3084
|
continue;
|
|
2815
3085
|
}
|
|
2816
3086
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3087
|
+
if (isNonProductionPath(relPath)) continue;
|
|
2817
3088
|
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2818
3089
|
for (const { spec, line } of imports) {
|
|
2819
3090
|
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
@@ -2941,7 +3212,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
2941
3212
|
//#endregion
|
|
2942
3213
|
//#region src/engines/ai-slop/meta-comment.ts
|
|
2943
3214
|
const PLAN_REFERENCE_RES = [
|
|
2944
|
-
|
|
3215
|
+
/^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
|
|
2945
3216
|
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
2946
3217
|
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
2947
3218
|
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
@@ -3043,24 +3314,6 @@ const looksLikeLicenseHeader = (block) => {
|
|
|
3043
3314
|
const text = block.rawLines.join(" ").toLowerCase();
|
|
3044
3315
|
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
3045
3316
|
};
|
|
3046
|
-
const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
|
|
3047
|
-
const isBareSectionLabel = (prose) => {
|
|
3048
|
-
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
3049
|
-
if (prose.endsWith(".")) return false;
|
|
3050
|
-
if (prose.split(/\s+/).length > 3) return false;
|
|
3051
|
-
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
3052
|
-
return true;
|
|
3053
|
-
};
|
|
3054
|
-
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
3055
|
-
const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
3056
|
-
if (nextLine === null) return false;
|
|
3057
|
-
if (!DATA_ENTRY_START.test(nextLine)) return false;
|
|
3058
|
-
const trimmed = nextLine.trim();
|
|
3059
|
-
if (trimmed.startsWith("case ")) return true;
|
|
3060
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
|
|
3061
|
-
if (/^\w+\s*:/.test(trimmed)) return true;
|
|
3062
|
-
return false;
|
|
3063
|
-
};
|
|
3064
3317
|
const looksLikeSuppressDirective = (block) => block.rawLines.some((l) => /\b(biome-ignore|eslint-disable|ts-ignore|ts-expect-error|@ts-\w+|noqa|pylint:\s*disable|rubocop:disable|noinspection|phpcs:disable)\b/.test(l));
|
|
3065
3318
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
3066
3319
|
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
@@ -3149,10 +3402,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3149
3402
|
matched: true,
|
|
3150
3403
|
reason: "phase/section header"
|
|
3151
3404
|
};
|
|
3152
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
3153
|
-
matched: true,
|
|
3154
|
-
reason: "bare section label"
|
|
3155
|
-
};
|
|
3156
3405
|
const joined = block.prose.join(" ");
|
|
3157
3406
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
3158
3407
|
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
@@ -3183,17 +3432,11 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
3183
3432
|
};
|
|
3184
3433
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
3185
3434
|
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
3186
|
-
if (nonEmptyProseCount >= 5) {
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
return {
|
|
3192
|
-
matched: true,
|
|
3193
|
-
reason: "long narrative block"
|
|
3194
|
-
};
|
|
3195
|
-
}
|
|
3196
|
-
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
|
|
3435
|
+
if (nonEmptyProseCount >= 5 && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
3436
|
+
matched: true,
|
|
3437
|
+
reason: "long narrative block"
|
|
3438
|
+
};
|
|
3439
|
+
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
3197
3440
|
matched: true,
|
|
3198
3441
|
reason: "multi-line narrative prose"
|
|
3199
3442
|
};
|
|
@@ -3627,7 +3870,7 @@ const detectRustPatterns = async (context) => {
|
|
|
3627
3870
|
|
|
3628
3871
|
//#endregion
|
|
3629
3872
|
//#region src/engines/ai-slop/silent-recovery.ts
|
|
3630
|
-
const JS_EXTS
|
|
3873
|
+
const JS_EXTS = new Set([
|
|
3631
3874
|
".ts",
|
|
3632
3875
|
".tsx",
|
|
3633
3876
|
".js",
|
|
@@ -3635,7 +3878,14 @@ const JS_EXTS$1 = new Set([
|
|
|
3635
3878
|
".mjs",
|
|
3636
3879
|
".cjs"
|
|
3637
3880
|
]);
|
|
3638
|
-
const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
|
|
3881
|
+
const CATCH_HEAD_RE = /\bcatch\s*(?:\(\s*([^)]*?)\s*\))?\s*\{/g;
|
|
3882
|
+
const isIdentifier = (s) => /^[A-Za-z_$][\w$]*$/.test(s);
|
|
3883
|
+
const recoveryDropsError = (binding, body) => {
|
|
3884
|
+
const name = binding?.trim() ?? "";
|
|
3885
|
+
if (name === "") return true;
|
|
3886
|
+
if (!isIdentifier(name)) return false;
|
|
3887
|
+
return !new RegExp(`\\b${name}\\b`).test(body);
|
|
3888
|
+
};
|
|
3639
3889
|
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3640
3890
|
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3641
3891
|
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -3685,14 +3935,15 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3685
3935
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3686
3936
|
if (body === null) continue;
|
|
3687
3937
|
if (!isLogOnlyBody(body)) continue;
|
|
3938
|
+
if (!recoveryDropsError(match[1], body)) continue;
|
|
3688
3939
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3689
3940
|
out.push({
|
|
3690
3941
|
filePath: relPath,
|
|
3691
3942
|
engine: "ai-slop",
|
|
3692
3943
|
rule: "ai-slop/silent-recovery",
|
|
3693
3944
|
severity: "warning",
|
|
3694
|
-
message: "Catch
|
|
3695
|
-
help: "
|
|
3945
|
+
message: "Catch logs without the caught error then continues; the failure cause is lost",
|
|
3946
|
+
help: "Include the caught error in the log, or rethrow / recover explicitly, so the failure stays diagnosable.",
|
|
3696
3947
|
line,
|
|
3697
3948
|
column: 0,
|
|
3698
3949
|
category: "AI Slop",
|
|
@@ -3702,6 +3953,7 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3702
3953
|
return out;
|
|
3703
3954
|
};
|
|
3704
3955
|
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3956
|
+
const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
|
|
3705
3957
|
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3706
3958
|
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3707
3959
|
const detectPySilentRecovery = (content, relPath) => {
|
|
@@ -3725,13 +3977,14 @@ const detectPySilentRecovery = (content, relPath) => {
|
|
|
3725
3977
|
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3726
3978
|
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3727
3979
|
if (!allLogs || !sawLog) continue;
|
|
3980
|
+
if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
|
|
3728
3981
|
out.push({
|
|
3729
3982
|
filePath: relPath,
|
|
3730
3983
|
engine: "ai-slop",
|
|
3731
3984
|
rule: "ai-slop/silent-recovery",
|
|
3732
3985
|
severity: "warning",
|
|
3733
|
-
message: "except
|
|
3734
|
-
help: "
|
|
3986
|
+
message: "except logs without the caught error then continues; the failure cause is lost",
|
|
3987
|
+
help: "Include the caught error in the log, or re-raise / recover explicitly, so the failure stays diagnosable.",
|
|
3735
3988
|
line: i + 1,
|
|
3736
3989
|
column: 0,
|
|
3737
3990
|
category: "AI Slop",
|
|
@@ -3746,7 +3999,7 @@ const detectSilentRecovery = async (context) => {
|
|
|
3746
3999
|
for (const filePath of files) {
|
|
3747
4000
|
if (isAutoGenerated(filePath)) continue;
|
|
3748
4001
|
const ext = path.extname(filePath);
|
|
3749
|
-
const isJs = JS_EXTS
|
|
4002
|
+
const isJs = JS_EXTS.has(ext);
|
|
3750
4003
|
if (!isJs && !(ext === ".py")) continue;
|
|
3751
4004
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3752
4005
|
if (isNonProductionPath(relPath)) continue;
|
|
@@ -3834,10 +4087,11 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3834
4087
|
const importLines = /* @__PURE__ */ new Set();
|
|
3835
4088
|
for (let i = 0; i < lines.length; i++) {
|
|
3836
4089
|
const trimmed = lines[i].trim();
|
|
3837
|
-
const fromMatch = trimmed.match(/^from\s+[\w.]
|
|
4090
|
+
const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
|
|
3838
4091
|
if (fromMatch) {
|
|
3839
4092
|
importLines.add(i);
|
|
3840
|
-
|
|
4093
|
+
if (fromMatch[1] === "__future__") continue;
|
|
4094
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
3841
4095
|
if (importPart === "*") continue;
|
|
3842
4096
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
3843
4097
|
for (const item of cleaned.split(",")) {
|
|
@@ -4225,12 +4479,92 @@ const findBraceFunctionEnd = (lines, startIndex) => {
|
|
|
4225
4479
|
maxNesting
|
|
4226
4480
|
};
|
|
4227
4481
|
};
|
|
4228
|
-
const
|
|
4229
|
-
|
|
4230
|
-
let
|
|
4482
|
+
const extractPythonSignature = (lines, startIndex) => {
|
|
4483
|
+
let depth = 0;
|
|
4484
|
+
let started = false;
|
|
4485
|
+
let params = "";
|
|
4486
|
+
for (let j = startIndex; j < lines.length; j++) {
|
|
4487
|
+
const l = lines[j];
|
|
4488
|
+
for (let ci = 0; ci < l.length; ci++) {
|
|
4489
|
+
const ch = l[ci];
|
|
4490
|
+
if (ch === "(") {
|
|
4491
|
+
depth++;
|
|
4492
|
+
if (depth === 1 && !started) {
|
|
4493
|
+
started = true;
|
|
4494
|
+
continue;
|
|
4495
|
+
}
|
|
4496
|
+
} else if (ch === ")") {
|
|
4497
|
+
depth--;
|
|
4498
|
+
if (depth === 0) return {
|
|
4499
|
+
params,
|
|
4500
|
+
sigEndIndex: j
|
|
4501
|
+
};
|
|
4502
|
+
}
|
|
4503
|
+
if (started) params += ch;
|
|
4504
|
+
}
|
|
4505
|
+
if (started) params += " ";
|
|
4506
|
+
}
|
|
4507
|
+
return {
|
|
4508
|
+
params,
|
|
4509
|
+
sigEndIndex: startIndex
|
|
4510
|
+
};
|
|
4511
|
+
};
|
|
4512
|
+
const countPythonParams = (signature) => {
|
|
4513
|
+
let depth = 0;
|
|
4514
|
+
const parts = [];
|
|
4515
|
+
let current = "";
|
|
4516
|
+
for (const ch of signature) {
|
|
4517
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
4518
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
4519
|
+
if (ch === "," && depth === 0) {
|
|
4520
|
+
parts.push(current);
|
|
4521
|
+
current = "";
|
|
4522
|
+
continue;
|
|
4523
|
+
}
|
|
4524
|
+
current += ch;
|
|
4525
|
+
}
|
|
4526
|
+
parts.push(current);
|
|
4527
|
+
let count = 0;
|
|
4528
|
+
for (const raw of parts) {
|
|
4529
|
+
const p = raw.trim();
|
|
4530
|
+
if (p.length === 0 || p === "*" || p === "/") continue;
|
|
4531
|
+
if (p.startsWith("*")) continue;
|
|
4532
|
+
if (p.includes("=")) continue;
|
|
4533
|
+
const name = p.split(":")[0].trim();
|
|
4534
|
+
if (name === "self" || name === "cls") continue;
|
|
4535
|
+
count++;
|
|
4536
|
+
}
|
|
4537
|
+
return count;
|
|
4538
|
+
};
|
|
4539
|
+
const countPythonBodyCodeLines = (lines, sigEndIndex, endLine) => {
|
|
4540
|
+
let count = 0;
|
|
4541
|
+
let inDoc = false;
|
|
4542
|
+
let delim = "";
|
|
4543
|
+
for (let j = sigEndIndex + 1; j <= endLine && j < lines.length; j++) {
|
|
4544
|
+
const t = lines[j].trim();
|
|
4545
|
+
if (inDoc) {
|
|
4546
|
+
if (t.includes(delim)) inDoc = false;
|
|
4547
|
+
continue;
|
|
4548
|
+
}
|
|
4549
|
+
if (t === "" || t.startsWith("#")) continue;
|
|
4550
|
+
const opener = t.startsWith("\"\"\"") ? "\"\"\"" : t.startsWith("'''") ? "'''" : "";
|
|
4551
|
+
if (opener) {
|
|
4552
|
+
if (!t.slice(3).includes(opener)) {
|
|
4553
|
+
inDoc = true;
|
|
4554
|
+
delim = opener;
|
|
4555
|
+
}
|
|
4556
|
+
continue;
|
|
4557
|
+
}
|
|
4558
|
+
count++;
|
|
4559
|
+
}
|
|
4560
|
+
return count;
|
|
4561
|
+
};
|
|
4562
|
+
const findPythonFunctionEnd = (lines, defIndex, bodyStartIndex) => {
|
|
4563
|
+
const baseIndent = lines[defIndex].match(/^(\s*)/)?.[1].length ?? 0;
|
|
4564
|
+
let endLine = bodyStartIndex;
|
|
4231
4565
|
let maxNesting = 0;
|
|
4232
4566
|
const controlIndentStack = [];
|
|
4233
|
-
for (let j =
|
|
4567
|
+
for (let j = bodyStartIndex + 1; j < lines.length; j++) {
|
|
4234
4568
|
const l = lines[j];
|
|
4235
4569
|
if (l.trim() === "") {
|
|
4236
4570
|
endLine = j;
|
|
@@ -4252,7 +4586,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
|
|
|
4252
4586
|
};
|
|
4253
4587
|
};
|
|
4254
4588
|
const findFunctionEnd = (lines, startIndex, isPython) => {
|
|
4255
|
-
if (isPython)
|
|
4589
|
+
if (isPython) {
|
|
4590
|
+
const { sigEndIndex } = extractPythonSignature(lines, startIndex);
|
|
4591
|
+
return findPythonFunctionEnd(lines, startIndex, sigEndIndex);
|
|
4592
|
+
}
|
|
4256
4593
|
return findBraceFunctionEnd(lines, startIndex);
|
|
4257
4594
|
};
|
|
4258
4595
|
const isBlockArrow = (lines, startIndex) => {
|
|
@@ -4317,7 +4654,7 @@ const FUNCTION_PATTERNS = [
|
|
|
4317
4654
|
]
|
|
4318
4655
|
},
|
|
4319
4656
|
{
|
|
4320
|
-
regex: /^\s*def\s+(\w+)\s*\(
|
|
4657
|
+
regex: /^\s*(?:async\s+)?def\s+(\w+)\s*\(/,
|
|
4321
4658
|
langFilter: [".py"]
|
|
4322
4659
|
},
|
|
4323
4660
|
{
|
|
@@ -4374,14 +4711,23 @@ const analyzeFunctions = (content, ext) => {
|
|
|
4374
4711
|
const isPython = fnMatch.patternIndex === 2;
|
|
4375
4712
|
if (fnMatch.patternIndex === 1 && !isBlockArrow(lines, i)) continue;
|
|
4376
4713
|
const { endLine, maxNesting } = findFunctionEnd(lines, i, isPython);
|
|
4377
|
-
|
|
4378
|
-
|
|
4714
|
+
let templateLines;
|
|
4715
|
+
let paramCount;
|
|
4716
|
+
if (isPython) {
|
|
4717
|
+
const sig = extractPythonSignature(lines, i);
|
|
4718
|
+
const codeLines = countPythonBodyCodeLines(lines, sig.sigEndIndex, endLine);
|
|
4719
|
+
templateLines = endLine - i + 1 - codeLines;
|
|
4720
|
+
paramCount = countPythonParams(sig.params);
|
|
4721
|
+
} else {
|
|
4722
|
+
templateLines = countTemplateLines(lines.slice(i + 1, endLine));
|
|
4723
|
+
paramCount = countParams(fnMatch.params);
|
|
4724
|
+
}
|
|
4379
4725
|
functions.push({
|
|
4380
4726
|
name: fnMatch.name,
|
|
4381
4727
|
startLine: i + 1,
|
|
4382
4728
|
lineCount: endLine - i + 1,
|
|
4383
4729
|
maxNesting,
|
|
4384
|
-
paramCount
|
|
4730
|
+
paramCount,
|
|
4385
4731
|
templateLines
|
|
4386
4732
|
});
|
|
4387
4733
|
}
|
|
@@ -6591,212 +6937,6 @@ const runCargoAudit = async (rootDir, timeout) => {
|
|
|
6591
6937
|
}
|
|
6592
6938
|
};
|
|
6593
6939
|
|
|
6594
|
-
//#endregion
|
|
6595
|
-
//#region src/utils/source-masker.ts
|
|
6596
|
-
const JS_EXTS = new Set([
|
|
6597
|
-
".ts",
|
|
6598
|
-
".tsx",
|
|
6599
|
-
".js",
|
|
6600
|
-
".jsx",
|
|
6601
|
-
".mjs",
|
|
6602
|
-
".cjs"
|
|
6603
|
-
]);
|
|
6604
|
-
const PY_EXTS = new Set([".py"]);
|
|
6605
|
-
const RB_EXTS = new Set([".rb"]);
|
|
6606
|
-
const PHP_EXTS = new Set([".php"]);
|
|
6607
|
-
const familyForExt = (ext) => {
|
|
6608
|
-
if (JS_EXTS.has(ext)) return "js";
|
|
6609
|
-
if (PY_EXTS.has(ext)) return "py";
|
|
6610
|
-
if (RB_EXTS.has(ext)) return "rb";
|
|
6611
|
-
if (PHP_EXTS.has(ext)) return "php";
|
|
6612
|
-
return "none";
|
|
6613
|
-
};
|
|
6614
|
-
const maskStringsAndComments = (content, ext) => {
|
|
6615
|
-
const family = familyForExt(ext);
|
|
6616
|
-
if (family === "none") return content;
|
|
6617
|
-
if (family === "js") return maskJs(content);
|
|
6618
|
-
return maskSimple(content, family);
|
|
6619
|
-
};
|
|
6620
|
-
const handleQuotesAndComments = (content, i, tplStack, mask) => {
|
|
6621
|
-
const len = content.length;
|
|
6622
|
-
const c = content[i];
|
|
6623
|
-
const next = content[i + 1];
|
|
6624
|
-
if (c === "\"" || c === "'") {
|
|
6625
|
-
const strStart = i;
|
|
6626
|
-
const end = consumeQuotedString(content, i, c);
|
|
6627
|
-
mask(strStart + 1, end - 1);
|
|
6628
|
-
return {
|
|
6629
|
-
handled: true,
|
|
6630
|
-
nextI: end
|
|
6631
|
-
};
|
|
6632
|
-
}
|
|
6633
|
-
if (c === "`") {
|
|
6634
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
6635
|
-
mask(i + 1, scan.maskEnd);
|
|
6636
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
6637
|
-
return {
|
|
6638
|
-
handled: true,
|
|
6639
|
-
nextI: scan.resumeAt
|
|
6640
|
-
};
|
|
6641
|
-
}
|
|
6642
|
-
if (c === "/" && next === "/") {
|
|
6643
|
-
const strStart = i;
|
|
6644
|
-
let k = i;
|
|
6645
|
-
while (k < len && content[k] !== "\n") k++;
|
|
6646
|
-
mask(strStart, k);
|
|
6647
|
-
return {
|
|
6648
|
-
handled: true,
|
|
6649
|
-
nextI: k
|
|
6650
|
-
};
|
|
6651
|
-
}
|
|
6652
|
-
if (c === "/" && next === "*") {
|
|
6653
|
-
const strStart = i;
|
|
6654
|
-
let k = i + 2;
|
|
6655
|
-
while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
|
|
6656
|
-
if (k < len - 1) k += 2;
|
|
6657
|
-
mask(strStart, k);
|
|
6658
|
-
return {
|
|
6659
|
-
handled: true,
|
|
6660
|
-
nextI: k
|
|
6661
|
-
};
|
|
6662
|
-
}
|
|
6663
|
-
return {
|
|
6664
|
-
handled: false,
|
|
6665
|
-
nextI: i
|
|
6666
|
-
};
|
|
6667
|
-
};
|
|
6668
|
-
const maskJs = (content) => {
|
|
6669
|
-
const out = content.split("");
|
|
6670
|
-
const len = content.length;
|
|
6671
|
-
const tplStack = [];
|
|
6672
|
-
let i = 0;
|
|
6673
|
-
const mask = (start, end) => {
|
|
6674
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
6675
|
-
};
|
|
6676
|
-
while (i < len) {
|
|
6677
|
-
const c = content[i];
|
|
6678
|
-
if (tplStack.length > 0) {
|
|
6679
|
-
if (c === "{") {
|
|
6680
|
-
tplStack[tplStack.length - 1]++;
|
|
6681
|
-
i++;
|
|
6682
|
-
continue;
|
|
6683
|
-
}
|
|
6684
|
-
if (c === "}") {
|
|
6685
|
-
if (tplStack[tplStack.length - 1] === 0) {
|
|
6686
|
-
tplStack.pop();
|
|
6687
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
6688
|
-
mask(i + 1, scan.maskEnd);
|
|
6689
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
6690
|
-
i = scan.resumeAt;
|
|
6691
|
-
continue;
|
|
6692
|
-
}
|
|
6693
|
-
tplStack[tplStack.length - 1]--;
|
|
6694
|
-
i++;
|
|
6695
|
-
continue;
|
|
6696
|
-
}
|
|
6697
|
-
}
|
|
6698
|
-
const handled = handleQuotesAndComments(content, i, tplStack, mask);
|
|
6699
|
-
if (handled.handled) {
|
|
6700
|
-
i = handled.nextI;
|
|
6701
|
-
continue;
|
|
6702
|
-
}
|
|
6703
|
-
i++;
|
|
6704
|
-
}
|
|
6705
|
-
return out.join("");
|
|
6706
|
-
};
|
|
6707
|
-
const consumeQuotedString = (content, start, quote) => {
|
|
6708
|
-
const len = content.length;
|
|
6709
|
-
let i = start + 1;
|
|
6710
|
-
while (i < len) {
|
|
6711
|
-
const c = content[i];
|
|
6712
|
-
if (c === "\\" && i + 1 < len) {
|
|
6713
|
-
i += 2;
|
|
6714
|
-
continue;
|
|
6715
|
-
}
|
|
6716
|
-
if (c === quote) return i + 1;
|
|
6717
|
-
if (c === "\n") return i;
|
|
6718
|
-
i++;
|
|
6719
|
-
}
|
|
6720
|
-
return i;
|
|
6721
|
-
};
|
|
6722
|
-
const consumeTemplateString = (content, start) => {
|
|
6723
|
-
const len = content.length;
|
|
6724
|
-
let i = start;
|
|
6725
|
-
while (i < len) {
|
|
6726
|
-
const c = content[i];
|
|
6727
|
-
if (c === "\\" && i + 1 < len) {
|
|
6728
|
-
i += 2;
|
|
6729
|
-
continue;
|
|
6730
|
-
}
|
|
6731
|
-
if (c === "`") return {
|
|
6732
|
-
maskEnd: i,
|
|
6733
|
-
resumeAt: i + 1,
|
|
6734
|
-
openedInterp: false
|
|
6735
|
-
};
|
|
6736
|
-
if (c === "$" && content[i + 1] === "{") return {
|
|
6737
|
-
maskEnd: i,
|
|
6738
|
-
resumeAt: i + 2,
|
|
6739
|
-
openedInterp: true
|
|
6740
|
-
};
|
|
6741
|
-
i++;
|
|
6742
|
-
}
|
|
6743
|
-
return {
|
|
6744
|
-
maskEnd: i,
|
|
6745
|
-
resumeAt: i,
|
|
6746
|
-
openedInterp: false
|
|
6747
|
-
};
|
|
6748
|
-
};
|
|
6749
|
-
const maskSimple = (content, family) => {
|
|
6750
|
-
const out = content.split("");
|
|
6751
|
-
const len = content.length;
|
|
6752
|
-
let i = 0;
|
|
6753
|
-
const mask = (start, end) => {
|
|
6754
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
6755
|
-
};
|
|
6756
|
-
while (i < len) {
|
|
6757
|
-
const c = content[i];
|
|
6758
|
-
const next = content[i + 1];
|
|
6759
|
-
if (family === "py" && (c === "\"" || c === "'")) {
|
|
6760
|
-
if (content[i + 1] === c && content[i + 2] === c) {
|
|
6761
|
-
const triple = c + c + c;
|
|
6762
|
-
const end = content.indexOf(triple, i + 3);
|
|
6763
|
-
const stop = end === -1 ? len : end + 3;
|
|
6764
|
-
mask(i + 3, stop - 3);
|
|
6765
|
-
i = stop;
|
|
6766
|
-
continue;
|
|
6767
|
-
}
|
|
6768
|
-
}
|
|
6769
|
-
if (c === "\"" || c === "'") {
|
|
6770
|
-
const strStart = i;
|
|
6771
|
-
i = consumeQuotedString(content, i, c);
|
|
6772
|
-
mask(strStart + 1, i - 1);
|
|
6773
|
-
continue;
|
|
6774
|
-
}
|
|
6775
|
-
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
6776
|
-
const strStart = i;
|
|
6777
|
-
while (i < len && content[i] !== "\n") i++;
|
|
6778
|
-
mask(strStart, i);
|
|
6779
|
-
continue;
|
|
6780
|
-
}
|
|
6781
|
-
if (family === "php" && c === "/" && next === "/") {
|
|
6782
|
-
const strStart = i;
|
|
6783
|
-
while (i < len && content[i] !== "\n") i++;
|
|
6784
|
-
mask(strStart, i);
|
|
6785
|
-
continue;
|
|
6786
|
-
}
|
|
6787
|
-
if (family === "php" && c === "/" && next === "*") {
|
|
6788
|
-
const strStart = i;
|
|
6789
|
-
i += 2;
|
|
6790
|
-
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
6791
|
-
if (i < len - 1) i += 2;
|
|
6792
|
-
mask(strStart, i);
|
|
6793
|
-
continue;
|
|
6794
|
-
}
|
|
6795
|
-
i++;
|
|
6796
|
-
}
|
|
6797
|
-
return out.join("");
|
|
6798
|
-
};
|
|
6799
|
-
|
|
6800
6940
|
//#endregion
|
|
6801
6941
|
//#region src/engines/security/risky.ts
|
|
6802
6942
|
const ev = "eval";
|
|
@@ -7071,6 +7211,7 @@ const scanSecrets = async (context) => {
|
|
|
7071
7211
|
} catch {
|
|
7072
7212
|
continue;
|
|
7073
7213
|
}
|
|
7214
|
+
content = maskComments(content, path.extname(filePath));
|
|
7074
7215
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
7075
7216
|
for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
|
|
7076
7217
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
@@ -7159,6 +7300,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
7159
7300
|
//#endregion
|
|
7160
7301
|
//#region src/scoring/index.ts
|
|
7161
7302
|
const PERFECT_SCORE = 100;
|
|
7303
|
+
const STYLE_RULES = new Set([
|
|
7304
|
+
"ai-slop/trivial-comment",
|
|
7305
|
+
"ai-slop/narrative-comment",
|
|
7306
|
+
"complexity/file-too-large",
|
|
7307
|
+
"complexity/function-too-long"
|
|
7308
|
+
]);
|
|
7309
|
+
const STYLE_WEIGHT = .5;
|
|
7162
7310
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
7163
7311
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
7164
7312
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
@@ -7173,7 +7321,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
7173
7321
|
for (const d of diagnostics) {
|
|
7174
7322
|
const engineWeight = weights[d.engine] ?? 1;
|
|
7175
7323
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
7176
|
-
|
|
7324
|
+
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
7325
|
+
deductions += severityPenalty * engineWeight * styleFactor;
|
|
7177
7326
|
}
|
|
7178
7327
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
7179
7328
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
@@ -11684,7 +11833,7 @@ const runNpmAuditFix = async (rootDir, onProgress) => {
|
|
|
11684
11833
|
});
|
|
11685
11834
|
if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout || "npm install failed after audit fix");
|
|
11686
11835
|
};
|
|
11687
|
-
const fetchLatestVersion = async (rootDir, pkgName, pm) => {
|
|
11836
|
+
const fetchLatestVersion$1 = async (rootDir, pkgName, pm) => {
|
|
11688
11837
|
try {
|
|
11689
11838
|
const result = await runSubprocess(pm, [
|
|
11690
11839
|
"view",
|
|
@@ -11704,7 +11853,7 @@ const collectOverrides = async (rootDir, vulnerabilities, pm) => {
|
|
|
11704
11853
|
const overrides = {};
|
|
11705
11854
|
for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
|
|
11706
11855
|
if (vuln.fixAvailable !== false || !vuln.range) continue;
|
|
11707
|
-
const latest = await fetchLatestVersion(rootDir, pkgName, pm);
|
|
11856
|
+
const latest = await fetchLatestVersion$1(rootDir, pkgName, pm);
|
|
11708
11857
|
if (latest) overrides[pkgName] = latest;
|
|
11709
11858
|
}
|
|
11710
11859
|
return overrides;
|
|
@@ -12667,6 +12816,86 @@ const trendCommand = (directory, limit) => {
|
|
|
12667
12816
|
}));
|
|
12668
12817
|
};
|
|
12669
12818
|
|
|
12819
|
+
//#endregion
|
|
12820
|
+
//#region src/update-notifier.ts
|
|
12821
|
+
const REGISTRY_URL = "https://registry.npmjs.org/aislop/latest";
|
|
12822
|
+
const CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
|
|
12823
|
+
const REQUEST_TIMEOUT_MS = 2e3;
|
|
12824
|
+
const CACHE_BASENAME = "update_check.json";
|
|
12825
|
+
const isUpdateNotifierDisabled = (env = process.env) => {
|
|
12826
|
+
if (env.AISLOP_NO_UPDATE_NOTIFIER === "1") return true;
|
|
12827
|
+
if (env.NO_UPDATE_NOTIFIER === "1") return true;
|
|
12828
|
+
if (env.DO_NOT_TRACK === "1") return true;
|
|
12829
|
+
return isCiEnv(env);
|
|
12830
|
+
};
|
|
12831
|
+
const resolveUpdateCachePath = (homedir = os.homedir(), env = process.env) => {
|
|
12832
|
+
if (process.platform === "linux" && env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "aislop", CACHE_BASENAME);
|
|
12833
|
+
return path.join(homedir, ".aislop", CACHE_BASENAME);
|
|
12834
|
+
};
|
|
12835
|
+
const parseVersion = (raw) => {
|
|
12836
|
+
const m = raw.trim().replace(/^v/, "").split(/[-+]/, 1)[0].match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
12837
|
+
if (!m) return null;
|
|
12838
|
+
return {
|
|
12839
|
+
major: Number(m[1]),
|
|
12840
|
+
minor: Number(m[2]),
|
|
12841
|
+
patch: Number(m[3])
|
|
12842
|
+
};
|
|
12843
|
+
};
|
|
12844
|
+
const isOutdated = (current, latest) => {
|
|
12845
|
+
const c = parseVersion(current);
|
|
12846
|
+
const l = parseVersion(latest);
|
|
12847
|
+
if (!c || !l) return false;
|
|
12848
|
+
if (l.major !== c.major) return l.major > c.major;
|
|
12849
|
+
if (l.minor !== c.minor) return l.minor > c.minor;
|
|
12850
|
+
return l.patch > c.patch;
|
|
12851
|
+
};
|
|
12852
|
+
const formatUpdateNotice = (current, latest) => `\nUpdate available: ${current} -> ${latest}. Run npx aislop@latest to upgrade.\n`;
|
|
12853
|
+
const readCache = (cachePath) => {
|
|
12854
|
+
try {
|
|
12855
|
+
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
12856
|
+
if (typeof parsed?.latest === "string" && typeof parsed?.checkedAt === "number") return {
|
|
12857
|
+
latest: parsed.latest,
|
|
12858
|
+
checkedAt: parsed.checkedAt
|
|
12859
|
+
};
|
|
12860
|
+
return null;
|
|
12861
|
+
} catch {
|
|
12862
|
+
return null;
|
|
12863
|
+
}
|
|
12864
|
+
};
|
|
12865
|
+
const writeCache = (cachePath, cache) => {
|
|
12866
|
+
try {
|
|
12867
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
12868
|
+
fs.writeFileSync(cachePath, JSON.stringify(cache));
|
|
12869
|
+
return true;
|
|
12870
|
+
} catch {
|
|
12871
|
+
return false;
|
|
12872
|
+
}
|
|
12873
|
+
};
|
|
12874
|
+
const fetchLatestVersion = async () => {
|
|
12875
|
+
try {
|
|
12876
|
+
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
12877
|
+
if (!res.ok) return null;
|
|
12878
|
+
const data = await res.json();
|
|
12879
|
+
return typeof data.version === "string" ? data.version : null;
|
|
12880
|
+
} catch {
|
|
12881
|
+
return null;
|
|
12882
|
+
}
|
|
12883
|
+
};
|
|
12884
|
+
const maybeNotifyUpdate = async (now = Date.now()) => {
|
|
12885
|
+
if (isUpdateNotifierDisabled()) return;
|
|
12886
|
+
if (!process.stderr.isTTY) return;
|
|
12887
|
+
const cachePath = resolveUpdateCachePath();
|
|
12888
|
+
const cache = readCache(cachePath);
|
|
12889
|
+
if (cache && isOutdated(APP_VERSION, cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
|
|
12890
|
+
if (!cache || now - cache.checkedAt > CHECK_INTERVAL_MS) {
|
|
12891
|
+
const latest = await fetchLatestVersion();
|
|
12892
|
+
if (latest) writeCache(cachePath, {
|
|
12893
|
+
latest,
|
|
12894
|
+
checkedAt: now
|
|
12895
|
+
});
|
|
12896
|
+
}
|
|
12897
|
+
};
|
|
12898
|
+
|
|
12670
12899
|
//#endregion
|
|
12671
12900
|
//#region src/cli.ts
|
|
12672
12901
|
process.on("SIGINT", () => process.exit(0));
|
|
@@ -12920,6 +13149,7 @@ const main = async () => {
|
|
|
12920
13149
|
fireInstalledOnce();
|
|
12921
13150
|
await program.parseAsync();
|
|
12922
13151
|
await flushTelemetry();
|
|
13152
|
+
await maybeNotifyUpdate();
|
|
12923
13153
|
};
|
|
12924
13154
|
main();
|
|
12925
13155
|
|