vibertest 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -364
- package/dist/index.js +361 -223
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -34,10 +34,10 @@ var rulesSchema = z.record(
|
|
|
34
34
|
).optional();
|
|
35
35
|
var thresholdsSchema = z.object({
|
|
36
36
|
maxFileLines: z.number().int().positive().default(500),
|
|
37
|
-
maxFunctionLines: z.number().int().positive().default(
|
|
38
|
-
maxFunctionParams: z.number().int().positive().default(
|
|
39
|
-
maxImports: z.number().int().positive().default(
|
|
40
|
-
minTestRatio: z.number().min(0).max(1).default(0.
|
|
37
|
+
maxFunctionLines: z.number().int().positive().default(60),
|
|
38
|
+
maxFunctionParams: z.number().int().positive().default(5),
|
|
39
|
+
maxImports: z.number().int().positive().default(20),
|
|
40
|
+
minTestRatio: z.number().min(0).max(1).default(0.05)
|
|
41
41
|
}).partial();
|
|
42
42
|
var configSchema = z.object({
|
|
43
43
|
rules: rulesSchema,
|
|
@@ -516,6 +516,251 @@ var DeadCodeRule = class _DeadCodeRule extends BaseRule {
|
|
|
516
516
|
}
|
|
517
517
|
};
|
|
518
518
|
|
|
519
|
+
// src/rules/analysis-helpers.ts
|
|
520
|
+
function detectWebFramework(context) {
|
|
521
|
+
const { packageJson, files } = context;
|
|
522
|
+
if (!packageJson) return "unknown";
|
|
523
|
+
const deps = {
|
|
524
|
+
...packageJson.dependencies,
|
|
525
|
+
...packageJson.devDependencies
|
|
526
|
+
};
|
|
527
|
+
if ("next" in deps) {
|
|
528
|
+
const hasAppDir = files.some((f) => f.path.startsWith("app/") || f.path.includes("/app/"));
|
|
529
|
+
const hasPagesDir = files.some((f) => f.path.startsWith("pages/") || f.path.includes("/pages/"));
|
|
530
|
+
if (hasAppDir) return "nextjs-app";
|
|
531
|
+
if (hasPagesDir) return "nextjs-pages";
|
|
532
|
+
return "nextjs-app";
|
|
533
|
+
}
|
|
534
|
+
if ("@remix-run/react" in deps || "@remix-run/node" in deps) {
|
|
535
|
+
return "remix";
|
|
536
|
+
}
|
|
537
|
+
if ("@sveltejs/kit" in deps) {
|
|
538
|
+
return "sveltekit";
|
|
539
|
+
}
|
|
540
|
+
if ("nuxt" in deps || "nuxt3" in deps) {
|
|
541
|
+
return "nuxt";
|
|
542
|
+
}
|
|
543
|
+
if ("astro" in deps) {
|
|
544
|
+
return "astro";
|
|
545
|
+
}
|
|
546
|
+
if ("vite" in deps) {
|
|
547
|
+
if ("react" in deps) return "vite-react";
|
|
548
|
+
if ("vue" in deps) return "vite-vue";
|
|
549
|
+
}
|
|
550
|
+
if ("react-scripts" in deps) {
|
|
551
|
+
return "cra";
|
|
552
|
+
}
|
|
553
|
+
if ("express" in deps && !("react" in deps) && !("vue" in deps)) {
|
|
554
|
+
return "express";
|
|
555
|
+
}
|
|
556
|
+
return "unknown";
|
|
557
|
+
}
|
|
558
|
+
function isWebApplication(context) {
|
|
559
|
+
const framework = detectWebFramework(context);
|
|
560
|
+
if (framework === "express") return false;
|
|
561
|
+
if (framework === "unknown") {
|
|
562
|
+
const pkg = context.packageJson;
|
|
563
|
+
if (pkg) {
|
|
564
|
+
const isLibrary = ("main" in pkg || "exports" in pkg || "bin" in pkg) && !("private" in pkg && pkg.private === true);
|
|
565
|
+
if (isLibrary) return false;
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
function getLayoutFiles(context) {
|
|
572
|
+
const framework = detectWebFramework(context);
|
|
573
|
+
const { files } = context;
|
|
574
|
+
const layoutPatterns = [];
|
|
575
|
+
switch (framework) {
|
|
576
|
+
case "nextjs-app":
|
|
577
|
+
layoutPatterns.push(
|
|
578
|
+
/^app\/layout\.[jt]sx?$/,
|
|
579
|
+
/^src\/app\/layout\.[jt]sx?$/,
|
|
580
|
+
/app\/\([^)]+\)\/layout\.[jt]sx?$/
|
|
581
|
+
);
|
|
582
|
+
break;
|
|
583
|
+
case "nextjs-pages":
|
|
584
|
+
layoutPatterns.push(
|
|
585
|
+
/^pages\/_app\.[jt]sx?$/,
|
|
586
|
+
/^pages\/_document\.[jt]sx?$/,
|
|
587
|
+
/^src\/pages\/_app\.[jt]sx?$/
|
|
588
|
+
);
|
|
589
|
+
break;
|
|
590
|
+
case "remix":
|
|
591
|
+
layoutPatterns.push(
|
|
592
|
+
/^app\/root\.[jt]sx?$/
|
|
593
|
+
);
|
|
594
|
+
break;
|
|
595
|
+
case "sveltekit":
|
|
596
|
+
layoutPatterns.push(
|
|
597
|
+
/^src\/routes\/\+layout\.svelte$/,
|
|
598
|
+
/^src\/app\.html$/
|
|
599
|
+
);
|
|
600
|
+
break;
|
|
601
|
+
case "nuxt":
|
|
602
|
+
layoutPatterns.push(
|
|
603
|
+
/^layouts\/default\.vue$/,
|
|
604
|
+
/^app\.vue$/
|
|
605
|
+
);
|
|
606
|
+
break;
|
|
607
|
+
case "astro":
|
|
608
|
+
layoutPatterns.push(
|
|
609
|
+
/^src\/layouts\/.+\.astro$/
|
|
610
|
+
);
|
|
611
|
+
break;
|
|
612
|
+
case "vite-react":
|
|
613
|
+
case "cra":
|
|
614
|
+
layoutPatterns.push(
|
|
615
|
+
/^src\/App\.[jt]sx?$/,
|
|
616
|
+
/^src\/main\.[jt]sx?$/,
|
|
617
|
+
/^index\.html$/
|
|
618
|
+
);
|
|
619
|
+
break;
|
|
620
|
+
case "vite-vue":
|
|
621
|
+
layoutPatterns.push(
|
|
622
|
+
/^src\/App\.vue$/,
|
|
623
|
+
/^src\/main\.[jt]s$/,
|
|
624
|
+
/^index\.html$/
|
|
625
|
+
);
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
return files.filter((f) => layoutPatterns.some((p) => p.test(f.path)));
|
|
629
|
+
}
|
|
630
|
+
function isTestFile(filePath) {
|
|
631
|
+
return /\.(test|spec)\.[jt]sx?$/.test(filePath) || filePath.includes("__tests__");
|
|
632
|
+
}
|
|
633
|
+
function isStoryFile(filePath) {
|
|
634
|
+
return /\.stories\.[jt]sx?$/.test(filePath);
|
|
635
|
+
}
|
|
636
|
+
function getLineNumber(content, index) {
|
|
637
|
+
return content.slice(0, index).split("\n").length;
|
|
638
|
+
}
|
|
639
|
+
function hasTryCatch(code) {
|
|
640
|
+
return /\btry\s*\{/.test(code);
|
|
641
|
+
}
|
|
642
|
+
function hasAwait(code) {
|
|
643
|
+
return /\bawait\s+/.test(code);
|
|
644
|
+
}
|
|
645
|
+
function isInsideStringOrComment(content, index) {
|
|
646
|
+
const lineStart = content.lastIndexOf("\n", index) + 1;
|
|
647
|
+
const lineContent = content.slice(lineStart, index);
|
|
648
|
+
if (/\/\//.test(lineContent)) return true;
|
|
649
|
+
const lastBlockOpen = content.lastIndexOf("/*", index);
|
|
650
|
+
if (lastBlockOpen !== -1) {
|
|
651
|
+
const lastBlockClose = content.lastIndexOf("*/", index);
|
|
652
|
+
if (lastBlockClose < lastBlockOpen) return true;
|
|
653
|
+
}
|
|
654
|
+
const singleQuotes = (lineContent.match(/'/g) ?? []).length;
|
|
655
|
+
const doubleQuotes = (lineContent.match(/"/g) ?? []).length;
|
|
656
|
+
const backticks = (lineContent.match(/`/g) ?? []).length;
|
|
657
|
+
if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1 || backticks % 2 === 1) {
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
function buildStringRanges(content) {
|
|
663
|
+
const ranges = [];
|
|
664
|
+
let i = 0;
|
|
665
|
+
while (i < content.length) {
|
|
666
|
+
const ch = content[i];
|
|
667
|
+
const next = content[i + 1];
|
|
668
|
+
if (ch === "/" && next === "/") {
|
|
669
|
+
const start = i;
|
|
670
|
+
i += 2;
|
|
671
|
+
while (i < content.length && content[i] !== "\n") i++;
|
|
672
|
+
ranges.push({ start, end: i });
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (ch === "/" && next === "*") {
|
|
676
|
+
const start = i;
|
|
677
|
+
i += 2;
|
|
678
|
+
while (i < content.length - 1) {
|
|
679
|
+
if (content[i] === "*" && content[i + 1] === "/") {
|
|
680
|
+
i += 2;
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
i++;
|
|
684
|
+
}
|
|
685
|
+
ranges.push({ start, end: i });
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (ch === "'" || ch === '"') {
|
|
689
|
+
const quote = ch;
|
|
690
|
+
const start = i;
|
|
691
|
+
i++;
|
|
692
|
+
while (i < content.length) {
|
|
693
|
+
if (content[i] === "\\") {
|
|
694
|
+
i += 2;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (content[i] === quote) {
|
|
698
|
+
i++;
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
if (content[i] === "\n") break;
|
|
702
|
+
i++;
|
|
703
|
+
}
|
|
704
|
+
ranges.push({ start, end: i });
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (ch === "`") {
|
|
708
|
+
const start = i;
|
|
709
|
+
i++;
|
|
710
|
+
while (i < content.length) {
|
|
711
|
+
if (content[i] === "\\") {
|
|
712
|
+
i += 2;
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (content[i] === "`") {
|
|
716
|
+
i++;
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
i++;
|
|
720
|
+
}
|
|
721
|
+
ranges.push({ start, end: i });
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
i++;
|
|
725
|
+
}
|
|
726
|
+
return ranges;
|
|
727
|
+
}
|
|
728
|
+
function isInStringRange(ranges, index) {
|
|
729
|
+
let lo = 0;
|
|
730
|
+
let hi = ranges.length - 1;
|
|
731
|
+
while (lo <= hi) {
|
|
732
|
+
const mid = lo + hi >>> 1;
|
|
733
|
+
const range = ranges[mid];
|
|
734
|
+
if (index < range.start) {
|
|
735
|
+
hi = mid - 1;
|
|
736
|
+
} else if (index >= range.end) {
|
|
737
|
+
lo = mid + 1;
|
|
738
|
+
} else {
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
function extractFunctionBody(content, startIndex) {
|
|
745
|
+
let braceCount = 0;
|
|
746
|
+
let foundOpen = false;
|
|
747
|
+
let bodyStart = startIndex;
|
|
748
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
749
|
+
const char = content[i];
|
|
750
|
+
if (char === "{") {
|
|
751
|
+
if (!foundOpen) bodyStart = i;
|
|
752
|
+
braceCount++;
|
|
753
|
+
foundOpen = true;
|
|
754
|
+
} else if (char === "}") {
|
|
755
|
+
braceCount--;
|
|
756
|
+
if (foundOpen && braceCount === 0) {
|
|
757
|
+
return content.slice(bodyStart, i + 1);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
|
|
519
764
|
// src/rules/hardcoded-secrets.ts
|
|
520
765
|
var HardcodedSecretsRule = class _HardcodedSecretsRule extends BaseRule {
|
|
521
766
|
id = RULE_ID.HARDCODED_SECRETS;
|
|
@@ -644,19 +889,29 @@ var HardcodedSecretsRule = class _HardcodedSecretsRule extends BaseRule {
|
|
|
644
889
|
if (this.shouldSkipFile(file.path)) {
|
|
645
890
|
continue;
|
|
646
891
|
}
|
|
892
|
+
const ranges = buildStringRanges(file.content);
|
|
647
893
|
const lines = file.content.split("\n");
|
|
894
|
+
let lineStartIndex = 0;
|
|
648
895
|
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
649
896
|
const line = lines[lineIndex];
|
|
897
|
+
if (isInStringRange(ranges, lineStartIndex)) {
|
|
898
|
+
lineStartIndex += line.length + 1;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
650
901
|
if (this.isComment(line)) {
|
|
902
|
+
lineStartIndex += line.length + 1;
|
|
651
903
|
continue;
|
|
652
904
|
}
|
|
653
905
|
if (this.referencesEnvVar(line)) {
|
|
906
|
+
lineStartIndex += line.length + 1;
|
|
654
907
|
continue;
|
|
655
908
|
}
|
|
656
909
|
if (this.isExampleValue(line)) {
|
|
910
|
+
lineStartIndex += line.length + 1;
|
|
657
911
|
continue;
|
|
658
912
|
}
|
|
659
913
|
if (this.isJsxCodeExample(line)) {
|
|
914
|
+
lineStartIndex += line.length + 1;
|
|
660
915
|
continue;
|
|
661
916
|
}
|
|
662
917
|
for (const secretPattern of _HardcodedSecretsRule.SECRET_PATTERNS) {
|
|
@@ -675,6 +930,7 @@ var HardcodedSecretsRule = class _HardcodedSecretsRule extends BaseRule {
|
|
|
675
930
|
break;
|
|
676
931
|
}
|
|
677
932
|
}
|
|
933
|
+
lineStartIndex += line.length + 1;
|
|
678
934
|
}
|
|
679
935
|
}
|
|
680
936
|
return issues;
|
|
@@ -907,175 +1163,12 @@ var UnusedDepsRule = class _UnusedDepsRule extends BaseRule {
|
|
|
907
1163
|
}
|
|
908
1164
|
};
|
|
909
1165
|
|
|
910
|
-
// src/rules/analysis-helpers.ts
|
|
911
|
-
function detectWebFramework(context) {
|
|
912
|
-
const { packageJson, files } = context;
|
|
913
|
-
if (!packageJson) return "unknown";
|
|
914
|
-
const deps = {
|
|
915
|
-
...packageJson.dependencies,
|
|
916
|
-
...packageJson.devDependencies
|
|
917
|
-
};
|
|
918
|
-
if ("next" in deps) {
|
|
919
|
-
const hasAppDir = files.some((f) => f.path.startsWith("app/") || f.path.includes("/app/"));
|
|
920
|
-
const hasPagesDir = files.some((f) => f.path.startsWith("pages/") || f.path.includes("/pages/"));
|
|
921
|
-
if (hasAppDir) return "nextjs-app";
|
|
922
|
-
if (hasPagesDir) return "nextjs-pages";
|
|
923
|
-
return "nextjs-app";
|
|
924
|
-
}
|
|
925
|
-
if ("@remix-run/react" in deps || "@remix-run/node" in deps) {
|
|
926
|
-
return "remix";
|
|
927
|
-
}
|
|
928
|
-
if ("@sveltejs/kit" in deps) {
|
|
929
|
-
return "sveltekit";
|
|
930
|
-
}
|
|
931
|
-
if ("nuxt" in deps || "nuxt3" in deps) {
|
|
932
|
-
return "nuxt";
|
|
933
|
-
}
|
|
934
|
-
if ("astro" in deps) {
|
|
935
|
-
return "astro";
|
|
936
|
-
}
|
|
937
|
-
if ("vite" in deps) {
|
|
938
|
-
if ("react" in deps) return "vite-react";
|
|
939
|
-
if ("vue" in deps) return "vite-vue";
|
|
940
|
-
}
|
|
941
|
-
if ("react-scripts" in deps) {
|
|
942
|
-
return "cra";
|
|
943
|
-
}
|
|
944
|
-
if ("express" in deps && !("react" in deps) && !("vue" in deps)) {
|
|
945
|
-
return "express";
|
|
946
|
-
}
|
|
947
|
-
return "unknown";
|
|
948
|
-
}
|
|
949
|
-
function isWebApplication(context) {
|
|
950
|
-
const framework = detectWebFramework(context);
|
|
951
|
-
if (framework === "express") return false;
|
|
952
|
-
if (framework === "unknown") {
|
|
953
|
-
const pkg = context.packageJson;
|
|
954
|
-
if (pkg) {
|
|
955
|
-
const isLibrary = ("main" in pkg || "exports" in pkg || "bin" in pkg) && !("private" in pkg && pkg.private === true);
|
|
956
|
-
if (isLibrary) return false;
|
|
957
|
-
}
|
|
958
|
-
return false;
|
|
959
|
-
}
|
|
960
|
-
return true;
|
|
961
|
-
}
|
|
962
|
-
function getLayoutFiles(context) {
|
|
963
|
-
const framework = detectWebFramework(context);
|
|
964
|
-
const { files } = context;
|
|
965
|
-
const layoutPatterns = [];
|
|
966
|
-
switch (framework) {
|
|
967
|
-
case "nextjs-app":
|
|
968
|
-
layoutPatterns.push(
|
|
969
|
-
/^app\/layout\.[jt]sx?$/,
|
|
970
|
-
/^src\/app\/layout\.[jt]sx?$/,
|
|
971
|
-
/app\/\([^)]+\)\/layout\.[jt]sx?$/
|
|
972
|
-
);
|
|
973
|
-
break;
|
|
974
|
-
case "nextjs-pages":
|
|
975
|
-
layoutPatterns.push(
|
|
976
|
-
/^pages\/_app\.[jt]sx?$/,
|
|
977
|
-
/^pages\/_document\.[jt]sx?$/,
|
|
978
|
-
/^src\/pages\/_app\.[jt]sx?$/
|
|
979
|
-
);
|
|
980
|
-
break;
|
|
981
|
-
case "remix":
|
|
982
|
-
layoutPatterns.push(
|
|
983
|
-
/^app\/root\.[jt]sx?$/
|
|
984
|
-
);
|
|
985
|
-
break;
|
|
986
|
-
case "sveltekit":
|
|
987
|
-
layoutPatterns.push(
|
|
988
|
-
/^src\/routes\/\+layout\.svelte$/,
|
|
989
|
-
/^src\/app\.html$/
|
|
990
|
-
);
|
|
991
|
-
break;
|
|
992
|
-
case "nuxt":
|
|
993
|
-
layoutPatterns.push(
|
|
994
|
-
/^layouts\/default\.vue$/,
|
|
995
|
-
/^app\.vue$/
|
|
996
|
-
);
|
|
997
|
-
break;
|
|
998
|
-
case "astro":
|
|
999
|
-
layoutPatterns.push(
|
|
1000
|
-
/^src\/layouts\/.+\.astro$/
|
|
1001
|
-
);
|
|
1002
|
-
break;
|
|
1003
|
-
case "vite-react":
|
|
1004
|
-
case "cra":
|
|
1005
|
-
layoutPatterns.push(
|
|
1006
|
-
/^src\/App\.[jt]sx?$/,
|
|
1007
|
-
/^src\/main\.[jt]sx?$/,
|
|
1008
|
-
/^index\.html$/
|
|
1009
|
-
);
|
|
1010
|
-
break;
|
|
1011
|
-
case "vite-vue":
|
|
1012
|
-
layoutPatterns.push(
|
|
1013
|
-
/^src\/App\.vue$/,
|
|
1014
|
-
/^src\/main\.[jt]s$/,
|
|
1015
|
-
/^index\.html$/
|
|
1016
|
-
);
|
|
1017
|
-
break;
|
|
1018
|
-
}
|
|
1019
|
-
return files.filter((f) => layoutPatterns.some((p) => p.test(f.path)));
|
|
1020
|
-
}
|
|
1021
|
-
function isTestFile(filePath) {
|
|
1022
|
-
return /\.(test|spec)\.[jt]sx?$/.test(filePath) || filePath.includes("__tests__");
|
|
1023
|
-
}
|
|
1024
|
-
function isStoryFile(filePath) {
|
|
1025
|
-
return /\.stories\.[jt]sx?$/.test(filePath);
|
|
1026
|
-
}
|
|
1027
|
-
function getLineNumber(content, index) {
|
|
1028
|
-
return content.slice(0, index).split("\n").length;
|
|
1029
|
-
}
|
|
1030
|
-
function hasTryCatch(code) {
|
|
1031
|
-
return /\btry\s*\{/.test(code);
|
|
1032
|
-
}
|
|
1033
|
-
function hasAwait(code) {
|
|
1034
|
-
return /\bawait\s+/.test(code);
|
|
1035
|
-
}
|
|
1036
|
-
function isInsideStringOrComment(content, index) {
|
|
1037
|
-
const lineStart = content.lastIndexOf("\n", index) + 1;
|
|
1038
|
-
const lineContent = content.slice(lineStart, index);
|
|
1039
|
-
if (/\/\//.test(lineContent)) return true;
|
|
1040
|
-
const lastBlockOpen = content.lastIndexOf("/*", index);
|
|
1041
|
-
if (lastBlockOpen !== -1) {
|
|
1042
|
-
const lastBlockClose = content.lastIndexOf("*/", index);
|
|
1043
|
-
if (lastBlockClose < lastBlockOpen) return true;
|
|
1044
|
-
}
|
|
1045
|
-
const singleQuotes = (lineContent.match(/'/g) ?? []).length;
|
|
1046
|
-
const doubleQuotes = (lineContent.match(/"/g) ?? []).length;
|
|
1047
|
-
const backticks = (lineContent.match(/`/g) ?? []).length;
|
|
1048
|
-
if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1 || backticks % 2 === 1) {
|
|
1049
|
-
return true;
|
|
1050
|
-
}
|
|
1051
|
-
return false;
|
|
1052
|
-
}
|
|
1053
|
-
function extractFunctionBody(content, startIndex) {
|
|
1054
|
-
let braceCount = 0;
|
|
1055
|
-
let foundOpen = false;
|
|
1056
|
-
let bodyStart = startIndex;
|
|
1057
|
-
for (let i = startIndex; i < content.length; i++) {
|
|
1058
|
-
const char = content[i];
|
|
1059
|
-
if (char === "{") {
|
|
1060
|
-
if (!foundOpen) bodyStart = i;
|
|
1061
|
-
braceCount++;
|
|
1062
|
-
foundOpen = true;
|
|
1063
|
-
} else if (char === "}") {
|
|
1064
|
-
braceCount--;
|
|
1065
|
-
if (foundOpen && braceCount === 0) {
|
|
1066
|
-
return content.slice(bodyStart, i + 1);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
return null;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
1166
|
// src/rules/missing-tests.ts
|
|
1074
1167
|
var MissingTestsRule = class _MissingTestsRule extends BaseRule {
|
|
1075
1168
|
id = RULE_ID.MISSING_TESTS;
|
|
1076
1169
|
name = "Missing Tests";
|
|
1077
1170
|
description = "Detects projects with no tests, low coverage, or low-quality tests";
|
|
1078
|
-
defaultSeverity = SEVERITY.
|
|
1171
|
+
defaultSeverity = SEVERITY.HIGH;
|
|
1079
1172
|
/** Patterns that identify test files */
|
|
1080
1173
|
static TEST_FILE_PATTERNS = [
|
|
1081
1174
|
/\.test\.[jt]sx?$/,
|
|
@@ -1935,11 +2028,12 @@ var MissingErrorHandlingRule = class extends BaseRule {
|
|
|
1935
2028
|
const issues = [];
|
|
1936
2029
|
for (const file of files) {
|
|
1937
2030
|
if (isTestFile(file.path)) continue;
|
|
1938
|
-
|
|
1939
|
-
issues.push(...this.
|
|
1940
|
-
issues.push(...this.
|
|
1941
|
-
issues.push(...this.
|
|
1942
|
-
issues.push(...this.
|
|
2031
|
+
const ranges = buildStringRanges(file.content);
|
|
2032
|
+
issues.push(...this.checkUnhandledAsync(file, ranges));
|
|
2033
|
+
issues.push(...this.checkUnhandledFetch(file, ranges));
|
|
2034
|
+
issues.push(...this.checkUnhandledPromiseChains(file, ranges));
|
|
2035
|
+
issues.push(...this.checkEmptyCatchBlocks(file, ranges));
|
|
2036
|
+
issues.push(...this.checkSwallowedErrors(file, ranges));
|
|
1943
2037
|
issues.push(...this.checkMissingErrorBoundary(files, context));
|
|
1944
2038
|
}
|
|
1945
2039
|
return this.deduplicateProjectIssues(issues);
|
|
@@ -1947,11 +2041,12 @@ var MissingErrorHandlingRule = class extends BaseRule {
|
|
|
1947
2041
|
// ---------------------------------------------------------------------------
|
|
1948
2042
|
// Unhandled Async Functions
|
|
1949
2043
|
// ---------------------------------------------------------------------------
|
|
1950
|
-
checkUnhandledAsync(file) {
|
|
2044
|
+
checkUnhandledAsync(file, ranges) {
|
|
1951
2045
|
const issues = [];
|
|
1952
2046
|
const asyncFuncPattern = /^(\s*)(?:export\s+)?async\s+function\s+(\w+)/gm;
|
|
1953
2047
|
let match;
|
|
1954
2048
|
while ((match = asyncFuncPattern.exec(file.content)) !== null) {
|
|
2049
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
1955
2050
|
const funcName = match[2];
|
|
1956
2051
|
const funcBody = extractFunctionBody(file.content, match.index);
|
|
1957
2052
|
if (funcBody && !hasTryCatch(funcBody) && hasAwait(funcBody)) {
|
|
@@ -1960,6 +2055,7 @@ var MissingErrorHandlingRule = class extends BaseRule {
|
|
|
1960
2055
|
}
|
|
1961
2056
|
const asyncArrowPattern = /(?:const|let|var)\s+(\w+)\s*=\s*async\s*\(/g;
|
|
1962
2057
|
while ((match = asyncArrowPattern.exec(file.content)) !== null) {
|
|
2058
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
1963
2059
|
const funcName = match[1];
|
|
1964
2060
|
const funcBody = extractFunctionBody(file.content, match.index);
|
|
1965
2061
|
if (funcBody && !hasTryCatch(funcBody) && hasAwait(funcBody)) {
|
|
@@ -1980,11 +2076,12 @@ var MissingErrorHandlingRule = class extends BaseRule {
|
|
|
1980
2076
|
// ---------------------------------------------------------------------------
|
|
1981
2077
|
// Unhandled Fetch/HTTP Calls
|
|
1982
2078
|
// ---------------------------------------------------------------------------
|
|
1983
|
-
checkUnhandledFetch(file) {
|
|
2079
|
+
checkUnhandledFetch(file, ranges) {
|
|
1984
2080
|
const issues = [];
|
|
1985
2081
|
const fetchPattern = /\b(fetch|axios\.(?:get|post|put|patch|delete))\s*\(/g;
|
|
1986
2082
|
let match;
|
|
1987
2083
|
while ((match = fetchPattern.exec(file.content)) !== null) {
|
|
2084
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
1988
2085
|
const callName = match[1];
|
|
1989
2086
|
const callIndex = match.index;
|
|
1990
2087
|
const surroundingCode = file.content.slice(Math.max(0, callIndex - 500), callIndex);
|
|
@@ -2007,12 +2104,12 @@ var MissingErrorHandlingRule = class extends BaseRule {
|
|
|
2007
2104
|
// ---------------------------------------------------------------------------
|
|
2008
2105
|
// Unhandled Promise Chains
|
|
2009
2106
|
// ---------------------------------------------------------------------------
|
|
2010
|
-
checkUnhandledPromiseChains(file) {
|
|
2107
|
+
checkUnhandledPromiseChains(file, ranges) {
|
|
2011
2108
|
const issues = [];
|
|
2012
2109
|
const thenPattern = /\.then\s*\([^)]*\)(?:\s*\.then\s*\([^)]*\))*/g;
|
|
2013
2110
|
let match;
|
|
2014
2111
|
while ((match = thenPattern.exec(file.content)) !== null) {
|
|
2015
|
-
if (
|
|
2112
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
2016
2113
|
const chainEnd = match.index + match[0].length;
|
|
2017
2114
|
const afterChain = file.content.slice(chainEnd, chainEnd + 50);
|
|
2018
2115
|
if (!/^\s*\.catch\s*\(/.test(afterChain)) {
|
|
@@ -2038,12 +2135,12 @@ var MissingErrorHandlingRule = class extends BaseRule {
|
|
|
2038
2135
|
* - Console-only catch: catch(e) { console.log(e) }
|
|
2039
2136
|
* - Generic catch without re-throw or reporting
|
|
2040
2137
|
*/
|
|
2041
|
-
checkEmptyCatchBlocks(file) {
|
|
2138
|
+
checkEmptyCatchBlocks(file, ranges) {
|
|
2042
2139
|
const issues = [];
|
|
2043
2140
|
const catchPattern = /\bcatch\s*\(\s*(\w+)?\s*\)\s*\{/g;
|
|
2044
2141
|
let match;
|
|
2045
2142
|
while ((match = catchPattern.exec(file.content)) !== null) {
|
|
2046
|
-
if (
|
|
2143
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
2047
2144
|
const catchStart = match.index + match[0].length;
|
|
2048
2145
|
const catchBody = this.extractCatchBody(file.content, catchStart);
|
|
2049
2146
|
if (!catchBody) continue;
|
|
@@ -2110,7 +2207,7 @@ var MissingErrorHandlingRule = class extends BaseRule {
|
|
|
2110
2207
|
* - .catch(() => undefined)
|
|
2111
2208
|
* - .catch(e => console.log(e))
|
|
2112
2209
|
*/
|
|
2113
|
-
checkSwallowedErrors(file) {
|
|
2210
|
+
checkSwallowedErrors(file, ranges) {
|
|
2114
2211
|
const issues = [];
|
|
2115
2212
|
const swallowedPatterns = [
|
|
2116
2213
|
// .catch(() => {}) or .catch(() => { })
|
|
@@ -2123,7 +2220,7 @@ var MissingErrorHandlingRule = class extends BaseRule {
|
|
|
2123
2220
|
for (const pattern of swallowedPatterns) {
|
|
2124
2221
|
let match2;
|
|
2125
2222
|
while ((match2 = pattern.exec(file.content)) !== null) {
|
|
2126
|
-
if (
|
|
2223
|
+
if (isInStringRange(ranges, match2.index)) continue;
|
|
2127
2224
|
issues.push(
|
|
2128
2225
|
this.createIssue({
|
|
2129
2226
|
severity: SEVERITY.HIGH,
|
|
@@ -2140,7 +2237,7 @@ var MissingErrorHandlingRule = class extends BaseRule {
|
|
|
2140
2237
|
const consoleOnlyCatchPattern = /\.catch\s*\(\s*\(?\s*(\w+)\s*\)?\s*=>\s*\{?\s*console\.(log|error|warn)\s*\(\s*\1\s*\)\s*;?\s*\}?\s*\)/g;
|
|
2141
2238
|
let match;
|
|
2142
2239
|
while ((match = consoleOnlyCatchPattern.exec(file.content)) !== null) {
|
|
2143
|
-
if (
|
|
2240
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
2144
2241
|
issues.push(
|
|
2145
2242
|
this.createIssue({
|
|
2146
2243
|
severity: SEVERITY.MEDIUM,
|
|
@@ -2207,6 +2304,7 @@ var AbandonedTodoRule = class _AbandonedTodoRule extends BaseRule {
|
|
|
2207
2304
|
const issues = [];
|
|
2208
2305
|
for (const file of context.files) {
|
|
2209
2306
|
if (isTestFile(file.path)) continue;
|
|
2307
|
+
const ranges = buildStringRanges(file.content);
|
|
2210
2308
|
const contentWithoutJsdoc = file.content.replace(
|
|
2211
2309
|
/\/\*\*[\s\S]*?\*\//g,
|
|
2212
2310
|
(match2) => "\n".repeat((match2.match(/\n/g) ?? []).length)
|
|
@@ -2217,6 +2315,7 @@ var AbandonedTodoRule = class _AbandonedTodoRule extends BaseRule {
|
|
|
2217
2315
|
);
|
|
2218
2316
|
let match;
|
|
2219
2317
|
while ((match = regex.exec(contentWithoutJsdoc)) !== null) {
|
|
2318
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
2220
2319
|
const keyword = match[1] ?? "TODO";
|
|
2221
2320
|
const comment = match[2]?.trim() ?? "";
|
|
2222
2321
|
const line = getLineNumber(contentWithoutJsdoc, match.index);
|
|
@@ -2794,31 +2893,34 @@ var ObsoletePatternsRule = class _ObsoletePatternsRule extends BaseRule {
|
|
|
2794
2893
|
const issues = [];
|
|
2795
2894
|
for (const file of context.files) {
|
|
2796
2895
|
if (isTestFile(file.path) || _ObsoletePatternsRule.CONFIG_PATTERNS.test(file.path)) continue;
|
|
2797
|
-
|
|
2798
|
-
this.
|
|
2799
|
-
this.
|
|
2800
|
-
this.
|
|
2801
|
-
this.
|
|
2896
|
+
const ranges = buildStringRanges(file.content);
|
|
2897
|
+
this.checkVarDeclarations(file, issues, ranges);
|
|
2898
|
+
this.checkCallbackHell(file, issues, ranges);
|
|
2899
|
+
this.checkClassComponents(file, issues, ranges);
|
|
2900
|
+
this.checkCommonJsInTs(file, issues, ranges);
|
|
2901
|
+
this.checkLegacyApis(file, issues, ranges);
|
|
2802
2902
|
}
|
|
2803
2903
|
return issues;
|
|
2804
2904
|
}
|
|
2805
2905
|
/** `var` declarations — should be const/let */
|
|
2806
|
-
checkVarDeclarations(file, issues) {
|
|
2807
|
-
const
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
if (
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2906
|
+
checkVarDeclarations(file, issues, ranges) {
|
|
2907
|
+
const regex = /\bvar\s+\w+/g;
|
|
2908
|
+
let match;
|
|
2909
|
+
while ((match = regex.exec(file.content)) !== null) {
|
|
2910
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
2911
|
+
const line = getLineNumber(file.content, match.index);
|
|
2912
|
+
const lineContent = file.content.split("\n")[line - 1]?.trim() ?? match[0];
|
|
2913
|
+
if (lineContent.startsWith("//") || lineContent.startsWith("*") || lineContent.startsWith("/*")) continue;
|
|
2914
|
+
issues.push(this.createObsoleteIssue(file.path, line, lineContent, "var", "const/let"));
|
|
2815
2915
|
}
|
|
2816
2916
|
}
|
|
2817
2917
|
/** .then() chains with 3+ nesting levels — should be async/await */
|
|
2818
|
-
checkCallbackHell(file, issues) {
|
|
2819
|
-
const thenChain = /\.then\([^)]*\)\s*\.then\([^)]*\)\s*\.then\(
|
|
2820
|
-
|
|
2821
|
-
|
|
2918
|
+
checkCallbackHell(file, issues, ranges) {
|
|
2919
|
+
const thenChain = /\.then\([^)]*\)\s*\.then\([^)]*\)\s*\.then\(/g;
|
|
2920
|
+
let match;
|
|
2921
|
+
while ((match = thenChain.exec(file.content)) !== null) {
|
|
2922
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
2923
|
+
const line = getLineNumber(file.content, match.index);
|
|
2822
2924
|
issues.push(
|
|
2823
2925
|
this.createIssue({
|
|
2824
2926
|
message: "Promise chain with 3+ .then() calls detected",
|
|
@@ -2828,14 +2930,16 @@ var ObsoletePatternsRule = class _ObsoletePatternsRule extends BaseRule {
|
|
|
2828
2930
|
learnMoreUrl: "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises"
|
|
2829
2931
|
})
|
|
2830
2932
|
);
|
|
2933
|
+
break;
|
|
2831
2934
|
}
|
|
2832
2935
|
}
|
|
2833
2936
|
/** React class components — should be function components */
|
|
2834
|
-
checkClassComponents(file, issues) {
|
|
2937
|
+
checkClassComponents(file, issues, ranges) {
|
|
2835
2938
|
if (file.extension !== ".tsx" && file.extension !== ".jsx") return;
|
|
2836
2939
|
const pattern = /extends\s+(?:React\.)?Component\b/g;
|
|
2837
2940
|
let match;
|
|
2838
2941
|
while ((match = pattern.exec(file.content)) !== null) {
|
|
2942
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
2839
2943
|
const line = getLineNumber(file.content, match.index);
|
|
2840
2944
|
issues.push(
|
|
2841
2945
|
this.createIssue({
|
|
@@ -2849,7 +2953,7 @@ var ObsoletePatternsRule = class _ObsoletePatternsRule extends BaseRule {
|
|
|
2849
2953
|
}
|
|
2850
2954
|
}
|
|
2851
2955
|
/** require() or module.exports in .ts/.tsx files */
|
|
2852
|
-
checkCommonJsInTs(file, issues) {
|
|
2956
|
+
checkCommonJsInTs(file, issues, ranges) {
|
|
2853
2957
|
if (file.extension !== ".ts" && file.extension !== ".tsx") return;
|
|
2854
2958
|
const patterns = [
|
|
2855
2959
|
{ regex: /\brequire\s*\(/g, name: "require()" },
|
|
@@ -2858,7 +2962,7 @@ var ObsoletePatternsRule = class _ObsoletePatternsRule extends BaseRule {
|
|
|
2858
2962
|
for (const { regex, name } of patterns) {
|
|
2859
2963
|
let match;
|
|
2860
2964
|
while ((match = regex.exec(file.content)) !== null) {
|
|
2861
|
-
if (
|
|
2965
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
2862
2966
|
const line = getLineNumber(file.content, match.index);
|
|
2863
2967
|
issues.push(
|
|
2864
2968
|
this.createObsoleteIssue(file.path, line, name, "CommonJS (require/module.exports)", "ES modules (import/export)")
|
|
@@ -2867,7 +2971,7 @@ var ObsoletePatternsRule = class _ObsoletePatternsRule extends BaseRule {
|
|
|
2867
2971
|
}
|
|
2868
2972
|
}
|
|
2869
2973
|
/** Legacy APIs: arguments keyword, new Array(), new Object() */
|
|
2870
|
-
checkLegacyApis(file, issues) {
|
|
2974
|
+
checkLegacyApis(file, issues, ranges) {
|
|
2871
2975
|
const legacyPatterns = [
|
|
2872
2976
|
{ regex: /\barguments\b/g, old: "arguments keyword", modern: "rest parameters (...args)" },
|
|
2873
2977
|
{ regex: /\bnew\s+Array\s*\(/g, old: "new Array()", modern: "array literal []" },
|
|
@@ -2876,7 +2980,7 @@ var ObsoletePatternsRule = class _ObsoletePatternsRule extends BaseRule {
|
|
|
2876
2980
|
for (const { regex, old, modern } of legacyPatterns) {
|
|
2877
2981
|
let match;
|
|
2878
2982
|
while ((match = regex.exec(file.content)) !== null) {
|
|
2879
|
-
if (
|
|
2983
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
2880
2984
|
const line = getLineNumber(file.content, match.index);
|
|
2881
2985
|
issues.push(this.createObsoleteIssue(file.path, line, match[0].trim(), old, modern));
|
|
2882
2986
|
}
|
|
@@ -2897,6 +3001,8 @@ var ObsoletePatternsRule = class _ObsoletePatternsRule extends BaseRule {
|
|
|
2897
3001
|
// src/rules/ai-smell.ts
|
|
2898
3002
|
var AiSmellRule = class _AiSmellRule extends BaseRule {
|
|
2899
3003
|
id = RULE_ID.AI_SMELL;
|
|
3004
|
+
/** Pre-computed string/comment ranges for the current file */
|
|
3005
|
+
currentRanges = [];
|
|
2900
3006
|
name = "AI Smell";
|
|
2901
3007
|
description = "Detects signs of unreviewed AI-generated code";
|
|
2902
3008
|
defaultSeverity = SEVERITY.LOW;
|
|
@@ -3021,6 +3127,7 @@ var AiSmellRule = class _AiSmellRule extends BaseRule {
|
|
|
3021
3127
|
if (![".ts", ".tsx", ".js", ".jsx"].includes(file.extension)) continue;
|
|
3022
3128
|
this.checkGenericFileName(file, issues);
|
|
3023
3129
|
if (isTestFile(file.path)) continue;
|
|
3130
|
+
this.currentRanges = buildStringRanges(file.content);
|
|
3024
3131
|
this.checkObviousComments(file, issues);
|
|
3025
3132
|
this.checkConsoleLog(file, issues);
|
|
3026
3133
|
this.checkGenericNaming(file, issues);
|
|
@@ -3072,6 +3179,7 @@ var AiSmellRule = class _AiSmellRule extends BaseRule {
|
|
|
3072
3179
|
const regex = /\bconsole\.log\s*\(/g;
|
|
3073
3180
|
let match;
|
|
3074
3181
|
while ((match = regex.exec(file.content)) !== null) {
|
|
3182
|
+
if (isInStringRange(this.currentRanges, match.index)) continue;
|
|
3075
3183
|
const line = getLineNumber(file.content, match.index);
|
|
3076
3184
|
issues.push(this.createIssue({
|
|
3077
3185
|
severity: SEVERITY.MEDIUM,
|
|
@@ -3087,6 +3195,7 @@ var AiSmellRule = class _AiSmellRule extends BaseRule {
|
|
|
3087
3195
|
const regex = /\b(?:const|let|var)\s+(\w+)\s*=/g;
|
|
3088
3196
|
let match;
|
|
3089
3197
|
while ((match = regex.exec(file.content)) !== null) {
|
|
3198
|
+
if (isInStringRange(this.currentRanges, match.index)) continue;
|
|
3090
3199
|
const name = match[1];
|
|
3091
3200
|
if (_AiSmellRule.GENERIC_NAMES.has(name)) {
|
|
3092
3201
|
const beforeMatch = file.content.slice(Math.max(0, match.index - 20), match.index);
|
|
@@ -3137,6 +3246,7 @@ var AiSmellRule = class _AiSmellRule extends BaseRule {
|
|
|
3137
3246
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
3138
3247
|
let match;
|
|
3139
3248
|
while ((match = regex.exec(file.content)) !== null) {
|
|
3249
|
+
if (isInStringRange(this.currentRanges, match.index)) continue;
|
|
3140
3250
|
const line = getLineNumber(file.content, match.index);
|
|
3141
3251
|
const snippet = match[0].slice(0, 60) + (match[0].length > 60 ? "..." : "");
|
|
3142
3252
|
issues.push(this.createIssue({
|
|
@@ -3159,6 +3269,7 @@ var AiSmellRule = class _AiSmellRule extends BaseRule {
|
|
|
3159
3269
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
3160
3270
|
let match;
|
|
3161
3271
|
while ((match = regex.exec(file.content)) !== null) {
|
|
3272
|
+
if (isInStringRange(this.currentRanges, match.index)) continue;
|
|
3162
3273
|
const line = getLineNumber(file.content, match.index);
|
|
3163
3274
|
issues.push(this.createIssue({
|
|
3164
3275
|
severity: SEVERITY.LOW,
|
|
@@ -3180,6 +3291,7 @@ var AiSmellRule = class _AiSmellRule extends BaseRule {
|
|
|
3180
3291
|
const regex = new RegExp(pattern.source, pattern.flags);
|
|
3181
3292
|
let match;
|
|
3182
3293
|
while ((match = regex.exec(file.content)) !== null) {
|
|
3294
|
+
if (isInStringRange(this.currentRanges, match.index)) continue;
|
|
3183
3295
|
const line = getLineNumber(file.content, match.index);
|
|
3184
3296
|
issues.push(this.createIssue({
|
|
3185
3297
|
severity: SEVERITY.MEDIUM,
|
|
@@ -3487,10 +3599,11 @@ var SecurityAntipatternsRule = class extends BaseRule {
|
|
|
3487
3599
|
const issues = [];
|
|
3488
3600
|
for (const file of files) {
|
|
3489
3601
|
if (isTestFile(file.path)) continue;
|
|
3490
|
-
|
|
3491
|
-
issues.push(...this.
|
|
3492
|
-
issues.push(...this.
|
|
3493
|
-
issues.push(...this.
|
|
3602
|
+
const ranges = buildStringRanges(file.content);
|
|
3603
|
+
issues.push(...this.checkSqlInjection(file, ranges));
|
|
3604
|
+
issues.push(...this.checkCorsWildcard(file, ranges));
|
|
3605
|
+
issues.push(...this.checkLocalStorageSensitive(file, ranges));
|
|
3606
|
+
issues.push(...this.checkDangerousHtml(file, ranges));
|
|
3494
3607
|
issues.push(...this.checkEvalUsage(file));
|
|
3495
3608
|
issues.push(...this.checkInsecureRandomness(file));
|
|
3496
3609
|
issues.push(...this.checkHardcodedCredentials(file));
|
|
@@ -3506,7 +3619,7 @@ var SecurityAntipatternsRule = class extends BaseRule {
|
|
|
3506
3619
|
* - Template literals in SQL queries
|
|
3507
3620
|
* - String concatenation in SQL queries
|
|
3508
3621
|
*/
|
|
3509
|
-
checkSqlInjection(file) {
|
|
3622
|
+
checkSqlInjection(file, ranges) {
|
|
3510
3623
|
const issues = [];
|
|
3511
3624
|
const lines = file.content.split("\n");
|
|
3512
3625
|
const sqlPatterns = [
|
|
@@ -3529,9 +3642,17 @@ var SecurityAntipatternsRule = class extends BaseRule {
|
|
|
3529
3642
|
/\bEXEC(UTE)?\s+\w+/i
|
|
3530
3643
|
// EXEC/EXECUTE procedure
|
|
3531
3644
|
];
|
|
3645
|
+
let lineStartIndex = 0;
|
|
3532
3646
|
for (let i = 0; i < lines.length; i++) {
|
|
3533
3647
|
const line = lines[i];
|
|
3534
|
-
if (
|
|
3648
|
+
if (isInStringRange(ranges, lineStartIndex)) {
|
|
3649
|
+
lineStartIndex += line.length + 1;
|
|
3650
|
+
continue;
|
|
3651
|
+
}
|
|
3652
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*")) {
|
|
3653
|
+
lineStartIndex += line.length + 1;
|
|
3654
|
+
continue;
|
|
3655
|
+
}
|
|
3535
3656
|
const templateMatch = /`([^`]*\$\{[^}]+\}[^`]*)`/.exec(line);
|
|
3536
3657
|
if (templateMatch) {
|
|
3537
3658
|
const templateContent = templateMatch[1];
|
|
@@ -3568,6 +3689,7 @@ var SecurityAntipatternsRule = class extends BaseRule {
|
|
|
3568
3689
|
break;
|
|
3569
3690
|
}
|
|
3570
3691
|
}
|
|
3692
|
+
lineStartIndex += line.length + 1;
|
|
3571
3693
|
}
|
|
3572
3694
|
return issues;
|
|
3573
3695
|
}
|
|
@@ -3577,7 +3699,7 @@ var SecurityAntipatternsRule = class extends BaseRule {
|
|
|
3577
3699
|
/**
|
|
3578
3700
|
* Detects CORS wildcard configuration outside test files
|
|
3579
3701
|
*/
|
|
3580
|
-
checkCorsWildcard(file) {
|
|
3702
|
+
checkCorsWildcard(file, ranges) {
|
|
3581
3703
|
const issues = [];
|
|
3582
3704
|
if (/\.(md|txt|json)$/.test(file.path)) return [];
|
|
3583
3705
|
const patterns = [
|
|
@@ -3593,6 +3715,7 @@ var SecurityAntipatternsRule = class extends BaseRule {
|
|
|
3593
3715
|
for (const pattern of patterns) {
|
|
3594
3716
|
const match = pattern.exec(file.content);
|
|
3595
3717
|
if (match) {
|
|
3718
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
3596
3719
|
const line = getLineNumber(file.content, match.index);
|
|
3597
3720
|
issues.push(
|
|
3598
3721
|
this.createIssue({
|
|
@@ -3615,7 +3738,7 @@ var SecurityAntipatternsRule = class extends BaseRule {
|
|
|
3615
3738
|
/**
|
|
3616
3739
|
* Detects storing sensitive data in localStorage
|
|
3617
3740
|
*/
|
|
3618
|
-
checkLocalStorageSensitive(file) {
|
|
3741
|
+
checkLocalStorageSensitive(file, ranges) {
|
|
3619
3742
|
const issues = [];
|
|
3620
3743
|
const sensitiveKeys = [
|
|
3621
3744
|
"token",
|
|
@@ -3636,6 +3759,7 @@ var SecurityAntipatternsRule = class extends BaseRule {
|
|
|
3636
3759
|
const pattern = /localStorage\.setItem\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
3637
3760
|
let match;
|
|
3638
3761
|
while ((match = pattern.exec(file.content)) !== null) {
|
|
3762
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
3639
3763
|
const key = match[1].toLowerCase();
|
|
3640
3764
|
if (sensitiveKeys.some((sensitive) => key.includes(sensitive))) {
|
|
3641
3765
|
issues.push(
|
|
@@ -3659,12 +3783,20 @@ var SecurityAntipatternsRule = class extends BaseRule {
|
|
|
3659
3783
|
/**
|
|
3660
3784
|
* Detects dangerous HTML injection patterns
|
|
3661
3785
|
*/
|
|
3662
|
-
checkDangerousHtml(file) {
|
|
3786
|
+
checkDangerousHtml(file, ranges) {
|
|
3663
3787
|
const issues = [];
|
|
3664
3788
|
const lines = file.content.split("\n");
|
|
3789
|
+
let lineStartIndex = 0;
|
|
3665
3790
|
for (let i = 0; i < lines.length; i++) {
|
|
3666
3791
|
const line = lines[i];
|
|
3667
|
-
if (
|
|
3792
|
+
if (isInStringRange(ranges, lineStartIndex)) {
|
|
3793
|
+
lineStartIndex += line.length + 1;
|
|
3794
|
+
continue;
|
|
3795
|
+
}
|
|
3796
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*")) {
|
|
3797
|
+
lineStartIndex += line.length + 1;
|
|
3798
|
+
continue;
|
|
3799
|
+
}
|
|
3668
3800
|
if (/\.innerHTML\s*=\s*(?!['"`]<)/.test(line)) {
|
|
3669
3801
|
if (!/\.innerHTML\s*=\s*['"`][^'"`]*['"`]\s*;?\s*$/.test(line)) {
|
|
3670
3802
|
issues.push(
|
|
@@ -3709,6 +3841,7 @@ var SecurityAntipatternsRule = class extends BaseRule {
|
|
|
3709
3841
|
})
|
|
3710
3842
|
);
|
|
3711
3843
|
}
|
|
3844
|
+
lineStartIndex += line.length + 1;
|
|
3712
3845
|
}
|
|
3713
3846
|
return issues;
|
|
3714
3847
|
}
|
|
@@ -4980,10 +5113,11 @@ var AccessibilityRule = class _AccessibilityRule extends BaseRule {
|
|
|
4980
5113
|
(f) => /\.[jt]sx?$/.test(f.extension) && !isTestFile(f.path) && !isStoryFile(f.path)
|
|
4981
5114
|
);
|
|
4982
5115
|
for (const file of uiFiles) {
|
|
4983
|
-
|
|
4984
|
-
issues.push(...this.
|
|
4985
|
-
issues.push(...this.
|
|
4986
|
-
issues.push(...this.
|
|
5116
|
+
const ranges = buildStringRanges(file.content);
|
|
5117
|
+
issues.push(...this.checkImagesWithoutAlt(file, ranges));
|
|
5118
|
+
issues.push(...this.checkNonSemanticClickHandlers(file, ranges));
|
|
5119
|
+
issues.push(...this.checkInputsWithoutLabels(file, ranges));
|
|
5120
|
+
issues.push(...this.checkGenericLinkText(file, ranges));
|
|
4987
5121
|
}
|
|
4988
5122
|
const layoutFiles = getLayoutFiles(context);
|
|
4989
5123
|
issues.push(...this.checkMissingLandmarks(layoutFiles));
|
|
@@ -4992,11 +5126,12 @@ var AccessibilityRule = class _AccessibilityRule extends BaseRule {
|
|
|
4992
5126
|
// ---------------------------------------------------------------------------
|
|
4993
5127
|
// 023-A: Images Without Alt Text
|
|
4994
5128
|
// ---------------------------------------------------------------------------
|
|
4995
|
-
checkImagesWithoutAlt(file) {
|
|
5129
|
+
checkImagesWithoutAlt(file, ranges) {
|
|
4996
5130
|
const issues = [];
|
|
4997
5131
|
const imgPattern = /<(?:img|Image)\s+([^>]*?)(?:\/>|>)/gi;
|
|
4998
5132
|
let match;
|
|
4999
5133
|
while ((match = imgPattern.exec(file.content)) !== null) {
|
|
5134
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
5000
5135
|
const attributes = match[1] ?? "";
|
|
5001
5136
|
const line = getLineNumber(file.content, match.index);
|
|
5002
5137
|
const altMatch = attributes.match(/alt\s*=\s*["']([^"']*)["']/i);
|
|
@@ -5066,11 +5201,12 @@ var AccessibilityRule = class _AccessibilityRule extends BaseRule {
|
|
|
5066
5201
|
// ---------------------------------------------------------------------------
|
|
5067
5202
|
// 023-B: Non-Semantic Click Handlers
|
|
5068
5203
|
// ---------------------------------------------------------------------------
|
|
5069
|
-
checkNonSemanticClickHandlers(file) {
|
|
5204
|
+
checkNonSemanticClickHandlers(file, ranges) {
|
|
5070
5205
|
const issues = [];
|
|
5071
5206
|
const onClickPattern = /<(\w+)\s+([^>]*onClick[^>]*)>/gi;
|
|
5072
5207
|
let match;
|
|
5073
5208
|
while ((match = onClickPattern.exec(file.content)) !== null) {
|
|
5209
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
5074
5210
|
const tagName = match[1]?.toLowerCase() ?? "";
|
|
5075
5211
|
const attributes = match[2] ?? "";
|
|
5076
5212
|
const line = getLineNumber(file.content, match.index);
|
|
@@ -5103,7 +5239,7 @@ var AccessibilityRule = class _AccessibilityRule extends BaseRule {
|
|
|
5103
5239
|
// ---------------------------------------------------------------------------
|
|
5104
5240
|
// 023-C: Form Inputs Without Labels
|
|
5105
5241
|
// ---------------------------------------------------------------------------
|
|
5106
|
-
checkInputsWithoutLabels(file) {
|
|
5242
|
+
checkInputsWithoutLabels(file, ranges) {
|
|
5107
5243
|
const issues = [];
|
|
5108
5244
|
const inputPattern = /<(input|select|textarea)\s+([^>]*)(?:\/>|>)/gi;
|
|
5109
5245
|
let match;
|
|
@@ -5115,6 +5251,7 @@ var AccessibilityRule = class _AccessibilityRule extends BaseRule {
|
|
|
5115
5251
|
}
|
|
5116
5252
|
inputPattern.lastIndex = 0;
|
|
5117
5253
|
while ((match = inputPattern.exec(file.content)) !== null) {
|
|
5254
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
5118
5255
|
const tagName = match[1]?.toLowerCase() ?? "";
|
|
5119
5256
|
const attributes = match[2] ?? "";
|
|
5120
5257
|
const line = getLineNumber(file.content, match.index);
|
|
@@ -5190,11 +5327,12 @@ var AccessibilityRule = class _AccessibilityRule extends BaseRule {
|
|
|
5190
5327
|
// ---------------------------------------------------------------------------
|
|
5191
5328
|
// 023-E: Non-Descriptive Link Text
|
|
5192
5329
|
// ---------------------------------------------------------------------------
|
|
5193
|
-
checkGenericLinkText(file) {
|
|
5330
|
+
checkGenericLinkText(file, ranges) {
|
|
5194
5331
|
const issues = [];
|
|
5195
5332
|
const linkPattern = /<(?:a|Link)\s+([^>]*)>([^<]+)<\/(?:a|Link)>/gi;
|
|
5196
5333
|
let match;
|
|
5197
5334
|
while ((match = linkPattern.exec(file.content)) !== null) {
|
|
5335
|
+
if (isInStringRange(ranges, match.index)) continue;
|
|
5198
5336
|
const attributes = match[1] ?? "";
|
|
5199
5337
|
const linkText = match[2]?.trim().toLowerCase() ?? "";
|
|
5200
5338
|
const line = getLineNumber(file.content, match.index);
|