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/mcp.js
CHANGED
|
@@ -263,6 +263,218 @@ const loadConfig = (directory) => {
|
|
|
263
263
|
}
|
|
264
264
|
};
|
|
265
265
|
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/utils/source-masker.ts
|
|
268
|
+
const JS_EXTS$2 = new Set([
|
|
269
|
+
".ts",
|
|
270
|
+
".tsx",
|
|
271
|
+
".js",
|
|
272
|
+
".jsx",
|
|
273
|
+
".mjs",
|
|
274
|
+
".cjs"
|
|
275
|
+
]);
|
|
276
|
+
const PY_EXTS = new Set([".py"]);
|
|
277
|
+
const RB_EXTS = new Set([".rb"]);
|
|
278
|
+
const PHP_EXTS = new Set([".php"]);
|
|
279
|
+
const familyForExt = (ext) => {
|
|
280
|
+
if (JS_EXTS$2.has(ext)) return "js";
|
|
281
|
+
if (PY_EXTS.has(ext)) return "py";
|
|
282
|
+
if (RB_EXTS.has(ext)) return "rb";
|
|
283
|
+
if (PHP_EXTS.has(ext)) return "php";
|
|
284
|
+
return "none";
|
|
285
|
+
};
|
|
286
|
+
const maskStringsAndComments = (content, ext) => {
|
|
287
|
+
const family = familyForExt(ext);
|
|
288
|
+
if (family === "none") return content;
|
|
289
|
+
if (family === "js") return maskJs(content, true);
|
|
290
|
+
return maskSimple(content, family, true);
|
|
291
|
+
};
|
|
292
|
+
const maskComments = (content, ext) => {
|
|
293
|
+
const family = familyForExt(ext);
|
|
294
|
+
if (family === "none") return content;
|
|
295
|
+
if (family === "js") return maskJs(content, false);
|
|
296
|
+
return maskSimple(content, family, false);
|
|
297
|
+
};
|
|
298
|
+
const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
|
|
299
|
+
const len = content.length;
|
|
300
|
+
const c = content[i];
|
|
301
|
+
const next = content[i + 1];
|
|
302
|
+
if (c === "\"" || c === "'") {
|
|
303
|
+
const strStart = i;
|
|
304
|
+
const end = consumeQuotedString(content, i, c);
|
|
305
|
+
if (maskStrings) mask(strStart + 1, end - 1);
|
|
306
|
+
return {
|
|
307
|
+
handled: true,
|
|
308
|
+
nextI: end
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (c === "`") {
|
|
312
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
313
|
+
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
314
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
315
|
+
return {
|
|
316
|
+
handled: true,
|
|
317
|
+
nextI: scan.resumeAt
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (c === "/" && next === "/") {
|
|
321
|
+
const strStart = i;
|
|
322
|
+
let k = i;
|
|
323
|
+
while (k < len && content[k] !== "\n") k++;
|
|
324
|
+
mask(strStart, k);
|
|
325
|
+
return {
|
|
326
|
+
handled: true,
|
|
327
|
+
nextI: k
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (c === "/" && next === "*") {
|
|
331
|
+
const strStart = i;
|
|
332
|
+
let k = i + 2;
|
|
333
|
+
while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
|
|
334
|
+
if (k < len - 1) k += 2;
|
|
335
|
+
mask(strStart, k);
|
|
336
|
+
return {
|
|
337
|
+
handled: true,
|
|
338
|
+
nextI: k
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
handled: false,
|
|
343
|
+
nextI: i
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
const maskJs = (content, maskStrings) => {
|
|
347
|
+
const out = content.split("");
|
|
348
|
+
const len = content.length;
|
|
349
|
+
const tplStack = [];
|
|
350
|
+
let i = 0;
|
|
351
|
+
const mask = (start, end) => {
|
|
352
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
353
|
+
};
|
|
354
|
+
while (i < len) {
|
|
355
|
+
const c = content[i];
|
|
356
|
+
if (tplStack.length > 0) {
|
|
357
|
+
if (c === "{") {
|
|
358
|
+
tplStack[tplStack.length - 1]++;
|
|
359
|
+
i++;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (c === "}") {
|
|
363
|
+
if (tplStack[tplStack.length - 1] === 0) {
|
|
364
|
+
tplStack.pop();
|
|
365
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
366
|
+
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
367
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
368
|
+
i = scan.resumeAt;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
tplStack[tplStack.length - 1]--;
|
|
372
|
+
i++;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
|
|
377
|
+
if (handled.handled) {
|
|
378
|
+
i = handled.nextI;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
i++;
|
|
382
|
+
}
|
|
383
|
+
return out.join("");
|
|
384
|
+
};
|
|
385
|
+
const consumeQuotedString = (content, start, quote) => {
|
|
386
|
+
const len = content.length;
|
|
387
|
+
let i = start + 1;
|
|
388
|
+
while (i < len) {
|
|
389
|
+
const c = content[i];
|
|
390
|
+
if (c === "\\" && i + 1 < len) {
|
|
391
|
+
i += 2;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (c === quote) return i + 1;
|
|
395
|
+
if (c === "\n") return i;
|
|
396
|
+
i++;
|
|
397
|
+
}
|
|
398
|
+
return i;
|
|
399
|
+
};
|
|
400
|
+
const consumeTemplateString = (content, start) => {
|
|
401
|
+
const len = content.length;
|
|
402
|
+
let i = start;
|
|
403
|
+
while (i < len) {
|
|
404
|
+
const c = content[i];
|
|
405
|
+
if (c === "\\" && i + 1 < len) {
|
|
406
|
+
i += 2;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (c === "`") return {
|
|
410
|
+
maskEnd: i,
|
|
411
|
+
resumeAt: i + 1,
|
|
412
|
+
openedInterp: false
|
|
413
|
+
};
|
|
414
|
+
if (c === "$" && content[i + 1] === "{") return {
|
|
415
|
+
maskEnd: i,
|
|
416
|
+
resumeAt: i + 2,
|
|
417
|
+
openedInterp: true
|
|
418
|
+
};
|
|
419
|
+
i++;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
maskEnd: i,
|
|
423
|
+
resumeAt: i,
|
|
424
|
+
openedInterp: false
|
|
425
|
+
};
|
|
426
|
+
};
|
|
427
|
+
const maskSimple = (content, family, maskStrings) => {
|
|
428
|
+
const out = content.split("");
|
|
429
|
+
const len = content.length;
|
|
430
|
+
let i = 0;
|
|
431
|
+
const mask = (start, end) => {
|
|
432
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
433
|
+
};
|
|
434
|
+
while (i < len) {
|
|
435
|
+
const c = content[i];
|
|
436
|
+
const next = content[i + 1];
|
|
437
|
+
if (family === "py" && (c === "\"" || c === "'")) {
|
|
438
|
+
if (content[i + 1] === c && content[i + 2] === c) {
|
|
439
|
+
const triple = c + c + c;
|
|
440
|
+
const end = content.indexOf(triple, i + 3);
|
|
441
|
+
const stop = end === -1 ? len : end + 3;
|
|
442
|
+
if (maskStrings) mask(i + 3, stop - 3);
|
|
443
|
+
i = stop;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (c === "\"" || c === "'") {
|
|
448
|
+
const strStart = i;
|
|
449
|
+
i = consumeQuotedString(content, i, c);
|
|
450
|
+
if (maskStrings) mask(strStart + 1, i - 1);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
454
|
+
const strStart = i;
|
|
455
|
+
while (i < len && content[i] !== "\n") i++;
|
|
456
|
+
mask(strStart, i);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (family === "php" && c === "/" && next === "/") {
|
|
460
|
+
const strStart = i;
|
|
461
|
+
while (i < len && content[i] !== "\n") i++;
|
|
462
|
+
mask(strStart, i);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (family === "php" && c === "/" && next === "*") {
|
|
466
|
+
const strStart = i;
|
|
467
|
+
i += 2;
|
|
468
|
+
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
469
|
+
if (i < len - 1) i += 2;
|
|
470
|
+
mask(strStart, i);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
i++;
|
|
474
|
+
}
|
|
475
|
+
return out.join("");
|
|
476
|
+
};
|
|
477
|
+
|
|
266
478
|
//#endregion
|
|
267
479
|
//#region src/utils/source-files.ts
|
|
268
480
|
const MAX_BUFFER = 50 * 1024 * 1024;
|
|
@@ -513,7 +725,7 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
|
513
725
|
|
|
514
726
|
//#endregion
|
|
515
727
|
//#region src/engines/ai-slop/abstractions.ts
|
|
516
|
-
const JS_EXTS$
|
|
728
|
+
const JS_EXTS$1 = new Set([
|
|
517
729
|
".ts",
|
|
518
730
|
".tsx",
|
|
519
731
|
".js",
|
|
@@ -524,11 +736,11 @@ const JS_EXTS$2 = new Set([
|
|
|
524
736
|
const THIN_WRAPPER_PATTERNS = [
|
|
525
737
|
{
|
|
526
738
|
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
527
|
-
extensions: JS_EXTS$
|
|
739
|
+
extensions: JS_EXTS$1
|
|
528
740
|
},
|
|
529
741
|
{
|
|
530
742
|
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
531
|
-
extensions: JS_EXTS$
|
|
743
|
+
extensions: JS_EXTS$1
|
|
532
744
|
},
|
|
533
745
|
{
|
|
534
746
|
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
@@ -538,14 +750,16 @@ const THIN_WRAPPER_PATTERNS = [
|
|
|
538
750
|
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+/];
|
|
539
751
|
const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
|
|
540
752
|
const DUNDER_PATTERN = /^__\w+__$/;
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
753
|
+
const stripParam = (p) => p.trim().split(/[:=]/)[0].trim().replace(/^[*&]+/, "");
|
|
754
|
+
const paramNames = (paramsText) => new Set(paramsText.split(",").map(stripParam).filter((p) => p && p !== "self" && p !== "cls"));
|
|
755
|
+
const isIdentityForward = (matchText) => {
|
|
756
|
+
const paramsMatch = matchText.match(/\(([^)]*)\)/);
|
|
757
|
+
const innerMatch = matchText.match(/(?:return\s+\w+|=>\s*\w+)\s*\(([^)]*)\)/);
|
|
758
|
+
if (!paramsMatch || !innerMatch) return false;
|
|
759
|
+
const params = paramNames(paramsMatch[1]);
|
|
760
|
+
const args = innerMatch[1].split(",").map((a) => a.trim()).filter((a) => a.length > 0);
|
|
761
|
+
if (args.length === 0) return false;
|
|
762
|
+
return args.every((a) => /^[A-Za-z_$][\w$]*$/.test(a) && params.has(a));
|
|
549
763
|
};
|
|
550
764
|
const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
|
|
551
765
|
const detectThinWrappers = (content, relativePath, ext) => {
|
|
@@ -565,7 +779,7 @@ const detectThinWrappers = (content, relativePath, ext) => {
|
|
|
565
779
|
const prevLine = lines[lineNumber - 2]?.trim();
|
|
566
780
|
if (prevLine && prevLine.startsWith("@")) continue;
|
|
567
781
|
}
|
|
568
|
-
if (
|
|
782
|
+
if (!isIdentityForward(matchText)) continue;
|
|
569
783
|
if (isUseContextWrapper(matchText)) continue;
|
|
570
784
|
diagnostics.push({
|
|
571
785
|
filePath: relativePath,
|
|
@@ -619,8 +833,9 @@ const detectOverAbstraction = async (context) => {
|
|
|
619
833
|
}
|
|
620
834
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
621
835
|
const ext = path.extname(filePath);
|
|
622
|
-
|
|
623
|
-
diagnostics.push(...
|
|
836
|
+
const codeOnly = maskComments(content, ext);
|
|
837
|
+
diagnostics.push(...detectThinWrappers(codeOnly, relativePath, ext));
|
|
838
|
+
diagnostics.push(...detectAiNaming(codeOnly, relativePath));
|
|
624
839
|
}
|
|
625
840
|
return diagnostics;
|
|
626
841
|
};
|
|
@@ -650,8 +865,7 @@ const JUSTIFICATION_OPENERS = [
|
|
|
650
865
|
/^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
|
|
651
866
|
];
|
|
652
867
|
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
653
|
-
const
|
|
654
|
-
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;
|
|
868
|
+
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;
|
|
655
869
|
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
656
870
|
"deprecated",
|
|
657
871
|
"see",
|
|
@@ -747,7 +961,7 @@ const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract
|
|
|
747
961
|
|
|
748
962
|
//#endregion
|
|
749
963
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
750
|
-
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;
|
|
964
|
+
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;
|
|
751
965
|
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;
|
|
752
966
|
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
753
967
|
|
|
@@ -756,7 +970,7 @@ const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) ||
|
|
|
756
970
|
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";
|
|
757
971
|
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")];
|
|
758
972
|
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")];
|
|
759
|
-
const EXPLANATORY_KEYWORDS = /\b(?:because|since|note|todo|fixme|hack|warn|warning|workaround|caveat|important|assumes?)\b/i;
|
|
973
|
+
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;
|
|
760
974
|
const COMMENTED_CODE_CHARS = /[({=;}\]>]/;
|
|
761
975
|
const MAX_TRIVIAL_COMMENT_LENGTH = 60;
|
|
762
976
|
const isJsComment = (trimmed) => trimmed.startsWith("//") && !trimmed.startsWith("///") && !trimmed.startsWith("//!");
|
|
@@ -905,6 +1119,7 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
|
|
|
905
1119
|
"PLACEHOLDER",
|
|
906
1120
|
"STUB"
|
|
907
1121
|
].join("|")})[:\\s]`);
|
|
1122
|
+
const TODO_TRACKING_RE = /https?:\/\/|#\d+|\bgh-\d+\b|\b[A-Z][A-Z0-9]+-\d+\b|\b(?:issue|ticket|jira)\b/i;
|
|
908
1123
|
const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
|
|
909
1124
|
const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
910
1125
|
const contextLines = [];
|
|
@@ -924,7 +1139,10 @@ const detectTodoStubs = (content, relativePath) => {
|
|
|
924
1139
|
for (let i = 0; i < lines.length; i++) {
|
|
925
1140
|
const trimmed = lines[i].trim();
|
|
926
1141
|
if (!trimmed.startsWith("//") && !trimmed.startsWith("#") && !trimmed.startsWith("*") && !trimmed.startsWith("/*")) continue;
|
|
927
|
-
if (TODO_PATTERN.test(trimmed))
|
|
1142
|
+
if (TODO_PATTERN.test(trimmed)) {
|
|
1143
|
+
if (TODO_TRACKING_RE.test(trimmed)) continue;
|
|
1144
|
+
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));
|
|
1145
|
+
}
|
|
928
1146
|
}
|
|
929
1147
|
return diagnostics;
|
|
930
1148
|
};
|
|
@@ -973,9 +1191,10 @@ const detectDeadPatterns = async (context) => {
|
|
|
973
1191
|
}
|
|
974
1192
|
const ext = path.extname(filePath);
|
|
975
1193
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
976
|
-
|
|
1194
|
+
const codeOnly = maskComments(content, ext);
|
|
1195
|
+
diagnostics.push(...detectConsoleLeftovers(codeOnly, relativePath, ext));
|
|
977
1196
|
diagnostics.push(...detectTodoStubs(content, relativePath));
|
|
978
|
-
diagnostics.push(...detectDeadCodePatterns(
|
|
1197
|
+
diagnostics.push(...detectDeadCodePatterns(codeOnly, relativePath, ext));
|
|
979
1198
|
diagnostics.push(...detectUnsafeTypePatterns(content, relativePath, ext));
|
|
980
1199
|
}
|
|
981
1200
|
return diagnostics;
|
|
@@ -1165,6 +1384,7 @@ const JS_EXTENSIONS$2 = new Set([
|
|
|
1165
1384
|
const IMPORT_FROM_RE = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
|
|
1166
1385
|
const TYPE_ONLY_RE = /^\s*type\b/;
|
|
1167
1386
|
const VALUE_BINDING_RE = /\{([^}]*)\}/;
|
|
1387
|
+
const NAMESPACE_RE = /\*\s+as\s+/;
|
|
1168
1388
|
const isTypeOnly = (clause) => {
|
|
1169
1389
|
if (TYPE_ONLY_RE.test(clause)) return true;
|
|
1170
1390
|
const braces = VALUE_BINDING_RE.exec(clause);
|
|
@@ -1182,7 +1402,8 @@ const extractImportLines = (content) => {
|
|
|
1182
1402
|
results.push({
|
|
1183
1403
|
spec: match[2],
|
|
1184
1404
|
line: i + 1,
|
|
1185
|
-
typeOnly: isTypeOnly(match[1])
|
|
1405
|
+
typeOnly: isTypeOnly(match[1]),
|
|
1406
|
+
namespace: NAMESPACE_RE.test(match[1])
|
|
1186
1407
|
});
|
|
1187
1408
|
}
|
|
1188
1409
|
return results;
|
|
@@ -1199,11 +1420,11 @@ const detectDuplicateImports = async (context) => {
|
|
|
1199
1420
|
} catch {
|
|
1200
1421
|
continue;
|
|
1201
1422
|
}
|
|
1202
|
-
const imports = extractImportLines(content);
|
|
1423
|
+
const imports = extractImportLines(maskComments(content, path.extname(filePath)));
|
|
1203
1424
|
if (imports.length < 2) continue;
|
|
1204
1425
|
const byBucket = /* @__PURE__ */ new Map();
|
|
1205
1426
|
for (const imp of imports) {
|
|
1206
|
-
const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
1427
|
+
const key = `${imp.namespace ? "ns" : imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
1207
1428
|
const list = byBucket.get(key) ?? [];
|
|
1208
1429
|
list.push(imp);
|
|
1209
1430
|
byBucket.set(key, list);
|
|
@@ -1450,6 +1671,30 @@ const LOOPBACK_HOSTS = new Set([
|
|
|
1450
1671
|
"0.0.0.0",
|
|
1451
1672
|
"::1"
|
|
1452
1673
|
]);
|
|
1674
|
+
const VENDOR_API_DOMAINS = [
|
|
1675
|
+
"github.com",
|
|
1676
|
+
"githubusercontent.com",
|
|
1677
|
+
"googleapis.com",
|
|
1678
|
+
"accounts.google.com",
|
|
1679
|
+
"stripe.com",
|
|
1680
|
+
"openai.com",
|
|
1681
|
+
"anthropic.com",
|
|
1682
|
+
"slack.com",
|
|
1683
|
+
"twilio.com",
|
|
1684
|
+
"sendgrid.com",
|
|
1685
|
+
"mailgun.net",
|
|
1686
|
+
"cloudflare.com",
|
|
1687
|
+
"discord.com",
|
|
1688
|
+
"telegram.org",
|
|
1689
|
+
"login.microsoftonline.com",
|
|
1690
|
+
"graph.microsoft.com",
|
|
1691
|
+
"twitter.com",
|
|
1692
|
+
"x.com",
|
|
1693
|
+
"twimg.com",
|
|
1694
|
+
"t.co",
|
|
1695
|
+
"api.telegram.org"
|
|
1696
|
+
];
|
|
1697
|
+
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
1453
1698
|
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
1454
1699
|
const HARDCODED_URL_FINDING = {
|
|
1455
1700
|
rule: "ai-slop/hardcoded-url",
|
|
@@ -1494,6 +1739,7 @@ const shouldFlagUrlLiteral = (line, urlText) => {
|
|
|
1494
1739
|
if (!host) return false;
|
|
1495
1740
|
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
1496
1741
|
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
1742
|
+
if (isVendorApiHost(host)) return false;
|
|
1497
1743
|
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
1498
1744
|
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
1499
1745
|
};
|
|
@@ -1545,7 +1791,7 @@ const detectHardcodedConfigLiterals = async (context) => {
|
|
|
1545
1791
|
}
|
|
1546
1792
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1547
1793
|
const ext = path.extname(filePath);
|
|
1548
|
-
diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
|
|
1794
|
+
diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
|
|
1549
1795
|
}
|
|
1550
1796
|
return diagnostics;
|
|
1551
1797
|
};
|
|
@@ -1678,7 +1924,9 @@ const PYTHON_STDLIB = new Set([
|
|
|
1678
1924
|
"builtins",
|
|
1679
1925
|
"bz2",
|
|
1680
1926
|
"calendar",
|
|
1927
|
+
"code",
|
|
1681
1928
|
"codecs",
|
|
1929
|
+
"codeop",
|
|
1682
1930
|
"collections",
|
|
1683
1931
|
"concurrent",
|
|
1684
1932
|
"configparser",
|
|
@@ -1751,6 +1999,7 @@ const PYTHON_STDLIB = new Set([
|
|
|
1751
1999
|
"readline",
|
|
1752
2000
|
"reprlib",
|
|
1753
2001
|
"resource",
|
|
2002
|
+
"rlcompleter",
|
|
1754
2003
|
"secrets",
|
|
1755
2004
|
"select",
|
|
1756
2005
|
"selectors",
|
|
@@ -1939,6 +2188,8 @@ const collectFromPyproject = (rootDir, pyDeps) => {
|
|
|
1939
2188
|
}
|
|
1940
2189
|
const extras = content.match(/\[project\.optional-dependencies\]([\s\S]*?)(?=\n\[|$)/);
|
|
1941
2190
|
if (extras) for (const m of extras[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
2191
|
+
const groups = content.match(/\[dependency-groups\]([\s\S]*?)(?=\n\[[^[]|$)/);
|
|
2192
|
+
if (groups) for (const m of groups[1].matchAll(/["']\s*([a-zA-Z][a-zA-Z0-9_\-.]+)/g)) addPyDep(pyDeps, m[1]);
|
|
1942
2193
|
const poetryRe = /\[tool\.poetry(?:\.group\.[a-z]+)?\.dependencies\]([\s\S]*?)(?=\n\[|$)/g;
|
|
1943
2194
|
let match = poetryRe.exec(content);
|
|
1944
2195
|
while (match !== null) {
|
|
@@ -2171,9 +2422,28 @@ const extractJsImports = (content) => {
|
|
|
2171
2422
|
const extractPyImports = (content) => {
|
|
2172
2423
|
const lines = content.split("\n");
|
|
2173
2424
|
const results = [];
|
|
2425
|
+
let inDoc = null;
|
|
2426
|
+
let typeCheckIndent = -1;
|
|
2174
2427
|
for (let i = 0; i < lines.length; i++) {
|
|
2175
|
-
const
|
|
2176
|
-
|
|
2428
|
+
const raw = lines[i];
|
|
2429
|
+
const line = raw.trim();
|
|
2430
|
+
if (inDoc) {
|
|
2431
|
+
if (line.includes(inDoc)) inDoc = null;
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
2435
|
+
const triples = line.match(/"""|'''/g);
|
|
2436
|
+
if (triples) {
|
|
2437
|
+
if (triples.length % 2 === 1) inDoc = triples[triples.length - 1];
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
const indent = raw.length - raw.trimStart().length;
|
|
2441
|
+
if (typeCheckIndent >= 0 && indent <= typeCheckIndent) typeCheckIndent = -1;
|
|
2442
|
+
if (/^if\s+(?:[\w.]+\.)?TYPE_CHECKING\b/.test(line)) {
|
|
2443
|
+
typeCheckIndent = indent;
|
|
2444
|
+
continue;
|
|
2445
|
+
}
|
|
2446
|
+
if (typeCheckIndent >= 0) continue;
|
|
2177
2447
|
const fromMatch = line.match(/^from\s+([\w.]+)\s+import\b/);
|
|
2178
2448
|
if (fromMatch && !fromMatch[1].startsWith(".")) {
|
|
2179
2449
|
results.push({
|
|
@@ -2183,8 +2453,8 @@ const extractPyImports = (content) => {
|
|
|
2183
2453
|
continue;
|
|
2184
2454
|
}
|
|
2185
2455
|
const importMatch = line.match(/^import\s+([\w.,\s]+?)(?:\s+as\s+\w+)?\s*$/);
|
|
2186
|
-
if (importMatch) for (const
|
|
2187
|
-
const cleaned =
|
|
2456
|
+
if (importMatch) for (const part of importMatch[1].split(",")) {
|
|
2457
|
+
const cleaned = part.trim().split(/\s+as\s+/)[0];
|
|
2188
2458
|
if (cleaned && !cleaned.startsWith(".")) results.push({
|
|
2189
2459
|
spec: cleaned,
|
|
2190
2460
|
line: i + 1
|
|
@@ -2241,6 +2511,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2241
2511
|
continue;
|
|
2242
2512
|
}
|
|
2243
2513
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
2514
|
+
if (isNonProductionPath(relPath)) continue;
|
|
2244
2515
|
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
2245
2516
|
for (const { spec, line } of imports) {
|
|
2246
2517
|
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
@@ -2368,7 +2639,7 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
2368
2639
|
//#endregion
|
|
2369
2640
|
//#region src/engines/ai-slop/meta-comment.ts
|
|
2370
2641
|
const PLAN_REFERENCE_RES = [
|
|
2371
|
-
|
|
2642
|
+
/^(?:stage|step|phase)\s+\d+\s*[:.\-–—]/i,
|
|
2372
2643
|
/\bstep\s+\d+\s+of\s+the\s+plan\b/i,
|
|
2373
2644
|
/\bas\s+(?:per|requested)\s+(?:the\s+)?(?:requirements?|spec|task|ticket|prompt|instructions?)\b/i,
|
|
2374
2645
|
/\bper\s+the\s+(?:spec|requirements?|task|ticket|plan|prompt|instructions?)\b/i,
|
|
@@ -2470,24 +2741,6 @@ const looksLikeLicenseHeader = (block) => {
|
|
|
2470
2741
|
const text = block.rawLines.join(" ").toLowerCase();
|
|
2471
2742
|
return text.includes("copyright") || text.includes("license") || text.includes("spdx-license-identifier");
|
|
2472
2743
|
};
|
|
2473
|
-
const BARE_LABEL_RE = /^[A-Z][A-Za-z0-9 ]{1,28}$/;
|
|
2474
|
-
const isBareSectionLabel = (prose) => {
|
|
2475
|
-
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
2476
|
-
if (prose.endsWith(".")) return false;
|
|
2477
|
-
if (prose.split(/\s+/).length > 3) return false;
|
|
2478
|
-
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
2479
|
-
return true;
|
|
2480
|
-
};
|
|
2481
|
-
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
2482
|
-
const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
2483
|
-
if (nextLine === null) return false;
|
|
2484
|
-
if (!DATA_ENTRY_START.test(nextLine)) return false;
|
|
2485
|
-
const trimmed = nextLine.trim();
|
|
2486
|
-
if (trimmed.startsWith("case ")) return true;
|
|
2487
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("\"") || trimmed.startsWith("'") || trimmed.startsWith("`")) return true;
|
|
2488
|
-
if (/^\w+\s*:/.test(trimmed)) return true;
|
|
2489
|
-
return false;
|
|
2490
|
-
};
|
|
2491
2744
|
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));
|
|
2492
2745
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
2493
2746
|
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
@@ -2576,10 +2829,6 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2576
2829
|
matched: true,
|
|
2577
2830
|
reason: "phase/section header"
|
|
2578
2831
|
};
|
|
2579
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
2580
|
-
matched: true,
|
|
2581
|
-
reason: "bare section label"
|
|
2582
|
-
};
|
|
2583
2832
|
const joined = block.prose.join(" ");
|
|
2584
2833
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
2585
2834
|
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
@@ -2610,17 +2859,11 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2610
2859
|
};
|
|
2611
2860
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
2612
2861
|
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
2613
|
-
if (nonEmptyProseCount >= 5) {
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
return {
|
|
2619
|
-
matched: true,
|
|
2620
|
-
reason: "long narrative block"
|
|
2621
|
-
};
|
|
2622
|
-
}
|
|
2623
|
-
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
|
|
2862
|
+
if (nonEmptyProseCount >= 5 && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
2863
|
+
matched: true,
|
|
2864
|
+
reason: "long narrative block"
|
|
2865
|
+
};
|
|
2866
|
+
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration && hasPreambleSlopSignal(block)) return {
|
|
2624
2867
|
matched: true,
|
|
2625
2868
|
reason: "multi-line narrative prose"
|
|
2626
2869
|
};
|
|
@@ -3054,7 +3297,7 @@ const detectRustPatterns = async (context) => {
|
|
|
3054
3297
|
|
|
3055
3298
|
//#endregion
|
|
3056
3299
|
//#region src/engines/ai-slop/silent-recovery.ts
|
|
3057
|
-
const JS_EXTS
|
|
3300
|
+
const JS_EXTS = new Set([
|
|
3058
3301
|
".ts",
|
|
3059
3302
|
".tsx",
|
|
3060
3303
|
".js",
|
|
@@ -3062,7 +3305,14 @@ const JS_EXTS$1 = new Set([
|
|
|
3062
3305
|
".mjs",
|
|
3063
3306
|
".cjs"
|
|
3064
3307
|
]);
|
|
3065
|
-
const CATCH_HEAD_RE = /\bcatch\s*(?:\([^)]*\))?\s*\{/g;
|
|
3308
|
+
const CATCH_HEAD_RE = /\bcatch\s*(?:\(\s*([^)]*?)\s*\))?\s*\{/g;
|
|
3309
|
+
const isIdentifier = (s) => /^[A-Za-z_$][\w$]*$/.test(s);
|
|
3310
|
+
const recoveryDropsError = (binding, body) => {
|
|
3311
|
+
const name = binding?.trim() ?? "";
|
|
3312
|
+
if (name === "") return true;
|
|
3313
|
+
if (!isIdentifier(name)) return false;
|
|
3314
|
+
return !new RegExp(`\\b${name}\\b`).test(body);
|
|
3315
|
+
};
|
|
3066
3316
|
const LOG_STATEMENT_RE = /^(?:console|[\w$]+(?:\.[\w$]+)*)\.(?:log|info|warn|warning|error|debug|trace)\s*\(/;
|
|
3067
3317
|
const HANDLING_TOKEN_RE = /\b(?:throw|return|reject|next|process\.exit|continue|break)\b/;
|
|
3068
3318
|
const stripBlockComments = (text) => text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -3112,14 +3362,15 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3112
3362
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3113
3363
|
if (body === null) continue;
|
|
3114
3364
|
if (!isLogOnlyBody(body)) continue;
|
|
3365
|
+
if (!recoveryDropsError(match[1], body)) continue;
|
|
3115
3366
|
const line = content.slice(0, match.index).split("\n").length;
|
|
3116
3367
|
out.push({
|
|
3117
3368
|
filePath: relPath,
|
|
3118
3369
|
engine: "ai-slop",
|
|
3119
3370
|
rule: "ai-slop/silent-recovery",
|
|
3120
3371
|
severity: "warning",
|
|
3121
|
-
message: "Catch
|
|
3122
|
-
help: "
|
|
3372
|
+
message: "Catch logs without the caught error then continues; the failure cause is lost",
|
|
3373
|
+
help: "Include the caught error in the log, or rethrow / recover explicitly, so the failure stays diagnosable.",
|
|
3123
3374
|
line,
|
|
3124
3375
|
column: 0,
|
|
3125
3376
|
category: "AI Slop",
|
|
@@ -3129,6 +3380,7 @@ const detectJsSilentRecovery = (content, relPath) => {
|
|
|
3129
3380
|
return out;
|
|
3130
3381
|
};
|
|
3131
3382
|
const PY_EXCEPT_RE = /^(\s*)except\b[^\n]*:\s*(?:#.*)?$/;
|
|
3383
|
+
const PY_EXCEPT_BINDING_RE = /\bas\s+(\w+)\s*:/;
|
|
3132
3384
|
const PY_LOG_STATEMENT_RE = /^(?:logging|logger|log|self\.log|self\.logger|print)(?:\.(?:debug|info|warning|warn|error|exception|critical))?\s*\(/;
|
|
3133
3385
|
const PY_HANDLING_TOKEN_RE = /^(?:raise\b|return\b|continue\b|break\b|self\.|[\w.]+\s*=)/;
|
|
3134
3386
|
const detectPySilentRecovery = (content, relPath) => {
|
|
@@ -3152,13 +3404,14 @@ const detectPySilentRecovery = (content, relPath) => {
|
|
|
3152
3404
|
const allLogs = bodyLines.every((line) => PY_LOG_STATEMENT_RE.test(line) || /^[\w"'(),.\s+:%{}[\]-]+$/.test(line));
|
|
3153
3405
|
const sawLog = bodyLines.some((line) => PY_LOG_STATEMENT_RE.test(line));
|
|
3154
3406
|
if (!allLogs || !sawLog) continue;
|
|
3407
|
+
if (!recoveryDropsError(PY_EXCEPT_BINDING_RE.exec(lines[i])?.[1], bodyLines.join(" "))) continue;
|
|
3155
3408
|
out.push({
|
|
3156
3409
|
filePath: relPath,
|
|
3157
3410
|
engine: "ai-slop",
|
|
3158
3411
|
rule: "ai-slop/silent-recovery",
|
|
3159
3412
|
severity: "warning",
|
|
3160
|
-
message: "except
|
|
3161
|
-
help: "
|
|
3413
|
+
message: "except logs without the caught error then continues; the failure cause is lost",
|
|
3414
|
+
help: "Include the caught error in the log, or re-raise / recover explicitly, so the failure stays diagnosable.",
|
|
3162
3415
|
line: i + 1,
|
|
3163
3416
|
column: 0,
|
|
3164
3417
|
category: "AI Slop",
|
|
@@ -3173,7 +3426,7 @@ const detectSilentRecovery = async (context) => {
|
|
|
3173
3426
|
for (const filePath of files) {
|
|
3174
3427
|
if (isAutoGenerated(filePath)) continue;
|
|
3175
3428
|
const ext = path.extname(filePath);
|
|
3176
|
-
const isJs = JS_EXTS
|
|
3429
|
+
const isJs = JS_EXTS.has(ext);
|
|
3177
3430
|
if (!isJs && !(ext === ".py")) continue;
|
|
3178
3431
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3179
3432
|
if (isNonProductionPath(relPath)) continue;
|
|
@@ -3260,10 +3513,11 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3260
3513
|
const importLines = /* @__PURE__ */ new Set();
|
|
3261
3514
|
for (let i = 0; i < lines.length; i++) {
|
|
3262
3515
|
const trimmed = lines[i].trim();
|
|
3263
|
-
const fromMatch = trimmed.match(/^from\s+[\w.]
|
|
3516
|
+
const fromMatch = trimmed.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
|
|
3264
3517
|
if (fromMatch) {
|
|
3265
3518
|
importLines.add(i);
|
|
3266
|
-
|
|
3519
|
+
if (fromMatch[1] === "__future__") continue;
|
|
3520
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
3267
3521
|
if (importPart === "*") continue;
|
|
3268
3522
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
3269
3523
|
for (const item of cleaned.split(",")) {
|
|
@@ -3651,12 +3905,92 @@ const findBraceFunctionEnd = (lines, startIndex) => {
|
|
|
3651
3905
|
maxNesting
|
|
3652
3906
|
};
|
|
3653
3907
|
};
|
|
3654
|
-
const
|
|
3655
|
-
|
|
3656
|
-
let
|
|
3908
|
+
const extractPythonSignature = (lines, startIndex) => {
|
|
3909
|
+
let depth = 0;
|
|
3910
|
+
let started = false;
|
|
3911
|
+
let params = "";
|
|
3912
|
+
for (let j = startIndex; j < lines.length; j++) {
|
|
3913
|
+
const l = lines[j];
|
|
3914
|
+
for (let ci = 0; ci < l.length; ci++) {
|
|
3915
|
+
const ch = l[ci];
|
|
3916
|
+
if (ch === "(") {
|
|
3917
|
+
depth++;
|
|
3918
|
+
if (depth === 1 && !started) {
|
|
3919
|
+
started = true;
|
|
3920
|
+
continue;
|
|
3921
|
+
}
|
|
3922
|
+
} else if (ch === ")") {
|
|
3923
|
+
depth--;
|
|
3924
|
+
if (depth === 0) return {
|
|
3925
|
+
params,
|
|
3926
|
+
sigEndIndex: j
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
if (started) params += ch;
|
|
3930
|
+
}
|
|
3931
|
+
if (started) params += " ";
|
|
3932
|
+
}
|
|
3933
|
+
return {
|
|
3934
|
+
params,
|
|
3935
|
+
sigEndIndex: startIndex
|
|
3936
|
+
};
|
|
3937
|
+
};
|
|
3938
|
+
const countPythonParams = (signature) => {
|
|
3939
|
+
let depth = 0;
|
|
3940
|
+
const parts = [];
|
|
3941
|
+
let current = "";
|
|
3942
|
+
for (const ch of signature) {
|
|
3943
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
3944
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
3945
|
+
if (ch === "," && depth === 0) {
|
|
3946
|
+
parts.push(current);
|
|
3947
|
+
current = "";
|
|
3948
|
+
continue;
|
|
3949
|
+
}
|
|
3950
|
+
current += ch;
|
|
3951
|
+
}
|
|
3952
|
+
parts.push(current);
|
|
3953
|
+
let count = 0;
|
|
3954
|
+
for (const raw of parts) {
|
|
3955
|
+
const p = raw.trim();
|
|
3956
|
+
if (p.length === 0 || p === "*" || p === "/") continue;
|
|
3957
|
+
if (p.startsWith("*")) continue;
|
|
3958
|
+
if (p.includes("=")) continue;
|
|
3959
|
+
const name = p.split(":")[0].trim();
|
|
3960
|
+
if (name === "self" || name === "cls") continue;
|
|
3961
|
+
count++;
|
|
3962
|
+
}
|
|
3963
|
+
return count;
|
|
3964
|
+
};
|
|
3965
|
+
const countPythonBodyCodeLines = (lines, sigEndIndex, endLine) => {
|
|
3966
|
+
let count = 0;
|
|
3967
|
+
let inDoc = false;
|
|
3968
|
+
let delim = "";
|
|
3969
|
+
for (let j = sigEndIndex + 1; j <= endLine && j < lines.length; j++) {
|
|
3970
|
+
const t = lines[j].trim();
|
|
3971
|
+
if (inDoc) {
|
|
3972
|
+
if (t.includes(delim)) inDoc = false;
|
|
3973
|
+
continue;
|
|
3974
|
+
}
|
|
3975
|
+
if (t === "" || t.startsWith("#")) continue;
|
|
3976
|
+
const opener = t.startsWith("\"\"\"") ? "\"\"\"" : t.startsWith("'''") ? "'''" : "";
|
|
3977
|
+
if (opener) {
|
|
3978
|
+
if (!t.slice(3).includes(opener)) {
|
|
3979
|
+
inDoc = true;
|
|
3980
|
+
delim = opener;
|
|
3981
|
+
}
|
|
3982
|
+
continue;
|
|
3983
|
+
}
|
|
3984
|
+
count++;
|
|
3985
|
+
}
|
|
3986
|
+
return count;
|
|
3987
|
+
};
|
|
3988
|
+
const findPythonFunctionEnd = (lines, defIndex, bodyStartIndex) => {
|
|
3989
|
+
const baseIndent = lines[defIndex].match(/^(\s*)/)?.[1].length ?? 0;
|
|
3990
|
+
let endLine = bodyStartIndex;
|
|
3657
3991
|
let maxNesting = 0;
|
|
3658
3992
|
const controlIndentStack = [];
|
|
3659
|
-
for (let j =
|
|
3993
|
+
for (let j = bodyStartIndex + 1; j < lines.length; j++) {
|
|
3660
3994
|
const l = lines[j];
|
|
3661
3995
|
if (l.trim() === "") {
|
|
3662
3996
|
endLine = j;
|
|
@@ -3678,7 +4012,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
|
|
|
3678
4012
|
};
|
|
3679
4013
|
};
|
|
3680
4014
|
const findFunctionEnd = (lines, startIndex, isPython) => {
|
|
3681
|
-
if (isPython)
|
|
4015
|
+
if (isPython) {
|
|
4016
|
+
const { sigEndIndex } = extractPythonSignature(lines, startIndex);
|
|
4017
|
+
return findPythonFunctionEnd(lines, startIndex, sigEndIndex);
|
|
4018
|
+
}
|
|
3682
4019
|
return findBraceFunctionEnd(lines, startIndex);
|
|
3683
4020
|
};
|
|
3684
4021
|
const isBlockArrow = (lines, startIndex) => {
|
|
@@ -3743,7 +4080,7 @@ const FUNCTION_PATTERNS = [
|
|
|
3743
4080
|
]
|
|
3744
4081
|
},
|
|
3745
4082
|
{
|
|
3746
|
-
regex: /^\s*def\s+(\w+)\s*\(
|
|
4083
|
+
regex: /^\s*(?:async\s+)?def\s+(\w+)\s*\(/,
|
|
3747
4084
|
langFilter: [".py"]
|
|
3748
4085
|
},
|
|
3749
4086
|
{
|
|
@@ -3800,14 +4137,23 @@ const analyzeFunctions = (content, ext) => {
|
|
|
3800
4137
|
const isPython = fnMatch.patternIndex === 2;
|
|
3801
4138
|
if (fnMatch.patternIndex === 1 && !isBlockArrow(lines, i)) continue;
|
|
3802
4139
|
const { endLine, maxNesting } = findFunctionEnd(lines, i, isPython);
|
|
3803
|
-
|
|
3804
|
-
|
|
4140
|
+
let templateLines;
|
|
4141
|
+
let paramCount;
|
|
4142
|
+
if (isPython) {
|
|
4143
|
+
const sig = extractPythonSignature(lines, i);
|
|
4144
|
+
const codeLines = countPythonBodyCodeLines(lines, sig.sigEndIndex, endLine);
|
|
4145
|
+
templateLines = endLine - i + 1 - codeLines;
|
|
4146
|
+
paramCount = countPythonParams(sig.params);
|
|
4147
|
+
} else {
|
|
4148
|
+
templateLines = countTemplateLines(lines.slice(i + 1, endLine));
|
|
4149
|
+
paramCount = countParams(fnMatch.params);
|
|
4150
|
+
}
|
|
3805
4151
|
functions.push({
|
|
3806
4152
|
name: fnMatch.name,
|
|
3807
4153
|
startLine: i + 1,
|
|
3808
4154
|
lineCount: endLine - i + 1,
|
|
3809
4155
|
maxNesting,
|
|
3810
|
-
paramCount
|
|
4156
|
+
paramCount,
|
|
3811
4157
|
templateLines
|
|
3812
4158
|
});
|
|
3813
4159
|
}
|
|
@@ -5519,212 +5865,6 @@ const runCargoAudit = async (rootDir, timeout) => {
|
|
|
5519
5865
|
}
|
|
5520
5866
|
};
|
|
5521
5867
|
|
|
5522
|
-
//#endregion
|
|
5523
|
-
//#region src/utils/source-masker.ts
|
|
5524
|
-
const JS_EXTS = new Set([
|
|
5525
|
-
".ts",
|
|
5526
|
-
".tsx",
|
|
5527
|
-
".js",
|
|
5528
|
-
".jsx",
|
|
5529
|
-
".mjs",
|
|
5530
|
-
".cjs"
|
|
5531
|
-
]);
|
|
5532
|
-
const PY_EXTS = new Set([".py"]);
|
|
5533
|
-
const RB_EXTS = new Set([".rb"]);
|
|
5534
|
-
const PHP_EXTS = new Set([".php"]);
|
|
5535
|
-
const familyForExt = (ext) => {
|
|
5536
|
-
if (JS_EXTS.has(ext)) return "js";
|
|
5537
|
-
if (PY_EXTS.has(ext)) return "py";
|
|
5538
|
-
if (RB_EXTS.has(ext)) return "rb";
|
|
5539
|
-
if (PHP_EXTS.has(ext)) return "php";
|
|
5540
|
-
return "none";
|
|
5541
|
-
};
|
|
5542
|
-
const maskStringsAndComments = (content, ext) => {
|
|
5543
|
-
const family = familyForExt(ext);
|
|
5544
|
-
if (family === "none") return content;
|
|
5545
|
-
if (family === "js") return maskJs(content);
|
|
5546
|
-
return maskSimple(content, family);
|
|
5547
|
-
};
|
|
5548
|
-
const handleQuotesAndComments = (content, i, tplStack, mask) => {
|
|
5549
|
-
const len = content.length;
|
|
5550
|
-
const c = content[i];
|
|
5551
|
-
const next = content[i + 1];
|
|
5552
|
-
if (c === "\"" || c === "'") {
|
|
5553
|
-
const strStart = i;
|
|
5554
|
-
const end = consumeQuotedString(content, i, c);
|
|
5555
|
-
mask(strStart + 1, end - 1);
|
|
5556
|
-
return {
|
|
5557
|
-
handled: true,
|
|
5558
|
-
nextI: end
|
|
5559
|
-
};
|
|
5560
|
-
}
|
|
5561
|
-
if (c === "`") {
|
|
5562
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
5563
|
-
mask(i + 1, scan.maskEnd);
|
|
5564
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
5565
|
-
return {
|
|
5566
|
-
handled: true,
|
|
5567
|
-
nextI: scan.resumeAt
|
|
5568
|
-
};
|
|
5569
|
-
}
|
|
5570
|
-
if (c === "/" && next === "/") {
|
|
5571
|
-
const strStart = i;
|
|
5572
|
-
let k = i;
|
|
5573
|
-
while (k < len && content[k] !== "\n") k++;
|
|
5574
|
-
mask(strStart, k);
|
|
5575
|
-
return {
|
|
5576
|
-
handled: true,
|
|
5577
|
-
nextI: k
|
|
5578
|
-
};
|
|
5579
|
-
}
|
|
5580
|
-
if (c === "/" && next === "*") {
|
|
5581
|
-
const strStart = i;
|
|
5582
|
-
let k = i + 2;
|
|
5583
|
-
while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
|
|
5584
|
-
if (k < len - 1) k += 2;
|
|
5585
|
-
mask(strStart, k);
|
|
5586
|
-
return {
|
|
5587
|
-
handled: true,
|
|
5588
|
-
nextI: k
|
|
5589
|
-
};
|
|
5590
|
-
}
|
|
5591
|
-
return {
|
|
5592
|
-
handled: false,
|
|
5593
|
-
nextI: i
|
|
5594
|
-
};
|
|
5595
|
-
};
|
|
5596
|
-
const maskJs = (content) => {
|
|
5597
|
-
const out = content.split("");
|
|
5598
|
-
const len = content.length;
|
|
5599
|
-
const tplStack = [];
|
|
5600
|
-
let i = 0;
|
|
5601
|
-
const mask = (start, end) => {
|
|
5602
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
5603
|
-
};
|
|
5604
|
-
while (i < len) {
|
|
5605
|
-
const c = content[i];
|
|
5606
|
-
if (tplStack.length > 0) {
|
|
5607
|
-
if (c === "{") {
|
|
5608
|
-
tplStack[tplStack.length - 1]++;
|
|
5609
|
-
i++;
|
|
5610
|
-
continue;
|
|
5611
|
-
}
|
|
5612
|
-
if (c === "}") {
|
|
5613
|
-
if (tplStack[tplStack.length - 1] === 0) {
|
|
5614
|
-
tplStack.pop();
|
|
5615
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
5616
|
-
mask(i + 1, scan.maskEnd);
|
|
5617
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
5618
|
-
i = scan.resumeAt;
|
|
5619
|
-
continue;
|
|
5620
|
-
}
|
|
5621
|
-
tplStack[tplStack.length - 1]--;
|
|
5622
|
-
i++;
|
|
5623
|
-
continue;
|
|
5624
|
-
}
|
|
5625
|
-
}
|
|
5626
|
-
const handled = handleQuotesAndComments(content, i, tplStack, mask);
|
|
5627
|
-
if (handled.handled) {
|
|
5628
|
-
i = handled.nextI;
|
|
5629
|
-
continue;
|
|
5630
|
-
}
|
|
5631
|
-
i++;
|
|
5632
|
-
}
|
|
5633
|
-
return out.join("");
|
|
5634
|
-
};
|
|
5635
|
-
const consumeQuotedString = (content, start, quote) => {
|
|
5636
|
-
const len = content.length;
|
|
5637
|
-
let i = start + 1;
|
|
5638
|
-
while (i < len) {
|
|
5639
|
-
const c = content[i];
|
|
5640
|
-
if (c === "\\" && i + 1 < len) {
|
|
5641
|
-
i += 2;
|
|
5642
|
-
continue;
|
|
5643
|
-
}
|
|
5644
|
-
if (c === quote) return i + 1;
|
|
5645
|
-
if (c === "\n") return i;
|
|
5646
|
-
i++;
|
|
5647
|
-
}
|
|
5648
|
-
return i;
|
|
5649
|
-
};
|
|
5650
|
-
const consumeTemplateString = (content, start) => {
|
|
5651
|
-
const len = content.length;
|
|
5652
|
-
let i = start;
|
|
5653
|
-
while (i < len) {
|
|
5654
|
-
const c = content[i];
|
|
5655
|
-
if (c === "\\" && i + 1 < len) {
|
|
5656
|
-
i += 2;
|
|
5657
|
-
continue;
|
|
5658
|
-
}
|
|
5659
|
-
if (c === "`") return {
|
|
5660
|
-
maskEnd: i,
|
|
5661
|
-
resumeAt: i + 1,
|
|
5662
|
-
openedInterp: false
|
|
5663
|
-
};
|
|
5664
|
-
if (c === "$" && content[i + 1] === "{") return {
|
|
5665
|
-
maskEnd: i,
|
|
5666
|
-
resumeAt: i + 2,
|
|
5667
|
-
openedInterp: true
|
|
5668
|
-
};
|
|
5669
|
-
i++;
|
|
5670
|
-
}
|
|
5671
|
-
return {
|
|
5672
|
-
maskEnd: i,
|
|
5673
|
-
resumeAt: i,
|
|
5674
|
-
openedInterp: false
|
|
5675
|
-
};
|
|
5676
|
-
};
|
|
5677
|
-
const maskSimple = (content, family) => {
|
|
5678
|
-
const out = content.split("");
|
|
5679
|
-
const len = content.length;
|
|
5680
|
-
let i = 0;
|
|
5681
|
-
const mask = (start, end) => {
|
|
5682
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
5683
|
-
};
|
|
5684
|
-
while (i < len) {
|
|
5685
|
-
const c = content[i];
|
|
5686
|
-
const next = content[i + 1];
|
|
5687
|
-
if (family === "py" && (c === "\"" || c === "'")) {
|
|
5688
|
-
if (content[i + 1] === c && content[i + 2] === c) {
|
|
5689
|
-
const triple = c + c + c;
|
|
5690
|
-
const end = content.indexOf(triple, i + 3);
|
|
5691
|
-
const stop = end === -1 ? len : end + 3;
|
|
5692
|
-
mask(i + 3, stop - 3);
|
|
5693
|
-
i = stop;
|
|
5694
|
-
continue;
|
|
5695
|
-
}
|
|
5696
|
-
}
|
|
5697
|
-
if (c === "\"" || c === "'") {
|
|
5698
|
-
const strStart = i;
|
|
5699
|
-
i = consumeQuotedString(content, i, c);
|
|
5700
|
-
mask(strStart + 1, i - 1);
|
|
5701
|
-
continue;
|
|
5702
|
-
}
|
|
5703
|
-
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
5704
|
-
const strStart = i;
|
|
5705
|
-
while (i < len && content[i] !== "\n") i++;
|
|
5706
|
-
mask(strStart, i);
|
|
5707
|
-
continue;
|
|
5708
|
-
}
|
|
5709
|
-
if (family === "php" && c === "/" && next === "/") {
|
|
5710
|
-
const strStart = i;
|
|
5711
|
-
while (i < len && content[i] !== "\n") i++;
|
|
5712
|
-
mask(strStart, i);
|
|
5713
|
-
continue;
|
|
5714
|
-
}
|
|
5715
|
-
if (family === "php" && c === "/" && next === "*") {
|
|
5716
|
-
const strStart = i;
|
|
5717
|
-
i += 2;
|
|
5718
|
-
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
5719
|
-
if (i < len - 1) i += 2;
|
|
5720
|
-
mask(strStart, i);
|
|
5721
|
-
continue;
|
|
5722
|
-
}
|
|
5723
|
-
i++;
|
|
5724
|
-
}
|
|
5725
|
-
return out.join("");
|
|
5726
|
-
};
|
|
5727
|
-
|
|
5728
5868
|
//#endregion
|
|
5729
5869
|
//#region src/engines/security/risky.ts
|
|
5730
5870
|
const ev = "eval";
|
|
@@ -5999,6 +6139,7 @@ const scanSecrets = async (context) => {
|
|
|
5999
6139
|
} catch {
|
|
6000
6140
|
continue;
|
|
6001
6141
|
}
|
|
6142
|
+
content = maskComments(content, path.extname(filePath));
|
|
6002
6143
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
6003
6144
|
for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
|
|
6004
6145
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
@@ -6087,6 +6228,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
|
|
|
6087
6228
|
//#endregion
|
|
6088
6229
|
//#region src/scoring/index.ts
|
|
6089
6230
|
const PERFECT_SCORE = 100;
|
|
6231
|
+
const STYLE_RULES = new Set([
|
|
6232
|
+
"ai-slop/trivial-comment",
|
|
6233
|
+
"ai-slop/narrative-comment",
|
|
6234
|
+
"complexity/file-too-large",
|
|
6235
|
+
"complexity/function-too-long"
|
|
6236
|
+
]);
|
|
6237
|
+
const STYLE_WEIGHT = .5;
|
|
6090
6238
|
const getEffectiveFileCount = (diagnostics, sourceFileCount) => {
|
|
6091
6239
|
if (typeof sourceFileCount === "number" && sourceFileCount > 0) return sourceFileCount;
|
|
6092
6240
|
const filesWithDiagnostics = new Set(diagnostics.map((d) => d.filePath)).size;
|
|
@@ -6101,7 +6249,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
6101
6249
|
for (const d of diagnostics) {
|
|
6102
6250
|
const engineWeight = weights[d.engine] ?? 1;
|
|
6103
6251
|
const severityPenalty = d.severity === "error" ? 3 : d.severity === "warning" ? 1 : .25;
|
|
6104
|
-
|
|
6252
|
+
const styleFactor = STYLE_RULES.has(d.rule) ? STYLE_WEIGHT : 1;
|
|
6253
|
+
deductions += severityPenalty * engineWeight * styleFactor;
|
|
6105
6254
|
}
|
|
6106
6255
|
const effectiveFileCount = getEffectiveFileCount(diagnostics, sourceFileCount);
|
|
6107
6256
|
const smoothingConstant = typeof smoothing === "number" ? smoothing : 10;
|
|
@@ -6468,7 +6617,7 @@ const handleAislopBaseline = (input) => {
|
|
|
6468
6617
|
|
|
6469
6618
|
//#endregion
|
|
6470
6619
|
//#region src/version.ts
|
|
6471
|
-
const APP_VERSION = "0.
|
|
6620
|
+
const APP_VERSION = "0.10.1";
|
|
6472
6621
|
|
|
6473
6622
|
//#endregion
|
|
6474
6623
|
//#region src/telemetry/env.ts
|