aislop 0.9.1 → 0.9.3
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 +674 -281
- package/dist/index.js +675 -282
- package/dist/{json-B_2_Zt7I.js → json-BhO1Ufj3.js} +1 -1
- package/dist/mcp.js +659 -271
- package/dist/{version-CBcgcofs.js → version-BNO_Lw7E.js} +1 -1
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -278,6 +278,7 @@ const EXCLUDED_DIRS = [
|
|
|
278
278
|
"build",
|
|
279
279
|
".git",
|
|
280
280
|
".agents",
|
|
281
|
+
".pnpm-store",
|
|
281
282
|
"vendor",
|
|
282
283
|
"examples",
|
|
283
284
|
"example",
|
|
@@ -314,6 +315,7 @@ const FIND_PRUNE_DIRS = [
|
|
|
314
315
|
"build",
|
|
315
316
|
".git",
|
|
316
317
|
".agents",
|
|
318
|
+
".pnpm-store",
|
|
317
319
|
"vendor",
|
|
318
320
|
"examples",
|
|
319
321
|
"example",
|
|
@@ -337,7 +339,11 @@ const FIND_PRUNE_DIRS = [
|
|
|
337
339
|
".turbo",
|
|
338
340
|
"public"
|
|
339
341
|
];
|
|
340
|
-
const BUILD_CACHE_FILE_PATTERNS = [
|
|
342
|
+
const BUILD_CACHE_FILE_PATTERNS = [
|
|
343
|
+
/\.timestamp-\d+-[a-z0-9]+\.[mc]?js$/i,
|
|
344
|
+
/\.min\.(?:js|css|mjs|cjs)$/i,
|
|
345
|
+
/\.bundle\.(?:js|css|mjs|cjs)$/i
|
|
346
|
+
];
|
|
341
347
|
const isBuildCacheFile = (filePath) => BUILD_CACHE_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
342
348
|
const TEST_FILE_PATTERNS = [
|
|
343
349
|
/(?:^|\/).*\.test\.[^/]+$/i,
|
|
@@ -365,6 +371,17 @@ const hasAllowedExtension = (filePath, extraExtensions) => {
|
|
|
365
371
|
const isExcludedPath = (filePath) => EXCLUDED_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`) || filePath.includes(`/${dir}/`));
|
|
366
372
|
const isExcludedFromScan = (relativePath) => isExcludedPath(relativePath) || isBuildCacheFile(relativePath);
|
|
367
373
|
const isTestFile$2 = (filePath) => TEST_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
374
|
+
const readBiomeExcludePatterns = (rootDirectory) => {
|
|
375
|
+
const biomePath = path.join(rootDirectory, "biome.json");
|
|
376
|
+
if (!fs.existsSync(biomePath)) return [];
|
|
377
|
+
try {
|
|
378
|
+
const includes = JSON.parse(fs.readFileSync(biomePath, "utf-8")).files?.includes;
|
|
379
|
+
if (!Array.isArray(includes)) return [];
|
|
380
|
+
return includes.filter((entry) => typeof entry === "string").filter((entry) => entry.startsWith("!") && entry.length > 1).map((entry) => entry.slice(1));
|
|
381
|
+
} catch {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
};
|
|
368
385
|
const getIgnoredPaths = (rootDirectory, files) => {
|
|
369
386
|
if (files.length === 0) return /* @__PURE__ */ new Set();
|
|
370
387
|
const result = spawnSync("git", [
|
|
@@ -432,7 +449,8 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
|
|
|
432
449
|
};
|
|
433
450
|
}).filter(({ relativePath }) => isWithinProject(relativePath));
|
|
434
451
|
const ignoredPaths = getIgnoredPaths(rootDirectory, normalizedFiles.map(({ relativePath }) => relativePath));
|
|
435
|
-
const
|
|
452
|
+
const excludePatterns = [...readBiomeExcludePatterns(rootDirectory), ...exclude];
|
|
453
|
+
const normalizedExcludePatterns = excludePatterns.length ? normalizeExcludePatterns(excludePatterns) : [];
|
|
436
454
|
const isUserExcluded = (relativePath) => {
|
|
437
455
|
if (!normalizedExcludePatterns.length) return false;
|
|
438
456
|
return micromatch.isMatch(relativePath, normalizedExcludePatterns, { dot: true });
|
|
@@ -443,7 +461,7 @@ const filterProjectFiles = (rootDirectory, files, extraExtensions = [], exclude
|
|
|
443
461
|
return micromatch.isMatch(relativePath, include, { dot: true });
|
|
444
462
|
};
|
|
445
463
|
return normalizedFiles.filter(({ absolutePath, relativePath }) => {
|
|
446
|
-
if (!fs.existsSync(absolutePath) || !isWithinProject(relativePath) || isExcludedPath(relativePath) || isTestFile$2(relativePath) || ignoredPaths.has(relativePath)) return false;
|
|
464
|
+
if (!fs.existsSync(absolutePath) || !isWithinProject(relativePath) || isExcludedPath(relativePath) || isTestFile$2(relativePath) || isBuildCacheFile(relativePath) || ignoredPaths.has(relativePath)) return false;
|
|
447
465
|
if (!isUserIncluded(relativePath)) return false;
|
|
448
466
|
if (isUserExcluded(relativePath)) return false;
|
|
449
467
|
return hasAllowedExtension(relativePath, extraSet);
|
|
@@ -487,10 +505,27 @@ const getSourceFilesWithExtras = (context, extraExtensions) => {
|
|
|
487
505
|
|
|
488
506
|
//#endregion
|
|
489
507
|
//#region src/engines/ai-slop/abstractions.ts
|
|
508
|
+
const JS_EXTS$1 = new Set([
|
|
509
|
+
".ts",
|
|
510
|
+
".tsx",
|
|
511
|
+
".js",
|
|
512
|
+
".jsx",
|
|
513
|
+
".mjs",
|
|
514
|
+
".cjs"
|
|
515
|
+
]);
|
|
490
516
|
const THIN_WRAPPER_PATTERNS = [
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
517
|
+
{
|
|
518
|
+
pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
|
|
519
|
+
extensions: JS_EXTS$1
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
|
|
523
|
+
extensions: JS_EXTS$1
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
|
|
527
|
+
extensions: new Set([".py"])
|
|
528
|
+
}
|
|
494
529
|
];
|
|
495
530
|
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+/];
|
|
496
531
|
const FRAMEWORK_METHOD_NAMES = /^(?:setUp|tearDown|setUpClass|tearDownClass|setUpModule|tearDownModule)$/;
|
|
@@ -505,10 +540,11 @@ const hasHardcodedArgs = (matchText) => {
|
|
|
505
540
|
return /['"`]\w+['"`]|(?<!\w)\d+(?!\w)/.test(innerCallMatch[1]);
|
|
506
541
|
};
|
|
507
542
|
const isUseContextWrapper = (matchText) => /\buse\w+/.test(matchText) && /useContext\s*\(/.test(matchText);
|
|
508
|
-
const detectThinWrappers = (content, relativePath) => {
|
|
543
|
+
const detectThinWrappers = (content, relativePath, ext) => {
|
|
509
544
|
const diagnostics = [];
|
|
510
545
|
const lines = content.split("\n");
|
|
511
|
-
for (const pattern of THIN_WRAPPER_PATTERNS) {
|
|
546
|
+
for (const { pattern, extensions } of THIN_WRAPPER_PATTERNS) {
|
|
547
|
+
if (!extensions.has(ext)) continue;
|
|
512
548
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
513
549
|
let match;
|
|
514
550
|
while ((match = regex.exec(content)) !== null) {
|
|
@@ -574,12 +610,133 @@ const detectOverAbstraction = async (context) => {
|
|
|
574
610
|
continue;
|
|
575
611
|
}
|
|
576
612
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
577
|
-
|
|
613
|
+
const ext = path.extname(filePath);
|
|
614
|
+
diagnostics.push(...detectThinWrappers(content, relativePath, ext));
|
|
578
615
|
diagnostics.push(...detectAiNaming(content, relativePath));
|
|
579
616
|
}
|
|
580
617
|
return diagnostics;
|
|
581
618
|
};
|
|
582
619
|
|
|
620
|
+
//#endregion
|
|
621
|
+
//#region src/engines/ai-slop/narrative-comments-patterns.ts
|
|
622
|
+
const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
|
|
623
|
+
const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
|
|
624
|
+
const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
|
|
625
|
+
const CROSS_REFERENCE_PHRASES = [
|
|
626
|
+
/\bwill then be\b/i,
|
|
627
|
+
/\bused by\b/i,
|
|
628
|
+
/\bcalled from\b/i,
|
|
629
|
+
/\bcalled later\b/i,
|
|
630
|
+
/\bsee (?:above|below|later|earlier)\b/i,
|
|
631
|
+
/\breplaces the\b/i,
|
|
632
|
+
/\bmatches the one\b/i,
|
|
633
|
+
/\bwe moved\b/i,
|
|
634
|
+
/\bwe used to\b/i,
|
|
635
|
+
/\brefactor(?:ed)? from\b/i,
|
|
636
|
+
/\bcombined with\b.*\bthis\b/i
|
|
637
|
+
];
|
|
638
|
+
const JUSTIFICATION_OPENERS = [
|
|
639
|
+
/^(The idea here|The trick is|This was needed|Originally,?)/i,
|
|
640
|
+
/^This\s+(?:function|method|class|module|component|hook|util|helper|handler|service)\b/i,
|
|
641
|
+
/^It\s+(?:does|handles|takes|returns|processes|reads|writes|sends|fetches|loads|creates|deletes|updates|parses|validates)\b/i,
|
|
642
|
+
/^(?:First|Then|Finally|Next|Lastly|Subsequently),?\s+(?:it|we|the\s+(?:function|method|class))\b/i
|
|
643
|
+
];
|
|
644
|
+
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
645
|
+
const STEP_COMMENT_VERB_RE = /^(?:Render|Enable|Disable|Initialize|Init|Setup|Set|Get|Fetch|Load|Save|Build|Create|Delete|Remove|Add|Update|Process|Execute|Run|Start|Stop|Clean|Cleanup|Configure|Validate|Check|Verify|Parse|Extract|Apply|Wait|Sleep|Skip|Allow|Deny|Lock|Unlock|Refresh|Reload|Reset|Clear|Send|Receive|Read|Write|Print|Log|Emit|Dispatch|Fire|Open|Close|Bind|Connect|Disconnect|Register|Unregister|Push|Pop|Insert|Append|Prepend|Sort|Filter|Find|Search|Replace|Encode|Decode|Convert|Transform|Map|Reduce|Iterate|Loop|Walk|Visit|Mark|Unmark|Toggle|Switch|Restart|Resume|Pause|Abort|Cancel|Compute|Calculate|Resolve|Reject|Ignore|Handle|Track|Trace|Increment|Decrement|Round|Truncate|Resize|Move|Copy|Clone|Merge|Split|Join|Wrap|Unwrap|Bump|Drain|Flush|Sync|Persist|Commit|Rollback|Yield|Return|Discard|Defer|Pin|Unpin|Mount|Unmount|Spawn|Kill|Restore)(?:\s|$)/;
|
|
646
|
+
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;
|
|
647
|
+
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
648
|
+
"deprecated",
|
|
649
|
+
"see",
|
|
650
|
+
"example",
|
|
651
|
+
"type",
|
|
652
|
+
"returns",
|
|
653
|
+
"return",
|
|
654
|
+
"param",
|
|
655
|
+
"throws",
|
|
656
|
+
"typedef",
|
|
657
|
+
"callback",
|
|
658
|
+
"override",
|
|
659
|
+
"template",
|
|
660
|
+
"internal",
|
|
661
|
+
"public",
|
|
662
|
+
"private",
|
|
663
|
+
"protected",
|
|
664
|
+
"experimental",
|
|
665
|
+
"alpha",
|
|
666
|
+
"beta",
|
|
667
|
+
"since",
|
|
668
|
+
"todo",
|
|
669
|
+
"link",
|
|
670
|
+
"license",
|
|
671
|
+
"preserve",
|
|
672
|
+
"swagger",
|
|
673
|
+
"openapi",
|
|
674
|
+
"route",
|
|
675
|
+
"group",
|
|
676
|
+
"summary",
|
|
677
|
+
"description",
|
|
678
|
+
"operationid",
|
|
679
|
+
"response",
|
|
680
|
+
"responses",
|
|
681
|
+
"request",
|
|
682
|
+
"requestbody",
|
|
683
|
+
"security",
|
|
684
|
+
"tag",
|
|
685
|
+
"tags",
|
|
686
|
+
"path",
|
|
687
|
+
"body",
|
|
688
|
+
"query",
|
|
689
|
+
"queryparam",
|
|
690
|
+
"header",
|
|
691
|
+
"headers",
|
|
692
|
+
"produces",
|
|
693
|
+
"accept",
|
|
694
|
+
"middleware",
|
|
695
|
+
"api",
|
|
696
|
+
"apiname",
|
|
697
|
+
"apidefine",
|
|
698
|
+
"apigroup",
|
|
699
|
+
"apiparam",
|
|
700
|
+
"apiquery",
|
|
701
|
+
"apibody",
|
|
702
|
+
"apiheader",
|
|
703
|
+
"apisuccess",
|
|
704
|
+
"apierror",
|
|
705
|
+
"apiexample",
|
|
706
|
+
"apiversion",
|
|
707
|
+
"apidescription",
|
|
708
|
+
"apipermission",
|
|
709
|
+
"apiuse",
|
|
710
|
+
"apiignore",
|
|
711
|
+
"apiprivate",
|
|
712
|
+
"namespace",
|
|
713
|
+
"category"
|
|
714
|
+
]);
|
|
715
|
+
const SUPPORTED_EXTS = new Set([
|
|
716
|
+
".ts",
|
|
717
|
+
".tsx",
|
|
718
|
+
".js",
|
|
719
|
+
".jsx",
|
|
720
|
+
".mjs",
|
|
721
|
+
".cjs",
|
|
722
|
+
".py",
|
|
723
|
+
".go",
|
|
724
|
+
".rs",
|
|
725
|
+
".rb",
|
|
726
|
+
".java",
|
|
727
|
+
".php"
|
|
728
|
+
]);
|
|
729
|
+
const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
|
|
730
|
+
const EXPORT_DEFAULT = /^\s*export\s+default\b/;
|
|
731
|
+
const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
|
|
732
|
+
const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
|
|
733
|
+
const GO_DECL_START = /^\s*(func|type|var|const|import)\b/;
|
|
734
|
+
const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
|
|
735
|
+
const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
|
|
736
|
+
const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
|
|
737
|
+
const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
|
|
738
|
+
const PHP_DECL_START = /^\s*(?:(?:public|private|protected|static|final|abstract|readonly)\s+)*(function|class|interface|trait|enum|const)\s+/;
|
|
739
|
+
|
|
583
740
|
//#endregion
|
|
584
741
|
//#region src/engines/ai-slop/non-production-paths.ts
|
|
585
742
|
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;
|
|
@@ -618,11 +775,37 @@ const isTrivialComment = (trimmed, nextLine) => {
|
|
|
618
775
|
if (/^-{3,}|─{3,}/.test(commentBody)) return false;
|
|
619
776
|
return (isJs ? TRIVIAL_JS_COMMENT_PATTERNS : TRIVIAL_PYTHON_COMMENT_PATTERNS).some((pattern) => pattern.test(trimmed));
|
|
620
777
|
};
|
|
621
|
-
const
|
|
778
|
+
const declStartForExt = (ext) => {
|
|
779
|
+
switch (ext) {
|
|
780
|
+
case ".rb": return [RUBY_DECL_START];
|
|
781
|
+
case ".java": return [JAVA_DECL_START, JAVA_DECL_START_FALLBACK];
|
|
782
|
+
case ".php": return [PHP_DECL_START];
|
|
783
|
+
default: return [];
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
const isCommentLineForExt = (line, ext) => {
|
|
787
|
+
const trimmed = line.trim();
|
|
788
|
+
if (ext === ".rb") return trimmed.startsWith("#") && !trimmed.startsWith("#!");
|
|
789
|
+
if (ext === ".java" || ext === ".php") return trimmed.startsWith("//") || trimmed.startsWith("#");
|
|
790
|
+
return false;
|
|
791
|
+
};
|
|
792
|
+
const isDocCommentForDeclaration = (lines, lineIdx, ext) => {
|
|
793
|
+
const patterns = declStartForExt(ext);
|
|
794
|
+
if (patterns.length === 0) return false;
|
|
795
|
+
for (let j = lineIdx + 1; j < lines.length; j++) {
|
|
796
|
+
const candidate = lines[j];
|
|
797
|
+
if (candidate.trim() === "") continue;
|
|
798
|
+
if (isCommentLineForExt(candidate, ext)) continue;
|
|
799
|
+
return patterns.some((re) => re.test(candidate));
|
|
800
|
+
}
|
|
801
|
+
return false;
|
|
802
|
+
};
|
|
803
|
+
const scanFileForTrivialComments = (content, relativePath, ext) => {
|
|
622
804
|
const diagnostics = [];
|
|
623
805
|
const lines = content.split("\n");
|
|
624
806
|
for (let i = 0; i < lines.length; i++) {
|
|
625
807
|
if (!isTrivialComment(lines[i].trim(), i + 1 < lines.length ? lines[i + 1] : void 0)) continue;
|
|
808
|
+
if (isDocCommentForDeclaration(lines, i, ext)) continue;
|
|
626
809
|
diagnostics.push({
|
|
627
810
|
filePath: relativePath,
|
|
628
811
|
engine: "ai-slop",
|
|
@@ -651,7 +834,7 @@ const detectTrivialComments = async (context) => {
|
|
|
651
834
|
} catch {
|
|
652
835
|
continue;
|
|
653
836
|
}
|
|
654
|
-
diagnostics.push(...scanFileForTrivialComments(content, relativePath));
|
|
837
|
+
diagnostics.push(...scanFileForTrivialComments(content, relativePath, path.extname(filePath)));
|
|
655
838
|
}
|
|
656
839
|
return diagnostics;
|
|
657
840
|
};
|
|
@@ -707,6 +890,19 @@ const TODO_PATTERN = new RegExp(`\\b(?:${[
|
|
|
707
890
|
"PLACEHOLDER",
|
|
708
891
|
"STUB"
|
|
709
892
|
].join("|")})[:\\s]`);
|
|
893
|
+
const isBlockCloserAfterReturn = (line) => line.startsWith("}") || line.startsWith("};") || line.startsWith("),") || line.startsWith(");") || line.startsWith("],") || line.startsWith("]);");
|
|
894
|
+
const isGuardedSingleLineExit = (lines, lineIndex) => {
|
|
895
|
+
const contextLines = [];
|
|
896
|
+
for (let i = lineIndex - 1; i >= 0 && contextLines.length < 16; i--) {
|
|
897
|
+
const trimmed = lines[i].trim();
|
|
898
|
+
if (!trimmed || trimmed.startsWith("//")) continue;
|
|
899
|
+
contextLines.unshift(trimmed);
|
|
900
|
+
if (/^(?:if|else\s+if|for|while)\b/.test(trimmed) || /^}\s*else\s+if\b/.test(trimmed)) break;
|
|
901
|
+
if (/;\s*$/.test(trimmed)) break;
|
|
902
|
+
}
|
|
903
|
+
const control = contextLines.join(" ");
|
|
904
|
+
return /(?:^|[}\s])(?:if|else\s+if|for|while)\s*\(/.test(control) && !/{\s*$/.test(control);
|
|
905
|
+
};
|
|
710
906
|
const detectTodoStubs = (content, relativePath) => {
|
|
711
907
|
const diagnostics = [];
|
|
712
908
|
const lines = content.split("\n");
|
|
@@ -723,7 +919,7 @@ const detectDeadCodePatterns = (content, relativePath, ext) => {
|
|
|
723
919
|
for (let i = 0; i < lines.length; i++) {
|
|
724
920
|
const trimmed = lines[i].trim();
|
|
725
921
|
const nextLine = i + 1 < lines.length ? lines[i + 1]?.trim() : void 0;
|
|
726
|
-
if (JS_EXTENSIONS$3.has(ext) && /^(?:return|throw)\b/.test(trimmed) && trimmed.endsWith(";") && nextLine && nextLine.length > 0 && !
|
|
922
|
+
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));
|
|
727
923
|
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));
|
|
728
924
|
if (JS_EXTENSIONS$3.has(ext) && /(?:function\s+\w+|=>\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));
|
|
729
925
|
}
|
|
@@ -891,6 +1087,25 @@ const SWALLOWED_EXCEPTION_PATTERNS = [
|
|
|
891
1087
|
message: "Empty catch block swallows errors silently"
|
|
892
1088
|
}
|
|
893
1089
|
];
|
|
1090
|
+
const INTENTIONAL_IGNORE_NAMES = new Set([
|
|
1091
|
+
"ignored",
|
|
1092
|
+
"ignore",
|
|
1093
|
+
"tolerated",
|
|
1094
|
+
"expected",
|
|
1095
|
+
"unused",
|
|
1096
|
+
"_",
|
|
1097
|
+
"_e",
|
|
1098
|
+
"_err",
|
|
1099
|
+
"_ex",
|
|
1100
|
+
"_t"
|
|
1101
|
+
]);
|
|
1102
|
+
const CATCH_PARAM_RE = /catch\s*\(\s*(?:\w+\s+)?([\w$]+)/;
|
|
1103
|
+
const RESCUE_PARAM_RE = /rescue(?:\s+[\w:]+)?\s*=>\s*([\w$]+)/;
|
|
1104
|
+
const isIntentionalIgnore = (matchText, ext) => {
|
|
1105
|
+
const m = (ext === ".rb" ? RESCUE_PARAM_RE : CATCH_PARAM_RE).exec(matchText);
|
|
1106
|
+
if (!m) return false;
|
|
1107
|
+
return INTENTIONAL_IGNORE_NAMES.has(m[1].toLowerCase());
|
|
1108
|
+
};
|
|
894
1109
|
const detectSwallowedExceptions = async (context) => {
|
|
895
1110
|
const files = getSourceFiles(context);
|
|
896
1111
|
const diagnostics = [];
|
|
@@ -909,6 +1124,7 @@ const detectSwallowedExceptions = async (context) => {
|
|
|
909
1124
|
let match;
|
|
910
1125
|
const regex = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
|
|
911
1126
|
while ((match = regex.exec(content)) !== null) {
|
|
1127
|
+
if (isIntentionalIgnore(match[0], ext)) continue;
|
|
912
1128
|
const line = content.slice(0, match.index).split("\n").length;
|
|
913
1129
|
diagnostics.push({
|
|
914
1130
|
filePath: relativePath,
|
|
@@ -1001,6 +1217,117 @@ const detectGoPatterns = async (context) => {
|
|
|
1001
1217
|
return diagnostics;
|
|
1002
1218
|
};
|
|
1003
1219
|
|
|
1220
|
+
//#endregion
|
|
1221
|
+
//#region src/engines/ai-slop/js-import-aliases.ts
|
|
1222
|
+
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
1223
|
+
const JS_RESOLUTION_EXTENSIONS = [
|
|
1224
|
+
"",
|
|
1225
|
+
".ts",
|
|
1226
|
+
".tsx",
|
|
1227
|
+
".js",
|
|
1228
|
+
".jsx",
|
|
1229
|
+
".mjs",
|
|
1230
|
+
".cjs",
|
|
1231
|
+
".json",
|
|
1232
|
+
"/index.ts",
|
|
1233
|
+
"/index.tsx",
|
|
1234
|
+
"/index.js",
|
|
1235
|
+
"/index.jsx"
|
|
1236
|
+
];
|
|
1237
|
+
const readJson$2 = (filePath) => {
|
|
1238
|
+
try {
|
|
1239
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1240
|
+
} catch {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
const buildAliasMatcher = (key) => {
|
|
1245
|
+
const starIdx = key.indexOf("*");
|
|
1246
|
+
if (starIdx === -1) return (spec) => spec === key;
|
|
1247
|
+
const before = key.slice(0, starIdx);
|
|
1248
|
+
const after = key.slice(starIdx + 1);
|
|
1249
|
+
return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
|
|
1250
|
+
};
|
|
1251
|
+
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
1252
|
+
const opts = readJson$2(configPath)?.compilerOptions;
|
|
1253
|
+
if (!opts || typeof opts !== "object") return;
|
|
1254
|
+
const configDir = path.dirname(configPath);
|
|
1255
|
+
const paths = opts.paths;
|
|
1256
|
+
if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
|
|
1257
|
+
const baseUrl = opts.baseUrl;
|
|
1258
|
+
if (typeof baseUrl === "string") {
|
|
1259
|
+
const baseDir = path.resolve(configDir, baseUrl);
|
|
1260
|
+
matchers.push((spec) => {
|
|
1261
|
+
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("@")) return false;
|
|
1262
|
+
return JS_RESOLUTION_EXTENSIONS.some((suffix) => fs.existsSync(path.join(baseDir, `${spec}${suffix}`)));
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
const collectTsPathAliases = (rootDir, workspaceDirs) => {
|
|
1267
|
+
const matchers = [];
|
|
1268
|
+
const dirs = [rootDir, ...workspaceDirs];
|
|
1269
|
+
for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
|
|
1270
|
+
return matchers;
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
//#endregion
|
|
1274
|
+
//#region src/engines/ai-slop/js-workspaces.ts
|
|
1275
|
+
const readJson$1 = (filePath) => {
|
|
1276
|
+
try {
|
|
1277
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1278
|
+
} catch {
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
1283
|
+
const globs = [];
|
|
1284
|
+
if (rootPkg && typeof rootPkg === "object") {
|
|
1285
|
+
const ws = rootPkg.workspaces;
|
|
1286
|
+
if (Array.isArray(ws)) {
|
|
1287
|
+
for (const g of ws) if (typeof g === "string") globs.push(g);
|
|
1288
|
+
} else if (ws && typeof ws === "object") {
|
|
1289
|
+
const pkgs = ws.packages;
|
|
1290
|
+
if (Array.isArray(pkgs)) {
|
|
1291
|
+
for (const g of pkgs) if (typeof g === "string") globs.push(g);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
const lerna = readJson$1(path.join(rootDir, "lerna.json"));
|
|
1296
|
+
if (lerna && Array.isArray(lerna.packages)) {
|
|
1297
|
+
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
1298
|
+
}
|
|
1299
|
+
try {
|
|
1300
|
+
const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
|
|
1301
|
+
let inPackages = false;
|
|
1302
|
+
for (const rawLine of pnpmWs.split("\n")) {
|
|
1303
|
+
if (/^packages\s*:\s*$/.test(rawLine)) {
|
|
1304
|
+
inPackages = true;
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
if (!inPackages) continue;
|
|
1308
|
+
if (/^\S/.test(rawLine)) break;
|
|
1309
|
+
const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
|
|
1310
|
+
if (m) globs.push(m[1].trim());
|
|
1311
|
+
}
|
|
1312
|
+
} catch {
|
|
1313
|
+
return globs;
|
|
1314
|
+
}
|
|
1315
|
+
return globs;
|
|
1316
|
+
};
|
|
1317
|
+
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
1318
|
+
const dirs = [];
|
|
1319
|
+
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
1320
|
+
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
1321
|
+
try {
|
|
1322
|
+
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
1323
|
+
} catch {
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
1327
|
+
return dirs;
|
|
1328
|
+
};
|
|
1329
|
+
const collectWorkspaceDirs = (rootDir, rootPkg) => expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, rootPkg));
|
|
1330
|
+
|
|
1004
1331
|
//#endregion
|
|
1005
1332
|
//#region src/engines/ai-slop/python-data.ts
|
|
1006
1333
|
const PYTHON_STDLIB = new Set([
|
|
@@ -1245,11 +1572,35 @@ const collectFromPipfile = (rootDir, pyDeps) => {
|
|
|
1245
1572
|
return false;
|
|
1246
1573
|
}
|
|
1247
1574
|
};
|
|
1575
|
+
const LOCAL_PACKAGE_ROOTS = [
|
|
1576
|
+
"",
|
|
1577
|
+
"src",
|
|
1578
|
+
"lib"
|
|
1579
|
+
];
|
|
1580
|
+
const collectLocalPythonPackages = (rootDir, pyDeps) => {
|
|
1581
|
+
for (const sub of LOCAL_PACKAGE_ROOTS) {
|
|
1582
|
+
const dir = sub ? path.join(rootDir, sub) : rootDir;
|
|
1583
|
+
let entries;
|
|
1584
|
+
try {
|
|
1585
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1586
|
+
} catch {
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
for (const entry of entries) {
|
|
1590
|
+
if (!entry.isDirectory()) continue;
|
|
1591
|
+
if (entry.name.startsWith(".")) continue;
|
|
1592
|
+
if (entry.name === "node_modules" || entry.name === "__pycache__") continue;
|
|
1593
|
+
const initPath = path.join(dir, entry.name, "__init__.py");
|
|
1594
|
+
if (fs.existsSync(initPath)) addPyDep(pyDeps, entry.name);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1248
1598
|
const collectPythonDeps = (rootDir) => {
|
|
1249
1599
|
const pyDeps = /* @__PURE__ */ new Set();
|
|
1250
1600
|
const hasReq = collectFromRequirementsTxt(rootDir, pyDeps);
|
|
1251
1601
|
const hasPyproject = collectFromPyproject(rootDir, pyDeps);
|
|
1252
1602
|
const hasPipfile = collectFromPipfile(rootDir, pyDeps);
|
|
1603
|
+
collectLocalPythonPackages(rootDir, pyDeps);
|
|
1253
1604
|
return {
|
|
1254
1605
|
pyDeps,
|
|
1255
1606
|
hasPyManifest: hasReq || hasPyproject || hasPipfile
|
|
@@ -1272,64 +1623,19 @@ const readJson = (filePath) => {
|
|
|
1272
1623
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1273
1624
|
} catch {
|
|
1274
1625
|
return null;
|
|
1275
|
-
}
|
|
1276
|
-
};
|
|
1277
|
-
const PKG_DEP_SECTIONS = [
|
|
1278
|
-
"dependencies",
|
|
1279
|
-
"devDependencies",
|
|
1280
|
-
"peerDependencies",
|
|
1281
|
-
"optionalDependencies"
|
|
1282
|
-
];
|
|
1283
|
-
const addDepsFromPkg = (pkg, jsDeps) => {
|
|
1284
|
-
for (const section of PKG_DEP_SECTIONS) {
|
|
1285
|
-
const deps = pkg[section];
|
|
1286
|
-
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
|
|
1287
|
-
}
|
|
1288
|
-
};
|
|
1289
|
-
const readWorkspaceGlobs = (rootDir, rootPkg) => {
|
|
1290
|
-
const globs = [];
|
|
1291
|
-
if (rootPkg && typeof rootPkg === "object") {
|
|
1292
|
-
const ws = rootPkg.workspaces;
|
|
1293
|
-
if (Array.isArray(ws)) {
|
|
1294
|
-
for (const g of ws) if (typeof g === "string") globs.push(g);
|
|
1295
|
-
} else if (ws && typeof ws === "object") {
|
|
1296
|
-
const pkgs = ws.packages;
|
|
1297
|
-
if (Array.isArray(pkgs)) {
|
|
1298
|
-
for (const g of pkgs) if (typeof g === "string") globs.push(g);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
const lerna = readJson(path.join(rootDir, "lerna.json"));
|
|
1303
|
-
if (lerna && Array.isArray(lerna.packages)) {
|
|
1304
|
-
for (const g of lerna.packages) if (typeof g === "string") globs.push(g);
|
|
1305
|
-
}
|
|
1306
|
-
try {
|
|
1307
|
-
const pnpmWs = fs.readFileSync(path.join(rootDir, "pnpm-workspace.yaml"), "utf-8");
|
|
1308
|
-
let inPackages = false;
|
|
1309
|
-
for (const rawLine of pnpmWs.split("\n")) {
|
|
1310
|
-
if (/^packages\s*:\s*$/.test(rawLine)) {
|
|
1311
|
-
inPackages = true;
|
|
1312
|
-
continue;
|
|
1313
|
-
}
|
|
1314
|
-
if (!inPackages) continue;
|
|
1315
|
-
if (/^\S/.test(rawLine)) break;
|
|
1316
|
-
const m = rawLine.match(/^\s*-\s*["']?([^"'\n]+?)["']?\s*$/);
|
|
1317
|
-
if (m) globs.push(m[1].trim());
|
|
1318
|
-
}
|
|
1319
|
-
} catch {}
|
|
1320
|
-
return globs;
|
|
1626
|
+
}
|
|
1321
1627
|
};
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1628
|
+
const PKG_DEP_SECTIONS = [
|
|
1629
|
+
"dependencies",
|
|
1630
|
+
"devDependencies",
|
|
1631
|
+
"peerDependencies",
|
|
1632
|
+
"optionalDependencies"
|
|
1633
|
+
];
|
|
1634
|
+
const addDepsFromPkg = (pkg, jsDeps) => {
|
|
1635
|
+
for (const section of PKG_DEP_SECTIONS) {
|
|
1636
|
+
const deps = pkg[section];
|
|
1637
|
+
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
|
|
1638
|
+
}
|
|
1333
1639
|
};
|
|
1334
1640
|
const SKIP_DIRS = new Set([
|
|
1335
1641
|
"node_modules",
|
|
@@ -1372,7 +1678,7 @@ const collectJsDeps = (rootDir, jsDeps) => {
|
|
|
1372
1678
|
if (!pkg || typeof pkg !== "object") return false;
|
|
1373
1679
|
addDepsFromPkg(pkg, jsDeps);
|
|
1374
1680
|
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
1375
|
-
const workspaceDirs =
|
|
1681
|
+
const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
|
|
1376
1682
|
for (const wsDir of workspaceDirs) {
|
|
1377
1683
|
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
1378
1684
|
if (!wsPkg) continue;
|
|
@@ -1382,43 +1688,6 @@ const collectJsDeps = (rootDir, jsDeps) => {
|
|
|
1382
1688
|
collectNestedManifests(rootDir, jsDeps);
|
|
1383
1689
|
return true;
|
|
1384
1690
|
};
|
|
1385
|
-
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
1386
|
-
const buildAliasMatcher = (key) => {
|
|
1387
|
-
const starIdx = key.indexOf("*");
|
|
1388
|
-
if (starIdx === -1) return (spec) => spec === key;
|
|
1389
|
-
const before = key.slice(0, starIdx);
|
|
1390
|
-
const after = key.slice(starIdx + 1);
|
|
1391
|
-
return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
|
|
1392
|
-
};
|
|
1393
|
-
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
1394
|
-
const opts = readJson(configPath)?.compilerOptions;
|
|
1395
|
-
if (!opts) return;
|
|
1396
|
-
const paths = opts.paths;
|
|
1397
|
-
if (paths && typeof paths === "object") for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
|
|
1398
|
-
const baseUrl = opts.baseUrl;
|
|
1399
|
-
if (typeof baseUrl === "string") {
|
|
1400
|
-
const baseUrlDir = path.resolve(path.dirname(configPath), baseUrl);
|
|
1401
|
-
let entries;
|
|
1402
|
-
try {
|
|
1403
|
-
entries = fs.readdirSync(baseUrlDir);
|
|
1404
|
-
} catch {
|
|
1405
|
-
return;
|
|
1406
|
-
}
|
|
1407
|
-
const baseSpecifiers = /* @__PURE__ */ new Set();
|
|
1408
|
-
for (const entry of entries) {
|
|
1409
|
-
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
1410
|
-
const base = entry.replace(/\.(?:[jt]sx?|mjs|cjs|d\.ts)$/i, "");
|
|
1411
|
-
if (base.length > 0) baseSpecifiers.add(base);
|
|
1412
|
-
}
|
|
1413
|
-
for (const name of baseSpecifiers) matchers.push((spec) => spec === name || spec.startsWith(`${name}/`));
|
|
1414
|
-
}
|
|
1415
|
-
};
|
|
1416
|
-
const collectTsPathAliases = (rootDir) => {
|
|
1417
|
-
const matchers = [];
|
|
1418
|
-
const dirs = [rootDir, ...expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, readJson(path.join(rootDir, "package.json"))))];
|
|
1419
|
-
for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
|
|
1420
|
-
return matchers;
|
|
1421
|
-
};
|
|
1422
1691
|
const loadManifest = (rootDir) => {
|
|
1423
1692
|
const jsDeps = /* @__PURE__ */ new Set();
|
|
1424
1693
|
const hasJsManifest = collectJsDeps(rootDir, jsDeps);
|
|
@@ -1440,14 +1709,19 @@ const VIRTUAL_MODULE_PREFIXES = [
|
|
|
1440
1709
|
"astro:",
|
|
1441
1710
|
"virtual:",
|
|
1442
1711
|
"bun:",
|
|
1443
|
-
"
|
|
1712
|
+
"file:"
|
|
1444
1713
|
];
|
|
1445
|
-
const isJsVirtualModule = (spec
|
|
1714
|
+
const isJsVirtualModule = (spec, manifest) => {
|
|
1715
|
+
if (VIRTUAL_MODULE_PREFIXES.some((p) => spec.startsWith(p))) return true;
|
|
1716
|
+
if (spec === "bun") return true;
|
|
1717
|
+
if (spec === "unfonts.css" && manifest.jsDeps.has("unplugin-fonts")) return true;
|
|
1718
|
+
if (spec.startsWith("~icons/") && manifest.jsDeps.has("unplugin-icons")) return true;
|
|
1719
|
+
return false;
|
|
1720
|
+
};
|
|
1446
1721
|
const stripImportQuery = (spec) => {
|
|
1447
1722
|
const idx = spec.indexOf("?");
|
|
1448
1723
|
return idx === -1 ? spec : spec.slice(0, idx);
|
|
1449
1724
|
};
|
|
1450
|
-
const VIRTUAL_ASSET_FILES = { "unfonts.css": "unplugin-fonts" };
|
|
1451
1725
|
const TEMPLATE_PLACEHOLDER_RE = /\$\{/;
|
|
1452
1726
|
const isLikelyRealImportSpec = (spec) => {
|
|
1453
1727
|
if (spec.length === 0) return false;
|
|
@@ -1519,9 +1793,7 @@ const checkJsImport = (rawSpec, manifest, tsAliasMatchers) => {
|
|
|
1519
1793
|
if (spec.length === 0) return null;
|
|
1520
1794
|
if (isJsRelativeOrAbsolute(spec)) return null;
|
|
1521
1795
|
if (isJsBuiltin(spec)) return null;
|
|
1522
|
-
if (isJsVirtualModule(spec)) return null;
|
|
1523
|
-
const virtualOwner = VIRTUAL_ASSET_FILES[spec];
|
|
1524
|
-
if (virtualOwner && manifest.jsDeps.has(virtualOwner)) return null;
|
|
1796
|
+
if (isJsVirtualModule(spec, manifest)) return null;
|
|
1525
1797
|
if (tsAliasMatchers.some((m) => m(spec))) return null;
|
|
1526
1798
|
const pkg = packageNameFromImport(spec);
|
|
1527
1799
|
if (manifest.jsDeps.has(pkg)) return null;
|
|
@@ -1541,9 +1813,11 @@ const checkPyImport = (spec, manifest) => {
|
|
|
1541
1813
|
return root;
|
|
1542
1814
|
};
|
|
1543
1815
|
const detectHallucinatedImports = async (context) => {
|
|
1816
|
+
const rootPkg = readJson(path.join(context.rootDirectory, "package.json"));
|
|
1817
|
+
const workspaceDirs = collectWorkspaceDirs(context.rootDirectory, rootPkg);
|
|
1544
1818
|
const manifest = loadManifest(context.rootDirectory);
|
|
1545
1819
|
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
1546
|
-
const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory) : [];
|
|
1820
|
+
const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory, workspaceDirs) : [];
|
|
1547
1821
|
const diagnostics = [];
|
|
1548
1822
|
const files = getSourceFiles(context);
|
|
1549
1823
|
for (const filePath of files) {
|
|
@@ -1584,121 +1858,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
1584
1858
|
};
|
|
1585
1859
|
|
|
1586
1860
|
//#endregion
|
|
1587
|
-
//#region src/engines/ai-slop/
|
|
1588
|
-
const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
|
|
1589
|
-
const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
|
|
1590
|
-
const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
|
|
1591
|
-
const CROSS_REFERENCE_PHRASES = [
|
|
1592
|
-
/\bwill then be\b/i,
|
|
1593
|
-
/\bused by\b/i,
|
|
1594
|
-
/\bcalled from\b/i,
|
|
1595
|
-
/\bcalled later\b/i,
|
|
1596
|
-
/\bsee (?:above|below|later|earlier)\b/i,
|
|
1597
|
-
/\breplaces the\b/i,
|
|
1598
|
-
/\bmatches the one\b/i,
|
|
1599
|
-
/\bwe moved\b/i,
|
|
1600
|
-
/\bwe used to\b/i,
|
|
1601
|
-
/\brefactor(?:ed)? from\b/i,
|
|
1602
|
-
/\bcombined with\b.*\bthis\b/i
|
|
1603
|
-
];
|
|
1604
|
-
const JUSTIFICATION_OPENERS = [/^(The idea here|The trick is|This was needed|Originally,?)/i];
|
|
1605
|
-
const EXPLANATORY_OPENERS = /^(Matches|Detects|Represents|Holds|Stores|Tracks|Handles|Manages|Controls|Contains|Captures|Encapsulates|Wraps|Describes)\s+[A-Za-z`'"]/;
|
|
1606
|
-
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:)\b/i;
|
|
1607
|
-
const MEANINGFUL_JSDOC_TAGS = new Set([
|
|
1608
|
-
"deprecated",
|
|
1609
|
-
"see",
|
|
1610
|
-
"example",
|
|
1611
|
-
"type",
|
|
1612
|
-
"returns",
|
|
1613
|
-
"return",
|
|
1614
|
-
"param",
|
|
1615
|
-
"throws",
|
|
1616
|
-
"typedef",
|
|
1617
|
-
"callback",
|
|
1618
|
-
"override",
|
|
1619
|
-
"template",
|
|
1620
|
-
"internal",
|
|
1621
|
-
"public",
|
|
1622
|
-
"private",
|
|
1623
|
-
"protected",
|
|
1624
|
-
"experimental",
|
|
1625
|
-
"alpha",
|
|
1626
|
-
"beta",
|
|
1627
|
-
"since",
|
|
1628
|
-
"todo",
|
|
1629
|
-
"link",
|
|
1630
|
-
"license",
|
|
1631
|
-
"preserve",
|
|
1632
|
-
"swagger",
|
|
1633
|
-
"openapi",
|
|
1634
|
-
"route",
|
|
1635
|
-
"group",
|
|
1636
|
-
"summary",
|
|
1637
|
-
"description",
|
|
1638
|
-
"operationid",
|
|
1639
|
-
"response",
|
|
1640
|
-
"responses",
|
|
1641
|
-
"request",
|
|
1642
|
-
"requestbody",
|
|
1643
|
-
"security",
|
|
1644
|
-
"tag",
|
|
1645
|
-
"tags",
|
|
1646
|
-
"path",
|
|
1647
|
-
"body",
|
|
1648
|
-
"query",
|
|
1649
|
-
"queryparam",
|
|
1650
|
-
"header",
|
|
1651
|
-
"headers",
|
|
1652
|
-
"produces",
|
|
1653
|
-
"accept",
|
|
1654
|
-
"middleware",
|
|
1655
|
-
"api",
|
|
1656
|
-
"apiname",
|
|
1657
|
-
"apidefine",
|
|
1658
|
-
"apigroup",
|
|
1659
|
-
"apiparam",
|
|
1660
|
-
"apiquery",
|
|
1661
|
-
"apibody",
|
|
1662
|
-
"apiheader",
|
|
1663
|
-
"apisuccess",
|
|
1664
|
-
"apierror",
|
|
1665
|
-
"apiexample",
|
|
1666
|
-
"apiversion",
|
|
1667
|
-
"apidescription",
|
|
1668
|
-
"apipermission",
|
|
1669
|
-
"apiuse",
|
|
1670
|
-
"apiignore",
|
|
1671
|
-
"apiprivate",
|
|
1672
|
-
"namespace",
|
|
1673
|
-
"category"
|
|
1674
|
-
]);
|
|
1675
|
-
const SUPPORTED_EXTS = new Set([
|
|
1676
|
-
".ts",
|
|
1677
|
-
".tsx",
|
|
1678
|
-
".js",
|
|
1679
|
-
".jsx",
|
|
1680
|
-
".mjs",
|
|
1681
|
-
".cjs",
|
|
1682
|
-
".py",
|
|
1683
|
-
".go",
|
|
1684
|
-
".rs",
|
|
1685
|
-
".rb",
|
|
1686
|
-
".java",
|
|
1687
|
-
".php"
|
|
1688
|
-
]);
|
|
1689
|
-
const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
|
|
1690
|
-
const EXPORT_DEFAULT = /^\s*export\s+default\b/;
|
|
1691
|
-
const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
|
|
1692
|
-
const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
|
|
1693
|
-
const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
|
|
1694
|
-
const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
|
|
1695
|
-
const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
|
|
1696
|
-
const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
|
|
1697
|
-
const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
|
|
1698
|
-
const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
|
|
1699
|
-
|
|
1700
|
-
//#endregion
|
|
1701
|
-
//#region src/engines/ai-slop/narrative-comments.ts
|
|
1861
|
+
//#region src/engines/ai-slop/comment-blocks.ts
|
|
1702
1862
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
1703
1863
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
1704
1864
|
const getCommentSyntax = (ext) => {
|
|
@@ -1798,6 +1958,9 @@ const collectBlocks = (sourceLines, syntax) => {
|
|
|
1798
1958
|
}
|
|
1799
1959
|
return blocks;
|
|
1800
1960
|
};
|
|
1961
|
+
|
|
1962
|
+
//#endregion
|
|
1963
|
+
//#region src/engines/ai-slop/narrative-comments.ts
|
|
1801
1964
|
const looksLikeDeclarationPreamble = (nextLine, ext) => {
|
|
1802
1965
|
if (nextLine === null) return false;
|
|
1803
1966
|
if (DECL_START.test(nextLine) || EXPORT_DEFAULT.test(nextLine)) return true;
|
|
@@ -1827,6 +1990,7 @@ const isBareSectionLabel = (prose) => {
|
|
|
1827
1990
|
if (!BARE_LABEL_RE.test(prose)) return false;
|
|
1828
1991
|
if (prose.endsWith(".")) return false;
|
|
1829
1992
|
if (prose.split(/\s+/).length > 3) return false;
|
|
1993
|
+
if (STEP_COMMENT_VERB_RE.test(prose)) return false;
|
|
1830
1994
|
return true;
|
|
1831
1995
|
};
|
|
1832
1996
|
const DATA_ENTRY_START = /^\s*(?:\{|\[|["'`]|\d|\w+:\s|case\s)/;
|
|
@@ -1841,15 +2005,45 @@ const nextLineLooksLikeDataEntry = (nextLine) => {
|
|
|
1841
2005
|
};
|
|
1842
2006
|
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));
|
|
1843
2007
|
const GO_DECL_NAME_RE = /^(?:func|type|var|const)\s+(?:\([^)]*\)\s*)?(\w+)/;
|
|
2008
|
+
const GO_FIELD_LEAD_RE = /^(\w+)\s+/;
|
|
2009
|
+
const GO_KEYWORDS = new Set([
|
|
2010
|
+
"return",
|
|
2011
|
+
"if",
|
|
2012
|
+
"for",
|
|
2013
|
+
"switch",
|
|
2014
|
+
"case",
|
|
2015
|
+
"default",
|
|
2016
|
+
"go",
|
|
2017
|
+
"select",
|
|
2018
|
+
"defer",
|
|
2019
|
+
"else",
|
|
2020
|
+
"break",
|
|
2021
|
+
"continue",
|
|
2022
|
+
"goto",
|
|
2023
|
+
"package",
|
|
2024
|
+
"import",
|
|
2025
|
+
"map",
|
|
2026
|
+
"chan",
|
|
2027
|
+
"range"
|
|
2028
|
+
]);
|
|
1844
2029
|
const looksLikeGoDocComment = (block, ext) => {
|
|
1845
2030
|
if (ext !== ".go" || block.kind !== "line") return false;
|
|
1846
2031
|
const next = block.nextNonBlankLine;
|
|
1847
2032
|
if (!next) return false;
|
|
1848
|
-
const
|
|
1849
|
-
|
|
1850
|
-
|
|
2033
|
+
const trimmedNext = next.trim();
|
|
2034
|
+
const firstWord = (block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "";
|
|
2035
|
+
const declMatch = GO_DECL_NAME_RE.exec(trimmedNext);
|
|
2036
|
+
if (declMatch && firstWord === declMatch[1]) return true;
|
|
2037
|
+
const fieldMatch = GO_FIELD_LEAD_RE.exec(trimmedNext);
|
|
2038
|
+
if (fieldMatch && !GO_KEYWORDS.has(fieldMatch[1]) && firstWord === fieldMatch[1]) return true;
|
|
2039
|
+
return false;
|
|
2040
|
+
};
|
|
2041
|
+
const RUBY_DOC_INDICATORS = /^\s*#\s*(?:#|@\w+|:[\w-]+:|=begin|=end)/;
|
|
2042
|
+
const looksLikeRubyDocBlock = (block, ext) => {
|
|
2043
|
+
if (ext !== ".rb" || block.kind !== "line") return false;
|
|
2044
|
+
return block.rawLines.some((line) => RUBY_DOC_INDICATORS.test(line));
|
|
1851
2045
|
};
|
|
1852
|
-
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)/i;
|
|
2046
|
+
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see|todo|fixme|hack|reason|deprecated|deprecation|migration|legacy|historical|context):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)|\b\w+\.\w+(?:\.\w+)+\b|\[[\w/.-]+\]/i;
|
|
1853
2047
|
const hasDocIndicator = (block) => {
|
|
1854
2048
|
const joined = block.prose.join(" ");
|
|
1855
2049
|
if (DOC_INDICATOR_RE.test(joined)) return true;
|
|
@@ -1885,6 +2079,10 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1885
2079
|
matched: false,
|
|
1886
2080
|
reason: ""
|
|
1887
2081
|
};
|
|
2082
|
+
if (looksLikeRubyDocBlock(block, ext)) return {
|
|
2083
|
+
matched: false,
|
|
2084
|
+
reason: ""
|
|
2085
|
+
};
|
|
1888
2086
|
if (block.kind === "line" && block.prose.some((l) => DECORATIVE_SEPARATOR.test(l) || DECORATIVE_SECTION_HEADER.test(l))) return {
|
|
1889
2087
|
matched: true,
|
|
1890
2088
|
reason: "decorative separator"
|
|
@@ -1893,17 +2091,17 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1893
2091
|
matched: true,
|
|
1894
2092
|
reason: "phase/section header"
|
|
1895
2093
|
};
|
|
1896
|
-
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine)) return {
|
|
2094
|
+
if (block.kind === "line" && block.prose.length === 1 && isBareSectionLabel(block.prose[0]) && !nextLineLooksLikeDataEntry(block.nextNonBlankLine) && !looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
1897
2095
|
matched: true,
|
|
1898
2096
|
reason: "bare section label"
|
|
1899
2097
|
};
|
|
1900
2098
|
const joined = block.prose.join(" ");
|
|
1901
2099
|
const hasWhyMarker = EXPLANATORY_WHY_MARKERS.test(joined);
|
|
1902
|
-
if (
|
|
2100
|
+
if (hasWhyMarker || hasDocIndicator(block)) return {
|
|
1903
2101
|
matched: false,
|
|
1904
2102
|
reason: ""
|
|
1905
2103
|
};
|
|
1906
|
-
if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
2104
|
+
if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext) && hasPreambleSlopSignal(block)) return {
|
|
1907
2105
|
matched: true,
|
|
1908
2106
|
reason: "multi-line preamble before declaration"
|
|
1909
2107
|
};
|
|
@@ -1926,11 +2124,18 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1926
2124
|
reason: "explanatory preamble"
|
|
1927
2125
|
};
|
|
1928
2126
|
const nonEmptyProseCount = block.prose.filter((l) => l.length > 0).length;
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2127
|
+
const isAboveDeclaration = looksLikeDeclarationPreamble(block.nextNonBlankLine, ext);
|
|
2128
|
+
if (nonEmptyProseCount >= 5) {
|
|
2129
|
+
if (isAboveDeclaration) return {
|
|
2130
|
+
matched: false,
|
|
2131
|
+
reason: ""
|
|
2132
|
+
};
|
|
2133
|
+
return {
|
|
2134
|
+
matched: true,
|
|
2135
|
+
reason: "long narrative block"
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
if (nonEmptyProseCount >= 3 && !hasWhyMarker && block.kind === "line" && !isAboveDeclaration) return {
|
|
1934
2139
|
matched: true,
|
|
1935
2140
|
reason: "multi-line narrative prose"
|
|
1936
2141
|
};
|
|
@@ -2162,7 +2367,23 @@ const isTestFile = (relPath) => {
|
|
|
2162
2367
|
if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
|
|
2163
2368
|
const basename = segments[segments.length - 1] ?? "";
|
|
2164
2369
|
if (TEST_BASENAMES.has(basename)) return true;
|
|
2165
|
-
return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
|
|
2370
|
+
return basename.endsWith("_tests.rs") || basename.endsWith("_test.rs") || basename.endsWith("_testutil.rs");
|
|
2371
|
+
};
|
|
2372
|
+
const buildBlockCommentRanges = (lines) => {
|
|
2373
|
+
const ranges = [];
|
|
2374
|
+
let openLine = -1;
|
|
2375
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2376
|
+
const line = lines[i];
|
|
2377
|
+
if (openLine === -1) {
|
|
2378
|
+
const openIdx = line.indexOf("/*");
|
|
2379
|
+
if (openIdx !== -1 && line.indexOf("*/", openIdx + 2) === -1) openLine = i;
|
|
2380
|
+
} else if (line.indexOf("*/") !== -1) {
|
|
2381
|
+
ranges.push([openLine, i]);
|
|
2382
|
+
openLine = -1;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (openLine !== -1) ranges.push([openLine, lines.length - 1]);
|
|
2386
|
+
return ranges;
|
|
2166
2387
|
};
|
|
2167
2388
|
const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
|
|
2168
2389
|
const UNWRAP_INTENT_LOOKBACK = 2;
|
|
@@ -2193,11 +2414,12 @@ const buildTestRanges = (lines) => {
|
|
|
2193
2414
|
return ranges;
|
|
2194
2415
|
};
|
|
2195
2416
|
const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
|
|
2196
|
-
const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
|
|
2417
|
+
const flagNonTestUnwrap = (lines, relPath, testRanges, blockCommentRanges, out) => {
|
|
2197
2418
|
for (let i = 0; i < lines.length; i++) {
|
|
2198
2419
|
const line = lines[i];
|
|
2199
2420
|
if (COMMENT_LINE_RE.test(line)) continue;
|
|
2200
2421
|
if (isInRange(testRanges, i)) continue;
|
|
2422
|
+
if (isInRange(blockCommentRanges, i)) continue;
|
|
2201
2423
|
if (!UNWRAP_CALL_RE.test(line)) continue;
|
|
2202
2424
|
if (WRITELN_UNWRAP_RE.test(line)) continue;
|
|
2203
2425
|
if (hasIntentComment(lines, i)) continue;
|
|
@@ -2254,7 +2476,7 @@ const detectRustPatterns = async (context) => {
|
|
|
2254
2476
|
flagTodoMacro(lines, relPath, diagnostics);
|
|
2255
2477
|
continue;
|
|
2256
2478
|
}
|
|
2257
|
-
flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
|
|
2479
|
+
flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), buildBlockCommentRanges(lines), diagnostics);
|
|
2258
2480
|
flagTodoMacro(lines, relPath, diagnostics);
|
|
2259
2481
|
}
|
|
2260
2482
|
return diagnostics;
|
|
@@ -2339,7 +2561,9 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
2339
2561
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
2340
2562
|
for (const item of cleaned.split(",")) {
|
|
2341
2563
|
const parts = item.trim().split(/\s+as\s+/);
|
|
2342
|
-
const
|
|
2564
|
+
const original = parts[0].trim();
|
|
2565
|
+
const localName = parts.length > 1 ? parts[1].trim() : original;
|
|
2566
|
+
if (parts.length > 1 && original === localName) continue;
|
|
2343
2567
|
if (localName && /^\w+$/.test(localName)) symbols.push({
|
|
2344
2568
|
name: localName,
|
|
2345
2569
|
line: i + 1,
|
|
@@ -2352,7 +2576,9 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
2352
2576
|
const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
|
|
2353
2577
|
if (importMatch) {
|
|
2354
2578
|
importLines.add(i);
|
|
2355
|
-
const
|
|
2579
|
+
const alias = importMatch[2];
|
|
2580
|
+
if (alias && alias === importMatch[1]) continue;
|
|
2581
|
+
const simpleName = (alias ?? importMatch[1]).split(".")[0];
|
|
2356
2582
|
if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
|
|
2357
2583
|
name: simpleName,
|
|
2358
2584
|
line: i + 1,
|
|
@@ -3008,9 +3234,17 @@ const isTrivialLine = (line) => {
|
|
|
3008
3234
|
if (trimmed.startsWith("/*") || trimmed.startsWith("*")) return true;
|
|
3009
3235
|
return false;
|
|
3010
3236
|
};
|
|
3237
|
+
const SVG_MARKUP_RE = /<\/?(?:svg|path|polyline|line|circle|rect|g)\b|(?:xmlns|viewBox|stroke(?:-width|-linecap|-linejoin)?|fill|fill-opacity|d|points|x1|x2|y1|y2)=/;
|
|
3238
|
+
const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
|
|
3011
3239
|
const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
3012
3240
|
const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
3013
3241
|
const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
|
|
3242
|
+
const isLowSignalMarkupWindow = (lines) => {
|
|
3243
|
+
return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
|
|
3244
|
+
};
|
|
3245
|
+
const isLowSignalDataWindow = (lines) => {
|
|
3246
|
+
return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= WINDOW_SIZE - 1;
|
|
3247
|
+
};
|
|
3014
3248
|
const findSuppressedLines = (lines) => {
|
|
3015
3249
|
const suppressed = /* @__PURE__ */ new Set();
|
|
3016
3250
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3037,6 +3271,8 @@ const collectMeaningfulLines = (content) => {
|
|
|
3037
3271
|
if (suppressed.has(i + 1)) continue;
|
|
3038
3272
|
const window = lines.slice(i, i + WINDOW_SIZE);
|
|
3039
3273
|
if (window.some((l) => !MEANINGFUL_LINE.test(l))) continue;
|
|
3274
|
+
if (isLowSignalMarkupWindow(window)) continue;
|
|
3275
|
+
if (isLowSignalDataWindow(window)) continue;
|
|
3040
3276
|
if (window.every(isTrivialLine)) continue;
|
|
3041
3277
|
const normalised = window.map(normaliseLine);
|
|
3042
3278
|
if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
|
|
@@ -3915,7 +4151,20 @@ const createOxlintConfig = (options) => {
|
|
|
3915
4151
|
...buildFrameworkPlugins(options.framework)
|
|
3916
4152
|
];
|
|
3917
4153
|
const globals = buildTestGlobals(options.testFramework ?? null);
|
|
3918
|
-
|
|
4154
|
+
for (const name of [
|
|
4155
|
+
"__DEV__",
|
|
4156
|
+
"__TEST__",
|
|
4157
|
+
"__BROWSER__",
|
|
4158
|
+
"__NODE__",
|
|
4159
|
+
"__GLOBAL__",
|
|
4160
|
+
"__SSR__",
|
|
4161
|
+
"__ESM_BROWSER__",
|
|
4162
|
+
"__ESM_BUNDLER__",
|
|
4163
|
+
"__VERSION__",
|
|
4164
|
+
"__COMMIT__",
|
|
4165
|
+
"__BUILD__"
|
|
4166
|
+
]) globals[name] = "readonly";
|
|
4167
|
+
for (const globalName of options.globals ?? []) globals[globalName] = "readonly";
|
|
3919
4168
|
if (options.framework === "astro") {
|
|
3920
4169
|
globals.Astro = "readonly";
|
|
3921
4170
|
rules["no-undef"] = "off";
|
|
@@ -3935,19 +4184,7 @@ const createOxlintConfig = (options) => {
|
|
|
3935
4184
|
};
|
|
3936
4185
|
|
|
3937
4186
|
//#endregion
|
|
3938
|
-
//#region src/engines/lint/oxlint.ts
|
|
3939
|
-
const esmRequire = createRequire(import.meta.url);
|
|
3940
|
-
const resolveOxlintBinary = () => {
|
|
3941
|
-
try {
|
|
3942
|
-
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
3943
|
-
const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
|
|
3944
|
-
return path.join(oxlintDir, "bin", "oxlint");
|
|
3945
|
-
} catch {
|
|
3946
|
-
return "oxlint";
|
|
3947
|
-
}
|
|
3948
|
-
};
|
|
3949
|
-
const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
|
|
3950
|
-
const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
|
|
4187
|
+
//#region src/engines/lint/oxlint-context-filters.ts
|
|
3951
4188
|
const AMBIENT_GLOBAL_DEPS = [
|
|
3952
4189
|
"unplugin-icons",
|
|
3953
4190
|
"@types/bun",
|
|
@@ -4009,6 +4246,9 @@ const isAmbientFalsePositive = (rule, message, sources) => {
|
|
|
4009
4246
|
return false;
|
|
4010
4247
|
};
|
|
4011
4248
|
const sstReferencedFiles = /* @__PURE__ */ new Map();
|
|
4249
|
+
const clearSstReferenceCache = () => {
|
|
4250
|
+
sstReferencedFiles.clear();
|
|
4251
|
+
};
|
|
4012
4252
|
const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
4013
4253
|
const cached = sstReferencedFiles.get(relativeFilePath);
|
|
4014
4254
|
if (cached !== void 0) return cached;
|
|
@@ -4029,12 +4269,114 @@ const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
|
4029
4269
|
sstReferencedFiles.set(relativeFilePath, referenced);
|
|
4030
4270
|
return referenced;
|
|
4031
4271
|
};
|
|
4272
|
+
|
|
4273
|
+
//#endregion
|
|
4274
|
+
//#region src/engines/lint/oxlint-globals.ts
|
|
4275
|
+
const readTextFile$1 = (filePath) => {
|
|
4276
|
+
try {
|
|
4277
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
4278
|
+
} catch {
|
|
4279
|
+
return null;
|
|
4280
|
+
}
|
|
4281
|
+
};
|
|
4282
|
+
const collectPackageNames = (dir) => {
|
|
4283
|
+
const names = /* @__PURE__ */ new Set();
|
|
4284
|
+
const raw = readTextFile$1(path.join(dir, "package.json"));
|
|
4285
|
+
if (!raw) return names;
|
|
4286
|
+
try {
|
|
4287
|
+
const pkg = JSON.parse(raw);
|
|
4288
|
+
for (const section of [
|
|
4289
|
+
"dependencies",
|
|
4290
|
+
"devDependencies",
|
|
4291
|
+
"peerDependencies",
|
|
4292
|
+
"optionalDependencies"
|
|
4293
|
+
]) {
|
|
4294
|
+
const deps = pkg[section];
|
|
4295
|
+
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
|
|
4296
|
+
}
|
|
4297
|
+
} catch {
|
|
4298
|
+
return names;
|
|
4299
|
+
}
|
|
4300
|
+
return names;
|
|
4301
|
+
};
|
|
4302
|
+
const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
4303
|
+
const collectAmbientGlobals = (rootDir) => {
|
|
4304
|
+
const globals = /* @__PURE__ */ new Set();
|
|
4305
|
+
const projectFiles = listProjectFiles(rootDir);
|
|
4306
|
+
for (const relativePath of projectFiles) {
|
|
4307
|
+
if (!relativePath.endsWith(".d.ts")) continue;
|
|
4308
|
+
const content = readTextFile$1(path.join(rootDir, relativePath));
|
|
4309
|
+
if (!content) continue;
|
|
4310
|
+
AMBIENT_GLOBAL_RE.lastIndex = 0;
|
|
4311
|
+
let match;
|
|
4312
|
+
while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
|
|
4313
|
+
}
|
|
4314
|
+
const deps = collectPackageNames(rootDir);
|
|
4315
|
+
if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
|
|
4316
|
+
if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
|
|
4317
|
+
"$app",
|
|
4318
|
+
"$config",
|
|
4319
|
+
"$dev",
|
|
4320
|
+
"$interpolate",
|
|
4321
|
+
"$resolve",
|
|
4322
|
+
"$jsonParse",
|
|
4323
|
+
"$jsonStringify",
|
|
4324
|
+
"aws",
|
|
4325
|
+
"cloudflare",
|
|
4326
|
+
"docker",
|
|
4327
|
+
"random",
|
|
4328
|
+
"sst",
|
|
4329
|
+
"vercel",
|
|
4330
|
+
"pulumi"
|
|
4331
|
+
]) globals.add(name);
|
|
4332
|
+
return [...globals];
|
|
4333
|
+
};
|
|
4334
|
+
|
|
4335
|
+
//#endregion
|
|
4336
|
+
//#region src/engines/lint/oxlint.ts
|
|
4337
|
+
const esmRequire = createRequire(import.meta.url);
|
|
4338
|
+
const OXLINT_EXTENSIONS = new Set([
|
|
4339
|
+
".ts",
|
|
4340
|
+
".tsx",
|
|
4341
|
+
".js",
|
|
4342
|
+
".jsx",
|
|
4343
|
+
".mjs",
|
|
4344
|
+
".cjs"
|
|
4345
|
+
]);
|
|
4346
|
+
const resolveOxlintBinary = () => {
|
|
4347
|
+
try {
|
|
4348
|
+
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
4349
|
+
const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
|
|
4350
|
+
return path.join(oxlintDir, "bin", "oxlint");
|
|
4351
|
+
} catch {
|
|
4352
|
+
return "oxlint";
|
|
4353
|
+
}
|
|
4354
|
+
};
|
|
4355
|
+
const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
|
|
4356
|
+
const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
|
|
4032
4357
|
const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
|
|
4033
4358
|
const isUnderscoreUnusedVar = (rule, message) => {
|
|
4034
4359
|
if (rule !== "eslint/no-unused-vars") return false;
|
|
4035
4360
|
const match = UNUSED_VAR_IDENT_RE.exec(message);
|
|
4036
4361
|
return match ? match[1].startsWith("_") : false;
|
|
4037
4362
|
};
|
|
4363
|
+
const readTextFile = (filePath) => {
|
|
4364
|
+
try {
|
|
4365
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
4366
|
+
} catch {
|
|
4367
|
+
return null;
|
|
4368
|
+
}
|
|
4369
|
+
};
|
|
4370
|
+
const isSolidRefFalsePositive = (context, diagnostic) => {
|
|
4371
|
+
if (diagnostic.rule !== "eslint/no-unassigned-vars") return false;
|
|
4372
|
+
const name = diagnostic.message.match(/^'([^']+)' is always 'undefined'/)?.[1];
|
|
4373
|
+
if (!name) return false;
|
|
4374
|
+
const content = readTextFile(path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath : path.join(context.rootDirectory, diagnostic.filePath));
|
|
4375
|
+
if (!content) return false;
|
|
4376
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4377
|
+
return new RegExp(`\\bref=\\{\\s*${escaped}\\s*\\}`).test(content);
|
|
4378
|
+
};
|
|
4379
|
+
const isContextualTypeScriptFalsePositive = (diagnostic) => diagnostic.rule === "typescript-eslint/triple-slash-reference" && (diagnostic.filePath.endsWith(".d.ts") || /(?:^|\/)sst\.config\.ts$/.test(diagnostic.filePath));
|
|
4038
4380
|
const parseRuleCode = (code) => {
|
|
4039
4381
|
if (!code) return {
|
|
4040
4382
|
plugin: "eslint",
|
|
@@ -4067,14 +4409,20 @@ const detectTestFramework = (rootDir) => {
|
|
|
4067
4409
|
} catch {}
|
|
4068
4410
|
return null;
|
|
4069
4411
|
};
|
|
4412
|
+
const getOxlintTargets = (context) => getSourceFiles(context).filter((filePath) => OXLINT_EXTENSIONS.has(path.extname(filePath).toLowerCase())).filter((filePath) => !isAutoGenerated(filePath)).map((filePath) => path.relative(context.rootDirectory, filePath).split(path.sep).join("/"));
|
|
4070
4413
|
const runOxlint = async (context) => {
|
|
4071
4414
|
const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
|
|
4415
|
+
const framework = context.frameworks.find((f) => f !== "none");
|
|
4416
|
+
const testFramework = detectTestFramework(context.rootDirectory);
|
|
4417
|
+
const targets = getOxlintTargets(context);
|
|
4418
|
+
if (targets.length === 0) return [];
|
|
4072
4419
|
const config = createOxlintConfig({
|
|
4073
|
-
framework
|
|
4074
|
-
testFramework
|
|
4420
|
+
framework,
|
|
4421
|
+
testFramework,
|
|
4422
|
+
globals: collectAmbientGlobals(context.rootDirectory)
|
|
4075
4423
|
});
|
|
4076
4424
|
const ambientSources = detectAmbientSources(context.rootDirectory);
|
|
4077
|
-
|
|
4425
|
+
clearSstReferenceCache();
|
|
4078
4426
|
try {
|
|
4079
4427
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
4080
4428
|
const args = [
|
|
@@ -4085,7 +4433,7 @@ const runOxlint = async (context) => {
|
|
|
4085
4433
|
"json"
|
|
4086
4434
|
];
|
|
4087
4435
|
if (context.languages.includes("typescript") && fs.existsSync(path.join(context.rootDirectory, "tsconfig.json"))) args.push("--tsconfig", "./tsconfig.json");
|
|
4088
|
-
args.push(
|
|
4436
|
+
args.push(...targets);
|
|
4089
4437
|
const result = await runSubprocess(process.execPath, args, {
|
|
4090
4438
|
cwd: context.rootDirectory,
|
|
4091
4439
|
timeout: 12e4
|
|
@@ -4117,6 +4465,8 @@ const runOxlint = async (context) => {
|
|
|
4117
4465
|
if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
|
|
4118
4466
|
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
4119
4467
|
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
4468
|
+
if (isSolidRefFalsePositive(context, d)) return false;
|
|
4469
|
+
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
4120
4470
|
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
4121
4471
|
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
4122
4472
|
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
@@ -4672,7 +5022,7 @@ const DB_RECEIVER = "(?:db|database|knex|client|connection|conn|pool|sql|prisma|
|
|
|
4672
5022
|
const DB_METHOD = "(?:query|execute|exec|raw|\\$queryRaw|\\$queryRawUnsafe|\\$executeRaw|\\$executeRawUnsafe)";
|
|
4673
5023
|
const RISKY_PATTERNS = [
|
|
4674
5024
|
{
|
|
4675
|
-
pattern: new RegExp(
|
|
5025
|
+
pattern: new RegExp(`(?<![\\w.>:\\\\])\\b${ev}\\s*\\(`, "g"),
|
|
4676
5026
|
extensions: [
|
|
4677
5027
|
".ts",
|
|
4678
5028
|
".tsx",
|
|
@@ -4779,6 +5129,16 @@ const RISKY_PATTERNS = [
|
|
|
4779
5129
|
help: "Use parameterized queries or an ORM instead of string concatenation"
|
|
4780
5130
|
}
|
|
4781
5131
|
];
|
|
5132
|
+
const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
|
|
5133
|
+
const start = Math.max(0, lineIndex - 2);
|
|
5134
|
+
return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
|
|
5135
|
+
};
|
|
5136
|
+
const isStructuredDataScript = (content, matchIndex) => {
|
|
5137
|
+
const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
|
|
5138
|
+
if (/type=["']application\/ld\+json["']/.test(before)) return true;
|
|
5139
|
+
const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
|
|
5140
|
+
return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
|
|
5141
|
+
};
|
|
4782
5142
|
const detectRiskyConstructs = async (context) => {
|
|
4783
5143
|
const files = getSourceFiles(context);
|
|
4784
5144
|
const diagnostics = [];
|
|
@@ -4794,6 +5154,7 @@ const detectRiskyConstructs = async (context) => {
|
|
|
4794
5154
|
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
4795
5155
|
const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
|
|
4796
5156
|
const masked = maskStringsAndComments(content, ext);
|
|
5157
|
+
const lines = content.split("\n");
|
|
4797
5158
|
for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
|
|
4798
5159
|
if (!extensions.includes(ext)) continue;
|
|
4799
5160
|
if (isMigrationOrSeeder && name === "sql-injection") continue;
|
|
@@ -4805,6 +5166,10 @@ const detectRiskyConstructs = async (context) => {
|
|
|
4805
5166
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
4806
5167
|
if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
|
|
4807
5168
|
}
|
|
5169
|
+
if (name === "dangerously-set-innerhtml") {
|
|
5170
|
+
if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
|
|
5171
|
+
if (isStructuredDataScript(content, match.index)) continue;
|
|
5172
|
+
}
|
|
4808
5173
|
diagnostics.push({
|
|
4809
5174
|
filePath: relativePath,
|
|
4810
5175
|
engine: "security",
|
|
@@ -4828,7 +5193,8 @@ const detectRiskyConstructs = async (context) => {
|
|
|
4828
5193
|
const SECRET_PATTERNS = [
|
|
4829
5194
|
{
|
|
4830
5195
|
pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
|
|
4831
|
-
name: "API key"
|
|
5196
|
+
name: "API key",
|
|
5197
|
+
keywordPrefixed: true
|
|
4832
5198
|
},
|
|
4833
5199
|
{
|
|
4834
5200
|
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
@@ -4836,11 +5202,13 @@ const SECRET_PATTERNS = [
|
|
|
4836
5202
|
},
|
|
4837
5203
|
{
|
|
4838
5204
|
pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
|
|
4839
|
-
name: "AWS Secret Key"
|
|
5205
|
+
name: "AWS Secret Key",
|
|
5206
|
+
keywordPrefixed: true
|
|
4840
5207
|
},
|
|
4841
5208
|
{
|
|
4842
5209
|
pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
|
|
4843
|
-
name: "Hardcoded password/secret"
|
|
5210
|
+
name: "Hardcoded password/secret",
|
|
5211
|
+
keywordPrefixed: true
|
|
4844
5212
|
},
|
|
4845
5213
|
{
|
|
4846
5214
|
pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
|
|
@@ -4852,7 +5220,8 @@ const SECRET_PATTERNS = [
|
|
|
4852
5220
|
},
|
|
4853
5221
|
{
|
|
4854
5222
|
pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
|
|
4855
|
-
name: "Authentication token"
|
|
5223
|
+
name: "Authentication token",
|
|
5224
|
+
keywordPrefixed: true
|
|
4856
5225
|
},
|
|
4857
5226
|
{
|
|
4858
5227
|
pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
|
|
@@ -4867,6 +5236,24 @@ const SECRET_PATTERNS = [
|
|
|
4867
5236
|
name: "Database connection string with credentials"
|
|
4868
5237
|
}
|
|
4869
5238
|
];
|
|
5239
|
+
const isInsideStringLiteral = (content, matchIndex) => {
|
|
5240
|
+
const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
|
|
5241
|
+
const prefix = content.slice(lineStart, matchIndex);
|
|
5242
|
+
let inDouble = false;
|
|
5243
|
+
let inSingle = false;
|
|
5244
|
+
let inBacktick = false;
|
|
5245
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
5246
|
+
const ch = prefix[i];
|
|
5247
|
+
if (ch === "\\") {
|
|
5248
|
+
i++;
|
|
5249
|
+
continue;
|
|
5250
|
+
}
|
|
5251
|
+
if (ch === "\"" && !inSingle && !inBacktick) inDouble = !inDouble;
|
|
5252
|
+
else if (ch === "'" && !inDouble && !inBacktick) inSingle = !inSingle;
|
|
5253
|
+
else if (ch === "`" && !inDouble && !inSingle) inBacktick = !inBacktick;
|
|
5254
|
+
}
|
|
5255
|
+
return inDouble || inSingle || inBacktick;
|
|
5256
|
+
};
|
|
4870
5257
|
const PLACEHOLDER_EXACT = new Set([
|
|
4871
5258
|
"changeme",
|
|
4872
5259
|
"password",
|
|
@@ -4902,11 +5289,12 @@ const scanSecrets = async (context) => {
|
|
|
4902
5289
|
continue;
|
|
4903
5290
|
}
|
|
4904
5291
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
4905
|
-
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
5292
|
+
for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
|
|
4906
5293
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
4907
5294
|
let match;
|
|
4908
5295
|
while ((match = regex.exec(content)) !== null) {
|
|
4909
5296
|
if (isPlaceholderValue(match[1] ?? match[0])) continue;
|
|
5297
|
+
if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
|
|
4910
5298
|
const line = content.slice(0, match.index).split("\n").length;
|
|
4911
5299
|
diagnostics.push({
|
|
4912
5300
|
filePath: relativePath,
|
|
@@ -5369,7 +5757,7 @@ const handleAislopBaseline = (input) => {
|
|
|
5369
5757
|
|
|
5370
5758
|
//#endregion
|
|
5371
5759
|
//#region src/version.ts
|
|
5372
|
-
const APP_VERSION = "0.9.
|
|
5760
|
+
const APP_VERSION = "0.9.3";
|
|
5373
5761
|
|
|
5374
5762
|
//#endregion
|
|
5375
5763
|
//#region src/telemetry/env.ts
|