aislop 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -8
- package/dist/cli.js +1145 -631
- package/dist/{expo-doctor-BcIkOte5.js → expo-doctor-c-jE6pR2.js} +1 -1
- package/dist/{generic-D_T4cUaC.js → generic-BsQa13CS.js} +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +3094 -2664
- package/dist/{json-DaFOYHcf.js → json-B01i-GOz.js} +7 -5
- package/dist/{json-OIzja7OM.js → json-CXV4D0Ib.js} +5 -3
- package/dist/mcp.js +656 -470
- package/dist/{sarif-BtSQ92c6.js → sarif-cy5SiDDq.js} +1 -1
- package/dist/{typecheck-DQSzG8fX.js → typecheck-BdQ7uFyK.js} +1 -1
- package/dist/version-BfJVwhN2.js +5 -0
- package/package.json +8 -11
- package/dist/version-DYg_ShBx.js +0 -5
- /package/dist/{engine-info-DCvIfZ0f.js → engine-info-Cpt36DqZ.js} +0 -0
- /package/dist/{subprocess-CQUJDGgn.js → subprocess-0uXz8HdE.js} +0 -0
package/dist/mcp.js
CHANGED
|
@@ -16,6 +16,10 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
18
|
|
|
19
|
+
//#region src/version.ts
|
|
20
|
+
const APP_VERSION = "0.10.2";
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
19
23
|
//#region src/config/defaults.ts
|
|
20
24
|
const DEFAULT_CONFIG = {
|
|
21
25
|
version: 1,
|
|
@@ -68,6 +72,22 @@ const DEFAULT_CONFIG = {
|
|
|
68
72
|
telemetry: { enabled: true },
|
|
69
73
|
rules: {}
|
|
70
74
|
};
|
|
75
|
+
const DEFAULT_GITHUB_WORKFLOW_YAML = `name: aislop
|
|
76
|
+
|
|
77
|
+
on:
|
|
78
|
+
push:
|
|
79
|
+
branches: [main]
|
|
80
|
+
pull_request:
|
|
81
|
+
|
|
82
|
+
jobs:
|
|
83
|
+
quality-gate:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- uses: scanaislop/aislop@v${APP_VERSION}
|
|
88
|
+
with:
|
|
89
|
+
version: ${APP_VERSION}
|
|
90
|
+
`;
|
|
71
91
|
|
|
72
92
|
//#endregion
|
|
73
93
|
//#region src/config/extends.ts
|
|
@@ -512,7 +532,7 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
|
512
532
|
};
|
|
513
533
|
|
|
514
534
|
//#endregion
|
|
515
|
-
//#region src/
|
|
535
|
+
//#region src/utils/source-masker.ts
|
|
516
536
|
const JS_EXTS$2 = new Set([
|
|
517
537
|
".ts",
|
|
518
538
|
".tsx",
|
|
@@ -521,14 +541,226 @@ const JS_EXTS$2 = new Set([
|
|
|
521
541
|
".mjs",
|
|
522
542
|
".cjs"
|
|
523
543
|
]);
|
|
544
|
+
const PY_EXTS = new Set([".py"]);
|
|
545
|
+
const RB_EXTS = new Set([".rb"]);
|
|
546
|
+
const PHP_EXTS = new Set([".php"]);
|
|
547
|
+
const familyForExt = (ext) => {
|
|
548
|
+
if (JS_EXTS$2.has(ext)) return "js";
|
|
549
|
+
if (PY_EXTS.has(ext)) return "py";
|
|
550
|
+
if (RB_EXTS.has(ext)) return "rb";
|
|
551
|
+
if (PHP_EXTS.has(ext)) return "php";
|
|
552
|
+
return "none";
|
|
553
|
+
};
|
|
554
|
+
const maskStringsAndComments = (content, ext) => {
|
|
555
|
+
const family = familyForExt(ext);
|
|
556
|
+
if (family === "none") return content;
|
|
557
|
+
if (family === "js") return maskJs(content, true);
|
|
558
|
+
return maskSimple(content, family, true);
|
|
559
|
+
};
|
|
560
|
+
const maskComments = (content, ext) => {
|
|
561
|
+
const family = familyForExt(ext);
|
|
562
|
+
if (family === "none") return content;
|
|
563
|
+
if (family === "js") return maskJs(content, false);
|
|
564
|
+
return maskSimple(content, family, false);
|
|
565
|
+
};
|
|
566
|
+
const handleQuotesAndComments = (content, i, tplStack, mask, maskStrings) => {
|
|
567
|
+
const len = content.length;
|
|
568
|
+
const c = content[i];
|
|
569
|
+
const next = content[i + 1];
|
|
570
|
+
if (c === "\"" || c === "'") {
|
|
571
|
+
const strStart = i;
|
|
572
|
+
const end = consumeQuotedString(content, i, c);
|
|
573
|
+
if (maskStrings) mask(strStart + 1, end - 1);
|
|
574
|
+
return {
|
|
575
|
+
handled: true,
|
|
576
|
+
nextI: end
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
if (c === "`") {
|
|
580
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
581
|
+
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
582
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
583
|
+
return {
|
|
584
|
+
handled: true,
|
|
585
|
+
nextI: scan.resumeAt
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
if (c === "/" && next === "/") {
|
|
589
|
+
const strStart = i;
|
|
590
|
+
let k = i;
|
|
591
|
+
while (k < len && content[k] !== "\n") k++;
|
|
592
|
+
mask(strStart, k);
|
|
593
|
+
return {
|
|
594
|
+
handled: true,
|
|
595
|
+
nextI: k
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
if (c === "/" && next === "*") {
|
|
599
|
+
const strStart = i;
|
|
600
|
+
let k = i + 2;
|
|
601
|
+
while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
|
|
602
|
+
if (k < len - 1) k += 2;
|
|
603
|
+
mask(strStart, k);
|
|
604
|
+
return {
|
|
605
|
+
handled: true,
|
|
606
|
+
nextI: k
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
handled: false,
|
|
611
|
+
nextI: i
|
|
612
|
+
};
|
|
613
|
+
};
|
|
614
|
+
const maskJs = (content, maskStrings) => {
|
|
615
|
+
const out = content.split("");
|
|
616
|
+
const len = content.length;
|
|
617
|
+
const tplStack = [];
|
|
618
|
+
let i = 0;
|
|
619
|
+
const mask = (start, end) => {
|
|
620
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
621
|
+
};
|
|
622
|
+
while (i < len) {
|
|
623
|
+
const c = content[i];
|
|
624
|
+
if (tplStack.length > 0) {
|
|
625
|
+
if (c === "{") {
|
|
626
|
+
tplStack[tplStack.length - 1]++;
|
|
627
|
+
i++;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (c === "}") {
|
|
631
|
+
if (tplStack[tplStack.length - 1] === 0) {
|
|
632
|
+
tplStack.pop();
|
|
633
|
+
const scan = consumeTemplateString(content, i + 1);
|
|
634
|
+
if (maskStrings) mask(i + 1, scan.maskEnd);
|
|
635
|
+
if (scan.openedInterp) tplStack.push(0);
|
|
636
|
+
i = scan.resumeAt;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
tplStack[tplStack.length - 1]--;
|
|
640
|
+
i++;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const handled = handleQuotesAndComments(content, i, tplStack, mask, maskStrings);
|
|
645
|
+
if (handled.handled) {
|
|
646
|
+
i = handled.nextI;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
i++;
|
|
650
|
+
}
|
|
651
|
+
return out.join("");
|
|
652
|
+
};
|
|
653
|
+
const consumeQuotedString = (content, start, quote) => {
|
|
654
|
+
const len = content.length;
|
|
655
|
+
let i = start + 1;
|
|
656
|
+
while (i < len) {
|
|
657
|
+
const c = content[i];
|
|
658
|
+
if (c === "\\" && i + 1 < len) {
|
|
659
|
+
i += 2;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (c === quote) return i + 1;
|
|
663
|
+
if (c === "\n") return i;
|
|
664
|
+
i++;
|
|
665
|
+
}
|
|
666
|
+
return i;
|
|
667
|
+
};
|
|
668
|
+
const consumeTemplateString = (content, start) => {
|
|
669
|
+
const len = content.length;
|
|
670
|
+
let i = start;
|
|
671
|
+
while (i < len) {
|
|
672
|
+
const c = content[i];
|
|
673
|
+
if (c === "\\" && i + 1 < len) {
|
|
674
|
+
i += 2;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (c === "`") return {
|
|
678
|
+
maskEnd: i,
|
|
679
|
+
resumeAt: i + 1,
|
|
680
|
+
openedInterp: false
|
|
681
|
+
};
|
|
682
|
+
if (c === "$" && content[i + 1] === "{") return {
|
|
683
|
+
maskEnd: i,
|
|
684
|
+
resumeAt: i + 2,
|
|
685
|
+
openedInterp: true
|
|
686
|
+
};
|
|
687
|
+
i++;
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
maskEnd: i,
|
|
691
|
+
resumeAt: i,
|
|
692
|
+
openedInterp: false
|
|
693
|
+
};
|
|
694
|
+
};
|
|
695
|
+
const maskSimple = (content, family, maskStrings) => {
|
|
696
|
+
const out = content.split("");
|
|
697
|
+
const len = content.length;
|
|
698
|
+
let i = 0;
|
|
699
|
+
const mask = (start, end) => {
|
|
700
|
+
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
701
|
+
};
|
|
702
|
+
while (i < len) {
|
|
703
|
+
const c = content[i];
|
|
704
|
+
const next = content[i + 1];
|
|
705
|
+
if (family === "py" && (c === "\"" || c === "'")) {
|
|
706
|
+
if (content[i + 1] === c && content[i + 2] === c) {
|
|
707
|
+
const triple = c + c + c;
|
|
708
|
+
const end = content.indexOf(triple, i + 3);
|
|
709
|
+
const stop = end === -1 ? len : end + 3;
|
|
710
|
+
if (maskStrings) mask(i + 3, stop - 3);
|
|
711
|
+
i = stop;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (c === "\"" || c === "'") {
|
|
716
|
+
const strStart = i;
|
|
717
|
+
i = consumeQuotedString(content, i, c);
|
|
718
|
+
if (maskStrings) mask(strStart + 1, i - 1);
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
722
|
+
const strStart = i;
|
|
723
|
+
while (i < len && content[i] !== "\n") i++;
|
|
724
|
+
mask(strStart, i);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (family === "php" && c === "/" && next === "/") {
|
|
728
|
+
const strStart = i;
|
|
729
|
+
while (i < len && content[i] !== "\n") i++;
|
|
730
|
+
mask(strStart, i);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (family === "php" && c === "/" && next === "*") {
|
|
734
|
+
const strStart = i;
|
|
735
|
+
i += 2;
|
|
736
|
+
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
737
|
+
if (i < len - 1) i += 2;
|
|
738
|
+
mask(strStart, i);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
i++;
|
|
742
|
+
}
|
|
743
|
+
return out.join("");
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region src/engines/ai-slop/abstractions.ts
|
|
748
|
+
const JS_EXTS$1 = new Set([
|
|
749
|
+
".ts",
|
|
750
|
+
".tsx",
|
|
751
|
+
".js",
|
|
752
|
+
".jsx",
|
|
753
|
+
".mjs",
|
|
754
|
+
".cjs"
|
|
755
|
+
]);
|
|
524
756
|
const THIN_WRAPPER_PATTERNS = [
|
|
525
757
|
{
|
|
526
758
|
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$
|
|
759
|
+
extensions: JS_EXTS$1
|
|
528
760
|
},
|
|
529
761
|
{
|
|
530
762
|
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
531
|
-
extensions: JS_EXTS$
|
|
763
|
+
extensions: JS_EXTS$1
|
|
532
764
|
},
|
|
533
765
|
{
|
|
534
766
|
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
@@ -556,16 +788,14 @@ const detectThinWrappers = (content, relativePath, ext) => {
|
|
|
556
788
|
for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
|
|
557
789
|
if (!extensions.has(ext)) continue;
|
|
558
790
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
559
|
-
|
|
560
|
-
while ((match = regex.exec(content)) !== null) {
|
|
791
|
+
for (const match of content.matchAll(regex)) {
|
|
561
792
|
const funcName = match[1];
|
|
562
793
|
const matchText = match[0];
|
|
563
794
|
const lineNumber = content.slice(0, match.index).split("\n").length;
|
|
564
795
|
if (DUNDER_PATTERN.test(funcName)) continue;
|
|
565
796
|
if (FRAMEWORK_METHOD_NAMES.test(funcName)) continue;
|
|
566
797
|
if (lineNumber >= 2) {
|
|
567
|
-
|
|
568
|
-
if (prevLine && prevLine.startsWith("@")) continue;
|
|
798
|
+
if ((lines[lineNumber - 2]?.trim())?.startsWith("@")) continue;
|
|
569
799
|
}
|
|
570
800
|
if (!isIdentityForward(matchText)) continue;
|
|
571
801
|
if (isUseContextWrapper(matchText)) continue;
|
|
@@ -621,8 +851,9 @@ const detectOverAbstraction = async (context) => {
|
|
|
621
851
|
}
|
|
622
852
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
623
853
|
const ext = path.extname(filePath);
|
|
624
|
-
|
|
625
|
-
diagnostics.push(...
|
|
854
|
+
const codeOnly = maskComments(content, ext);
|
|
855
|
+
diagnostics.push(...detectThinWrappers(codeOnly, relativePath, ext));
|
|
856
|
+
diagnostics.push(...detectAiNaming(codeOnly, relativePath));
|
|
626
857
|
}
|
|
627
858
|
return diagnostics;
|
|
628
859
|
};
|
|
@@ -941,7 +1172,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
941
1172
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
942
1173
|
if (JS_EXTENSIONS$3.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !isGuardedSingleLineExit(lines, i) && !isBlockCloserAfterReturn(nextLine) && !nextLine.startsWith("//") && !nextLine.startsWith("/*") && !nextLine.startsWith("case ") && !nextLine.startsWith("default:") && !nextLine.startsWith("if ") && !nextLine.startsWith("if(") && !nextLine.startsWith("else")) diagnostics.push(slop(relativePath, i + 2, "ai-slop/unreachable-code", "warning", "Code after return/throw statement is unreachable", "Remove the unreachable code or restructure the control flow", false));
|
|
943
1174
|
if (/\bif\s*\(\s*(?:false|true|0|1)\s*\)/.test(trimmed) && !trimmed.startsWith("//") && !trimmed.startsWith("*") && !/["'`].*\bif\s*\(/.test(trimmed) && !/\/.*\bif\s*\(/.test(trimmed.replace(/\/\/.*$/, ""))) diagnostics.push(slop(relativePath, i + 1, "ai-slop/constant-condition", "warning", "Conditional with a constant value — likely debugging leftover", "Remove the constant condition or replace with proper logic", false));
|
|
944
|
-
if (JS_EXTENSIONS$3.has(ext) && /(?:function\s+\w
|
|
1175
|
+
if (JS_EXTENSIONS$3.has(ext) && /(?:function\s+\w+\s*\([^)]*\)|=>\s*)\s*\{\s*\}\s*;?\s*$/.test(trimmed) && !trimmed.startsWith("interface") && !trimmed.startsWith("type ")) diagnostics.push(slop(relativePath, i + 1, "ai-slop/empty-function", "info", "Empty function body — possible stub or unfinished implementation", "Implement the function body or add a comment explaining why it's empty", false));
|
|
945
1176
|
}
|
|
946
1177
|
return diagnostics;
|
|
947
1178
|
};
|
|
@@ -978,9 +1209,10 @@ const detectDeadPatterns = async (context) => {
|
|
|
978
1209
|
}
|
|
979
1210
|
const ext = path.extname(filePath);
|
|
980
1211
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
981
|
-
|
|
1212
|
+
const codeOnly = maskComments(content, ext);
|
|
1213
|
+
diagnostics.push(...detectConsoleLeftovers(codeOnly, relativePath, ext));
|
|
982
1214
|
diagnostics.push(...detectTodoStubs(content, relativePath));
|
|
983
|
-
diagnostics.push(...detectDeadCodePatterns(
|
|
1215
|
+
diagnostics.push(...detectDeadCodePatterns(codeOnly, relativePath, ext));
|
|
984
1216
|
diagnostics.push(...detectUnsafeTypePatterns(content, relativePath, ext));
|
|
985
1217
|
}
|
|
986
1218
|
return diagnostics;
|
|
@@ -1170,6 +1402,7 @@ const JS_EXTENSIONS$2 = new Set([
|
|
|
1170
1402
|
const IMPORT_FROM_RE = /^\s*import\s+([^;]*?)\s+from\s+["']([^"']+)["']/;
|
|
1171
1403
|
const TYPE_ONLY_RE = /^\s*type\b/;
|
|
1172
1404
|
const VALUE_BINDING_RE = /\{([^}]*)\}/;
|
|
1405
|
+
const NAMESPACE_RE = /\*\s+as\s+/;
|
|
1173
1406
|
const isTypeOnly = (clause) => {
|
|
1174
1407
|
if (TYPE_ONLY_RE.test(clause)) return true;
|
|
1175
1408
|
const braces = VALUE_BINDING_RE.exec(clause);
|
|
@@ -1187,7 +1420,8 @@ const extractImportLines = (content) => {
|
|
|
1187
1420
|
results.push({
|
|
1188
1421
|
spec: match[2],
|
|
1189
1422
|
line: i + 1,
|
|
1190
|
-
typeOnly: isTypeOnly(match[1])
|
|
1423
|
+
typeOnly: isTypeOnly(match[1]),
|
|
1424
|
+
namespace: NAMESPACE_RE.test(match[1])
|
|
1191
1425
|
});
|
|
1192
1426
|
}
|
|
1193
1427
|
return results;
|
|
@@ -1204,11 +1438,11 @@ const detectDuplicateImports = async (context) => {
|
|
|
1204
1438
|
} catch {
|
|
1205
1439
|
continue;
|
|
1206
1440
|
}
|
|
1207
|
-
const imports = extractImportLines(content);
|
|
1441
|
+
const imports = extractImportLines(maskComments(content, path.extname(filePath)));
|
|
1208
1442
|
if (imports.length < 2) continue;
|
|
1209
1443
|
const byBucket = /* @__PURE__ */ new Map();
|
|
1210
1444
|
for (const imp of imports) {
|
|
1211
|
-
const key = `${imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
1445
|
+
const key = `${imp.namespace ? "ns" : imp.typeOnly ? "type" : "value"}\0${imp.spec}`;
|
|
1212
1446
|
const list = byBucket.get(key) ?? [];
|
|
1213
1447
|
list.push(imp);
|
|
1214
1448
|
byBucket.set(key, list);
|
|
@@ -1324,9 +1558,8 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
1324
1558
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1325
1559
|
for (const { pattern, languages, message } of SWALLOWED_EXCEPTION_PATTERNS) {
|
|
1326
1560
|
if (!languages.includes(ext)) continue;
|
|
1327
|
-
let match;
|
|
1328
1561
|
const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
|
|
1329
|
-
|
|
1562
|
+
for (const match of content.matchAll(regex)) {
|
|
1330
1563
|
if (isIntentionalIgnore(match[0], ext)) continue;
|
|
1331
1564
|
const line = content.slice(0, match.index).split("\n").length;
|
|
1332
1565
|
diagnostics.push({
|
|
@@ -1421,170 +1654,10 @@ const detectGoPatterns = async (context) => {
|
|
|
1421
1654
|
};
|
|
1422
1655
|
|
|
1423
1656
|
//#endregion
|
|
1424
|
-
//#region src/engines/ai-slop/
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
"
|
|
1428
|
-
".js",
|
|
1429
|
-
".jsx",
|
|
1430
|
-
".mjs",
|
|
1431
|
-
".cjs",
|
|
1432
|
-
".py",
|
|
1433
|
-
".go",
|
|
1434
|
-
".rs",
|
|
1435
|
-
".rb",
|
|
1436
|
-
".java",
|
|
1437
|
-
".php"
|
|
1438
|
-
]);
|
|
1439
|
-
const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
|
|
1440
|
-
const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
|
|
1441
|
-
const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
|
|
1442
|
-
const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
|
|
1443
|
-
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
1444
|
-
const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
|
|
1445
|
-
const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
|
|
1446
|
-
const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
|
|
1447
|
-
const PLACEHOLDER_HOSTS = new Set([
|
|
1448
|
-
"example.com",
|
|
1449
|
-
"example.org",
|
|
1450
|
-
"example.net"
|
|
1451
|
-
]);
|
|
1452
|
-
const LOOPBACK_HOSTS = new Set([
|
|
1453
|
-
"localhost",
|
|
1454
|
-
"127.0.0.1",
|
|
1455
|
-
"0.0.0.0",
|
|
1456
|
-
"::1"
|
|
1457
|
-
]);
|
|
1458
|
-
const VENDOR_API_DOMAINS = [
|
|
1459
|
-
"github.com",
|
|
1460
|
-
"githubusercontent.com",
|
|
1461
|
-
"googleapis.com",
|
|
1462
|
-
"accounts.google.com",
|
|
1463
|
-
"stripe.com",
|
|
1464
|
-
"openai.com",
|
|
1465
|
-
"anthropic.com",
|
|
1466
|
-
"slack.com",
|
|
1467
|
-
"twilio.com",
|
|
1468
|
-
"sendgrid.com",
|
|
1469
|
-
"mailgun.net",
|
|
1470
|
-
"cloudflare.com",
|
|
1471
|
-
"discord.com",
|
|
1472
|
-
"telegram.org",
|
|
1473
|
-
"login.microsoftonline.com",
|
|
1474
|
-
"graph.microsoft.com",
|
|
1475
|
-
"twitter.com",
|
|
1476
|
-
"x.com",
|
|
1477
|
-
"twimg.com",
|
|
1478
|
-
"t.co",
|
|
1479
|
-
"api.telegram.org"
|
|
1480
|
-
];
|
|
1481
|
-
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
1482
|
-
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
1483
|
-
const HARDCODED_URL_FINDING = {
|
|
1484
|
-
rule: "ai-slop/hardcoded-url",
|
|
1485
|
-
message: "Hardcoded environment URL in production code",
|
|
1486
|
-
help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
|
|
1487
|
-
};
|
|
1488
|
-
const HARDCODED_ID_FINDING = {
|
|
1489
|
-
rule: "ai-slop/hardcoded-id",
|
|
1490
|
-
message: "Hardcoded provider/project ID in production code",
|
|
1491
|
-
help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
|
|
1492
|
-
};
|
|
1493
|
-
const makeFinding = (filePath, line, spec) => ({
|
|
1494
|
-
filePath,
|
|
1495
|
-
engine: "ai-slop",
|
|
1496
|
-
rule: spec.rule,
|
|
1497
|
-
severity: "warning",
|
|
1498
|
-
message: spec.message,
|
|
1499
|
-
help: spec.help,
|
|
1500
|
-
line,
|
|
1501
|
-
column: 0,
|
|
1502
|
-
category: "AI Slop",
|
|
1503
|
-
fixable: false
|
|
1504
|
-
});
|
|
1505
|
-
const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
1506
|
-
const commentStartsBefore = (line, index, ext) => {
|
|
1507
|
-
const prefix = line.slice(0, index);
|
|
1508
|
-
if (ext === ".py" || ext === ".rb") return prefix.includes("#");
|
|
1509
|
-
if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
|
|
1510
|
-
return prefix.includes("//") || prefix.includes("/*");
|
|
1511
|
-
};
|
|
1512
|
-
const safeUrlHost = (urlText) => {
|
|
1513
|
-
try {
|
|
1514
|
-
return new URL(urlText).hostname.toLowerCase();
|
|
1515
|
-
} catch {
|
|
1516
|
-
return null;
|
|
1517
|
-
}
|
|
1518
|
-
};
|
|
1519
|
-
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
1520
|
-
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
1521
|
-
if (isEnvBackedLine(line)) return false;
|
|
1522
|
-
const host = safeUrlHost(urlText);
|
|
1523
|
-
if (!host) return false;
|
|
1524
|
-
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
1525
|
-
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
1526
|
-
if (isVendorApiHost(host)) return false;
|
|
1527
|
-
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
1528
|
-
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
1529
|
-
};
|
|
1530
|
-
const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
|
|
1531
|
-
const hasUsefulIdShape = (value) => {
|
|
1532
|
-
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
1533
|
-
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
1534
|
-
if (/^https?:\/\//i.test(value)) return false;
|
|
1535
|
-
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
1536
|
-
return /[0-9]/.test(value);
|
|
1537
|
-
};
|
|
1538
|
-
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
1539
|
-
const diagnostics = [];
|
|
1540
|
-
if (isCommentOnlyLine(line.trim())) return diagnostics;
|
|
1541
|
-
URL_LITERAL_RE.lastIndex = 0;
|
|
1542
|
-
let urlMatch;
|
|
1543
|
-
while ((urlMatch = URL_LITERAL_RE.exec(line)) !== null) {
|
|
1544
|
-
const urlText = urlMatch[2];
|
|
1545
|
-
if (commentStartsBefore(line, urlMatch.index, ext)) continue;
|
|
1546
|
-
if (!shouldFlagUrlLiteral(line, urlText)) continue;
|
|
1547
|
-
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
|
|
1548
|
-
}
|
|
1549
|
-
if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
|
|
1550
|
-
ID_LITERAL_RE.lastIndex = 0;
|
|
1551
|
-
let idMatch;
|
|
1552
|
-
while ((idMatch = ID_LITERAL_RE.exec(line)) !== null) {
|
|
1553
|
-
const value = idMatch[2];
|
|
1554
|
-
if (commentStartsBefore(line, idMatch.index, ext)) continue;
|
|
1555
|
-
if (!hasUsefulIdShape(value)) continue;
|
|
1556
|
-
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
|
|
1557
|
-
}
|
|
1558
|
-
return diagnostics;
|
|
1559
|
-
};
|
|
1560
|
-
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
1561
|
-
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
1562
|
-
if (isNonProductionPath(relativePath)) return [];
|
|
1563
|
-
if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
|
|
1564
|
-
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
1565
|
-
};
|
|
1566
|
-
const detectHardcodedConfigLiterals = async (context) => {
|
|
1567
|
-
const diagnostics = [];
|
|
1568
|
-
for (const filePath of getSourceFiles(context)) {
|
|
1569
|
-
if (isAutoGenerated(filePath)) continue;
|
|
1570
|
-
let content;
|
|
1571
|
-
try {
|
|
1572
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
1573
|
-
} catch {
|
|
1574
|
-
continue;
|
|
1575
|
-
}
|
|
1576
|
-
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1577
|
-
const ext = path.extname(filePath);
|
|
1578
|
-
diagnostics.push(...scanFileForConfigLiterals(content, relativePath, ext));
|
|
1579
|
-
}
|
|
1580
|
-
return diagnostics;
|
|
1581
|
-
};
|
|
1582
|
-
|
|
1583
|
-
//#endregion
|
|
1584
|
-
//#region src/engines/ai-slop/js-import-aliases.ts
|
|
1585
|
-
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
1586
|
-
const JS_RESOLUTION_EXTENSIONS = [
|
|
1587
|
-
"",
|
|
1657
|
+
//#region src/engines/ai-slop/js-import-aliases.ts
|
|
1658
|
+
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
1659
|
+
const JS_RESOLUTION_EXTENSIONS = [
|
|
1660
|
+
"",
|
|
1588
1661
|
".ts",
|
|
1589
1662
|
".tsx",
|
|
1590
1663
|
".js",
|
|
@@ -1677,15 +1750,18 @@ const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
|
1677
1750
|
}
|
|
1678
1751
|
return globs;
|
|
1679
1752
|
};
|
|
1753
|
+
const readWorkspaceEntries = (dir) => {
|
|
1754
|
+
try {
|
|
1755
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
1756
|
+
} catch {
|
|
1757
|
+
return [];
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1680
1760
|
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
1681
1761
|
const dirs = [];
|
|
1682
1762
|
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
1683
1763
|
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
1684
|
-
|
|
1685
|
-
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
1686
|
-
} catch {
|
|
1687
|
-
continue;
|
|
1688
|
-
}
|
|
1764
|
+
for (const entry of readWorkspaceEntries(parent)) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
1689
1765
|
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
1690
1766
|
return dirs;
|
|
1691
1767
|
};
|
|
@@ -2318,6 +2394,167 @@ const detectHallucinatedImports = async (context) => {
|
|
|
2318
2394
|
return diagnostics;
|
|
2319
2395
|
};
|
|
2320
2396
|
|
|
2397
|
+
//#endregion
|
|
2398
|
+
//#region src/engines/ai-slop/hardcoded-config.ts
|
|
2399
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
2400
|
+
".ts",
|
|
2401
|
+
".tsx",
|
|
2402
|
+
".js",
|
|
2403
|
+
".jsx",
|
|
2404
|
+
".mjs",
|
|
2405
|
+
".cjs",
|
|
2406
|
+
".py",
|
|
2407
|
+
".go",
|
|
2408
|
+
".rs",
|
|
2409
|
+
".rb",
|
|
2410
|
+
".java",
|
|
2411
|
+
".php"
|
|
2412
|
+
]);
|
|
2413
|
+
const URL_LITERAL_RE = /(["'`])(https?:\/\/[^"'`\s<>]+)\1/g;
|
|
2414
|
+
const ID_LITERAL_RE = /(["'])([A-Za-z][A-Za-z0-9_-]{15,})\1/g;
|
|
2415
|
+
const ENV_REFERENCE_RE = /\b(?:process\.env|import\.meta\.env|Deno\.env|os\.environ|getenv|env\()\b/i;
|
|
2416
|
+
const DOC_URL_CONTEXT_RE = /\b(?:docs?|documentation|homepage|repository|bugs|license|readme|source|svgUrl|pageUrl|href|link|install)\b/i;
|
|
2417
|
+
const URL_CONFIG_CONTEXT_RE = /\b(?:api|base[_-]?url|baseUrl|endpoint|host|origin|webhook|callback|redirect|server|service|domain|url)\b/i;
|
|
2418
|
+
const ENVIRONMENT_HOST_RE = /(?:^|[.-])(?:api|app|admin|auth|staging|stage|prod|dev|sandbox|webhook|internal)(?:[.-]|$)|^(?:localhost|127\.0\.0\.1|0\.0\.0\.0)$/i;
|
|
2419
|
+
const ID_CONTEXT_RE = /(?:^|[^A-Za-z0-9])(?:api[_-]?key|client[_-]?id|project[_-]?id|org(?:anization)?[_-]?id|workspace[_-]?id|tenant[_-]?id|price[_-]?id|product[_-]?id|customer[_-]?id|subscription[_-]?id|account[_-]?id|app[_-]?id|key|token|secret)(?:$|[^A-Za-z0-9])/i;
|
|
2420
|
+
const MIGRATION_PATH_RE$1 = /(?:^|[\\/])(?:migrations?|db[\\/]migrate)[\\/]/i;
|
|
2421
|
+
const PLACEHOLDER_HOSTS = new Set([
|
|
2422
|
+
"example.com",
|
|
2423
|
+
"example.org",
|
|
2424
|
+
"example.net"
|
|
2425
|
+
]);
|
|
2426
|
+
const LOOPBACK_HOSTS = new Set([
|
|
2427
|
+
"localhost",
|
|
2428
|
+
"127.0.0.1",
|
|
2429
|
+
"0.0.0.0",
|
|
2430
|
+
"::1"
|
|
2431
|
+
]);
|
|
2432
|
+
const VENDOR_API_DOMAINS = [
|
|
2433
|
+
"github.com",
|
|
2434
|
+
"githubusercontent.com",
|
|
2435
|
+
"googleapis.com",
|
|
2436
|
+
"accounts.google.com",
|
|
2437
|
+
"stripe.com",
|
|
2438
|
+
"openai.com",
|
|
2439
|
+
"anthropic.com",
|
|
2440
|
+
"slack.com",
|
|
2441
|
+
"twilio.com",
|
|
2442
|
+
"sendgrid.com",
|
|
2443
|
+
"mailgun.net",
|
|
2444
|
+
"cloudflare.com",
|
|
2445
|
+
"discord.com",
|
|
2446
|
+
"telegram.org",
|
|
2447
|
+
"login.microsoftonline.com",
|
|
2448
|
+
"graph.microsoft.com",
|
|
2449
|
+
"twitter.com",
|
|
2450
|
+
"x.com",
|
|
2451
|
+
"twimg.com",
|
|
2452
|
+
"t.co",
|
|
2453
|
+
"api.telegram.org"
|
|
2454
|
+
];
|
|
2455
|
+
const isVendorApiHost = (host) => VENDOR_API_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));
|
|
2456
|
+
const PLACEHOLDER_ID_RE = /^(?:changeme|replace[_-]?me|your[_-]|example|placeholder|todo)/i;
|
|
2457
|
+
const HARDCODED_URL_FINDING = {
|
|
2458
|
+
rule: "ai-slop/hardcoded-url",
|
|
2459
|
+
message: "Hardcoded environment URL in production code",
|
|
2460
|
+
help: "Move deployment-specific URLs to environment variables or a typed config module. Keep only stable documentation/public links inline."
|
|
2461
|
+
};
|
|
2462
|
+
const HARDCODED_ID_FINDING = {
|
|
2463
|
+
rule: "ai-slop/hardcoded-id",
|
|
2464
|
+
message: "Hardcoded provider/project ID in production code",
|
|
2465
|
+
help: "Move provider IDs, tenant IDs, price IDs, and similar deployment-specific identifiers to env/config so agents do not bake one environment into source."
|
|
2466
|
+
};
|
|
2467
|
+
const makeFinding = (filePath, line, spec) => ({
|
|
2468
|
+
filePath,
|
|
2469
|
+
engine: "ai-slop",
|
|
2470
|
+
rule: spec.rule,
|
|
2471
|
+
severity: "warning",
|
|
2472
|
+
message: spec.message,
|
|
2473
|
+
help: spec.help,
|
|
2474
|
+
line,
|
|
2475
|
+
column: 0,
|
|
2476
|
+
category: "AI Slop",
|
|
2477
|
+
fixable: false
|
|
2478
|
+
});
|
|
2479
|
+
const isCommentOnlyLine = (trimmed) => trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*");
|
|
2480
|
+
const commentStartsBefore = (line, index, ext) => {
|
|
2481
|
+
const prefix = line.slice(0, index);
|
|
2482
|
+
if (ext === ".py" || ext === ".rb") return prefix.includes("#");
|
|
2483
|
+
if (ext === ".php") return prefix.includes("//") || prefix.includes("#");
|
|
2484
|
+
return prefix.includes("//") || prefix.includes("/*");
|
|
2485
|
+
};
|
|
2486
|
+
const safeUrlHost = (urlText) => {
|
|
2487
|
+
try {
|
|
2488
|
+
return new URL(urlText).hostname.toLowerCase();
|
|
2489
|
+
} catch {
|
|
2490
|
+
return null;
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
const isEnvBackedLine = (line) => ENV_REFERENCE_RE.test(line);
|
|
2494
|
+
const shouldFlagUrlLiteral = (line, urlText) => {
|
|
2495
|
+
if (isEnvBackedLine(line)) return false;
|
|
2496
|
+
const host = safeUrlHost(urlText);
|
|
2497
|
+
if (!host) return false;
|
|
2498
|
+
if (PLACEHOLDER_HOSTS.has(host)) return false;
|
|
2499
|
+
if (LOOPBACK_HOSTS.has(host)) return false;
|
|
2500
|
+
if (isVendorApiHost(host)) return false;
|
|
2501
|
+
if (DOC_URL_CONTEXT_RE.test(line) && !ENVIRONMENT_HOST_RE.test(host)) return false;
|
|
2502
|
+
return URL_CONFIG_CONTEXT_RE.test(line) || ENVIRONMENT_HOST_RE.test(host);
|
|
2503
|
+
};
|
|
2504
|
+
const ENV_VAR_NAME_RE = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/;
|
|
2505
|
+
const hasUsefulIdShape = (value) => {
|
|
2506
|
+
if (PLACEHOLDER_ID_RE.test(value)) return false;
|
|
2507
|
+
if (ENV_VAR_NAME_RE.test(value)) return false;
|
|
2508
|
+
if (/^https?:\/\//i.test(value)) return false;
|
|
2509
|
+
if (/^[A-Za-z]+$/.test(value)) return false;
|
|
2510
|
+
return /[0-9]/.test(value);
|
|
2511
|
+
};
|
|
2512
|
+
const scanLineForConfigLiterals = (line, relativePath, ext, lineNumber) => {
|
|
2513
|
+
const diagnostics = [];
|
|
2514
|
+
if (isCommentOnlyLine(line.trim())) return diagnostics;
|
|
2515
|
+
for (const urlMatch of line.matchAll(URL_LITERAL_RE)) {
|
|
2516
|
+
const urlText = urlMatch[2];
|
|
2517
|
+
if (commentStartsBefore(line, urlMatch.index, ext)) continue;
|
|
2518
|
+
if (!shouldFlagUrlLiteral(line, urlText)) continue;
|
|
2519
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_URL_FINDING));
|
|
2520
|
+
}
|
|
2521
|
+
if (!ID_CONTEXT_RE.test(line) || isEnvBackedLine(line) || DOC_URL_CONTEXT_RE.test(line)) return diagnostics;
|
|
2522
|
+
for (const idMatch of line.matchAll(ID_LITERAL_RE)) {
|
|
2523
|
+
const value = idMatch[2];
|
|
2524
|
+
if (commentStartsBefore(line, idMatch.index, ext)) continue;
|
|
2525
|
+
if (!hasUsefulIdShape(value)) continue;
|
|
2526
|
+
diagnostics.push(makeFinding(relativePath, lineNumber, HARDCODED_ID_FINDING));
|
|
2527
|
+
}
|
|
2528
|
+
return diagnostics;
|
|
2529
|
+
};
|
|
2530
|
+
const scanFileForConfigLiterals = (content, relativePath, ext) => {
|
|
2531
|
+
if (!SOURCE_EXTENSIONS.has(ext)) return [];
|
|
2532
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
2533
|
+
if (MIGRATION_PATH_RE$1.test(relativePath)) return [];
|
|
2534
|
+
return content.split("\n").flatMap((line, index) => scanLineForConfigLiterals(line, relativePath, ext, index + 1));
|
|
2535
|
+
};
|
|
2536
|
+
const detectHardcodedConfigLiterals = async (context) => {
|
|
2537
|
+
const diagnostics = [];
|
|
2538
|
+
for (const filePath of getSourceFiles(context)) {
|
|
2539
|
+
if (isAutoGenerated(filePath)) continue;
|
|
2540
|
+
let content;
|
|
2541
|
+
try {
|
|
2542
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
2543
|
+
} catch {
|
|
2544
|
+
continue;
|
|
2545
|
+
}
|
|
2546
|
+
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2547
|
+
const ext = path.extname(filePath);
|
|
2548
|
+
diagnostics.push(...scanFileForConfigLiterals(maskComments(content, ext), relativePath, ext));
|
|
2549
|
+
}
|
|
2550
|
+
return diagnostics;
|
|
2551
|
+
};
|
|
2552
|
+
|
|
2553
|
+
//#endregion
|
|
2554
|
+
//#region src/utils/suppress.ts
|
|
2555
|
+
const DIRECTIVE_RE = /(?:\/\/|\/\*|#|<!--|\*)\s*aislop-ignore-(next-line|line|file)\b([^\n]*)/;
|
|
2556
|
+
const isAislopDirectiveLine = (line) => DIRECTIVE_RE.test(line);
|
|
2557
|
+
|
|
2321
2558
|
//#endregion
|
|
2322
2559
|
//#region src/engines/ai-slop/comment-blocks.ts
|
|
2323
2560
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
@@ -2341,6 +2578,7 @@ const getCommentSyntax = (ext) => {
|
|
|
2341
2578
|
};
|
|
2342
2579
|
const getMatchedLinePrefix = (line, syntax) => {
|
|
2343
2580
|
const trimmed = line.trimStart();
|
|
2581
|
+
if (isAislopDirectiveLine(trimmed)) return null;
|
|
2344
2582
|
for (const prefix of syntax.linePrefixes) {
|
|
2345
2583
|
if (!trimmed.startsWith(prefix)) continue;
|
|
2346
2584
|
if (prefix === "#" && trimmed.startsWith("#!")) return null;
|
|
@@ -3081,7 +3319,7 @@ const detectRustPatterns = async (context) => {
|
|
|
3081
3319
|
|
|
3082
3320
|
//#endregion
|
|
3083
3321
|
//#region src/engines/ai-slop/silent-recovery.ts
|
|
3084
|
-
const JS_EXTS
|
|
3322
|
+
const JS_EXTS = new Set([
|
|
3085
3323
|
".ts",
|
|
3086
3324
|
".tsx",
|
|
3087
3325
|
".js",
|
|
@@ -3140,9 +3378,7 @@ const isLogOnlyBody = (body) => {
|
|
|
3140
3378
|
};
|
|
3141
3379
|
const detectJsSilentRecovery = (content, relPath) => {
|
|
3142
3380
|
const out = [];
|
|
3143
|
-
CATCH_HEAD_RE
|
|
3144
|
-
let match;
|
|
3145
|
-
while ((match = CATCH_HEAD_RE.exec(content)) !== null) {
|
|
3381
|
+
for (const match of content.matchAll(CATCH_HEAD_RE)) {
|
|
3146
3382
|
const body = extractCatchBody(content, match.index + match[0].length - 1);
|
|
3147
3383
|
if (body === null) continue;
|
|
3148
3384
|
if (!isLogOnlyBody(body)) continue;
|
|
@@ -3210,7 +3446,7 @@ const detectSilentRecovery = async (context) => {
|
|
|
3210
3446
|
for (const filePath of files) {
|
|
3211
3447
|
if (isAutoGenerated(filePath)) continue;
|
|
3212
3448
|
const ext = path.extname(filePath);
|
|
3213
|
-
const isJs = JS_EXTS
|
|
3449
|
+
const isJs = JS_EXTS.has(ext);
|
|
3214
3450
|
if (!isJs && !(ext === ".py")) continue;
|
|
3215
3451
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
3216
3452
|
if (isNonProductionPath(relPath)) continue;
|
|
@@ -3318,18 +3554,22 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3318
3554
|
}
|
|
3319
3555
|
continue;
|
|
3320
3556
|
}
|
|
3321
|
-
const importMatch = trimmed.match(/^import\s+(
|
|
3557
|
+
const importMatch = trimmed.match(/^import\s+(.+)/);
|
|
3322
3558
|
if (importMatch) {
|
|
3323
3559
|
importLines.add(i);
|
|
3324
|
-
const
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3560
|
+
for (const clause of importMatch[1].replace(/#.*$/, "").split(",")) {
|
|
3561
|
+
const clauseMatch = clause.trim().match(/^([\w.]+)(?:\s+as\s+(\w+))?/);
|
|
3562
|
+
if (!clauseMatch) continue;
|
|
3563
|
+
const alias = clauseMatch[2];
|
|
3564
|
+
if (alias && alias === clauseMatch[1]) continue;
|
|
3565
|
+
const simpleName = (alias ?? clauseMatch[1]).split(".")[0];
|
|
3566
|
+
if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
|
|
3567
|
+
name: simpleName,
|
|
3568
|
+
line: i + 1,
|
|
3569
|
+
isDefault: false,
|
|
3570
|
+
isNamespace: true
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3333
3573
|
}
|
|
3334
3574
|
}
|
|
3335
3575
|
return {
|
|
@@ -3339,8 +3579,7 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
3339
3579
|
};
|
|
3340
3580
|
const isSymbolUsed = (name, content, importLines, lines) => {
|
|
3341
3581
|
const pattern = new RegExp(`\\b${name}\\b`, "g");
|
|
3342
|
-
|
|
3343
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
3582
|
+
for (const match of content.matchAll(pattern)) {
|
|
3344
3583
|
const lineIndex = content.slice(0, match.index).split("\n").length - 1;
|
|
3345
3584
|
if (!importLines.has(lineIndex)) return true;
|
|
3346
3585
|
}
|
|
@@ -3441,6 +3680,18 @@ const aiSlopEngine = {
|
|
|
3441
3680
|
|
|
3442
3681
|
//#endregion
|
|
3443
3682
|
//#region src/engines/architecture/matchers.ts
|
|
3683
|
+
const REGEX_SPECIAL_CHARS = new Set([
|
|
3684
|
+
".",
|
|
3685
|
+
"+",
|
|
3686
|
+
"^",
|
|
3687
|
+
"$",
|
|
3688
|
+
"{",
|
|
3689
|
+
"}",
|
|
3690
|
+
"(",
|
|
3691
|
+
")",
|
|
3692
|
+
"|",
|
|
3693
|
+
"\\"
|
|
3694
|
+
]);
|
|
3444
3695
|
const minimatch = (filePath, pattern) => {
|
|
3445
3696
|
let regex = "";
|
|
3446
3697
|
let i = 0;
|
|
@@ -3465,7 +3716,7 @@ const minimatch = (filePath, pattern) => {
|
|
|
3465
3716
|
regex += pattern.slice(i, closeIndex + 1);
|
|
3466
3717
|
i = closeIndex + 1;
|
|
3467
3718
|
}
|
|
3468
|
-
} else if (
|
|
3719
|
+
} else if (REGEX_SPECIAL_CHARS.has(ch)) {
|
|
3469
3720
|
regex += `\\${ch}`;
|
|
3470
3721
|
i++;
|
|
3471
3722
|
} else {
|
|
@@ -3485,27 +3736,15 @@ const extractImports = (content, ext) => {
|
|
|
3485
3736
|
".mjs",
|
|
3486
3737
|
".cjs"
|
|
3487
3738
|
].includes(ext)) {
|
|
3488
|
-
const
|
|
3489
|
-
|
|
3490
|
-
while ((match = esPattern.exec(content)) !== null) imports.push(match[1]);
|
|
3491
|
-
const reqPattern = /require\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
3492
|
-
while ((match = reqPattern.exec(content)) !== null) imports.push(match[1]);
|
|
3493
|
-
}
|
|
3494
|
-
if (ext === ".py") {
|
|
3495
|
-
const pyPattern = /(?:from|import)\s+([\w.]+)/g;
|
|
3496
|
-
let match;
|
|
3497
|
-
while ((match = pyPattern.exec(content)) !== null) imports.push(match[1]);
|
|
3739
|
+
for (const match of content.matchAll(/(?:import|from)\s+["']([^"']+)["']/g)) imports.push(match[1]);
|
|
3740
|
+
for (const match of content.matchAll(/require\s*\(\s*["']([^"']+)["']\s*\)/g)) imports.push(match[1]);
|
|
3498
3741
|
}
|
|
3742
|
+
if (ext === ".py") for (const match of content.matchAll(/(?:from|import)\s+([\w.]+)/g)) imports.push(match[1]);
|
|
3499
3743
|
if (ext === ".go") {
|
|
3500
|
-
const
|
|
3501
|
-
|
|
3502
|
-
while ((match = goSingleImport.exec(content)) !== null) imports.push(match[1]);
|
|
3503
|
-
const goMultiImport = /import\s*\(([^)]*)\)/gs;
|
|
3504
|
-
while ((match = goMultiImport.exec(content)) !== null) {
|
|
3744
|
+
for (const match of content.matchAll(/^\s*import\s+"([^"]+)"/gm)) imports.push(match[1]);
|
|
3745
|
+
for (const match of content.matchAll(/import\s*\(([^)]*)\)/gs)) {
|
|
3505
3746
|
const block = match[1];
|
|
3506
|
-
const
|
|
3507
|
-
let pkgMatch;
|
|
3508
|
-
while ((pkgMatch = pkgPattern.exec(block)) !== null) imports.push(pkgMatch[1]);
|
|
3747
|
+
for (const pkgMatch of block.matchAll(/"([^"]+)"/g)) imports.push(pkgMatch[1]);
|
|
3509
3748
|
}
|
|
3510
3749
|
}
|
|
3511
3750
|
return imports;
|
|
@@ -3632,10 +3871,10 @@ const architectureEngine = {
|
|
|
3632
3871
|
//#endregion
|
|
3633
3872
|
//#region src/engines/code-quality/function-boundaries.ts
|
|
3634
3873
|
const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
|
|
3635
|
-
const ARROW_BLOCK_RE =
|
|
3636
|
-
const ARROW_END_RE =
|
|
3637
|
-
const BRACE_START_RE =
|
|
3638
|
-
const NEW_STATEMENT_RE =
|
|
3874
|
+
const ARROW_BLOCK_RE = /=>\s*\{/;
|
|
3875
|
+
const ARROW_END_RE = /=>\s*$/;
|
|
3876
|
+
const BRACE_START_RE = /^\s*\{/;
|
|
3877
|
+
const NEW_STATEMENT_RE = /^(?:export\s+)?(?:const|let|var|function|class)\s/;
|
|
3639
3878
|
const isControlFlowBrace = (lineText, braceIndex) => {
|
|
3640
3879
|
const before = lineText.substring(0, braceIndex).trimEnd();
|
|
3641
3880
|
if (before.endsWith(")")) return true;
|
|
@@ -3689,12 +3928,92 @@ const findBraceFunctionEnd = (lines, startIndex) => {
|
|
|
3689
3928
|
maxNesting
|
|
3690
3929
|
};
|
|
3691
3930
|
};
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3694
|
-
let
|
|
3931
|
+
const extractPythonSignature = (lines, startIndex) => {
|
|
3932
|
+
let depth = 0;
|
|
3933
|
+
let started = false;
|
|
3934
|
+
let params = "";
|
|
3935
|
+
for (let j = startIndex; j < lines.length; j++) {
|
|
3936
|
+
const l = lines[j];
|
|
3937
|
+
for (let ci = 0; ci < l.length; ci++) {
|
|
3938
|
+
const ch = l[ci];
|
|
3939
|
+
if (ch === "(") {
|
|
3940
|
+
depth++;
|
|
3941
|
+
if (depth === 1 && !started) {
|
|
3942
|
+
started = true;
|
|
3943
|
+
continue;
|
|
3944
|
+
}
|
|
3945
|
+
} else if (ch === ")") {
|
|
3946
|
+
depth--;
|
|
3947
|
+
if (depth === 0) return {
|
|
3948
|
+
params,
|
|
3949
|
+
sigEndIndex: j
|
|
3950
|
+
};
|
|
3951
|
+
}
|
|
3952
|
+
if (started) params += ch;
|
|
3953
|
+
}
|
|
3954
|
+
if (started) params += " ";
|
|
3955
|
+
}
|
|
3956
|
+
return {
|
|
3957
|
+
params,
|
|
3958
|
+
sigEndIndex: startIndex
|
|
3959
|
+
};
|
|
3960
|
+
};
|
|
3961
|
+
const countPythonParams = (signature) => {
|
|
3962
|
+
let depth = 0;
|
|
3963
|
+
const parts = [];
|
|
3964
|
+
let current = "";
|
|
3965
|
+
for (const ch of signature) {
|
|
3966
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
3967
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
3968
|
+
if (ch === "," && depth === 0) {
|
|
3969
|
+
parts.push(current);
|
|
3970
|
+
current = "";
|
|
3971
|
+
continue;
|
|
3972
|
+
}
|
|
3973
|
+
current += ch;
|
|
3974
|
+
}
|
|
3975
|
+
parts.push(current);
|
|
3976
|
+
let count = 0;
|
|
3977
|
+
for (const raw of parts) {
|
|
3978
|
+
const p = raw.trim();
|
|
3979
|
+
if (p.length === 0 || p === "*" || p === "/") continue;
|
|
3980
|
+
if (p.startsWith("*")) continue;
|
|
3981
|
+
if (p.includes("=")) continue;
|
|
3982
|
+
const name = p.split(":")[0].trim();
|
|
3983
|
+
if (name === "self" || name === "cls") continue;
|
|
3984
|
+
count++;
|
|
3985
|
+
}
|
|
3986
|
+
return count;
|
|
3987
|
+
};
|
|
3988
|
+
const countPythonBodyCodeLines = (lines, sigEndIndex, endLine) => {
|
|
3989
|
+
let count = 0;
|
|
3990
|
+
let inDoc = false;
|
|
3991
|
+
let delim = "";
|
|
3992
|
+
for (let j = sigEndIndex + 1; j <= endLine && j < lines.length; j++) {
|
|
3993
|
+
const t = lines[j].trim();
|
|
3994
|
+
if (inDoc) {
|
|
3995
|
+
if (t.includes(delim)) inDoc = false;
|
|
3996
|
+
continue;
|
|
3997
|
+
}
|
|
3998
|
+
if (t === "" || t.startsWith("#")) continue;
|
|
3999
|
+
const opener = t.startsWith("\"\"\"") ? "\"\"\"" : t.startsWith("'''") ? "'''" : "";
|
|
4000
|
+
if (opener) {
|
|
4001
|
+
if (!t.slice(3).includes(opener)) {
|
|
4002
|
+
inDoc = true;
|
|
4003
|
+
delim = opener;
|
|
4004
|
+
}
|
|
4005
|
+
continue;
|
|
4006
|
+
}
|
|
4007
|
+
count++;
|
|
4008
|
+
}
|
|
4009
|
+
return count;
|
|
4010
|
+
};
|
|
4011
|
+
const findPythonFunctionEnd = (lines, defIndex, bodyStartIndex) => {
|
|
4012
|
+
const baseIndent = lines[defIndex].match(/^(\s*)/)?.[1].length ?? 0;
|
|
4013
|
+
let endLine = bodyStartIndex;
|
|
3695
4014
|
let maxNesting = 0;
|
|
3696
4015
|
const controlIndentStack = [];
|
|
3697
|
-
for (let j =
|
|
4016
|
+
for (let j = bodyStartIndex + 1; j < lines.length; j++) {
|
|
3698
4017
|
const l = lines[j];
|
|
3699
4018
|
if (l.trim() === "") {
|
|
3700
4019
|
endLine = j;
|
|
@@ -3716,7 +4035,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
|
|
|
3716
4035
|
};
|
|
3717
4036
|
};
|
|
3718
4037
|
const findFunctionEnd = (lines, startIndex, isPython) => {
|
|
3719
|
-
if (isPython)
|
|
4038
|
+
if (isPython) {
|
|
4039
|
+
const { sigEndIndex } = extractPythonSignature(lines, startIndex);
|
|
4040
|
+
return findPythonFunctionEnd(lines, startIndex, sigEndIndex);
|
|
4041
|
+
}
|
|
3720
4042
|
return findBraceFunctionEnd(lines, startIndex);
|
|
3721
4043
|
};
|
|
3722
4044
|
const isBlockArrow = (lines, startIndex) => {
|
|
@@ -3738,14 +4060,14 @@ const countTemplateLines = (bodyLines) => {
|
|
|
3738
4060
|
let templateLineCount = 0;
|
|
3739
4061
|
for (const line of bodyLines) {
|
|
3740
4062
|
const startedInside = insideTemplate;
|
|
3741
|
-
let
|
|
4063
|
+
let escaped = false;
|
|
3742
4064
|
for (const ch of line) {
|
|
3743
|
-
if (
|
|
3744
|
-
|
|
4065
|
+
if (escaped) {
|
|
4066
|
+
escaped = false;
|
|
3745
4067
|
continue;
|
|
3746
4068
|
}
|
|
3747
4069
|
if (ch === "\\") {
|
|
3748
|
-
|
|
4070
|
+
escaped = true;
|
|
3749
4071
|
continue;
|
|
3750
4072
|
}
|
|
3751
4073
|
if (ch === "`") insideTemplate = !insideTemplate;
|
|
@@ -3781,7 +4103,7 @@ const FUNCTION_PATTERNS = [
|
|
|
3781
4103
|
]
|
|
3782
4104
|
},
|
|
3783
4105
|
{
|
|
3784
|
-
regex: /^\s*def\s+(\w+)\s*\(
|
|
4106
|
+
regex: /^\s*(?:async\s+)?def\s+(\w+)\s*\(/,
|
|
3785
4107
|
langFilter: [".py"]
|
|
3786
4108
|
},
|
|
3787
4109
|
{
|
|
@@ -3838,14 +4160,23 @@ const analyzeFunctions = (content, ext) => {
|
|
|
3838
4160
|
const isPython = fnMatch.patternIndex === 2;
|
|
3839
4161
|
if (fnMatch.patternIndex === 1 && !isBlockArrow(lines, i)) continue;
|
|
3840
4162
|
const { endLine, maxNesting } = findFunctionEnd(lines, i, isPython);
|
|
3841
|
-
|
|
3842
|
-
|
|
4163
|
+
let templateLines;
|
|
4164
|
+
let paramCount;
|
|
4165
|
+
if (isPython) {
|
|
4166
|
+
const sig = extractPythonSignature(lines, i);
|
|
4167
|
+
const codeLines = countPythonBodyCodeLines(lines, sig.sigEndIndex, endLine);
|
|
4168
|
+
templateLines = endLine - i + 1 - codeLines;
|
|
4169
|
+
paramCount = countPythonParams(sig.params);
|
|
4170
|
+
} else {
|
|
4171
|
+
templateLines = countTemplateLines(lines.slice(i + 1, endLine));
|
|
4172
|
+
paramCount = countParams(fnMatch.params);
|
|
4173
|
+
}
|
|
3843
4174
|
functions.push({
|
|
3844
4175
|
name: fnMatch.name,
|
|
3845
4176
|
startLine: i + 1,
|
|
3846
4177
|
lineCount: endLine - i + 1,
|
|
3847
4178
|
maxNesting,
|
|
3848
|
-
paramCount
|
|
4179
|
+
paramCount,
|
|
3849
4180
|
templateLines
|
|
3850
4181
|
});
|
|
3851
4182
|
}
|
|
@@ -4656,9 +4987,7 @@ const runRuffFormat = async (context) => {
|
|
|
4656
4987
|
};
|
|
4657
4988
|
const parseRuffFormatOutput = (output, rootDir) => {
|
|
4658
4989
|
const diagnostics = [];
|
|
4659
|
-
const
|
|
4660
|
-
let match;
|
|
4661
|
-
while ((match = filePattern.exec(output)) !== null) {
|
|
4990
|
+
for (const match of output.matchAll(/^--- (.+)$/gm)) {
|
|
4662
4991
|
const filePath = getRuffDiagnosticPath(rootDir, match[1]);
|
|
4663
4992
|
diagnostics.push({
|
|
4664
4993
|
filePath,
|
|
@@ -4685,10 +5014,10 @@ const formatEngine = {
|
|
|
4685
5014
|
const { languages, installedTools } = context;
|
|
4686
5015
|
const promises = [];
|
|
4687
5016
|
if (languages.includes("typescript") || languages.includes("javascript")) promises.push(runBiomeFormat(context));
|
|
4688
|
-
if (languages.includes("python") && installedTools
|
|
4689
|
-
if (languages.includes("go") && installedTools
|
|
4690
|
-
if (languages.includes("rust") && installedTools
|
|
4691
|
-
if (languages.includes("ruby") && installedTools
|
|
5017
|
+
if (languages.includes("python") && installedTools.ruff) promises.push(runRuffFormat(context));
|
|
5018
|
+
if (languages.includes("go") && installedTools.gofmt) promises.push(runGofmt(context));
|
|
5019
|
+
if (languages.includes("rust") && installedTools.rustfmt) promises.push(runGenericFormatter(context, "rust"));
|
|
5020
|
+
if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericFormatter(context, "ruby"));
|
|
4692
5021
|
if (languages.includes("php") && installedTools["php-cs-fixer"]) promises.push(runGenericFormatter(context, "php"));
|
|
4693
5022
|
const results = await Promise.allSettled(promises);
|
|
4694
5023
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
@@ -4892,6 +5221,8 @@ const createOxlintConfig = (options) => {
|
|
|
4892
5221
|
if (options.mode === "fix") {
|
|
4893
5222
|
rules["no-unused-vars"] = "off";
|
|
4894
5223
|
rules["react-hooks/exhaustive-deps"] = "off";
|
|
5224
|
+
rules["jsx-a11y/no-aria-hidden-on-focusable"] = "off";
|
|
5225
|
+
rules["unicorn/no-useless-fallback-in-spread"] = "off";
|
|
4895
5226
|
}
|
|
4896
5227
|
const plugins = [
|
|
4897
5228
|
"import",
|
|
@@ -5056,9 +5387,7 @@ const collectAmbientGlobals = (rootDir) => {
|
|
|
5056
5387
|
if (!relativePath.endsWith(".d.ts")) continue;
|
|
5057
5388
|
const content = readTextFile$1(path.join(rootDir, relativePath));
|
|
5058
5389
|
if (!content) continue;
|
|
5059
|
-
AMBIENT_GLOBAL_RE.
|
|
5060
|
-
let match;
|
|
5061
|
-
while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
|
|
5390
|
+
for (const match of content.matchAll(AMBIENT_GLOBAL_RE)) globals.add(match[1]);
|
|
5062
5391
|
}
|
|
5063
5392
|
const deps = collectPackageNames(rootDir);
|
|
5064
5393
|
if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
|
|
@@ -5274,10 +5603,10 @@ const lintEngine = {
|
|
|
5274
5603
|
if (context.config.lint.typecheck) promises.push(import("./typecheck-By967nny.js").then((mod) => mod.runTypecheck(context)));
|
|
5275
5604
|
}
|
|
5276
5605
|
if (context.frameworks.includes("expo")) promises.push(import("./expo-doctor-T4DswmX5.js").then((mod) => mod.runExpoDoctor(context)));
|
|
5277
|
-
if (languages.includes("python") && installedTools
|
|
5606
|
+
if (languages.includes("python") && installedTools.ruff) promises.push(runRuffLint(context));
|
|
5278
5607
|
if (languages.includes("go") && installedTools["golangci-lint"]) promises.push(runGolangciLint(context));
|
|
5279
|
-
if (languages.includes("rust") && installedTools
|
|
5280
|
-
if (languages.includes("ruby") && installedTools
|
|
5608
|
+
if (languages.includes("rust") && installedTools.cargo) promises.push(runGenericLinter(context, "rust"));
|
|
5609
|
+
if (languages.includes("ruby") && installedTools.rubocop) promises.push(runGenericLinter(context, "ruby"));
|
|
5281
5610
|
const results = await Promise.allSettled(promises);
|
|
5282
5611
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
5283
5612
|
return {
|
|
@@ -5307,7 +5636,7 @@ const runDependencyAudit = async (context) => {
|
|
|
5307
5636
|
else if (fs.existsSync(path.join(context.rootDirectory, "package-lock.json")) || fs.existsSync(path.join(context.rootDirectory, "package.json"))) promises.push(runNpmAudit(context.rootDirectory, timeout));
|
|
5308
5637
|
}
|
|
5309
5638
|
if (context.languages.includes("python") && context.installedTools["pip-audit"]) promises.push(runPipAudit(context.rootDirectory, timeout));
|
|
5310
|
-
if (context.languages.includes("go") && context.installedTools
|
|
5639
|
+
if (context.languages.includes("go") && context.installedTools.govulncheck) promises.push(runGovulncheck(context.rootDirectory, timeout));
|
|
5311
5640
|
if (context.languages.includes("rust")) promises.push(runCargoAudit(context.rootDirectory, timeout));
|
|
5312
5641
|
const results = await Promise.allSettled(promises);
|
|
5313
5642
|
for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
|
|
@@ -5412,9 +5741,12 @@ const parseLegacyAdvisories = (advisories, source) => {
|
|
|
5412
5741
|
for (const [key, advisory] of Object.entries(advisories)) upsertVuln(bucket, advisory.module_name ?? advisory.name ?? advisory.package ?? key, (advisory.severity ?? "moderate").toLowerCase(), advisory.recommendation ?? advisory.title ?? "");
|
|
5413
5742
|
return [...bucket.values()].map((agg) => aggregateToDiagnostic(agg, source));
|
|
5414
5743
|
};
|
|
5744
|
+
const carriesAdvisory = (vulnerability) => Array.isArray(vulnerability.via) && vulnerability.via.some((entry) => entry !== null && typeof entry === "object");
|
|
5415
5745
|
const parseModernVulnerabilities = (vulnerabilities, source) => {
|
|
5416
5746
|
const bucket = /* @__PURE__ */ new Map();
|
|
5747
|
+
const hasRootCauses = Object.values(vulnerabilities).some(carriesAdvisory);
|
|
5417
5748
|
for (const [packageName, vulnerability] of Object.entries(vulnerabilities)) {
|
|
5749
|
+
if (hasRootCauses && !carriesAdvisory(vulnerability)) continue;
|
|
5418
5750
|
const severity = (vulnerability.severity ?? "moderate").toLowerCase();
|
|
5419
5751
|
const fixAvailable = vulnerability.fixAvailable;
|
|
5420
5752
|
const isDirect = vulnerability.isDirect === true;
|
|
@@ -5557,212 +5889,6 @@ const runCargoAudit = async (rootDir, timeout) => {
|
|
|
5557
5889
|
}
|
|
5558
5890
|
};
|
|
5559
5891
|
|
|
5560
|
-
//#endregion
|
|
5561
|
-
//#region src/utils/source-masker.ts
|
|
5562
|
-
const JS_EXTS = new Set([
|
|
5563
|
-
".ts",
|
|
5564
|
-
".tsx",
|
|
5565
|
-
".js",
|
|
5566
|
-
".jsx",
|
|
5567
|
-
".mjs",
|
|
5568
|
-
".cjs"
|
|
5569
|
-
]);
|
|
5570
|
-
const PY_EXTS = new Set([".py"]);
|
|
5571
|
-
const RB_EXTS = new Set([".rb"]);
|
|
5572
|
-
const PHP_EXTS = new Set([".php"]);
|
|
5573
|
-
const familyForExt = (ext) => {
|
|
5574
|
-
if (JS_EXTS.has(ext)) return "js";
|
|
5575
|
-
if (PY_EXTS.has(ext)) return "py";
|
|
5576
|
-
if (RB_EXTS.has(ext)) return "rb";
|
|
5577
|
-
if (PHP_EXTS.has(ext)) return "php";
|
|
5578
|
-
return "none";
|
|
5579
|
-
};
|
|
5580
|
-
const maskStringsAndComments = (content, ext) => {
|
|
5581
|
-
const family = familyForExt(ext);
|
|
5582
|
-
if (family === "none") return content;
|
|
5583
|
-
if (family === "js") return maskJs(content);
|
|
5584
|
-
return maskSimple(content, family);
|
|
5585
|
-
};
|
|
5586
|
-
const handleQuotesAndComments = (content, i, tplStack, mask) => {
|
|
5587
|
-
const len = content.length;
|
|
5588
|
-
const c = content[i];
|
|
5589
|
-
const next = content[i + 1];
|
|
5590
|
-
if (c === "\"" || c === "'") {
|
|
5591
|
-
const strStart = i;
|
|
5592
|
-
const end = consumeQuotedString(content, i, c);
|
|
5593
|
-
mask(strStart + 1, end - 1);
|
|
5594
|
-
return {
|
|
5595
|
-
handled: true,
|
|
5596
|
-
nextI: end
|
|
5597
|
-
};
|
|
5598
|
-
}
|
|
5599
|
-
if (c === "`") {
|
|
5600
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
5601
|
-
mask(i + 1, scan.maskEnd);
|
|
5602
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
5603
|
-
return {
|
|
5604
|
-
handled: true,
|
|
5605
|
-
nextI: scan.resumeAt
|
|
5606
|
-
};
|
|
5607
|
-
}
|
|
5608
|
-
if (c === "/" && next === "/") {
|
|
5609
|
-
const strStart = i;
|
|
5610
|
-
let k = i;
|
|
5611
|
-
while (k < len && content[k] !== "\n") k++;
|
|
5612
|
-
mask(strStart, k);
|
|
5613
|
-
return {
|
|
5614
|
-
handled: true,
|
|
5615
|
-
nextI: k
|
|
5616
|
-
};
|
|
5617
|
-
}
|
|
5618
|
-
if (c === "/" && next === "*") {
|
|
5619
|
-
const strStart = i;
|
|
5620
|
-
let k = i + 2;
|
|
5621
|
-
while (k < len - 1 && !(content[k] === "*" && content[k + 1] === "/")) k++;
|
|
5622
|
-
if (k < len - 1) k += 2;
|
|
5623
|
-
mask(strStart, k);
|
|
5624
|
-
return {
|
|
5625
|
-
handled: true,
|
|
5626
|
-
nextI: k
|
|
5627
|
-
};
|
|
5628
|
-
}
|
|
5629
|
-
return {
|
|
5630
|
-
handled: false,
|
|
5631
|
-
nextI: i
|
|
5632
|
-
};
|
|
5633
|
-
};
|
|
5634
|
-
const maskJs = (content) => {
|
|
5635
|
-
const out = content.split("");
|
|
5636
|
-
const len = content.length;
|
|
5637
|
-
const tplStack = [];
|
|
5638
|
-
let i = 0;
|
|
5639
|
-
const mask = (start, end) => {
|
|
5640
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
5641
|
-
};
|
|
5642
|
-
while (i < len) {
|
|
5643
|
-
const c = content[i];
|
|
5644
|
-
if (tplStack.length > 0) {
|
|
5645
|
-
if (c === "{") {
|
|
5646
|
-
tplStack[tplStack.length - 1]++;
|
|
5647
|
-
i++;
|
|
5648
|
-
continue;
|
|
5649
|
-
}
|
|
5650
|
-
if (c === "}") {
|
|
5651
|
-
if (tplStack[tplStack.length - 1] === 0) {
|
|
5652
|
-
tplStack.pop();
|
|
5653
|
-
const scan = consumeTemplateString(content, i + 1);
|
|
5654
|
-
mask(i + 1, scan.maskEnd);
|
|
5655
|
-
if (scan.openedInterp) tplStack.push(0);
|
|
5656
|
-
i = scan.resumeAt;
|
|
5657
|
-
continue;
|
|
5658
|
-
}
|
|
5659
|
-
tplStack[tplStack.length - 1]--;
|
|
5660
|
-
i++;
|
|
5661
|
-
continue;
|
|
5662
|
-
}
|
|
5663
|
-
}
|
|
5664
|
-
const handled = handleQuotesAndComments(content, i, tplStack, mask);
|
|
5665
|
-
if (handled.handled) {
|
|
5666
|
-
i = handled.nextI;
|
|
5667
|
-
continue;
|
|
5668
|
-
}
|
|
5669
|
-
i++;
|
|
5670
|
-
}
|
|
5671
|
-
return out.join("");
|
|
5672
|
-
};
|
|
5673
|
-
const consumeQuotedString = (content, start, quote) => {
|
|
5674
|
-
const len = content.length;
|
|
5675
|
-
let i = start + 1;
|
|
5676
|
-
while (i < len) {
|
|
5677
|
-
const c = content[i];
|
|
5678
|
-
if (c === "\\" && i + 1 < len) {
|
|
5679
|
-
i += 2;
|
|
5680
|
-
continue;
|
|
5681
|
-
}
|
|
5682
|
-
if (c === quote) return i + 1;
|
|
5683
|
-
if (c === "\n") return i;
|
|
5684
|
-
i++;
|
|
5685
|
-
}
|
|
5686
|
-
return i;
|
|
5687
|
-
};
|
|
5688
|
-
const consumeTemplateString = (content, start) => {
|
|
5689
|
-
const len = content.length;
|
|
5690
|
-
let i = start;
|
|
5691
|
-
while (i < len) {
|
|
5692
|
-
const c = content[i];
|
|
5693
|
-
if (c === "\\" && i + 1 < len) {
|
|
5694
|
-
i += 2;
|
|
5695
|
-
continue;
|
|
5696
|
-
}
|
|
5697
|
-
if (c === "`") return {
|
|
5698
|
-
maskEnd: i,
|
|
5699
|
-
resumeAt: i + 1,
|
|
5700
|
-
openedInterp: false
|
|
5701
|
-
};
|
|
5702
|
-
if (c === "$" && content[i + 1] === "{") return {
|
|
5703
|
-
maskEnd: i,
|
|
5704
|
-
resumeAt: i + 2,
|
|
5705
|
-
openedInterp: true
|
|
5706
|
-
};
|
|
5707
|
-
i++;
|
|
5708
|
-
}
|
|
5709
|
-
return {
|
|
5710
|
-
maskEnd: i,
|
|
5711
|
-
resumeAt: i,
|
|
5712
|
-
openedInterp: false
|
|
5713
|
-
};
|
|
5714
|
-
};
|
|
5715
|
-
const maskSimple = (content, family) => {
|
|
5716
|
-
const out = content.split("");
|
|
5717
|
-
const len = content.length;
|
|
5718
|
-
let i = 0;
|
|
5719
|
-
const mask = (start, end) => {
|
|
5720
|
-
for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
|
|
5721
|
-
};
|
|
5722
|
-
while (i < len) {
|
|
5723
|
-
const c = content[i];
|
|
5724
|
-
const next = content[i + 1];
|
|
5725
|
-
if (family === "py" && (c === "\"" || c === "'")) {
|
|
5726
|
-
if (content[i + 1] === c && content[i + 2] === c) {
|
|
5727
|
-
const triple = c + c + c;
|
|
5728
|
-
const end = content.indexOf(triple, i + 3);
|
|
5729
|
-
const stop = end === -1 ? len : end + 3;
|
|
5730
|
-
mask(i + 3, stop - 3);
|
|
5731
|
-
i = stop;
|
|
5732
|
-
continue;
|
|
5733
|
-
}
|
|
5734
|
-
}
|
|
5735
|
-
if (c === "\"" || c === "'") {
|
|
5736
|
-
const strStart = i;
|
|
5737
|
-
i = consumeQuotedString(content, i, c);
|
|
5738
|
-
mask(strStart + 1, i - 1);
|
|
5739
|
-
continue;
|
|
5740
|
-
}
|
|
5741
|
-
if ((family === "py" || family === "rb" || family === "php") && c === "#") {
|
|
5742
|
-
const strStart = i;
|
|
5743
|
-
while (i < len && content[i] !== "\n") i++;
|
|
5744
|
-
mask(strStart, i);
|
|
5745
|
-
continue;
|
|
5746
|
-
}
|
|
5747
|
-
if (family === "php" && c === "/" && next === "/") {
|
|
5748
|
-
const strStart = i;
|
|
5749
|
-
while (i < len && content[i] !== "\n") i++;
|
|
5750
|
-
mask(strStart, i);
|
|
5751
|
-
continue;
|
|
5752
|
-
}
|
|
5753
|
-
if (family === "php" && c === "/" && next === "*") {
|
|
5754
|
-
const strStart = i;
|
|
5755
|
-
i += 2;
|
|
5756
|
-
while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
|
|
5757
|
-
if (i < len - 1) i += 2;
|
|
5758
|
-
mask(strStart, i);
|
|
5759
|
-
continue;
|
|
5760
|
-
}
|
|
5761
|
-
i++;
|
|
5762
|
-
}
|
|
5763
|
-
return out.join("");
|
|
5764
|
-
};
|
|
5765
|
-
|
|
5766
5892
|
//#endregion
|
|
5767
5893
|
//#region src/engines/security/risky.ts
|
|
5768
5894
|
const ev = "eval";
|
|
@@ -5908,8 +6034,7 @@ const detectRiskyConstructs = async (context) => {
|
|
|
5908
6034
|
if (!extensions.includes(ext)) continue;
|
|
5909
6035
|
if (isMigrationOrSeeder && name === "sql-injection") continue;
|
|
5910
6036
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
5911
|
-
|
|
5912
|
-
while ((match = regex.exec(masked)) !== null) {
|
|
6037
|
+
for (const match of masked.matchAll(regex)) {
|
|
5913
6038
|
const line = content.slice(0, match.index).split("\n").length;
|
|
5914
6039
|
if (name === "innerhtml") {
|
|
5915
6040
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
@@ -6037,11 +6162,11 @@ const scanSecrets = async (context) => {
|
|
|
6037
6162
|
} catch {
|
|
6038
6163
|
continue;
|
|
6039
6164
|
}
|
|
6165
|
+
content = maskComments(content, path.extname(filePath));
|
|
6040
6166
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
6041
6167
|
for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
|
|
6042
6168
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
6043
|
-
|
|
6044
|
-
while ((match = regex.exec(content)) !== null) {
|
|
6169
|
+
for (const match of content.matchAll(regex)) {
|
|
6045
6170
|
if (isPlaceholderValue(match[1] ?? match[0])) continue;
|
|
6046
6171
|
if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
|
|
6047
6172
|
const line = content.slice(0, match.index).split("\n").length;
|
|
@@ -6162,6 +6287,64 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
|
|
|
6162
6287
|
|
|
6163
6288
|
//#endregion
|
|
6164
6289
|
//#region src/utils/discover.ts
|
|
6290
|
+
const UNSUPPORTED_CODE_EXTENSIONS = {
|
|
6291
|
+
".c": "C/C++",
|
|
6292
|
+
".h": "C/C++",
|
|
6293
|
+
".cc": "C/C++",
|
|
6294
|
+
".cpp": "C/C++",
|
|
6295
|
+
".cxx": "C/C++",
|
|
6296
|
+
".hpp": "C/C++",
|
|
6297
|
+
".hh": "C/C++",
|
|
6298
|
+
".hxx": "C/C++",
|
|
6299
|
+
".cs": "C#",
|
|
6300
|
+
".swift": "Swift",
|
|
6301
|
+
".kt": "Kotlin",
|
|
6302
|
+
".kts": "Kotlin",
|
|
6303
|
+
".m": "Objective-C",
|
|
6304
|
+
".mm": "Objective-C",
|
|
6305
|
+
".scala": "Scala",
|
|
6306
|
+
".dart": "Dart",
|
|
6307
|
+
".ex": "Elixir",
|
|
6308
|
+
".exs": "Elixir",
|
|
6309
|
+
".erl": "Erlang",
|
|
6310
|
+
".hs": "Haskell",
|
|
6311
|
+
".clj": "Clojure",
|
|
6312
|
+
".cljs": "Clojure",
|
|
6313
|
+
".lua": "Lua",
|
|
6314
|
+
".jl": "Julia",
|
|
6315
|
+
".zig": "Zig",
|
|
6316
|
+
".nim": "Nim",
|
|
6317
|
+
".ml": "OCaml",
|
|
6318
|
+
".fs": "F#",
|
|
6319
|
+
".sol": "Solidity",
|
|
6320
|
+
".groovy": "Groovy"
|
|
6321
|
+
};
|
|
6322
|
+
const analyzeCoverage = (rootDirectory, excludePatterns = []) => {
|
|
6323
|
+
const allFiles = listProjectFiles(rootDirectory);
|
|
6324
|
+
const supportedFiles = filterProjectFiles(rootDirectory, allFiles, [], excludePatterns).length;
|
|
6325
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6326
|
+
let unsupportedFiles = 0;
|
|
6327
|
+
const candidates = filterProjectFiles(rootDirectory, allFiles, Object.keys(UNSUPPORTED_CODE_EXTENSIONS), excludePatterns);
|
|
6328
|
+
for (const file of candidates) {
|
|
6329
|
+
const lang = UNSUPPORTED_CODE_EXTENSIONS[path.extname(file).toLowerCase()];
|
|
6330
|
+
if (!lang) continue;
|
|
6331
|
+
unsupportedFiles += 1;
|
|
6332
|
+
counts.set(lang, (counts.get(lang) ?? 0) + 1);
|
|
6333
|
+
}
|
|
6334
|
+
let dominantUnsupported = null;
|
|
6335
|
+
let max = 0;
|
|
6336
|
+
for (const [lang, count] of counts) if (count > max) {
|
|
6337
|
+
max = count;
|
|
6338
|
+
dominantUnsupported = lang;
|
|
6339
|
+
}
|
|
6340
|
+
const negligible = supportedFiles === 0 || unsupportedFiles >= 10 && unsupportedFiles > supportedFiles * 3;
|
|
6341
|
+
return {
|
|
6342
|
+
supportedFiles,
|
|
6343
|
+
unsupportedFiles,
|
|
6344
|
+
dominantUnsupported,
|
|
6345
|
+
scoreable: !negligible
|
|
6346
|
+
};
|
|
6347
|
+
};
|
|
6165
6348
|
const LANGUAGE_SIGNALS = {
|
|
6166
6349
|
"tsconfig.json": "typescript",
|
|
6167
6350
|
"go.mod": "go",
|
|
@@ -6281,11 +6464,12 @@ const checkInstalledTools = async () => {
|
|
|
6281
6464
|
}));
|
|
6282
6465
|
return results;
|
|
6283
6466
|
};
|
|
6284
|
-
const discoverProject = async (directory) => {
|
|
6467
|
+
const discoverProject = async (directory, excludePatterns = []) => {
|
|
6285
6468
|
const resolvedDir = path.resolve(directory);
|
|
6286
6469
|
const languages = detectLanguages(resolvedDir);
|
|
6287
6470
|
const frameworks = detectFrameworks(resolvedDir);
|
|
6288
6471
|
const sourceFileCount = countSourceFiles(resolvedDir);
|
|
6472
|
+
const coverage = analyzeCoverage(resolvedDir, excludePatterns);
|
|
6289
6473
|
const installedTools = await checkInstalledTools();
|
|
6290
6474
|
return {
|
|
6291
6475
|
rootDirectory: resolvedDir,
|
|
@@ -6293,6 +6477,7 @@ const discoverProject = async (directory) => {
|
|
|
6293
6477
|
languages,
|
|
6294
6478
|
frameworks,
|
|
6295
6479
|
sourceFileCount,
|
|
6480
|
+
coverage,
|
|
6296
6481
|
installedTools
|
|
6297
6482
|
};
|
|
6298
6483
|
};
|
|
@@ -6512,10 +6697,6 @@ const handleAislopBaseline = (input) => {
|
|
|
6512
6697
|
};
|
|
6513
6698
|
};
|
|
6514
6699
|
|
|
6515
|
-
//#endregion
|
|
6516
|
-
//#region src/version.ts
|
|
6517
|
-
const APP_VERSION = "0.10.0";
|
|
6518
|
-
|
|
6519
6700
|
//#endregion
|
|
6520
6701
|
//#region src/telemetry/env.ts
|
|
6521
6702
|
const detectPackageManager = (env = process.env) => {
|
|
@@ -6710,9 +6891,14 @@ const track = (input) => {
|
|
|
6710
6891
|
pendingRequests.add(request);
|
|
6711
6892
|
return { installCreated };
|
|
6712
6893
|
};
|
|
6713
|
-
const flushTelemetry = async () => {
|
|
6894
|
+
const flushTelemetry = async (timeoutMs) => {
|
|
6714
6895
|
if (pendingRequests.size === 0) return;
|
|
6715
|
-
|
|
6896
|
+
const all = Promise.all(pendingRequests);
|
|
6897
|
+
if (timeoutMs == null) {
|
|
6898
|
+
await all;
|
|
6899
|
+
return;
|
|
6900
|
+
}
|
|
6901
|
+
await Promise.race([all, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
|
|
6716
6902
|
};
|
|
6717
6903
|
|
|
6718
6904
|
//#endregion
|