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/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(50),
38
- maxFunctionParams: z.number().int().positive().default(4),
39
- maxImports: z.number().int().positive().default(15),
40
- minTestRatio: z.number().min(0).max(1).default(0.2)
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.CRITICAL;
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
- issues.push(...this.checkUnhandledAsync(file));
1939
- issues.push(...this.checkUnhandledFetch(file));
1940
- issues.push(...this.checkUnhandledPromiseChains(file));
1941
- issues.push(...this.checkEmptyCatchBlocks(file));
1942
- issues.push(...this.checkSwallowedErrors(file));
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 (isInsideStringOrComment(file.content, match.index)) continue;
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 (isInsideStringOrComment(file.content, match.index)) continue;
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 (isInsideStringOrComment(file.content, match2.index)) continue;
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 (isInsideStringOrComment(file.content, match.index)) continue;
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
- this.checkVarDeclarations(file, issues);
2798
- this.checkCallbackHell(file, issues);
2799
- this.checkClassComponents(file, issues);
2800
- this.checkCommonJsInTs(file, issues);
2801
- this.checkLegacyApis(file, issues);
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 lines = file.content.split("\n");
2808
- for (let i = 0; i < lines.length; i++) {
2809
- const line = lines[i];
2810
- if (/\bvar\s+\w+/.test(line) && !isInsideStringOrComment(file.content, file.content.indexOf(line))) {
2811
- const trimmed = line.trim();
2812
- if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
2813
- issues.push(this.createObsoleteIssue(file.path, i + 1, trimmed, "var", "const/let"));
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
- if (thenChain.test(file.content)) {
2821
- const line = getLineNumber(file.content, file.content.search(thenChain));
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 (isInsideStringOrComment(file.content, match.index)) continue;
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 (isInsideStringOrComment(file.content, match.index)) continue;
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
- issues.push(...this.checkSqlInjection(file));
3491
- issues.push(...this.checkCorsWildcard(file));
3492
- issues.push(...this.checkLocalStorageSensitive(file));
3493
- issues.push(...this.checkDangerousHtml(file));
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 (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
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 (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
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
- issues.push(...this.checkImagesWithoutAlt(file));
4984
- issues.push(...this.checkNonSemanticClickHandlers(file));
4985
- issues.push(...this.checkInputsWithoutLabels(file));
4986
- issues.push(...this.checkGenericLinkText(file));
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);