aislop 0.9.2 → 0.9.4
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 +8 -2
- package/dist/cli.js +764 -274
- package/dist/index.js +765 -275
- package/dist/{json-DZHn6AE3.js → json-CXiEvR_M.js} +1 -1
- package/dist/mcp.js +748 -275
- package/dist/{version-C3JZkQGA.js → version-C45P3Q1N.js} +1 -1
- package/package.json +92 -91
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 });
|
|
@@ -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
|
|
@@ -1286,51 +1637,6 @@ const addDepsFromPkg = (pkg, jsDeps) => {
|
|
|
1286
1637
|
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) jsDeps.add(name);
|
|
1287
1638
|
}
|
|
1288
1639
|
};
|
|
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;
|
|
1321
|
-
};
|
|
1322
|
-
const expandWorkspaceDirs = (rootDir, globs) => {
|
|
1323
|
-
const dirs = [];
|
|
1324
|
-
for (const glob of globs) if (glob.endsWith("/*")) {
|
|
1325
|
-
const parent = path.join(rootDir, glob.slice(0, -2));
|
|
1326
|
-
try {
|
|
1327
|
-
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) if (entry.isDirectory()) dirs.push(path.join(parent, entry.name));
|
|
1328
|
-
} catch {
|
|
1329
|
-
continue;
|
|
1330
|
-
}
|
|
1331
|
-
} else if (!glob.includes("*")) dirs.push(path.join(rootDir, glob));
|
|
1332
|
-
return dirs;
|
|
1333
|
-
};
|
|
1334
1640
|
const SKIP_DIRS = new Set([
|
|
1335
1641
|
"node_modules",
|
|
1336
1642
|
".git",
|
|
@@ -1362,62 +1668,25 @@ const collectNestedManifests = (rootDir, jsDeps) => {
|
|
|
1362
1668
|
addDepsFromPkg(wsPkg, jsDeps);
|
|
1363
1669
|
}
|
|
1364
1670
|
}
|
|
1365
|
-
};
|
|
1366
|
-
walk(rootDir, 0);
|
|
1367
|
-
};
|
|
1368
|
-
const collectJsDeps = (rootDir, jsDeps) => {
|
|
1369
|
-
const pkgPath = path.join(rootDir, "package.json");
|
|
1370
|
-
if (!fs.existsSync(pkgPath)) return false;
|
|
1371
|
-
const pkg = readJson(pkgPath);
|
|
1372
|
-
if (!pkg || typeof pkg !== "object") return false;
|
|
1373
|
-
addDepsFromPkg(pkg, jsDeps);
|
|
1374
|
-
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
1375
|
-
const workspaceDirs = expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, pkg));
|
|
1376
|
-
for (const wsDir of workspaceDirs) {
|
|
1377
|
-
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
1378
|
-
if (!wsPkg) continue;
|
|
1379
|
-
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
1380
|
-
addDepsFromPkg(wsPkg, jsDeps);
|
|
1381
|
-
}
|
|
1382
|
-
collectNestedManifests(rootDir, jsDeps);
|
|
1383
|
-
return true;
|
|
1384
|
-
};
|
|
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
|
-
}
|
|
1671
|
+
};
|
|
1672
|
+
walk(rootDir, 0);
|
|
1415
1673
|
};
|
|
1416
|
-
const
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
return
|
|
1674
|
+
const collectJsDeps = (rootDir, jsDeps) => {
|
|
1675
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
1676
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
1677
|
+
const pkg = readJson(pkgPath);
|
|
1678
|
+
if (!pkg || typeof pkg !== "object") return false;
|
|
1679
|
+
addDepsFromPkg(pkg, jsDeps);
|
|
1680
|
+
if (typeof pkg.name === "string") jsDeps.add(pkg.name);
|
|
1681
|
+
const workspaceDirs = collectWorkspaceDirs(rootDir, pkg);
|
|
1682
|
+
for (const wsDir of workspaceDirs) {
|
|
1683
|
+
const wsPkg = readJson(path.join(wsDir, "package.json"));
|
|
1684
|
+
if (!wsPkg) continue;
|
|
1685
|
+
if (typeof wsPkg.name === "string") jsDeps.add(wsPkg.name);
|
|
1686
|
+
addDepsFromPkg(wsPkg, jsDeps);
|
|
1687
|
+
}
|
|
1688
|
+
collectNestedManifests(rootDir, jsDeps);
|
|
1689
|
+
return true;
|
|
1421
1690
|
};
|
|
1422
1691
|
const loadManifest = (rootDir) => {
|
|
1423
1692
|
const jsDeps = /* @__PURE__ */ new Set();
|
|
@@ -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;
|
|
1851
2040
|
};
|
|
1852
|
-
const
|
|
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));
|
|
2045
|
+
};
|
|
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
|
};
|
|
@@ -1985,6 +2190,11 @@ const BROAD_EXCEPT_RE = /^\s*except\s+(Exception|BaseException)\s*(?:as\s+\w+)?\
|
|
|
1985
2190
|
const PRINT_RE = /^\s*print\s*\(/;
|
|
1986
2191
|
const DEF_RE = /^\s*(?:async\s+)?def\s+\w+\s*\(/;
|
|
1987
2192
|
const MUTABLE_DEFAULT_RE = /(\w+)\s*(?::\s*[^,)=]+)?\s*=\s*(\[\s*\]|\{\s*\}|set\(\s*\))/;
|
|
2193
|
+
const RANGE_LEN_LOOP_RE = /^\s*for\s+([A-Za-z_]\w*)\s+in\s+range\s*\(\s*len\s*\(\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\)\s*\)\s*:\s*(?:#.*)?$/;
|
|
2194
|
+
const CHAINED_DICT_GET_RE = /\.get\s*\([^)]*,\s*\{\s*\}\s*\)\s*\.get\s*\(/;
|
|
2195
|
+
const SAME_VALUE_BRANCH_RE = /^(\s*)(?:if|elif)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*==\s*["'][^"']+["']\s*:/;
|
|
2196
|
+
const INSTANCE_BRANCH_RE = /^(\s*)(?:if|elif)\s+isinstance\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^)]+\)\s*:/;
|
|
2197
|
+
const BRANCH_LADDER_THRESHOLD = 4;
|
|
1988
2198
|
const isTestFile$1 = (relPath, basename) => basename.startsWith("test_") || basename.endsWith("_test.py") || basename === "conftest.py" || relPath.split(path.sep).some((seg) => seg === "tests" || seg === "test");
|
|
1989
2199
|
const isScriptOrEntrypoint = (basename) => basename === "__main__.py" || basename === "manage.py" || basename === "setup.py";
|
|
1990
2200
|
const SCRIPT_DIR_NAMES = new Set([
|
|
@@ -2037,6 +2247,13 @@ const pushFinding = (out, a) => {
|
|
|
2037
2247
|
fixable: false
|
|
2038
2248
|
});
|
|
2039
2249
|
};
|
|
2250
|
+
const pushLineFinding = (out, relPath, line, finding) => {
|
|
2251
|
+
pushFinding(out, {
|
|
2252
|
+
relPath,
|
|
2253
|
+
line,
|
|
2254
|
+
...finding
|
|
2255
|
+
});
|
|
2256
|
+
};
|
|
2040
2257
|
const flagBareExcept = (lines, relPath, out) => {
|
|
2041
2258
|
for (let i = 0; i < lines.length; i++) {
|
|
2042
2259
|
if (!BARE_EXCEPT_RE.test(lines[i])) continue;
|
|
@@ -2118,6 +2335,76 @@ const flagPrintInProduction = (lines, relPath, basename, out) => {
|
|
|
2118
2335
|
});
|
|
2119
2336
|
}
|
|
2120
2337
|
};
|
|
2338
|
+
const flagRangeLenLoops = (lines, relPath, out) => {
|
|
2339
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2340
|
+
const match = RANGE_LEN_LOOP_RE.exec(lines[i]);
|
|
2341
|
+
if (!match) continue;
|
|
2342
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2343
|
+
rule: "ai-slop/python-range-len-loop",
|
|
2344
|
+
severity: "info",
|
|
2345
|
+
message: `\`range(len(${match[2]}))\` loop — usually a hand-rolled iteration pattern.`,
|
|
2346
|
+
help: "Prefer direct iteration (`for item in items`) or `enumerate(items)` when the index is needed. Keeping index plumbing out of the loop reduces checkpoint-to-checkpoint bloat."
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
const flagChainedDictGets = (lines, relPath, out) => {
|
|
2351
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2352
|
+
if (!CHAINED_DICT_GET_RE.test(lines[i])) continue;
|
|
2353
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2354
|
+
rule: "ai-slop/python-chained-dict-get",
|
|
2355
|
+
severity: "warning",
|
|
2356
|
+
message: "Chained `.get(..., {})` defaults hide missing-data cases.",
|
|
2357
|
+
help: "Normalize the input at the boundary, use a typed object, or split the lookup into explicit steps. Empty-dict fallback chains are a common agent shortcut that becomes brittle as schemas evolve."
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
const countBranchLadder = (lines, start, pattern, selector, indent) => {
|
|
2362
|
+
let count = 1;
|
|
2363
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
2364
|
+
const line = lines[i];
|
|
2365
|
+
const trimmed = line.trim();
|
|
2366
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
2367
|
+
const match = pattern.exec(line);
|
|
2368
|
+
if (match?.[1] === indent && match[2] === selector) {
|
|
2369
|
+
count++;
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
if (line.startsWith(`${indent}elif `)) break;
|
|
2373
|
+
if (line.length - line.trimStart().length <= indent.length && !line.startsWith(`${indent}else`)) break;
|
|
2374
|
+
}
|
|
2375
|
+
return count;
|
|
2376
|
+
};
|
|
2377
|
+
const flagBranchLadders = (lines, relPath, out) => {
|
|
2378
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2379
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2380
|
+
if (reported.has(i)) continue;
|
|
2381
|
+
const valueMatch = SAME_VALUE_BRANCH_RE.exec(lines[i]);
|
|
2382
|
+
if (valueMatch) {
|
|
2383
|
+
const count = countBranchLadder(lines, i, SAME_VALUE_BRANCH_RE, valueMatch[2], valueMatch[1]);
|
|
2384
|
+
if (count >= BRANCH_LADDER_THRESHOLD) {
|
|
2385
|
+
reported.add(i);
|
|
2386
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2387
|
+
rule: "ai-slop/python-repetitive-dispatch",
|
|
2388
|
+
severity: "warning",
|
|
2389
|
+
message: `${count} repeated branches dispatch on \`${valueMatch[2]}\`.`,
|
|
2390
|
+
help: "Use a table, set membership, or handler map when branches share the same shape. SlopCodeBench highlights these selector ladders as code that keeps growing instead of absorbing new cases cleanly."
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
const instanceMatch = INSTANCE_BRANCH_RE.exec(lines[i]);
|
|
2396
|
+
if (!instanceMatch) continue;
|
|
2397
|
+
const count = countBranchLadder(lines, i, INSTANCE_BRANCH_RE, instanceMatch[2], instanceMatch[1]);
|
|
2398
|
+
if (count < BRANCH_LADDER_THRESHOLD) continue;
|
|
2399
|
+
reported.add(i);
|
|
2400
|
+
pushLineFinding(out, relPath, i + 1, {
|
|
2401
|
+
rule: "ai-slop/python-isinstance-ladder",
|
|
2402
|
+
severity: "warning",
|
|
2403
|
+
message: `${count} repeated \`isinstance(${instanceMatch[2]}, ...)\` branches.`,
|
|
2404
|
+
help: "Prefer a handler map, protocol, or normalized intermediate representation when each type branch has the same role. Repeated type ladders are one of the maintainability smells SCBench-style checks look for."
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2121
2408
|
const detectPythonPatterns = async (context) => {
|
|
2122
2409
|
const diagnostics = [];
|
|
2123
2410
|
const files = getSourceFiles(context);
|
|
@@ -2137,6 +2424,9 @@ const detectPythonPatterns = async (context) => {
|
|
|
2137
2424
|
flagBroadExceptWithSilentBody(lines, relPath, diagnostics);
|
|
2138
2425
|
flagMutableDefaults(lines, relPath, diagnostics);
|
|
2139
2426
|
flagPrintInProduction(lines, relPath, basename, diagnostics);
|
|
2427
|
+
flagRangeLenLoops(lines, relPath, diagnostics);
|
|
2428
|
+
flagChainedDictGets(lines, relPath, diagnostics);
|
|
2429
|
+
flagBranchLadders(lines, relPath, diagnostics);
|
|
2140
2430
|
}
|
|
2141
2431
|
return diagnostics;
|
|
2142
2432
|
};
|
|
@@ -2162,7 +2452,23 @@ const isTestFile = (relPath) => {
|
|
|
2162
2452
|
if (segments.some((s) => TEST_CRATE_SEGMENT_RE.test(s))) return true;
|
|
2163
2453
|
const basename = segments[segments.length - 1] ?? "";
|
|
2164
2454
|
if (TEST_BASENAMES.has(basename)) return true;
|
|
2165
|
-
return basename.endsWith("_tests.rs") || basename.endsWith("_testutil.rs");
|
|
2455
|
+
return basename.endsWith("_tests.rs") || basename.endsWith("_test.rs") || basename.endsWith("_testutil.rs");
|
|
2456
|
+
};
|
|
2457
|
+
const buildBlockCommentRanges = (lines) => {
|
|
2458
|
+
const ranges = [];
|
|
2459
|
+
let openLine = -1;
|
|
2460
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2461
|
+
const line = lines[i];
|
|
2462
|
+
if (openLine === -1) {
|
|
2463
|
+
const openIdx = line.indexOf("/*");
|
|
2464
|
+
if (openIdx !== -1 && line.indexOf("*/", openIdx + 2) === -1) openLine = i;
|
|
2465
|
+
} else if (line.indexOf("*/") !== -1) {
|
|
2466
|
+
ranges.push([openLine, i]);
|
|
2467
|
+
openLine = -1;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
if (openLine !== -1) ranges.push([openLine, lines.length - 1]);
|
|
2471
|
+
return ranges;
|
|
2166
2472
|
};
|
|
2167
2473
|
const isExampleFile = (relPath) => relPath.split(path.sep).some((seg) => seg === "examples" || seg === "benches");
|
|
2168
2474
|
const UNWRAP_INTENT_LOOKBACK = 2;
|
|
@@ -2193,11 +2499,12 @@ const buildTestRanges = (lines) => {
|
|
|
2193
2499
|
return ranges;
|
|
2194
2500
|
};
|
|
2195
2501
|
const isInRange = (ranges, lineIdx) => ranges.some(([start, end]) => lineIdx >= start && lineIdx <= end);
|
|
2196
|
-
const flagNonTestUnwrap = (lines, relPath, testRanges, out) => {
|
|
2502
|
+
const flagNonTestUnwrap = (lines, relPath, testRanges, blockCommentRanges, out) => {
|
|
2197
2503
|
for (let i = 0; i < lines.length; i++) {
|
|
2198
2504
|
const line = lines[i];
|
|
2199
2505
|
if (COMMENT_LINE_RE.test(line)) continue;
|
|
2200
2506
|
if (isInRange(testRanges, i)) continue;
|
|
2507
|
+
if (isInRange(blockCommentRanges, i)) continue;
|
|
2201
2508
|
if (!UNWRAP_CALL_RE.test(line)) continue;
|
|
2202
2509
|
if (WRITELN_UNWRAP_RE.test(line)) continue;
|
|
2203
2510
|
if (hasIntentComment(lines, i)) continue;
|
|
@@ -2254,7 +2561,7 @@ const detectRustPatterns = async (context) => {
|
|
|
2254
2561
|
flagTodoMacro(lines, relPath, diagnostics);
|
|
2255
2562
|
continue;
|
|
2256
2563
|
}
|
|
2257
|
-
flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), diagnostics);
|
|
2564
|
+
flagNonTestUnwrap(lines, relPath, buildTestRanges(lines), buildBlockCommentRanges(lines), diagnostics);
|
|
2258
2565
|
flagTodoMacro(lines, relPath, diagnostics);
|
|
2259
2566
|
}
|
|
2260
2567
|
return diagnostics;
|
|
@@ -2339,7 +2646,9 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
2339
2646
|
const cleaned = importPart.replace(/[()]/g, "");
|
|
2340
2647
|
for (const item of cleaned.split(",")) {
|
|
2341
2648
|
const parts = item.trim().split(/\s+as\s+/);
|
|
2342
|
-
const
|
|
2649
|
+
const original = parts[0].trim();
|
|
2650
|
+
const localName = parts.length > 1 ? parts[1].trim() : original;
|
|
2651
|
+
if (parts.length > 1 && original === localName) continue;
|
|
2343
2652
|
if (localName && /^\w+$/.test(localName)) symbols.push({
|
|
2344
2653
|
name: localName,
|
|
2345
2654
|
line: i + 1,
|
|
@@ -2352,7 +2661,9 @@ const extractPyImportedSymbols = (lines) => {
|
|
|
2352
2661
|
const importMatch = trimmed.match(/^import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
|
|
2353
2662
|
if (importMatch) {
|
|
2354
2663
|
importLines.add(i);
|
|
2355
|
-
const
|
|
2664
|
+
const alias = importMatch[2];
|
|
2665
|
+
if (alias && alias === importMatch[1]) continue;
|
|
2666
|
+
const simpleName = (alias ?? importMatch[1]).split(".")[0];
|
|
2356
2667
|
if (simpleName && /^\w+$/.test(simpleName)) symbols.push({
|
|
2357
2668
|
name: simpleName,
|
|
2358
2669
|
line: i + 1,
|
|
@@ -3008,9 +3319,17 @@ const isTrivialLine = (line) => {
|
|
|
3008
3319
|
if (trimmed.startsWith("/*") || trimmed.startsWith("*")) return true;
|
|
3009
3320
|
return false;
|
|
3010
3321
|
};
|
|
3322
|
+
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)=/;
|
|
3323
|
+
const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
|
|
3011
3324
|
const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
3012
3325
|
const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
|
|
3013
3326
|
const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
|
|
3327
|
+
const isLowSignalMarkupWindow = (lines) => {
|
|
3328
|
+
return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
|
|
3329
|
+
};
|
|
3330
|
+
const isLowSignalDataWindow = (lines) => {
|
|
3331
|
+
return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= WINDOW_SIZE - 1;
|
|
3332
|
+
};
|
|
3014
3333
|
const findSuppressedLines = (lines) => {
|
|
3015
3334
|
const suppressed = /* @__PURE__ */ new Set();
|
|
3016
3335
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -3037,6 +3356,8 @@ const collectMeaningfulLines = (content) => {
|
|
|
3037
3356
|
if (suppressed.has(i + 1)) continue;
|
|
3038
3357
|
const window = lines.slice(i, i + WINDOW_SIZE);
|
|
3039
3358
|
if (window.some((l) => !MEANINGFUL_LINE.test(l))) continue;
|
|
3359
|
+
if (isLowSignalMarkupWindow(window)) continue;
|
|
3360
|
+
if (isLowSignalDataWindow(window)) continue;
|
|
3040
3361
|
if (window.every(isTrivialLine)) continue;
|
|
3041
3362
|
const normalised = window.map(normaliseLine);
|
|
3042
3363
|
if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
|
|
@@ -3915,7 +4236,20 @@ const createOxlintConfig = (options) => {
|
|
|
3915
4236
|
...buildFrameworkPlugins(options.framework)
|
|
3916
4237
|
];
|
|
3917
4238
|
const globals = buildTestGlobals(options.testFramework ?? null);
|
|
3918
|
-
|
|
4239
|
+
for (const name of [
|
|
4240
|
+
"__DEV__",
|
|
4241
|
+
"__TEST__",
|
|
4242
|
+
"__BROWSER__",
|
|
4243
|
+
"__NODE__",
|
|
4244
|
+
"__GLOBAL__",
|
|
4245
|
+
"__SSR__",
|
|
4246
|
+
"__ESM_BROWSER__",
|
|
4247
|
+
"__ESM_BUNDLER__",
|
|
4248
|
+
"__VERSION__",
|
|
4249
|
+
"__COMMIT__",
|
|
4250
|
+
"__BUILD__"
|
|
4251
|
+
]) globals[name] = "readonly";
|
|
4252
|
+
for (const globalName of options.globals ?? []) globals[globalName] = "readonly";
|
|
3919
4253
|
if (options.framework === "astro") {
|
|
3920
4254
|
globals.Astro = "readonly";
|
|
3921
4255
|
rules["no-undef"] = "off";
|
|
@@ -3935,19 +4269,7 @@ const createOxlintConfig = (options) => {
|
|
|
3935
4269
|
};
|
|
3936
4270
|
|
|
3937
4271
|
//#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);
|
|
4272
|
+
//#region src/engines/lint/oxlint-context-filters.ts
|
|
3951
4273
|
const AMBIENT_GLOBAL_DEPS = [
|
|
3952
4274
|
"unplugin-icons",
|
|
3953
4275
|
"@types/bun",
|
|
@@ -4009,6 +4331,9 @@ const isAmbientFalsePositive = (rule, message, sources) => {
|
|
|
4009
4331
|
return false;
|
|
4010
4332
|
};
|
|
4011
4333
|
const sstReferencedFiles = /* @__PURE__ */ new Map();
|
|
4334
|
+
const clearSstReferenceCache = () => {
|
|
4335
|
+
sstReferencedFiles.clear();
|
|
4336
|
+
};
|
|
4012
4337
|
const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
4013
4338
|
const cached = sstReferencedFiles.get(relativeFilePath);
|
|
4014
4339
|
if (cached !== void 0) return cached;
|
|
@@ -4029,12 +4354,114 @@ const fileReferencesSstPlatform = (rootDir, relativeFilePath) => {
|
|
|
4029
4354
|
sstReferencedFiles.set(relativeFilePath, referenced);
|
|
4030
4355
|
return referenced;
|
|
4031
4356
|
};
|
|
4357
|
+
|
|
4358
|
+
//#endregion
|
|
4359
|
+
//#region src/engines/lint/oxlint-globals.ts
|
|
4360
|
+
const readTextFile$1 = (filePath) => {
|
|
4361
|
+
try {
|
|
4362
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
4363
|
+
} catch {
|
|
4364
|
+
return null;
|
|
4365
|
+
}
|
|
4366
|
+
};
|
|
4367
|
+
const collectPackageNames = (dir) => {
|
|
4368
|
+
const names = /* @__PURE__ */ new Set();
|
|
4369
|
+
const raw = readTextFile$1(path.join(dir, "package.json"));
|
|
4370
|
+
if (!raw) return names;
|
|
4371
|
+
try {
|
|
4372
|
+
const pkg = JSON.parse(raw);
|
|
4373
|
+
for (const section of [
|
|
4374
|
+
"dependencies",
|
|
4375
|
+
"devDependencies",
|
|
4376
|
+
"peerDependencies",
|
|
4377
|
+
"optionalDependencies"
|
|
4378
|
+
]) {
|
|
4379
|
+
const deps = pkg[section];
|
|
4380
|
+
if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
|
|
4381
|
+
}
|
|
4382
|
+
} catch {
|
|
4383
|
+
return names;
|
|
4384
|
+
}
|
|
4385
|
+
return names;
|
|
4386
|
+
};
|
|
4387
|
+
const AMBIENT_GLOBAL_RE = /^\s*(?:declare\s+)?(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gm;
|
|
4388
|
+
const collectAmbientGlobals = (rootDir) => {
|
|
4389
|
+
const globals = /* @__PURE__ */ new Set();
|
|
4390
|
+
const projectFiles = listProjectFiles(rootDir);
|
|
4391
|
+
for (const relativePath of projectFiles) {
|
|
4392
|
+
if (!relativePath.endsWith(".d.ts")) continue;
|
|
4393
|
+
const content = readTextFile$1(path.join(rootDir, relativePath));
|
|
4394
|
+
if (!content) continue;
|
|
4395
|
+
AMBIENT_GLOBAL_RE.lastIndex = 0;
|
|
4396
|
+
let match;
|
|
4397
|
+
while ((match = AMBIENT_GLOBAL_RE.exec(content)) !== null) globals.add(match[1]);
|
|
4398
|
+
}
|
|
4399
|
+
const deps = collectPackageNames(rootDir);
|
|
4400
|
+
if (deps.has("@types/bun") || deps.has("bun-types")) globals.add("Bun");
|
|
4401
|
+
if (projectFiles.some((filePath) => /(?:^|\/)sst\.config\.ts$/.test(filePath))) for (const name of [
|
|
4402
|
+
"$app",
|
|
4403
|
+
"$config",
|
|
4404
|
+
"$dev",
|
|
4405
|
+
"$interpolate",
|
|
4406
|
+
"$resolve",
|
|
4407
|
+
"$jsonParse",
|
|
4408
|
+
"$jsonStringify",
|
|
4409
|
+
"aws",
|
|
4410
|
+
"cloudflare",
|
|
4411
|
+
"docker",
|
|
4412
|
+
"random",
|
|
4413
|
+
"sst",
|
|
4414
|
+
"vercel",
|
|
4415
|
+
"pulumi"
|
|
4416
|
+
]) globals.add(name);
|
|
4417
|
+
return [...globals];
|
|
4418
|
+
};
|
|
4419
|
+
|
|
4420
|
+
//#endregion
|
|
4421
|
+
//#region src/engines/lint/oxlint.ts
|
|
4422
|
+
const esmRequire = createRequire(import.meta.url);
|
|
4423
|
+
const OXLINT_EXTENSIONS = new Set([
|
|
4424
|
+
".ts",
|
|
4425
|
+
".tsx",
|
|
4426
|
+
".js",
|
|
4427
|
+
".jsx",
|
|
4428
|
+
".mjs",
|
|
4429
|
+
".cjs"
|
|
4430
|
+
]);
|
|
4431
|
+
const resolveOxlintBinary = () => {
|
|
4432
|
+
try {
|
|
4433
|
+
const oxlintMainPath = esmRequire.resolve("oxlint");
|
|
4434
|
+
const oxlintDir = path.resolve(path.dirname(oxlintMainPath), "..");
|
|
4435
|
+
return path.join(oxlintDir, "bin", "oxlint");
|
|
4436
|
+
} catch {
|
|
4437
|
+
return "oxlint";
|
|
4438
|
+
}
|
|
4439
|
+
};
|
|
4440
|
+
const VITE_QUERY_RE = /["'][^"']*\?(worker|sharedworker|worker-url|url|raw|inline|init)\b/;
|
|
4441
|
+
const isViteVirtualImportFalsePositive = (rule, message) => rule.startsWith("import/") && VITE_QUERY_RE.test(message);
|
|
4032
4442
|
const UNUSED_VAR_IDENT_RE = /(?:Variable|Parameter|Catch parameter) '([^']+)' (?:is declared but never used|is caught but never used)/;
|
|
4033
4443
|
const isUnderscoreUnusedVar = (rule, message) => {
|
|
4034
4444
|
if (rule !== "eslint/no-unused-vars") return false;
|
|
4035
4445
|
const match = UNUSED_VAR_IDENT_RE.exec(message);
|
|
4036
4446
|
return match ? match[1].startsWith("_") : false;
|
|
4037
4447
|
};
|
|
4448
|
+
const readTextFile = (filePath) => {
|
|
4449
|
+
try {
|
|
4450
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
4451
|
+
} catch {
|
|
4452
|
+
return null;
|
|
4453
|
+
}
|
|
4454
|
+
};
|
|
4455
|
+
const isSolidRefFalsePositive = (context, diagnostic) => {
|
|
4456
|
+
if (diagnostic.rule !== "eslint/no-unassigned-vars") return false;
|
|
4457
|
+
const name = diagnostic.message.match(/^'([^']+)' is always 'undefined'/)?.[1];
|
|
4458
|
+
if (!name) return false;
|
|
4459
|
+
const content = readTextFile(path.isAbsolute(diagnostic.filePath) ? diagnostic.filePath : path.join(context.rootDirectory, diagnostic.filePath));
|
|
4460
|
+
if (!content) return false;
|
|
4461
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4462
|
+
return new RegExp(`\\bref=\\{\\s*${escaped}\\s*\\}`).test(content);
|
|
4463
|
+
};
|
|
4464
|
+
const isContextualTypeScriptFalsePositive = (diagnostic) => diagnostic.rule === "typescript-eslint/triple-slash-reference" && (diagnostic.filePath.endsWith(".d.ts") || /(?:^|\/)sst\.config\.ts$/.test(diagnostic.filePath));
|
|
4038
4465
|
const parseRuleCode = (code) => {
|
|
4039
4466
|
if (!code) return {
|
|
4040
4467
|
plugin: "eslint",
|
|
@@ -4067,14 +4494,20 @@ const detectTestFramework = (rootDir) => {
|
|
|
4067
4494
|
} catch {}
|
|
4068
4495
|
return null;
|
|
4069
4496
|
};
|
|
4497
|
+
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
4498
|
const runOxlint = async (context) => {
|
|
4071
4499
|
const configPath = path.join(os.tmpdir(), `aislop-oxlintrc-${process.pid}.json`);
|
|
4500
|
+
const framework = context.frameworks.find((f) => f !== "none");
|
|
4501
|
+
const testFramework = detectTestFramework(context.rootDirectory);
|
|
4502
|
+
const targets = getOxlintTargets(context);
|
|
4503
|
+
if (targets.length === 0) return [];
|
|
4072
4504
|
const config = createOxlintConfig({
|
|
4073
|
-
framework
|
|
4074
|
-
testFramework
|
|
4505
|
+
framework,
|
|
4506
|
+
testFramework,
|
|
4507
|
+
globals: collectAmbientGlobals(context.rootDirectory)
|
|
4075
4508
|
});
|
|
4076
4509
|
const ambientSources = detectAmbientSources(context.rootDirectory);
|
|
4077
|
-
|
|
4510
|
+
clearSstReferenceCache();
|
|
4078
4511
|
try {
|
|
4079
4512
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
4080
4513
|
const args = [
|
|
@@ -4085,7 +4518,7 @@ const runOxlint = async (context) => {
|
|
|
4085
4518
|
"json"
|
|
4086
4519
|
];
|
|
4087
4520
|
if (context.languages.includes("typescript") && fs.existsSync(path.join(context.rootDirectory, "tsconfig.json"))) args.push("--tsconfig", "./tsconfig.json");
|
|
4088
|
-
args.push(
|
|
4521
|
+
args.push(...targets);
|
|
4089
4522
|
const result = await runSubprocess(process.execPath, args, {
|
|
4090
4523
|
cwd: context.rootDirectory,
|
|
4091
4524
|
timeout: 12e4
|
|
@@ -4117,6 +4550,8 @@ const runOxlint = async (context) => {
|
|
|
4117
4550
|
if (isExcludedFromScan(path.isAbsolute(d.filePath) ? path.relative(context.rootDirectory, d.filePath) : d.filePath)) return false;
|
|
4118
4551
|
if (isViteVirtualImportFalsePositive(d.rule, d.message)) return false;
|
|
4119
4552
|
if (isAmbientFalsePositive(d.rule, d.message, ambientSources)) return false;
|
|
4553
|
+
if (isSolidRefFalsePositive(context, d)) return false;
|
|
4554
|
+
if (isContextualTypeScriptFalsePositive(d)) return false;
|
|
4120
4555
|
if (isUnderscoreUnusedVar(d.rule, d.message)) return false;
|
|
4121
4556
|
if (d.rule === "eslint/no-undef" && fileReferencesSstPlatform(context.rootDirectory, d.filePath)) return false;
|
|
4122
4557
|
const key = `${d.filePath}:${d.line}:${d.rule}:${d.message}`;
|
|
@@ -4672,7 +5107,7 @@ const DB_RECEIVER = "(?:db|database|knex|client|connection|conn|pool|sql|prisma|
|
|
|
4672
5107
|
const DB_METHOD = "(?:query|execute|exec|raw|\\$queryRaw|\\$queryRawUnsafe|\\$executeRaw|\\$executeRawUnsafe)";
|
|
4673
5108
|
const RISKY_PATTERNS = [
|
|
4674
5109
|
{
|
|
4675
|
-
pattern: new RegExp(
|
|
5110
|
+
pattern: new RegExp(`(?<![\\w.>:\\\\])\\b${ev}\\s*\\(`, "g"),
|
|
4676
5111
|
extensions: [
|
|
4677
5112
|
".ts",
|
|
4678
5113
|
".tsx",
|
|
@@ -4779,6 +5214,16 @@ const RISKY_PATTERNS = [
|
|
|
4779
5214
|
help: "Use parameterized queries or an ORM instead of string concatenation"
|
|
4780
5215
|
}
|
|
4781
5216
|
];
|
|
5217
|
+
const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
|
|
5218
|
+
const start = Math.max(0, lineIndex - 2);
|
|
5219
|
+
return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
|
|
5220
|
+
};
|
|
5221
|
+
const isStructuredDataScript = (content, matchIndex) => {
|
|
5222
|
+
const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
|
|
5223
|
+
if (/type=["']application\/ld\+json["']/.test(before)) return true;
|
|
5224
|
+
const after = content.slice(matchIndex, Math.min(content.length, matchIndex + 180));
|
|
5225
|
+
return /__html\s*:\s*JSON\.stringify\s*\(/.test(after);
|
|
5226
|
+
};
|
|
4782
5227
|
const detectRiskyConstructs = async (context) => {
|
|
4783
5228
|
const files = getSourceFiles(context);
|
|
4784
5229
|
const diagnostics = [];
|
|
@@ -4794,6 +5239,7 @@ const detectRiskyConstructs = async (context) => {
|
|
|
4794
5239
|
const normalizedPath = relativePath.split(path.sep).join("/");
|
|
4795
5240
|
const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
|
|
4796
5241
|
const masked = maskStringsAndComments(content, ext);
|
|
5242
|
+
const lines = content.split("\n");
|
|
4797
5243
|
for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
|
|
4798
5244
|
if (!extensions.includes(ext)) continue;
|
|
4799
5245
|
if (isMigrationOrSeeder && name === "sql-injection") continue;
|
|
@@ -4805,6 +5251,10 @@ const detectRiskyConstructs = async (context) => {
|
|
|
4805
5251
|
const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
|
|
4806
5252
|
if (/(?:template|tmpl|tpl)$/i.test(beforeMatch.trimEnd()) || /createElement\s*\(\s*['"]template['"]\s*\)$/.test(beforeMatch.trimEnd())) continue;
|
|
4807
5253
|
}
|
|
5254
|
+
if (name === "dangerously-set-innerhtml") {
|
|
5255
|
+
if (hasDangerouslySetInnerHtmlIgnore(lines, line - 1)) continue;
|
|
5256
|
+
if (isStructuredDataScript(content, match.index)) continue;
|
|
5257
|
+
}
|
|
4808
5258
|
diagnostics.push({
|
|
4809
5259
|
filePath: relativePath,
|
|
4810
5260
|
engine: "security",
|
|
@@ -4828,7 +5278,8 @@ const detectRiskyConstructs = async (context) => {
|
|
|
4828
5278
|
const SECRET_PATTERNS = [
|
|
4829
5279
|
{
|
|
4830
5280
|
pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
|
|
4831
|
-
name: "API key"
|
|
5281
|
+
name: "API key",
|
|
5282
|
+
keywordPrefixed: true
|
|
4832
5283
|
},
|
|
4833
5284
|
{
|
|
4834
5285
|
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
@@ -4836,11 +5287,13 @@ const SECRET_PATTERNS = [
|
|
|
4836
5287
|
},
|
|
4837
5288
|
{
|
|
4838
5289
|
pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
|
|
4839
|
-
name: "AWS Secret Key"
|
|
5290
|
+
name: "AWS Secret Key",
|
|
5291
|
+
keywordPrefixed: true
|
|
4840
5292
|
},
|
|
4841
5293
|
{
|
|
4842
5294
|
pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
|
|
4843
|
-
name: "Hardcoded password/secret"
|
|
5295
|
+
name: "Hardcoded password/secret",
|
|
5296
|
+
keywordPrefixed: true
|
|
4844
5297
|
},
|
|
4845
5298
|
{
|
|
4846
5299
|
pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
|
|
@@ -4852,7 +5305,8 @@ const SECRET_PATTERNS = [
|
|
|
4852
5305
|
},
|
|
4853
5306
|
{
|
|
4854
5307
|
pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
|
|
4855
|
-
name: "Authentication token"
|
|
5308
|
+
name: "Authentication token",
|
|
5309
|
+
keywordPrefixed: true
|
|
4856
5310
|
},
|
|
4857
5311
|
{
|
|
4858
5312
|
pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
|
|
@@ -4867,6 +5321,24 @@ const SECRET_PATTERNS = [
|
|
|
4867
5321
|
name: "Database connection string with credentials"
|
|
4868
5322
|
}
|
|
4869
5323
|
];
|
|
5324
|
+
const isInsideStringLiteral = (content, matchIndex) => {
|
|
5325
|
+
const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
|
|
5326
|
+
const prefix = content.slice(lineStart, matchIndex);
|
|
5327
|
+
let inDouble = false;
|
|
5328
|
+
let inSingle = false;
|
|
5329
|
+
let inBacktick = false;
|
|
5330
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
5331
|
+
const ch = prefix[i];
|
|
5332
|
+
if (ch === "\\") {
|
|
5333
|
+
i++;
|
|
5334
|
+
continue;
|
|
5335
|
+
}
|
|
5336
|
+
if (ch === "\"" && !inSingle && !inBacktick) inDouble = !inDouble;
|
|
5337
|
+
else if (ch === "'" && !inDouble && !inBacktick) inSingle = !inSingle;
|
|
5338
|
+
else if (ch === "`" && !inDouble && !inSingle) inBacktick = !inBacktick;
|
|
5339
|
+
}
|
|
5340
|
+
return inDouble || inSingle || inBacktick;
|
|
5341
|
+
};
|
|
4870
5342
|
const PLACEHOLDER_EXACT = new Set([
|
|
4871
5343
|
"changeme",
|
|
4872
5344
|
"password",
|
|
@@ -4902,11 +5374,12 @@ const scanSecrets = async (context) => {
|
|
|
4902
5374
|
continue;
|
|
4903
5375
|
}
|
|
4904
5376
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
4905
|
-
for (const { pattern, name } of SECRET_PATTERNS) {
|
|
5377
|
+
for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
|
|
4906
5378
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
4907
5379
|
let match;
|
|
4908
5380
|
while ((match = regex.exec(content)) !== null) {
|
|
4909
5381
|
if (isPlaceholderValue(match[1] ?? match[0])) continue;
|
|
5382
|
+
if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
|
|
4910
5383
|
const line = content.slice(0, match.index).split("\n").length;
|
|
4911
5384
|
diagnostics.push({
|
|
4912
5385
|
filePath: relativePath,
|
|
@@ -5369,7 +5842,7 @@ const handleAislopBaseline = (input) => {
|
|
|
5369
5842
|
|
|
5370
5843
|
//#endregion
|
|
5371
5844
|
//#region src/version.ts
|
|
5372
|
-
const APP_VERSION = "0.9.
|
|
5845
|
+
const APP_VERSION = "0.9.4";
|
|
5373
5846
|
|
|
5374
5847
|
//#endregion
|
|
5375
5848
|
//#region src/telemetry/env.ts
|