oxlint-plugin-react-doctor 0.6.2-dev.b4faf74 → 0.6.2-dev.d8628d7

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.
Files changed (2) hide show
  1. package/dist/index.js +673 -495
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -294,15 +294,38 @@ const defineRule = (rule) => {
294
294
  };
295
295
  //#endregion
296
296
  //#region src/plugin/rules/security-scan/utils/get-location-at-index.ts
297
+ let lastContentLineIndex;
298
+ const buildLineStartOffsets = (content) => {
299
+ const lineStartOffsets = [0];
300
+ for (let newlineIndex = content.indexOf("\n"); newlineIndex !== -1; newlineIndex = content.indexOf("\n", newlineIndex + 1)) lineStartOffsets.push(newlineIndex + 1);
301
+ return lineStartOffsets;
302
+ };
303
+ const getLineStartOffsets = (content) => {
304
+ if (lastContentLineIndex?.content === content) return lastContentLineIndex.lineStartOffsets;
305
+ const lineStartOffsets = buildLineStartOffsets(content);
306
+ lastContentLineIndex = {
307
+ content,
308
+ lineStartOffsets
309
+ };
310
+ return lineStartOffsets;
311
+ };
297
312
  const getLocationAtIndex = (content, matchIndex) => {
298
- if (matchIndex < 0) return {
313
+ if (Number.isNaN(matchIndex) || matchIndex < 0) return {
299
314
  line: 1,
300
315
  column: 1
301
316
  };
302
- const lines = content.slice(0, matchIndex).split(/\r?\n/);
317
+ const lineStartOffsets = getLineStartOffsets(content);
318
+ const boundedIndex = Math.min(Math.trunc(matchIndex), content.length);
319
+ let lowLineIndex = 0;
320
+ let highLineIndex = lineStartOffsets.length - 1;
321
+ while (lowLineIndex < highLineIndex) {
322
+ const midLineIndex = lowLineIndex + highLineIndex + 1 >> 1;
323
+ if (lineStartOffsets[midLineIndex] <= boundedIndex) lowLineIndex = midLineIndex;
324
+ else highLineIndex = midLineIndex - 1;
325
+ }
303
326
  return {
304
- line: lines.length,
305
- column: (lines[lines.length - 1]?.length ?? 0) + 1
327
+ line: lowLineIndex + 1,
328
+ column: boundedIndex - lineStartOffsets[lowLineIndex] + 1
306
329
  };
307
330
  };
308
331
  //#endregion
@@ -355,13 +378,18 @@ const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
355
378
  const isConfigOrCiPath = (relativePath) => /(?:^|\/)(?:package\.json|Dockerfile|docker-compose\.ya?ml|\.github\/workflows\/[^/]+\.ya?ml|vercel\.json|next\.config\.[cm]?[jt]s|netlify\.toml)$/i.test(relativePath);
356
379
  //#endregion
357
380
  //#region src/plugin/rules/security-scan/utils/is-production-file-path.ts
381
+ const classificationCacheByPattern = /* @__PURE__ */ new Map();
358
382
  const isProductionFilePath = (relativePath, sourceFilePattern) => {
359
- if (!sourceFilePattern.test(relativePath)) return false;
360
- if (TEST_CONTEXT_PATTERN.test(relativePath)) return false;
361
- if (BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath)) return false;
362
- if (DOCUMENTATION_CONTEXT_PATTERN.test(relativePath)) return false;
363
- if (GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath)) return false;
364
- return true;
383
+ let classificationByPath = classificationCacheByPattern.get(sourceFilePattern);
384
+ if (!classificationByPath) {
385
+ classificationByPath = /* @__PURE__ */ new Map();
386
+ classificationCacheByPattern.set(sourceFilePattern, classificationByPath);
387
+ }
388
+ const cached = classificationByPath.get(relativePath);
389
+ if (cached !== void 0) return cached;
390
+ const isProduction = sourceFilePattern.test(relativePath) && !TEST_CONTEXT_PATTERN.test(relativePath) && !BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(relativePath) && !GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath);
391
+ classificationByPath.set(relativePath, isProduction);
392
+ return isProduction;
365
393
  };
366
394
  //#endregion
367
395
  //#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
@@ -760,24 +788,6 @@ const collectChildComponentNames = (element, into) => {
760
788
  into.add(name);
761
789
  });
762
790
  };
763
- const findSameFileComponentBody = (programRoot, componentName) => {
764
- let foundBody = null;
765
- walkAst(programRoot, (node) => {
766
- if (foundBody) return false;
767
- if (isNodeOfType(node, "FunctionDeclaration") && node.id && node.id.name === componentName) {
768
- foundBody = node.body;
769
- return false;
770
- }
771
- if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.id.name === componentName) {
772
- const initializer = node.init;
773
- if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) {
774
- foundBody = initializer.body;
775
- return false;
776
- }
777
- }
778
- });
779
- return foundBody;
780
- };
781
791
  const countEffectHookCalls = (body) => {
782
792
  if (!body) return 0;
783
793
  let count = 0;
@@ -787,6 +797,36 @@ const countEffectHookCalls = (body) => {
787
797
  });
788
798
  return count;
789
799
  };
800
+ const componentEffectIndexCache = /* @__PURE__ */ new WeakMap();
801
+ const getComponentEffectIndex = (programRoot) => {
802
+ const cached = componentEffectIndexCache.get(programRoot);
803
+ if (cached) return cached;
804
+ const bodyByName = /* @__PURE__ */ new Map();
805
+ walkAst(programRoot, (node) => {
806
+ if (isNodeOfType(node, "FunctionDeclaration") && node.id && !bodyByName.has(node.id.name)) {
807
+ bodyByName.set(node.id.name, node.body);
808
+ return;
809
+ }
810
+ if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && !bodyByName.has(node.id.name)) {
811
+ const initializer = node.init;
812
+ if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) bodyByName.set(node.id.name, initializer.body);
813
+ }
814
+ });
815
+ const index = {
816
+ bodyByName,
817
+ effectCountByName: /* @__PURE__ */ new Map()
818
+ };
819
+ componentEffectIndexCache.set(programRoot, index);
820
+ return index;
821
+ };
822
+ const getSameFileComponentEffectCount = (programRoot, componentName) => {
823
+ const index = getComponentEffectIndex(programRoot);
824
+ const cachedCount = index.effectCountByName.get(componentName);
825
+ if (cachedCount !== void 0) return cachedCount;
826
+ const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
827
+ index.effectCountByName.set(componentName, count);
828
+ return count;
829
+ };
790
830
  const activityWrapsEffectHeavySubtree = defineRule({
791
831
  id: "activity-wraps-effect-heavy-subtree",
792
832
  title: "Activity wraps an effect-heavy subtree",
@@ -843,9 +883,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
843
883
  let totalEffects = 0;
844
884
  const effectfulChildren = [];
845
885
  for (const componentName of childComponentNames) {
846
- const body = findSameFileComponentBody(programRoot, componentName);
847
- if (!body) continue;
848
- const effectCount = countEffectHookCalls(body);
886
+ const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
849
887
  if (effectCount === 0) continue;
850
888
  totalEffects += effectCount;
851
889
  effectfulChildren.push(`<${componentName}>`);
@@ -1394,11 +1432,16 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
1394
1432
  };
1395
1433
  //#endregion
1396
1434
  //#region src/plugin/utils/get-element-type.ts
1435
+ const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
1436
+ const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
1397
1437
  const readJsxA11ySettings = (settings) => {
1398
- if (!settings) return {};
1438
+ if (!settings) return EMPTY_JSX_A11Y_SETTINGS;
1439
+ const cachedSettings = jsxA11ySettingsCache.get(settings);
1440
+ if (cachedSettings) return cachedSettings;
1399
1441
  const block = settings["jsx-a11y"];
1400
- if (!block || typeof block !== "object") return {};
1401
- return block;
1442
+ const a11ySettings = block && typeof block === "object" ? block : EMPTY_JSX_A11Y_SETTINGS;
1443
+ jsxA11ySettingsCache.set(settings, a11ySettings);
1444
+ return a11ySettings;
1402
1445
  };
1403
1446
  const flattenJsxName$2 = (name) => {
1404
1447
  if (isNodeOfType(name, "JSXIdentifier")) return name.name;
@@ -1406,8 +1449,7 @@ const flattenJsxName$2 = (name) => {
1406
1449
  if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
1407
1450
  return "";
1408
1451
  };
1409
- const getElementType = (openingElement, settings) => {
1410
- const a11ySettings = readJsxA11ySettings(settings);
1452
+ const computeElementType = (openingElement, a11ySettings) => {
1411
1453
  const baseName = flattenJsxName$2(openingElement.name);
1412
1454
  if (a11ySettings.polymorphicPropName) {
1413
1455
  const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
@@ -1419,6 +1461,23 @@ const getElementType = (openingElement, settings) => {
1419
1461
  if (a11ySettings.components && baseName in a11ySettings.components) return a11ySettings.components[baseName];
1420
1462
  return baseName;
1421
1463
  };
1464
+ const elementTypeCache = /* @__PURE__ */ new WeakMap();
1465
+ const getElementType = (openingElement, settings) => {
1466
+ const cachedEntry = elementTypeCache.get(openingElement);
1467
+ if (cachedEntry && cachedEntry.settings === settings) return cachedEntry.elementType;
1468
+ const elementType = computeElementType(openingElement, readJsxA11ySettings(settings));
1469
+ elementTypeCache.set(openingElement, {
1470
+ settings,
1471
+ elementType
1472
+ });
1473
+ return elementType;
1474
+ };
1475
+ //#endregion
1476
+ //#region src/plugin/utils/has-jsx-a11y-settings.ts
1477
+ const hasJsxA11ySettings = (settings) => {
1478
+ const block = settings?.["jsx-a11y"];
1479
+ return typeof block === "object" && block !== null;
1480
+ };
1422
1481
  //#endregion
1423
1482
  //#region src/plugin/utils/find-import-source-for-name.ts
1424
1483
  const collectFromProgram = (programRoot) => {
@@ -1623,7 +1682,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
1623
1682
  };
1624
1683
  //#endregion
1625
1684
  //#region src/plugin/utils/normalize-filename.ts
1626
- const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
1685
+ const normalizeFilename = (filename) => filename.includes("\\") ? filename.replaceAll("\\", "/") : filename;
1627
1686
  //#endregion
1628
1687
  //#region src/plugin/utils/is-generated-image-render-context.ts
1629
1688
  const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
@@ -1972,7 +2031,12 @@ const altText = defineRule({
1972
2031
  const objectAliases = new Set(settings.object ?? []);
1973
2032
  const areaAliases = new Set(settings.area ?? []);
1974
2033
  const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
2034
+ const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
1975
2035
  return { JSXOpeningElement(node) {
2036
+ if (!fileHasJsxA11ySettings && isNodeOfType(node.name, "JSXIdentifier")) {
2037
+ const rawName = node.name.name;
2038
+ if (rawName !== "img" && rawName !== "object" && rawName !== "area" && rawName.toLowerCase() !== "input" && !imgAliases.has(rawName) && !objectAliases.has(rawName) && !areaAliases.has(rawName) && !inputImageAliases.has(rawName)) return;
2039
+ }
1976
2040
  if (isGeneratedImageRenderContext(context, node)) return;
1977
2041
  const tag = getElementType(node, context.settings);
1978
2042
  if (checkImg && (tag === "img" || imgAliases.has(tag))) {
@@ -2155,7 +2219,9 @@ const anchorIsValid = defineRule({
2155
2219
  category: "Accessibility",
2156
2220
  create: (context) => {
2157
2221
  const settings = resolveSettings$50(context.settings);
2222
+ const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
2158
2223
  return { JSXOpeningElement(node) {
2224
+ if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
2159
2225
  if (getElementType(node, context.settings) !== "a") return;
2160
2226
  let hrefAttribute;
2161
2227
  for (const attributeName of settings.hrefAttributeNames) {
@@ -3581,8 +3647,9 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
3581
3647
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3582
3648
  //#endregion
3583
3649
  //#region src/plugin/rules/security-scan/utils/find-suspicious-public-env-secret-name.ts
3650
+ const PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN = new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi");
3584
3651
  const findSuspiciousPublicEnvSecretNamePattern = (content) => {
3585
- for (const match of content.matchAll(new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi"))) {
3652
+ for (const match of content.matchAll(PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN)) {
3586
3653
  const value = match[0] ?? "";
3587
3654
  if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
3588
3655
  }
@@ -4049,62 +4116,56 @@ const collectPatternIdentifiers = (pattern, target) => {
4049
4116
  for (const element of pattern.elements ?? []) if (element) collectPatternIdentifiers(element, target);
4050
4117
  } else if (isNodeOfType(pattern, "AssignmentPattern") && pattern.left) collectPatternIdentifiers(pattern.left, target);
4051
4118
  };
4052
- const collectAssignedIdentifiers = (block) => {
4053
- const assigned = /* @__PURE__ */ new Set();
4054
- walkAst(block, (child) => {
4055
- if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
4056
- if (isNodeOfType(child, "AssignmentExpression") && child.left) collectPatternIdentifiers(child.left, assigned);
4057
- });
4058
- return assigned;
4059
- };
4060
- const collectAwaitedArgIdentifiers = (block) => {
4061
- const referenced = /* @__PURE__ */ new Set();
4062
- walkAst(block, (child) => {
4063
- if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
4064
- if (!isNodeOfType(child, "AwaitExpression") || !child.argument) return;
4065
- collectReferenceIdentifierNames(child.argument, referenced);
4066
- });
4067
- return referenced;
4068
- };
4069
4119
  const ARRAY_MUTATION_METHOD_NAMES = new Set([
4070
4120
  "push",
4071
4121
  "unshift",
4072
4122
  "splice"
4073
4123
  ]);
4074
- const collectMutatedArrayNames = (block) => {
4075
- const mutated = /* @__PURE__ */ new Set();
4124
+ const addDerivedBindings = (block, names) => {
4125
+ const declaratorBindings = [];
4076
4126
  walkAst(block, (child) => {
4077
4127
  if (child !== block && isFunctionLike$1(child)) return false;
4078
- if (!isNodeOfType(child, "CallExpression")) return;
4079
- const callee = child.callee;
4080
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_MUTATION_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier")) mutated.add(callee.object.name);
4128
+ if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
4129
+ if (!isNodeOfType(child.id, "Identifier")) return;
4130
+ const referencedNames = /* @__PURE__ */ new Set();
4131
+ collectReferenceIdentifierNames(child.init, referencedNames);
4132
+ declaratorBindings.push({
4133
+ declaredName: child.id.name,
4134
+ referencedNames
4135
+ });
4081
4136
  });
4082
- return mutated;
4083
- };
4084
- const addDerivedBindings = (block, names) => {
4085
4137
  let didGrow = true;
4086
4138
  while (didGrow) {
4087
4139
  didGrow = false;
4088
- walkAst(block, (child) => {
4089
- if (child !== block && isFunctionLike$1(child)) return false;
4090
- if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
4091
- if (!isNodeOfType(child.id, "Identifier") || names.has(child.id.name)) return;
4092
- const initReferences = /* @__PURE__ */ new Set();
4093
- collectReferenceIdentifierNames(child.init, initReferences);
4094
- for (const referenced of initReferences) if (names.has(referenced)) {
4095
- names.add(child.id.name);
4140
+ for (const { declaredName, referencedNames } of declaratorBindings) {
4141
+ if (names.has(declaredName)) continue;
4142
+ for (const referenced of referencedNames) if (names.has(referenced)) {
4143
+ names.add(declaredName);
4096
4144
  didGrow = true;
4097
4145
  break;
4098
4146
  }
4099
- });
4147
+ }
4100
4148
  }
4101
4149
  };
4102
4150
  const hasLoopCarriedDependency = (block) => {
4103
- const carried = collectAssignedIdentifiers(block);
4104
- for (const name of collectMutatedArrayNames(block)) carried.add(name);
4151
+ const carried = /* @__PURE__ */ new Set();
4152
+ const awaitedReferences = /* @__PURE__ */ new Set();
4153
+ walkAst(block, (child) => {
4154
+ if (child !== block && isFunctionLike$1(child)) return false;
4155
+ if (isNodeOfType(child, "AssignmentExpression") && child.left) {
4156
+ collectPatternIdentifiers(child.left, carried);
4157
+ return;
4158
+ }
4159
+ if (isNodeOfType(child, "AwaitExpression") && child.argument) {
4160
+ collectReferenceIdentifierNames(child.argument, awaitedReferences);
4161
+ return;
4162
+ }
4163
+ if (!isNodeOfType(child, "CallExpression")) return;
4164
+ const callee = child.callee;
4165
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_MUTATION_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier")) carried.add(callee.object.name);
4166
+ });
4105
4167
  if (carried.size === 0) return false;
4106
4168
  addDerivedBindings(block, carried);
4107
- const awaitedReferences = collectAwaitedArgIdentifiers(block);
4108
4169
  for (const name of carried) if (awaitedReferences.has(name)) return true;
4109
4170
  return false;
4110
4171
  };
@@ -4839,9 +4900,8 @@ const isCreateElementCall = (node) => {
4839
4900
  const callee = node.callee;
4840
4901
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createElement";
4841
4902
  if (isNodeOfType(callee, "MemberExpression")) {
4842
- if (memberChainContainsDocument(callee.object)) return false;
4843
- if (callee.computed) return isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement";
4844
- return isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement";
4903
+ if (!(callee.computed ? isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement" : isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement")) return false;
4904
+ return !memberChainContainsDocument(callee.object);
4845
4905
  }
4846
4906
  return false;
4847
4907
  };
@@ -5658,12 +5718,20 @@ const getTemplateInterpolations = (valueTail) => {
5658
5718
  return interpolations === null ? "" : interpolations.join(" ");
5659
5719
  };
5660
5720
  const isInertParseTarget = (target, fileContent) => {
5721
+ const fileHasCreateElement = fileContent.includes("createElement");
5722
+ const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
5723
+ if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
5661
5724
  const escapedTarget = escapeRegExp(target);
5662
5725
  const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
5663
5726
  if (new RegExp(`\\b${escapedRoot}\\s*=\\s*[^\\n;]*(?:getElementById|querySelector|getElementsBy|\\.current\\b|document\\.(?:body|head|documentElement))`).test(fileContent)) return false;
5664
- if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
5665
- if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
5666
- if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
5727
+ if (fileHasCreateElement) {
5728
+ if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
5729
+ if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
5730
+ }
5731
+ if (fileHasIsolatedDocument) {
5732
+ if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
5733
+ }
5734
+ if (!fileHasCreateElement) return false;
5667
5735
  if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
5668
5736
  const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
5669
5737
  const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
@@ -5756,10 +5824,9 @@ const dangerousHtmlSink = defineRule({
5756
5824
  const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
5757
5825
  if (valueIdentifier !== void 0) {
5758
5826
  const escapedIdentifier = escapeRegExp(valueIdentifier);
5759
- const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
5760
- const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
5761
- const fromDomContent = new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`);
5762
- if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
5827
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5828
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5829
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
5763
5830
  }
5764
5831
  }
5765
5832
  const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
@@ -5908,6 +5975,7 @@ const noRedundantPaddingAxes = defineRule({
5908
5975
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5909
5976
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5910
5977
  if (!classNameLiteral) return;
5978
+ if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
5911
5979
  if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
5912
5980
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
5913
5981
  if (matchedPairs.length === 0) return;
@@ -5932,6 +6000,7 @@ const noRedundantSizeAxes = defineRule({
5932
6000
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5933
6001
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5934
6002
  if (!classNameLiteral) return;
6003
+ if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
5935
6004
  if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
5936
6005
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
5937
6006
  if (matchedPairs.length === 0) return;
@@ -5956,6 +6025,7 @@ const noSpaceOnFlexChildren = defineRule({
5956
6025
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5957
6026
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5958
6027
  if (!classNameLiteral) return;
6028
+ if (!classNameLiteral.includes("space-")) return;
5959
6029
  const tokens = tokenizeClassName(classNameLiteral);
5960
6030
  let hasFlexOrGridLayout = false;
5961
6031
  for (const token of tokens) {
@@ -6147,7 +6217,10 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
6147
6217
  const actualMinor = Number(match[2]);
6148
6218
  return actualMajor > major || actualMajor === major && actualMinor >= minor;
6149
6219
  };
6220
+ const containsJsxCache = /* @__PURE__ */ new WeakMap();
6150
6221
  const containsJsx$1 = (root) => {
6222
+ const cached = containsJsxCache.get(root);
6223
+ if (cached !== void 0) return cached;
6151
6224
  let found = false;
6152
6225
  const visit = (node) => {
6153
6226
  if (found) return;
@@ -6170,6 +6243,7 @@ const containsJsx$1 = (root) => {
6170
6243
  }
6171
6244
  };
6172
6245
  visit(root);
6246
+ containsJsxCache.set(root, found);
6173
6247
  return found;
6174
6248
  };
6175
6249
  const getStaticMemberName = (node) => {
@@ -6261,27 +6335,6 @@ const hasDisplayNameMember = (classNode) => {
6261
6335
  for (const member of members) if ((isNodeOfType(member, "PropertyDefinition") || isNodeOfType(member, "MethodDefinition")) && "static" in member && member.static && isNodeOfType(member.key, "Identifier") && member.key.name === "displayName") return true;
6262
6336
  return false;
6263
6337
  };
6264
- const hasDisplayNameAssignment = (className, programRoot) => {
6265
- let found = false;
6266
- const visit = (node) => {
6267
- if (found) return;
6268
- if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && isNodeOfType(node.left.object, "Identifier") && node.left.object.name === className && getStaticMemberName(node.left) === "displayName") {
6269
- found = true;
6270
- return;
6271
- }
6272
- const record = node;
6273
- for (const key of Object.keys(record)) {
6274
- if (key === "parent") continue;
6275
- const child = record[key];
6276
- if (Array.isArray(child)) {
6277
- for (const item of child) if (isAstNode(item)) visit(item);
6278
- } else if (isAstNode(child)) visit(child);
6279
- if (found) return;
6280
- }
6281
- };
6282
- visit(programRoot);
6283
- return found;
6284
- };
6285
6338
  const memberExpressionPath = (node) => {
6286
6339
  if (isNodeOfType(node, "Identifier")) return [node.name];
6287
6340
  if (!isNodeOfType(node, "MemberExpression")) return [];
@@ -6289,15 +6342,16 @@ const memberExpressionPath = (node) => {
6289
6342
  const propertyName = getStaticMemberName(node);
6290
6343
  return propertyName ? [...objectPath, propertyName] : objectPath;
6291
6344
  };
6292
- const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6293
- let found = false;
6345
+ const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
6346
+ const getDisplayNameAssignmentIndex = (programRoot) => {
6347
+ const cached = displayNameAssignmentIndexCache.get(programRoot);
6348
+ if (cached) return cached;
6349
+ const identifierTargets = /* @__PURE__ */ new Set();
6350
+ const memberPathSegments = /* @__PURE__ */ new Set();
6294
6351
  const visit = (node) => {
6295
- if (found) return;
6296
6352
  if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
6297
- if (memberExpressionPath(node.left.object).includes(propertyName)) {
6298
- found = true;
6299
- return;
6300
- }
6353
+ if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
6354
+ for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
6301
6355
  }
6302
6356
  const record = node;
6303
6357
  for (const key of Object.keys(record)) {
@@ -6306,12 +6360,18 @@ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6306
6360
  if (Array.isArray(child)) {
6307
6361
  for (const item of child) if (isAstNode(item)) visit(item);
6308
6362
  } else if (isAstNode(child)) visit(child);
6309
- if (found) return;
6310
6363
  }
6311
6364
  };
6312
6365
  visit(programRoot);
6313
- return found;
6366
+ const index = {
6367
+ identifierTargets,
6368
+ memberPathSegments
6369
+ };
6370
+ displayNameAssignmentIndexCache.set(programRoot, index);
6371
+ return index;
6314
6372
  };
6373
+ const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
6374
+ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
6315
6375
  const displayName = defineRule({
6316
6376
  id: "display-name",
6317
6377
  title: "Component missing display name",
@@ -7183,27 +7243,8 @@ const isDescendantScope = (inner, outer) => {
7183
7243
  return false;
7184
7244
  };
7185
7245
  //#endregion
7186
- //#region src/plugin/utils/is-ast-descendant.ts
7187
- /**
7188
- * True when `inner` is `outer` itself or any descendant in the AST
7189
- * `parent` chain. Walks `inner.parent` upward and stops at either a
7190
- * match (`true`) or the chain's root (`false`).
7191
- *
7192
- * Was duplicated byte-identical in:
7193
- * - exhaustive-deps-symbol-stability.ts
7194
- * - semantic/closure-captures.ts
7195
- */
7196
- const isAstDescendant = (inner, outer) => {
7197
- let current = inner;
7198
- while (current) {
7199
- if (current === outer) return true;
7200
- current = current.parent ?? null;
7201
- }
7202
- return false;
7203
- };
7204
- //#endregion
7205
7246
  //#region src/plugin/semantic/closure-captures.ts
7206
- const closureCaptures = (functionNode, scopes) => {
7247
+ const computeClosureCaptures = (functionNode, scopes) => {
7207
7248
  const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
7208
7249
  const out = [];
7209
7250
  const seen = /* @__PURE__ */ new Set();
@@ -7238,7 +7279,20 @@ const closureCaptures = (functionNode, scopes) => {
7238
7279
  }
7239
7280
  };
7240
7281
  visit(functionNode);
7241
- return out.filter((reference) => isAstDescendant(reference.identifier, functionNode));
7282
+ return out;
7283
+ };
7284
+ const capturesByAnalysis = /* @__PURE__ */ new WeakMap();
7285
+ const closureCaptures = (functionNode, scopes) => {
7286
+ let capturesByFunction = capturesByAnalysis.get(scopes);
7287
+ if (!capturesByFunction) {
7288
+ capturesByFunction = /* @__PURE__ */ new WeakMap();
7289
+ capturesByAnalysis.set(scopes, capturesByFunction);
7290
+ }
7291
+ const memoizedCaptures = capturesByFunction.get(functionNode);
7292
+ if (memoizedCaptures) return memoizedCaptures;
7293
+ const computedCaptures = computeClosureCaptures(functionNode, scopes);
7294
+ capturesByFunction.set(functionNode, computedCaptures);
7295
+ return computedCaptures;
7242
7296
  };
7243
7297
  //#endregion
7244
7298
  //#region src/plugin/utils/is-react-hook-name.ts
@@ -7367,6 +7421,25 @@ const resolveExhaustiveDepsSettings = (settings) => {
7367
7421
  };
7368
7422
  };
7369
7423
  //#endregion
7424
+ //#region src/plugin/utils/is-ast-descendant.ts
7425
+ /**
7426
+ * True when `inner` is `outer` itself or any descendant in the AST
7427
+ * `parent` chain. Walks `inner.parent` upward and stops at either a
7428
+ * match (`true`) or the chain's root (`false`).
7429
+ *
7430
+ * Was duplicated byte-identical in:
7431
+ * - exhaustive-deps-symbol-stability.ts
7432
+ * - semantic/closure-captures.ts
7433
+ */
7434
+ const isAstDescendant = (inner, outer) => {
7435
+ let current = inner;
7436
+ while (current) {
7437
+ if (current === outer) return true;
7438
+ current = current.parent ?? null;
7439
+ }
7440
+ return false;
7441
+ };
7442
+ //#endregion
7370
7443
  //#region src/plugin/rules/react-builtins/exhaustive-deps-symbol-stability.ts
7371
7444
  /**
7372
7445
  * Symbol-stability helpers consumed by the `exhaustive-deps` rule.
@@ -8266,9 +8339,14 @@ const firebaseQueryFilterAsAuth = defineRule({
8266
8339
  * `*Handler`), so the cheap escape-then-replace shape is enough
8267
8340
  * and avoids pulling picomatch into the per-file rule path.
8268
8341
  */
8342
+ const compiledGlobs = /* @__PURE__ */ new Map();
8269
8343
  const compileGlob = (pattern) => {
8344
+ const cached = compiledGlobs.get(pattern);
8345
+ if (cached) return cached;
8270
8346
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
8271
- return new RegExp(`^${escaped}$`);
8347
+ const compiled = new RegExp(`^${escaped}$`);
8348
+ compiledGlobs.set(pattern, compiled);
8349
+ return compiled;
8272
8350
  };
8273
8351
  //#endregion
8274
8352
  //#region src/plugin/rules/react-builtins/forbid-component-props.ts
@@ -9210,10 +9288,10 @@ const imgRedundantAlt = defineRule({
9210
9288
  if (isGeneratedImageRenderContext(context)) return {};
9211
9289
  const settings = resolveSettings$37(context.settings);
9212
9290
  return { JSXOpeningElement(node) {
9213
- if (isGeneratedImageRenderContext(context, node)) return;
9214
9291
  const tag = getElementType(node, context.settings);
9215
9292
  if (!settings.components.includes(tag)) return;
9216
9293
  if (isHiddenFromScreenReader(node, context.settings)) return;
9294
+ if (isGeneratedImageRenderContext(context, node)) return;
9217
9295
  const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
9218
9296
  if (!altAttribute) return;
9219
9297
  if (altValueRedundant(altAttribute, settings.words)) context.report({
@@ -9268,6 +9346,7 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
9268
9346
  const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
9269
9347
  const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
9270
9348
  const SECURITY_CONTEXT_WINDOW_CHARS = 250;
9349
+ const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
9271
9350
  const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
9272
9351
  for (const callMatch of content.matchAll(pattern)) {
9273
9352
  const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
@@ -9297,6 +9376,7 @@ const insecureCryptoRisk = defineRule({
9297
9376
  if (!isProductionSourcePath(file.relativePath)) return [];
9298
9377
  if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
9299
9378
  if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
9379
+ if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
9300
9380
  const content = getScannableContent(file);
9301
9381
  let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
9302
9382
  if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
@@ -9529,15 +9609,16 @@ const interactiveSupportsFocus = defineRule({
9529
9609
  const settings = resolveSettings$36(context.settings);
9530
9610
  const tabbableSet = new Set(settings.tabbable);
9531
9611
  return { JSXOpeningElement(node) {
9612
+ if (node.attributes.length === 0) return;
9532
9613
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
9533
9614
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
9534
9615
  const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
9616
+ if (!role) return;
9617
+ if (!ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)))) return;
9535
9618
  const elementType = getElementType(node, context.settings);
9536
9619
  if (!HTML_TAGS.has(elementType)) return;
9537
- const hasInteractiveHandler = ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)));
9620
+ if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9538
9621
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
9539
- if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9540
- if (!role) return;
9541
9622
  if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
9542
9623
  const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
9543
9624
  context.report({
@@ -10176,8 +10257,6 @@ const jsCachePropertyAccess = defineRule({
10176
10257
  walkAst(loopBody, (child) => {
10177
10258
  if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
10178
10259
  if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
10179
- });
10180
- walkAst(loopBody, (child) => {
10181
10260
  if (!isNodeOfType(child, "MemberExpression")) return;
10182
10261
  if (child.computed) return;
10183
10262
  if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
@@ -10637,7 +10716,6 @@ const jsHoistIntl = defineRule({
10637
10716
  recommendation: "Move `new Intl.NumberFormat(...)` to the top of the file or wrap it in `useMemo`. Building one is slow, so don't redo it on every call",
10638
10717
  create: (context) => ({ NewExpression(node) {
10639
10718
  if (!isIntlNewExpression(node)) return;
10640
- if (isInsideCacheMemo(node)) return;
10641
10719
  let cursor = node.parent ?? null;
10642
10720
  let inFunctionBody = false;
10643
10721
  while (cursor) {
@@ -10654,6 +10732,7 @@ const jsHoistIntl = defineRule({
10654
10732
  cursor = cursor.parent ?? null;
10655
10733
  }
10656
10734
  if (!inFunctionBody) return;
10735
+ if (isInsideCacheMemo(node)) return;
10657
10736
  const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
10658
10737
  context.report({
10659
10738
  node,
@@ -10728,7 +10807,11 @@ const isSingleFieldEqualityPredicate = (node) => {
10728
10807
  if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
10729
10808
  return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
10730
10809
  };
10731
- const collectLoopBoundNames = (loop, names) => {
10810
+ const loopBoundNamesCache = /* @__PURE__ */ new WeakMap();
10811
+ const getLoopBoundNames = (loop) => {
10812
+ const cached = loopBoundNamesCache.get(loop);
10813
+ if (cached) return cached;
10814
+ const names = /* @__PURE__ */ new Set();
10732
10815
  if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
10733
10816
  if (isNodeOfType(child, "Identifier")) names.add(child.name);
10734
10817
  });
@@ -10745,6 +10828,8 @@ const collectLoopBoundNames = (loop, names) => {
10745
10828
  if (targetRoot) names.add(targetRoot);
10746
10829
  }
10747
10830
  });
10831
+ loopBoundNamesCache.set(loop, names);
10832
+ return names;
10748
10833
  };
10749
10834
  const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
10750
10835
  let cursor = receiver;
@@ -10770,7 +10855,7 @@ const isLoopVariantReceiver = (node) => {
10770
10855
  const loopBoundNames = /* @__PURE__ */ new Set();
10771
10856
  let ancestor = node.parent;
10772
10857
  while (ancestor) {
10773
- if (LOOP_TYPES.includes(ancestor.type)) collectLoopBoundNames(ancestor, loopBoundNames);
10858
+ if (LOOP_TYPES.includes(ancestor.type)) for (const name of getLoopBoundNames(ancestor)) loopBoundNames.add(name);
10774
10859
  ancestor = ancestor.parent;
10775
10860
  }
10776
10861
  if (loopBoundNames.has(receiverRoot)) return true;
@@ -15441,16 +15526,30 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
15441
15526
  const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
15442
15527
  const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
15443
15528
  const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
15444
- const doesSourceTextExportName = (sourceText, exportedName) => {
15529
+ const collectSourceTextExportNames = (sourceText) => {
15445
15530
  const strippedSource = stripJsComments(sourceText);
15446
- if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
15447
- for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
15448
- for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
15449
- return false;
15531
+ const exportedNames = /* @__PURE__ */ new Set();
15532
+ if (DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) exportedNames.add("default");
15533
+ for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1]) exportedNames.add(match[1]);
15534
+ for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) {
15535
+ const specifiersText = match[1] ?? "";
15536
+ for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
15537
+ }
15538
+ return exportedNames;
15450
15539
  };
15540
+ const exportNamesCache = /* @__PURE__ */ new Map();
15451
15541
  const doesModuleExportName = (filePath, exportedName) => {
15452
15542
  try {
15453
- return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
15543
+ const fileStat = fs.statSync(filePath);
15544
+ const cached = exportNamesCache.get(filePath);
15545
+ if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
15546
+ const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
15547
+ exportNamesCache.set(filePath, {
15548
+ mtimeMs: fileStat.mtimeMs,
15549
+ size: fileStat.size,
15550
+ exportedNames
15551
+ });
15552
+ return exportedNames.has(exportedName);
15454
15553
  } catch {
15455
15554
  return false;
15456
15555
  }
@@ -15595,8 +15694,12 @@ const containsFetchCall = (node, options) => {
15595
15694
  const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
15596
15695
  let didFindFetchCall = false;
15597
15696
  walkAst(node, (child) => {
15697
+ if (didFindFetchCall) return false;
15598
15698
  if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
15599
- if (isFetchCall$1(child)) didFindFetchCall = true;
15699
+ if (isFetchCall$1(child)) {
15700
+ didFindFetchCall = true;
15701
+ return false;
15702
+ }
15600
15703
  });
15601
15704
  return didFindFetchCall;
15602
15705
  };
@@ -15610,6 +15713,8 @@ const nextjsNoClientFetchForServerData = defineRule({
15610
15713
  severity: "warn",
15611
15714
  recommendation: "Remove 'use client' and fetch directly in the Server Component. No API round-trip, and secrets stay on the server.",
15612
15715
  create: (context) => {
15716
+ const filename = normalizeFilename(context.filename ?? "");
15717
+ if (!(PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages"))) return {};
15613
15718
  let fileHasUseClient = false;
15614
15719
  return {
15615
15720
  Program(programNode) {
@@ -15619,8 +15724,7 @@ const nextjsNoClientFetchForServerData = defineRule({
15619
15724
  if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
15620
15725
  const callback = getEffectCallback(node);
15621
15726
  if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
15622
- const filename = normalizeFilename(context.filename ?? "");
15623
- if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
15727
+ context.report({
15624
15728
  node,
15625
15729
  message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
15626
15730
  });
@@ -16285,16 +16389,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
16285
16389
  recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
16286
16390
  create: (context) => {
16287
16391
  let resolveBinding = () => null;
16392
+ let isRouteHandlerFile = false;
16393
+ let mutatingSegment = null;
16288
16394
  return {
16289
16395
  Program(node) {
16290
16396
  resolveBinding = buildProgramBindingLookup(node);
16397
+ const filename = normalizeFilename(context.filename ?? "");
16398
+ isRouteHandlerFile = ROUTE_HANDLER_FILE_PATTERN.test(filename) && !CRON_ROUTE_PATTERN.test(filename);
16399
+ mutatingSegment = isRouteHandlerFile ? extractMutatingRouteSegment(filename) : null;
16291
16400
  },
16292
16401
  ExportNamedDeclaration(node) {
16293
- const filename = normalizeFilename(context.filename ?? "");
16294
- if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
16295
- if (CRON_ROUTE_PATTERN.test(filename)) return;
16402
+ if (!isRouteHandlerFile) return;
16296
16403
  if (!isExportedGetHandler(node)) return;
16297
- const mutatingSegment = extractMutatingRouteSegment(filename);
16298
16404
  const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
16299
16405
  for (const handlerBody of handlerBodies) {
16300
16406
  const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
@@ -17326,8 +17432,15 @@ const getProgramAnalysis = (anyNode) => {
17326
17432
  programToAnalysis.set(programNode, analysis);
17327
17433
  return analysis;
17328
17434
  };
17435
+ const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
17329
17436
  const getScopeForNode = (node, manager) => {
17330
17437
  if (!node.range) return null;
17438
+ let scopeByNode = scopeByNodeCache.get(manager);
17439
+ if (!scopeByNode) {
17440
+ scopeByNode = /* @__PURE__ */ new WeakMap();
17441
+ scopeByNodeCache.set(manager, scopeByNode);
17442
+ }
17443
+ if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
17331
17444
  let bestScope = null;
17332
17445
  let bestSize = Infinity;
17333
17446
  for (const scope of manager.scopes) {
@@ -17340,6 +17453,7 @@ const getScopeForNode = (node, manager) => {
17340
17453
  bestScope = scope;
17341
17454
  }
17342
17455
  }
17456
+ scopeByNode.set(node, bestScope);
17343
17457
  return bestScope;
17344
17458
  };
17345
17459
  //#endregion
@@ -17374,11 +17488,20 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
17374
17488
  } else if (isAstNode(child)) descend(child, visit, visited);
17375
17489
  }
17376
17490
  };
17491
+ const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
17377
17492
  const getUpstreamRefs = (analysis, ref) => {
17493
+ let upstreamByRef = upstreamRefsCache.get(analysis);
17494
+ if (!upstreamByRef) {
17495
+ upstreamByRef = /* @__PURE__ */ new WeakMap();
17496
+ upstreamRefsCache.set(analysis, upstreamByRef);
17497
+ }
17498
+ const cached = upstreamByRef.get(ref);
17499
+ if (cached) return cached;
17378
17500
  const refs = [];
17379
17501
  ascend(analysis, ref, (upRef) => {
17380
17502
  refs.push(upRef);
17381
17503
  });
17504
+ upstreamByRef.set(ref, refs);
17382
17505
  return refs;
17383
17506
  };
17384
17507
  const findDownstreamNodes = (topNode, type) => {
@@ -17388,11 +17511,24 @@ const findDownstreamNodes = (topNode, type) => {
17388
17511
  });
17389
17512
  return nodes;
17390
17513
  };
17514
+ const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
17391
17515
  const getRef = (analysis, identifier) => {
17516
+ let refByIdentifier = refByIdentifierCache.get(analysis);
17517
+ if (!refByIdentifier) {
17518
+ refByIdentifier = /* @__PURE__ */ new WeakMap();
17519
+ refByIdentifierCache.set(analysis, refByIdentifier);
17520
+ }
17521
+ if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
17522
+ let resolvedReference = null;
17392
17523
  const scope = getScopeForNode(identifier, analysis.scopeManager);
17393
- if (!scope) return null;
17394
- for (const reference of scope.references) if (reference.identifier === identifier) return reference;
17395
- return null;
17524
+ if (scope) {
17525
+ for (const reference of scope.references) if (reference.identifier === identifier) {
17526
+ resolvedReference = reference;
17527
+ break;
17528
+ }
17529
+ }
17530
+ refByIdentifier.set(identifier, resolvedReference);
17531
+ return resolvedReference;
17396
17532
  };
17397
17533
  const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
17398
17534
  const getDownstreamRefs = (analysis, node) => {
@@ -17467,22 +17603,6 @@ const isEventualCallTo = (analysis, ref, predicate) => {
17467
17603
  };
17468
17604
  //#endregion
17469
17605
  //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
17470
- const getOuterScopeContaining = (analysis, node) => {
17471
- if (!node.range) return null;
17472
- let best = null;
17473
- let bestSize = Infinity;
17474
- for (const scope of analysis.scopeManager.scopes) {
17475
- const block = scope.block;
17476
- if (!block?.range) continue;
17477
- if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
17478
- const size = block.range[1] - block.range[0];
17479
- if (size <= bestSize) {
17480
- bestSize = size;
17481
- best = scope;
17482
- }
17483
- }
17484
- return best;
17485
- };
17486
17606
  const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
17487
17607
  const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
17488
17608
  const isReactFunctionalComponent = (node) => {
@@ -17513,7 +17633,7 @@ const isReactFunctionalHOC = (analysis, node) => {
17513
17633
  const isWrappedSeparately = () => {
17514
17634
  if (!isNodeOfType(node.id, "Identifier")) return false;
17515
17635
  const bindingName = node.id.name;
17516
- const containingScope = getOuterScopeContaining(analysis, node);
17636
+ const containingScope = getScopeForNode(node, analysis.scopeManager);
17517
17637
  if (!containingScope) return false;
17518
17638
  const variable = containingScope.variables.find((v) => v.name === bindingName);
17519
17639
  if (!variable) return false;
@@ -18863,7 +18983,17 @@ const containsRenderOutput = (node, rootNode, scopes) => {
18863
18983
  }
18864
18984
  return false;
18865
18985
  };
18866
- const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
18986
+ const renderOutputCache = /* @__PURE__ */ new WeakMap();
18987
+ const functionContainsReactRenderOutput = (functionNode, scopes) => {
18988
+ const cachedEntry = renderOutputCache.get(functionNode);
18989
+ if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
18990
+ const hasRenderOutput = containsRenderOutput(functionNode, functionNode, scopes);
18991
+ renderOutputCache.set(functionNode, {
18992
+ scopes,
18993
+ hasRenderOutput
18994
+ });
18995
+ return hasRenderOutput;
18996
+ };
18867
18997
  //#endregion
18868
18998
  //#region src/plugin/utils/is-component-declaration.ts
18869
18999
  const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
@@ -19368,8 +19498,8 @@ const CONTEXT_MODULES = [
19368
19498
  ];
19369
19499
  const isCreateContextCallee = (callee) => {
19370
19500
  if (isNodeOfType(callee, "Identifier")) {
19371
- for (const moduleName of CONTEXT_MODULES) if (getImportedNameFromModule(callee, callee.name, moduleName) === "createContext") return true;
19372
- return false;
19501
+ const binding = getImportBindingForName(callee, callee.name);
19502
+ return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
19373
19503
  }
19374
19504
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
19375
19505
  const namespaceIdentifier = callee.object;
@@ -19379,8 +19509,8 @@ const isCreateContextCallee = (callee) => {
19379
19509
  if (propertyIdentifier.name !== "createContext") return false;
19380
19510
  const namespaceName = namespaceIdentifier.name;
19381
19511
  if (isCanonicalReactNamespaceName(namespaceName)) return true;
19382
- for (const moduleName of CONTEXT_MODULES) if (isImportedFromModule(namespaceIdentifier, namespaceName, moduleName)) return true;
19383
- return false;
19512
+ const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
19513
+ return importSource !== null && CONTEXT_MODULES.includes(importSource);
19384
19514
  }
19385
19515
  return false;
19386
19516
  };
@@ -19782,38 +19912,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
19782
19912
  };
19783
19913
  const parseColorToRgb = (value) => {
19784
19914
  const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
19785
- const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
19786
- if (hex8Match) return {
19787
- red: parseInt(hex8Match[1], 16),
19788
- green: parseInt(hex8Match[2], 16),
19789
- blue: parseInt(hex8Match[3], 16)
19790
- };
19791
- const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
19792
- if (hex6Match) return {
19793
- red: parseInt(hex6Match[1], 16),
19794
- green: parseInt(hex6Match[2], 16),
19795
- blue: parseInt(hex6Match[3], 16)
19796
- };
19797
- const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
19798
- if (hex4Match) return {
19799
- red: parseInt(hex4Match[1] + hex4Match[1], 16),
19800
- green: parseInt(hex4Match[2] + hex4Match[2], 16),
19801
- blue: parseInt(hex4Match[3] + hex4Match[3], 16)
19802
- };
19803
- const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
19804
- if (hex3Match) return {
19805
- red: parseInt(hex3Match[1] + hex3Match[1], 16),
19806
- green: parseInt(hex3Match[2] + hex3Match[2], 16),
19807
- blue: parseInt(hex3Match[3] + hex3Match[3], 16)
19808
- };
19809
- const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
19810
- if (rgbMatch) return {
19811
- red: parseInt(rgbMatch[1], 10),
19812
- green: parseInt(rgbMatch[2], 10),
19813
- blue: parseInt(rgbMatch[3], 10)
19814
- };
19815
- const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
19816
- if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
19915
+ if (trimmed.startsWith("#")) {
19916
+ const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
19917
+ if (hex8Match) return {
19918
+ red: parseInt(hex8Match[1], 16),
19919
+ green: parseInt(hex8Match[2], 16),
19920
+ blue: parseInt(hex8Match[3], 16)
19921
+ };
19922
+ const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
19923
+ if (hex6Match) return {
19924
+ red: parseInt(hex6Match[1], 16),
19925
+ green: parseInt(hex6Match[2], 16),
19926
+ blue: parseInt(hex6Match[3], 16)
19927
+ };
19928
+ const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
19929
+ if (hex4Match) return {
19930
+ red: parseInt(hex4Match[1] + hex4Match[1], 16),
19931
+ green: parseInt(hex4Match[2] + hex4Match[2], 16),
19932
+ blue: parseInt(hex4Match[3] + hex4Match[3], 16)
19933
+ };
19934
+ const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
19935
+ if (hex3Match) return {
19936
+ red: parseInt(hex3Match[1] + hex3Match[1], 16),
19937
+ green: parseInt(hex3Match[2] + hex3Match[2], 16),
19938
+ blue: parseInt(hex3Match[3] + hex3Match[3], 16)
19939
+ };
19940
+ }
19941
+ if (trimmed.includes("rgb")) {
19942
+ const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
19943
+ if (rgbMatch) return {
19944
+ red: parseInt(rgbMatch[1], 10),
19945
+ green: parseInt(rgbMatch[2], 10),
19946
+ blue: parseInt(rgbMatch[3], 10)
19947
+ };
19948
+ }
19949
+ if (trimmed.includes("hsl")) {
19950
+ const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
19951
+ if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
19952
+ }
19817
19953
  return null;
19818
19954
  };
19819
19955
  //#endregion
@@ -19841,9 +19977,18 @@ const extractColorFromShadowLayer = (layer) => {
19841
19977
  if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
19842
19978
  return null;
19843
19979
  };
19980
+ const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
19981
+ const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
19982
+ const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
19983
+ const SHADOW_BLUR_TOKEN_INDEX = 2;
19844
19984
  const parseShadowLayerBlur = (layer) => {
19845
- const numericTokens = [...layer.replace(/rgba?\([^)]*\)/g, "").replace(/#[0-9a-f]{3,8}\b/gi, "").matchAll(/(\d+(?:\.\d+)?)(px)?/g)].map((match) => parseFloat(match[1]));
19846
- return numericTokens.length >= 3 ? numericTokens[2] : 0;
19985
+ const withoutColors = layer.replace(RGB_FUNCTION_PATTERN, "").replace(HEX_COLOR_PATTERN, "");
19986
+ let tokenIndex = 0;
19987
+ for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
19988
+ if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
19989
+ tokenIndex += 1;
19990
+ }
19991
+ return 0;
19847
19992
  };
19848
19993
  const hasColoredGlowShadow = (shadowValue) => {
19849
19994
  for (const layer of splitShadowLayers(shadowValue)) {
@@ -19963,7 +20108,10 @@ const isIntrinsicJsxAttribute = (node) => {
19963
20108
  };
19964
20109
  //#endregion
19965
20110
  //#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
20111
+ const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
19966
20112
  const collectComponentPropNames = (componentFunction) => {
20113
+ const cached = componentPropNamesCache.get(componentFunction);
20114
+ if (cached) return cached;
19967
20115
  const propNames = /* @__PURE__ */ new Set();
19968
20116
  if (!isFunctionLike$1(componentFunction)) return propNames;
19969
20117
  const propsObjectParamNames = /* @__PURE__ */ new Set();
@@ -19977,27 +20125,23 @@ const collectComponentPropNames = (componentFunction) => {
19977
20125
  if (child !== componentBody && isFunctionLike$1(child)) return false;
19978
20126
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
19979
20127
  });
20128
+ componentPropNamesCache.set(componentFunction, propNames);
19980
20129
  return propNames;
19981
20130
  };
19982
- const declaresBindingNamed = (functionNode, bindingName) => {
20131
+ const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
20132
+ const getOwnScopeBoundNames = (functionNode) => {
20133
+ const cached = ownScopeBoundNamesCache.get(functionNode);
20134
+ if (cached) return cached;
19983
20135
  const boundNames = /* @__PURE__ */ new Set();
19984
20136
  if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
19985
- if (boundNames.has(bindingName)) return true;
19986
- let declaresName = false;
19987
20137
  walkAst(functionNode, (child) => {
19988
- if (declaresName) return false;
19989
20138
  if (child !== functionNode && isFunctionLike$1(child)) return false;
19990
- if (isNodeOfType(child, "VariableDeclarator")) {
19991
- const declaratorNames = /* @__PURE__ */ new Set();
19992
- collectPatternNames(child.id, declaratorNames);
19993
- if (declaratorNames.has(bindingName)) {
19994
- declaresName = true;
19995
- return false;
19996
- }
19997
- }
20139
+ if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
19998
20140
  });
19999
- return declaresName;
20141
+ ownScopeBoundNamesCache.set(functionNode, boundNames);
20142
+ return boundNames;
20000
20143
  };
20144
+ const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
20001
20145
  const isPropertyNamePosition = (identifier) => {
20002
20146
  const parent = identifier.parent;
20003
20147
  if (!parent) return false;
@@ -20420,36 +20564,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
20420
20564
  }
20421
20565
  return hasNestedFunction;
20422
20566
  };
20423
- const isReseededDraftBuffer = (useStateCall, isPropName) => {
20424
- const setterName = getStateSetterName(useStateCall);
20425
- if (!setterName) return false;
20426
- const componentFunction = findEnclosingFunction(useStateCall);
20427
- if (!componentFunction) return false;
20428
- let isReseeded = false;
20429
- walkAst(componentFunction, (child) => {
20430
- if (isReseeded) return false;
20431
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
20432
- isReseeded = true;
20433
- return false;
20434
- }
20435
- });
20436
- return isReseeded;
20567
+ const isInRenderScope = (node, componentFunction) => {
20568
+ let cursor = node.parent ?? null;
20569
+ while (cursor && cursor !== componentFunction) {
20570
+ if (isFunctionLike$1(cursor)) return false;
20571
+ cursor = cursor.parent ?? null;
20572
+ }
20573
+ return true;
20437
20574
  };
20438
- const isAdjustedDuringRender = (useStateCall, isPropName) => {
20575
+ const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
20439
20576
  const setterName = getStateSetterName(useStateCall);
20440
20577
  if (!setterName) return false;
20441
20578
  const componentFunction = findEnclosingFunction(useStateCall);
20442
20579
  if (!componentFunction) return false;
20443
- let isAdjusted = false;
20580
+ let isExempt = false;
20444
20581
  walkAst(componentFunction, (child) => {
20445
- if (isAdjusted) return false;
20446
- if (child !== componentFunction && isFunctionLike$1(child)) return false;
20447
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName)) {
20448
- isAdjusted = true;
20582
+ if (isExempt) return false;
20583
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && (isHandlerShapedReseed(child, componentFunction) || isInRenderScope(child, componentFunction))) {
20584
+ isExempt = true;
20449
20585
  return false;
20450
20586
  }
20451
20587
  });
20452
- return isAdjusted;
20588
+ return isExempt;
20453
20589
  };
20454
20590
  const noDerivedUseState = defineRule({
20455
20591
  id: "no-derived-useState",
@@ -20466,8 +20602,7 @@ const noDerivedUseState = defineRule({
20466
20602
  const initializer = node.arguments[0];
20467
20603
  if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
20468
20604
  if (isInitialOnlyPropName(initializer.name)) return;
20469
- if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20470
- if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20605
+ if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20471
20606
  context.report({
20472
20607
  node,
20473
20608
  message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
@@ -20478,8 +20613,7 @@ const noDerivedUseState = defineRule({
20478
20613
  const rootIdentifierName = getRootIdentifierName(initializer);
20479
20614
  if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
20480
20615
  if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
20481
- if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20482
- if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20616
+ if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20483
20617
  context.report({
20484
20618
  node,
20485
20619
  message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
@@ -22178,20 +22312,20 @@ const getTriggerGuardRootName = (testNode) => {
22178
22312
  return null;
22179
22313
  };
22180
22314
  const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
22315
+ const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
22316
+ const settersWithAnyCall = /* @__PURE__ */ new Set();
22317
+ const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
22318
+ walkAst(componentBody, (child) => {
22319
+ if (!isNodeOfType(child, "CallExpression")) return;
22320
+ if (!isNodeOfType(child.callee, "Identifier")) return;
22321
+ const setterName = child.callee.name;
22322
+ if (!setterNames.has(setterName)) return;
22323
+ settersWithAnyCall.add(setterName);
22324
+ if (settersWithNonHandlerCall.has(setterName)) return;
22325
+ if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
22326
+ });
22181
22327
  const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
22182
- for (const binding of useStateBindings) {
22183
- let didFindAnySetterCall = false;
22184
- let areAllSetterCallsInHandlers = true;
22185
- walkAst(componentBody, (child) => {
22186
- if (!areAllSetterCallsInHandlers) return false;
22187
- if (!isNodeOfType(child, "CallExpression")) return;
22188
- if (!isNodeOfType(child.callee, "Identifier")) return;
22189
- if (child.callee.name !== binding.setterName) return;
22190
- didFindAnySetterCall = true;
22191
- if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
22192
- });
22193
- if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
22194
- }
22328
+ for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
22195
22329
  return handlerOnlyWriteStateNames;
22196
22330
  };
22197
22331
  const noEventTriggerState = defineRule({
@@ -22531,6 +22665,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
22531
22665
  const TEXT_COLOR_PATTERN = /^text-(?:white|black|transparent|current|inherit|\[|(?:gray|slate|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-)/;
22532
22666
  const BG_COLOR_PATTERN = /^bg-(?:white|black|transparent|current|inherit|\[|(?:gray|slate|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-)/;
22533
22667
  const splitVariantScope = (token) => {
22668
+ if (!token.includes(":")) return {
22669
+ scope: "",
22670
+ utility: token.startsWith("!") ? token.slice(1) : token
22671
+ };
22534
22672
  const segments = token.split(":");
22535
22673
  const rawUtility = segments[segments.length - 1];
22536
22674
  return {
@@ -22554,6 +22692,7 @@ const noGrayOnColoredBackground = defineRule({
22554
22692
  const bgColorScopes = /* @__PURE__ */ new Set();
22555
22693
  for (const token of classStr.split(/\s+/)) {
22556
22694
  if (!token) continue;
22695
+ if (!token.includes("text-") && !token.includes("bg-")) continue;
22557
22696
  const { scope, utility } = splitVariantScope(token);
22558
22697
  if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
22559
22698
  if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
@@ -23283,6 +23422,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
23283
23422
  return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
23284
23423
  });
23285
23424
  const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
23425
+ const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
23426
+ const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
23286
23427
  const noLongTransitionDuration = defineRule({
23287
23428
  id: "no-long-transition-duration",
23288
23429
  title: "Transition duration too long",
@@ -23306,11 +23447,10 @@ const noLongTransitionDuration = defineRule({
23306
23447
  if (key === "transitionDuration" || key === "animationDuration") {
23307
23448
  let longestDurationPropertyMs = 0;
23308
23449
  for (const segment of value.split(",")) {
23309
- const trimmedSegment = segment.trim();
23310
- const msMatch = trimmedSegment.match(/^([\d.]+)ms$/);
23311
- const secondsMatch = trimmedSegment.match(/^([\d.]+)s$/);
23312
- if (msMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(msMatch[1]));
23313
- else if (secondsMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(secondsMatch[1]) * 1e3);
23450
+ const durationMatch = segment.trim().match(DURATION_SEGMENT_PATTERN);
23451
+ if (!durationMatch) continue;
23452
+ const segmentDurationMs = durationMatch[2] === "ms" ? parseFloat(durationMatch[1]) : parseFloat(durationMatch[1]) * 1e3;
23453
+ longestDurationPropertyMs = Math.max(longestDurationPropertyMs, segmentDurationMs);
23314
23454
  }
23315
23455
  if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
23316
23456
  }
@@ -23318,7 +23458,7 @@ const noLongTransitionDuration = defineRule({
23318
23458
  let longestDurationMs = 0;
23319
23459
  for (const segment of value.split(",")) {
23320
23460
  if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
23321
- const firstTimeMatch = segment.match(/(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/);
23461
+ const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
23322
23462
  if (!firstTimeMatch) continue;
23323
23463
  const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
23324
23464
  longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
@@ -26076,14 +26216,14 @@ const noRenderInRender = defineRule({
26076
26216
  const expression = node.expression;
26077
26217
  if (!isNodeOfType(expression, "CallExpression")) return;
26078
26218
  let calleeName = null;
26219
+ if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
26220
+ else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
26221
+ if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
26079
26222
  if (isNodeOfType(expression.callee, "Identifier")) {
26080
26223
  if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
26081
- calleeName = expression.callee.name;
26082
- } else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
26224
+ } else if (isNodeOfType(expression.callee, "MemberExpression")) {
26083
26225
  if (rootsInProps(expression.callee.object, context.scopes)) return;
26084
- calleeName = expression.callee.property.name;
26085
26226
  }
26086
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
26087
26227
  context.report({
26088
26228
  node: expression,
26089
26229
  message: `Your users lose state because "${calleeName}()" builds UI from an inline call that React remounts, so pull it into its own component instead.`
@@ -28798,32 +28938,26 @@ const isFirstArgumentOfHocCall = (node) => {
28798
28938
  if (!isHocCallee$1(parent)) return false;
28799
28939
  return parent.arguments[0] === node;
28800
28940
  };
28941
+ const MAP_LIKE_METHOD_NAMES = new Set([
28942
+ "map",
28943
+ "forEach",
28944
+ "filter",
28945
+ "flatMap",
28946
+ "reduce",
28947
+ "reduceRight"
28948
+ ]);
28801
28949
  const isReturnOfMapCallback = (node) => {
28802
28950
  const parent = node.parent;
28803
28951
  if (!parent) return false;
28804
28952
  if (isNodeOfType(parent, "CallExpression")) {
28805
28953
  const callee = parent.callee;
28806
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return [
28807
- "map",
28808
- "forEach",
28809
- "filter",
28810
- "flatMap",
28811
- "reduce",
28812
- "reduceRight"
28813
- ].includes(callee.property.name);
28954
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28814
28955
  }
28815
28956
  if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
28816
28957
  const callbackParent = parent.parent;
28817
28958
  if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
28818
28959
  const callee = callbackParent.callee;
28819
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return [
28820
- "map",
28821
- "forEach",
28822
- "filter",
28823
- "flatMap",
28824
- "reduce",
28825
- "reduceRight"
28826
- ].includes(callee.property.name);
28960
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28827
28961
  }
28828
28962
  }
28829
28963
  return false;
@@ -28859,10 +28993,10 @@ const isRenderFlowingReadReference = (identifier) => {
28859
28993
  valueNode = parent;
28860
28994
  parent = parent.parent;
28861
28995
  continue;
28862
- case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isUppercaseName(parent.id.name);
28996
+ case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isReactComponentName(parent.id.name);
28863
28997
  case "AssignmentExpression": {
28864
28998
  const assignmentTarget = parent.left;
28865
- return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isUppercaseName(assignmentTarget.name);
28999
+ return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
28866
29000
  }
28867
29001
  case "Property":
28868
29002
  if (parent.value !== valueNode) return false;
@@ -28955,14 +29089,14 @@ const noUnstableNestedComponents = defineRule({
28955
29089
  };
28956
29090
  return {
28957
29091
  JSXOpeningElement(node) {
28958
- if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) {
29092
+ if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
28959
29093
  recordInstantiation(node.name, node.name.name);
28960
29094
  return;
28961
29095
  }
28962
29096
  if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
28963
29097
  },
28964
29098
  Identifier(node) {
28965
- if (!isUppercaseName(node.name)) return;
29099
+ if (!isReactComponentName(node.name)) return;
28966
29100
  if (!isRenderFlowingReadReference(node)) return;
28967
29101
  recordInstantiation(node, node.name);
28968
29102
  },
@@ -30772,13 +30906,13 @@ const preferStableEmptyFallback = defineRule({
30772
30906
  memoRegistry = buildSameFileMemoRegistry(node);
30773
30907
  },
30774
30908
  JSXAttribute(node) {
30775
- if (!isInsideFunctionScope(node)) return;
30776
- if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30777
30909
  if (!node.value) return;
30778
30910
  if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
30779
30911
  const innerExpression = node.value.expression;
30780
30912
  if (!innerExpression) return;
30781
30913
  if (innerExpression.type === "JSXEmptyExpression") return;
30914
+ if (!isInsideFunctionScope(node)) return;
30915
+ if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30782
30916
  const parentJsxOpening = node.parent;
30783
30917
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
30784
30918
  if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
@@ -32150,41 +32284,11 @@ const callsIdentifier = (root, identifierName) => {
32150
32284
  });
32151
32285
  return found;
32152
32286
  };
32153
- const setterIsCalledInAsyncContext = (componentBody, setterName) => {
32154
- if (!componentBody) return false;
32155
- let found = false;
32156
- walkAst(componentBody, (child) => {
32157
- if (found) return;
32158
- if (!isFunctionLike$1(child)) return;
32159
- const functionBody = child.body;
32160
- if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
32161
- if (callsIdentifier(functionBody, setterName)) found = true;
32162
- });
32163
- return found;
32164
- };
32165
32287
  const PROMISE_CHAIN_METHOD_NAMES = new Set([
32166
32288
  "then",
32167
32289
  "catch",
32168
32290
  "finally"
32169
32291
  ]);
32170
- const setterIsCalledInPromiseChain = (componentBody, setterName) => {
32171
- if (!componentBody) return false;
32172
- let found = false;
32173
- walkAst(componentBody, (child) => {
32174
- if (found) return;
32175
- if (!isNodeOfType(child, "CallExpression")) return;
32176
- const callee = child.callee;
32177
- if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
32178
- for (const argument of child.arguments ?? []) {
32179
- if (!isFunctionLike$1(argument)) continue;
32180
- if (callsIdentifier(argument.body, setterName)) {
32181
- found = true;
32182
- return;
32183
- }
32184
- }
32185
- });
32186
- return found;
32187
- };
32188
32292
  const ASYNC_DATA_CALLEE_NAMES = new Set([
32189
32293
  "useApolloClient",
32190
32294
  "useMutation",
@@ -32197,20 +32301,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
32197
32301
  "fetch",
32198
32302
  "axios"
32199
32303
  ]);
32200
- const referencesAsyncDataApi = (body) => {
32201
- if (!body) return false;
32304
+ const hasAsyncLoadingWork = (fnBody, setterName) => {
32202
32305
  let found = false;
32203
- walkAst(body, (child) => {
32204
- if (found) return;
32306
+ walkAst(fnBody, (child) => {
32307
+ if (found) return false;
32205
32308
  if (isNodeOfType(child, "CallExpression")) {
32206
32309
  const callee = child.callee;
32207
32310
  if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
32208
32311
  found = true;
32209
- return;
32312
+ return false;
32210
32313
  }
32211
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
32314
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
32315
+ if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
32316
+ found = true;
32317
+ return false;
32318
+ }
32319
+ if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
32320
+ if (!isFunctionLike$1(argument)) continue;
32321
+ if (callsIdentifier(argument.body, setterName)) {
32322
+ found = true;
32323
+ return false;
32324
+ }
32325
+ }
32326
+ }
32327
+ return;
32328
+ }
32329
+ if (setterName !== null && isFunctionLike$1(child)) {
32330
+ const functionBody = child.body;
32331
+ if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
32212
32332
  found = true;
32213
- return;
32333
+ return false;
32214
32334
  }
32215
32335
  }
32216
32336
  });
@@ -32235,7 +32355,7 @@ const renderingUsetransitionLoading = defineRule({
32235
32355
  const secondBinding = node.id.elements[1];
32236
32356
  const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
32237
32357
  const fnBody = enclosingFunctionBody(node);
32238
- if (fnBody && (setterName && setterIsCalledInAsyncContext(fnBody, setterName) || setterName && setterIsCalledInPromiseChain(fnBody, setterName) || referencesAsyncDataApi(fnBody))) return;
32358
+ if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
32239
32359
  context.report({
32240
32360
  node: node.init,
32241
32361
  message: `This adds an extra render because useState for "${stateVariableName}" re-renders just for the loading flag, so if it's a state change & not a data fetch, use useTransition instead`
@@ -33029,17 +33149,17 @@ const rerenderStateOnlyInHandlers = defineRule({
33029
33149
  const effectTriggerNames = /* @__PURE__ */ new Set();
33030
33150
  for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
33031
33151
  for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
33152
+ const setterNames = new Set(bindings.map((binding) => binding.setterName));
33153
+ const calledSetterNames = /* @__PURE__ */ new Set();
33154
+ walkAst(componentBody, (child) => {
33155
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
33156
+ });
33032
33157
  for (const binding of bindings) {
33033
33158
  if (renderReachableNames.has(binding.valueName)) continue;
33034
33159
  if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
33035
33160
  const setterSuffix = binding.setterName.slice(3);
33036
33161
  if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
33037
- let setterCalled = false;
33038
- walkAst(componentBody, (child) => {
33039
- if (setterCalled) return;
33040
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
33041
- });
33042
- if (!setterCalled) continue;
33162
+ if (!calledSetterNames.has(binding.setterName)) continue;
33043
33163
  if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
33044
33164
  context.report({
33045
33165
  node: binding.declarator,
@@ -33371,6 +33491,7 @@ const RECYCLABLE_LIST_PACKAGES = {
33371
33491
  FlashList: ["@shopify/flash-list"],
33372
33492
  LegendList: ["@legendapp/list"]
33373
33493
  };
33494
+ const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
33374
33495
  const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
33375
33496
  const RENDER_ITEM_PROP_NAMES = new Set([
33376
33497
  "renderItem",
@@ -33613,33 +33734,42 @@ const rnListMissingEstimatedItemSize = defineRule({
33613
33734
  requires: ["react-native"],
33614
33735
  severity: "warn",
33615
33736
  recommendation: "Without `estimatedItemSize` the list guesses row height and can flash blank cells on fast scroll. Add `estimatedItemSize={<avg-row-height-in-px>}` so it matches your rows.",
33616
- create: (context) => ({ JSXOpeningElement(node) {
33617
- const localElementName = resolveJsxElementName(node);
33618
- if (!localElementName) return;
33619
- const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
33620
- if (canonicalRecyclerName === null) return;
33621
- if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
33622
- let hasSizingHint = false;
33623
- let dataIsEmptyLiteral = false;
33624
- let hasDataProp = false;
33625
- for (const attribute of node.attributes ?? []) {
33626
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
33627
- if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
33628
- const attributeName = attribute.name.name;
33629
- if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
33630
- if (attributeName === "data") {
33631
- hasDataProp = true;
33632
- if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
33737
+ create: (context) => {
33738
+ let fileImportsRecycler = false;
33739
+ return {
33740
+ Program(node) {
33741
+ fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
33742
+ },
33743
+ JSXOpeningElement(node) {
33744
+ if (!fileImportsRecycler) return;
33745
+ const localElementName = resolveJsxElementName(node);
33746
+ if (!localElementName) return;
33747
+ const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
33748
+ if (canonicalRecyclerName === null) return;
33749
+ if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
33750
+ let hasSizingHint = false;
33751
+ let dataIsEmptyLiteral = false;
33752
+ let hasDataProp = false;
33753
+ for (const attribute of node.attributes ?? []) {
33754
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
33755
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
33756
+ const attributeName = attribute.name.name;
33757
+ if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
33758
+ if (attributeName === "data") {
33759
+ hasDataProp = true;
33760
+ if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
33761
+ }
33762
+ }
33763
+ if (hasSizingHint) return;
33764
+ if (dataIsEmptyLiteral) return;
33765
+ if (!hasDataProp) return;
33766
+ context.report({
33767
+ node,
33768
+ message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
33769
+ });
33633
33770
  }
33634
- }
33635
- if (hasSizingHint) return;
33636
- if (dataIsEmptyLiteral) return;
33637
- if (!hasDataProp) return;
33638
- context.report({
33639
- node,
33640
- message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
33641
- });
33642
- } })
33771
+ };
33772
+ }
33643
33773
  });
33644
33774
  //#endregion
33645
33775
  //#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
@@ -33650,25 +33780,34 @@ const rnListRecyclableWithoutTypes = defineRule({
33650
33780
  requires: ["react-native"],
33651
33781
  severity: "warn",
33652
33782
  recommendation: "When rows have different shapes, reused cells can show the wrong layout. Add `getItemType={item => item.kind}` so FlashList keeps a separate pool per row type.",
33653
- create: (context) => ({ JSXOpeningElement(node) {
33654
- const elementName = resolveJsxElementName(node);
33655
- if (!elementName) return;
33656
- if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
33657
- let hasRecycleItemsEnabled = false;
33658
- let hasGetItemType = false;
33659
- for (const attr of node.attributes ?? []) {
33660
- if (!isNodeOfType(attr, "JSXAttribute")) continue;
33661
- if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
33662
- if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
33663
- else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
33664
- else hasRecycleItemsEnabled = true;
33665
- if (attr.name.name === "getItemType") hasGetItemType = true;
33666
- }
33667
- if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
33668
- node,
33669
- message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
33670
- });
33671
- } })
33783
+ create: (context) => {
33784
+ let fileImportsRecycler = false;
33785
+ return {
33786
+ Program(node) {
33787
+ fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
33788
+ },
33789
+ JSXOpeningElement(node) {
33790
+ if (!fileImportsRecycler) return;
33791
+ const elementName = resolveJsxElementName(node);
33792
+ if (!elementName) return;
33793
+ if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
33794
+ let hasRecycleItemsEnabled = false;
33795
+ let hasGetItemType = false;
33796
+ for (const attr of node.attributes ?? []) {
33797
+ if (!isNodeOfType(attr, "JSXAttribute")) continue;
33798
+ if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
33799
+ if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
33800
+ else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
33801
+ else hasRecycleItemsEnabled = true;
33802
+ if (attr.name.name === "getItemType") hasGetItemType = true;
33803
+ }
33804
+ if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
33805
+ node,
33806
+ message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
33807
+ });
33808
+ }
33809
+ };
33810
+ }
33672
33811
  });
33673
33812
  //#endregion
33674
33813
  //#region src/plugin/rules/react-native/rn-no-deep-imports.ts
@@ -34698,6 +34837,7 @@ const rnNoRawText = defineRule({
34698
34837
  return {
34699
34838
  Program(programNode) {
34700
34839
  isDomComponentFile = hasDirective(programNode, "use dom");
34840
+ if (!containsJsxElement(programNode)) return;
34701
34841
  const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
34702
34842
  autoDetectedTextWrappers = childrenForwarding.textWrappers;
34703
34843
  autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
@@ -38652,14 +38792,7 @@ const roleSupportsAriaProps = defineRule({
38652
38792
  recommendation: "Only use `aria-*` attributes that the element's role supports.",
38653
38793
  category: "Accessibility",
38654
38794
  create: (context) => ({ JSXOpeningElement(node) {
38655
- const elementType = getElementType(node, context.settings);
38656
- const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
38657
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
38658
- if (!role) return;
38659
- if (!VALID_ARIA_ROLES.has(role)) return;
38660
- const isImplicit = !roleAttribute;
38661
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
38662
- if (!supported) return;
38795
+ let ariaAttributes = null;
38663
38796
  for (const attribute of node.attributes) {
38664
38797
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
38665
38798
  const attributeNode = attribute;
@@ -38669,6 +38802,21 @@ const roleSupportsAriaProps = defineRule({
38669
38802
  const propName = propRawName.toLowerCase();
38670
38803
  if (!propName.startsWith("aria-")) continue;
38671
38804
  if (!ARIA_PROPERTIES.has(propName)) continue;
38805
+ (ariaAttributes ??= []).push({
38806
+ attribute,
38807
+ propName
38808
+ });
38809
+ }
38810
+ if (!ariaAttributes) return;
38811
+ const elementType = getElementType(node, context.settings);
38812
+ const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
38813
+ const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
38814
+ if (!role) return;
38815
+ if (!VALID_ARIA_ROLES.has(role)) return;
38816
+ const isImplicit = !roleAttribute;
38817
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
38818
+ if (!supported) return;
38819
+ for (const { attribute, propName } of ariaAttributes) {
38672
38820
  if (supported.has(propName)) continue;
38673
38821
  context.report({
38674
38822
  node: attribute,
@@ -39506,6 +39654,7 @@ const serverCacheWithObjectLiteral = defineRule({
39506
39654
  cachedFunctionNames.add(node.id.name);
39507
39655
  },
39508
39656
  CallExpression(node) {
39657
+ if (cachedFunctionNames.size === 0) return;
39509
39658
  if (!isNodeOfType(node.callee, "Identifier")) return;
39510
39659
  if (!cachedFunctionNames.has(node.callee.name)) return;
39511
39660
  const firstArg = node.arguments?.[0];
@@ -40432,7 +40581,16 @@ const supabaseRlsPolicyRisk = defineRule({
40432
40581
  //#endregion
40433
40582
  //#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
40434
40583
  const CREATE_PUBLIC_TABLE_PATTERN = /create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)\s*\.)(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi;
40435
- const enableRlsForTablePattern = (tableName) => new RegExp(`alter\\s+table\\s+(?:if\\s+exists\\s+)?(?:only\\s+)?(?:public\\s*\\.\\s*)?["\`]?${escapeRegExp(tableName)}["\`]?\\s+(?:force\\s+)?enable\\s+row\\s+level\\s+security`, "i");
40584
+ const ENABLE_RLS_PATTERN = /alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?\s+(?:force\s+)?enable\s+row\s+level\s+security/gi;
40585
+ const collectLastEnableRlsIndexByTable = (content) => {
40586
+ const lastEnableIndexByTable = /* @__PURE__ */ new Map();
40587
+ for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
40588
+ const tableName = match[1];
40589
+ if (tableName === void 0) continue;
40590
+ lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
40591
+ }
40592
+ return lastEnableIndexByTable;
40593
+ };
40436
40594
  const supabaseTableMissingRls = defineRule({
40437
40595
  id: "supabase-table-missing-rls",
40438
40596
  title: "Supabase table created without Row Level Security",
@@ -40443,11 +40601,13 @@ const supabaseTableMissingRls = defineRule({
40443
40601
  const content = sanitizeSqlForScan(file.content);
40444
40602
  if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
40445
40603
  const findings = [];
40604
+ const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
40446
40605
  CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
40447
40606
  for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
40448
40607
  const tableName = match[1];
40449
40608
  if (tableName === void 0) continue;
40450
- if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
40609
+ const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
40610
+ if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
40451
40611
  const location = getLocationAtIndex(content, match.index);
40452
40612
  findings.push({
40453
40613
  message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
@@ -40723,6 +40883,7 @@ const tanstackStartMissingHeadContent = defineRule({
40723
40883
  severity: "warn",
40724
40884
  recommendation: "Add `<HeadContent />` inside `<head>` in your __root route. Without it, route `head()` meta tags are dropped.",
40725
40885
  create: (context) => {
40886
+ if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(context.filename ?? "")) return {};
40726
40887
  let hasHeadContentElement = false;
40727
40888
  let hasDocumentHeadElement = false;
40728
40889
  let hasCustomHeadChildElement = false;
@@ -40759,8 +40920,6 @@ const tanstackStartMissingHeadContent = defineRule({
40759
40920
  };
40760
40921
  return {
40761
40922
  Program(node) {
40762
- const filename = context.filename ?? "";
40763
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40764
40923
  const statements = node.body ?? [];
40765
40924
  for (const statement of statements) collectImportBindings(statement);
40766
40925
  for (const statement of statements) {
@@ -40769,18 +40928,12 @@ const tanstackStartMissingHeadContent = defineRule({
40769
40928
  }
40770
40929
  },
40771
40930
  ImportDeclaration(node) {
40772
- const filename = context.filename ?? "";
40773
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40774
40931
  collectImportBindings(node);
40775
40932
  },
40776
40933
  VariableDeclarator(node) {
40777
- const filename = context.filename ?? "";
40778
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40779
40934
  collectVariableAlias(node);
40780
40935
  },
40781
40936
  JSXOpeningElement(node) {
40782
- const filename = normalizeFilename(context.filename ?? "");
40783
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40784
40937
  if (isNodeOfType(node.name, "JSXIdentifier")) {
40785
40938
  if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
40786
40939
  if (headContentComponentNames.has(node.name.name)) hasHeadContentElement = true;
@@ -40794,8 +40947,6 @@ const tanstackStartMissingHeadContent = defineRule({
40794
40947
  if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
40795
40948
  },
40796
40949
  "Program:exit"(programNode) {
40797
- const filename = normalizeFilename(context.filename ?? "");
40798
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40799
40950
  if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
40800
40951
  node: programNode,
40801
40952
  message: "Without <HeadContent /> in the __root route, your route head() meta tags never render."
@@ -40819,27 +40970,29 @@ const tanstackStartNoAnchorElement = defineRule({
40819
40970
  requires: ["tanstack-start"],
40820
40971
  severity: "warn",
40821
40972
  recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
40822
- create: (context) => ({ JSXOpeningElement(node) {
40823
- if (!isInProjectDirectory(context, "routes")) return;
40824
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
40825
- const attributes = node.attributes ?? [];
40826
- const hrefAttribute = findJsxAttribute(attributes, "href");
40827
- if (!hrefAttribute?.value) return;
40828
- let hrefValue = null;
40829
- if (isNodeOfType(hrefAttribute.value, "Literal")) hrefValue = hrefAttribute.value.value;
40830
- else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
40831
- if (typeof hrefValue !== "string" || !hrefValue.startsWith("/")) return;
40832
- if (hrefValue.startsWith("//")) return;
40833
- const pathname = hrefValue.split(/[?#]/)[0] ?? hrefValue;
40834
- if (pathname.startsWith("/api/")) return;
40835
- if (/\.[a-z0-9]{1,8}$/i.test(pathname)) return;
40836
- if (findJsxAttribute(attributes, "download")) return;
40837
- if (getAttributeStringValue(findJsxAttribute(attributes, "target")) === "_blank") return;
40838
- context.report({
40839
- node,
40840
- message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
40841
- });
40842
- } })
40973
+ create: (context) => {
40974
+ if (!isInProjectDirectory(context, "routes")) return {};
40975
+ return { JSXOpeningElement(node) {
40976
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
40977
+ const attributes = node.attributes ?? [];
40978
+ const hrefAttribute = findJsxAttribute(attributes, "href");
40979
+ if (!hrefAttribute?.value) return;
40980
+ let hrefValue = null;
40981
+ if (isNodeOfType(hrefAttribute.value, "Literal")) hrefValue = hrefAttribute.value.value;
40982
+ else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
40983
+ if (typeof hrefValue !== "string" || !hrefValue.startsWith("/")) return;
40984
+ if (hrefValue.startsWith("//")) return;
40985
+ const pathname = hrefValue.split(/[?#]/)[0] ?? hrefValue;
40986
+ if (pathname.startsWith("/api/")) return;
40987
+ if (/\.[a-z0-9]{1,8}$/i.test(pathname)) return;
40988
+ if (findJsxAttribute(attributes, "download")) return;
40989
+ if (getAttributeStringValue(findJsxAttribute(attributes, "target")) === "_blank") return;
40990
+ context.report({
40991
+ node,
40992
+ message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
40993
+ });
40994
+ } };
40995
+ }
40843
40996
  });
40844
40997
  //#endregion
40845
40998
  //#region src/plugin/rules/tanstack-start/tanstack-start-no-direct-fetch-in-loader.ts
@@ -40903,7 +41056,7 @@ const tanstackStartNoNavigateInRender = defineRule({
40903
41056
  severity: "warn",
40904
41057
  recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
40905
41058
  create: (context) => {
40906
- const isRouteFile = isInProjectDirectory(context, "routes");
41059
+ if (!isInProjectDirectory(context, "routes")) return {};
40907
41060
  let deferredCallbackDepth = 0;
40908
41061
  let eventHandlerDepth = 0;
40909
41062
  const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
@@ -40967,7 +41120,6 @@ const tanstackStartNoNavigateInRender = defineRule({
40967
41120
  };
40968
41121
  return {
40969
41122
  CallExpression(node) {
40970
- if (!isRouteFile) return;
40971
41123
  if (isDeferredHookCall(node)) deferredCallbackDepth++;
40972
41124
  if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
40973
41125
  if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
@@ -40979,39 +41131,30 @@ const tanstackStartNoNavigateInRender = defineRule({
40979
41131
  }
40980
41132
  },
40981
41133
  "CallExpression:exit"(node) {
40982
- if (!isRouteFile) return;
40983
41134
  if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
40984
41135
  },
40985
41136
  JSXAttribute(node) {
40986
- if (!isRouteFile) return;
40987
41137
  if (isEventHandlerAttribute(node)) eventHandlerDepth++;
40988
41138
  },
40989
41139
  "JSXAttribute:exit"(node) {
40990
- if (!isRouteFile) return;
40991
41140
  if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40992
41141
  },
40993
41142
  Property(node) {
40994
- if (!isRouteFile) return;
40995
41143
  if (isEventHandlerProperty(node)) eventHandlerDepth++;
40996
41144
  },
40997
41145
  "Property:exit"(node) {
40998
- if (!isRouteFile) return;
40999
41146
  if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
41000
41147
  },
41001
41148
  VariableDeclarator(node) {
41002
- if (!isRouteFile) return;
41003
41149
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
41004
41150
  },
41005
41151
  "VariableDeclarator:exit"(node) {
41006
- if (!isRouteFile) return;
41007
41152
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
41008
41153
  },
41009
41154
  FunctionDeclaration(node) {
41010
- if (!isRouteFile) return;
41011
41155
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
41012
41156
  },
41013
41157
  "FunctionDeclaration:exit"(node) {
41014
- if (!isRouteFile) return;
41015
41158
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
41016
41159
  }
41017
41160
  };
@@ -41095,23 +41238,25 @@ const tanstackStartNoUseEffectFetch = defineRule({
41095
41238
  requires: ["tanstack-start"],
41096
41239
  severity: "warn",
41097
41240
  recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
41098
- create: (context) => ({ CallExpression(node) {
41099
- if (!isInProjectDirectory(context, "routes")) return;
41100
- if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
41101
- const callback = node.arguments?.[0];
41102
- if (!callback) return;
41103
- let hasFetchCall = false;
41104
- const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
41105
- walkAst(callback, (child) => {
41106
- if (hasFetchCall) return false;
41107
- if (child !== callback && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
41108
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === "fetch") hasFetchCall = true;
41109
- });
41110
- if (hasFetchCall) context.report({
41111
- node,
41112
- message: "fetch() inside useEffect makes your users wait through a loading spinner after render."
41113
- });
41114
- } })
41241
+ create: (context) => {
41242
+ if (!isInProjectDirectory(context, "routes")) return {};
41243
+ return { CallExpression(node) {
41244
+ if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
41245
+ const callback = node.arguments?.[0];
41246
+ if (!callback) return;
41247
+ let hasFetchCall = false;
41248
+ const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
41249
+ walkAst(callback, (child) => {
41250
+ if (hasFetchCall) return false;
41251
+ if (child !== callback && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
41252
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === "fetch") hasFetchCall = true;
41253
+ });
41254
+ if (hasFetchCall) context.report({
41255
+ node,
41256
+ message: "fetch() inside useEffect makes your users wait through a loading spinner after render."
41257
+ });
41258
+ } };
41259
+ }
41115
41260
  });
41116
41261
  //#endregion
41117
41262
  //#region src/plugin/rules/tanstack-start/tanstack-start-redirect-in-try-catch.ts
@@ -41479,13 +41624,21 @@ const webhookSignatureRisk = defineRule({
41479
41624
  //#endregion
41480
41625
  //#region src/plugin/rules/zod/utils/zod-ast.ts
41481
41626
  const ZOD_MODULE = "zod";
41627
+ const ZOD_MODULE_SOURCES = [ZOD_MODULE];
41482
41628
  const getStaticPropertyName = (member) => {
41483
41629
  const property = member.property;
41484
41630
  if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
41485
41631
  if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
41486
41632
  return null;
41487
41633
  };
41634
+ const importInfoCache = /* @__PURE__ */ new WeakMap();
41488
41635
  const getImportInfoForIdentifier = (identifier) => {
41636
+ if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
41637
+ const importInfo = computeImportInfoForIdentifier(identifier);
41638
+ importInfoCache.set(identifier, importInfo);
41639
+ return importInfo;
41640
+ };
41641
+ const computeImportInfoForIdentifier = (identifier) => {
41489
41642
  const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
41490
41643
  if (!specifier) return null;
41491
41644
  const declaration = specifier.parent;
@@ -41622,25 +41775,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
41622
41775
  tags: ["migration-hint"],
41623
41776
  severity: "warn",
41624
41777
  recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
41625
- create: (context) => ({
41626
- CallExpression(node) {
41627
- if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
41628
- if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
41629
- context.report({
41630
- node,
41631
- message: ZOD_ERROR_API_MESSAGE
41632
- });
41633
- },
41634
- MemberExpression(node) {
41635
- const parent = node.parent;
41636
- if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41637
- if (!isDeprecatedZodErrorMemberAccess(node)) return;
41638
- context.report({
41639
- node,
41640
- message: ZOD_ERROR_API_MESSAGE
41641
- });
41642
- }
41643
- })
41778
+ create: (context) => {
41779
+ let fileImportsZod = false;
41780
+ return {
41781
+ Program(node) {
41782
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
41783
+ },
41784
+ CallExpression(node) {
41785
+ if (!fileImportsZod) return;
41786
+ if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
41787
+ if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
41788
+ context.report({
41789
+ node,
41790
+ message: ZOD_ERROR_API_MESSAGE
41791
+ });
41792
+ },
41793
+ MemberExpression(node) {
41794
+ if (!fileImportsZod) return;
41795
+ const parent = node.parent;
41796
+ if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41797
+ if (!isDeprecatedZodErrorMemberAccess(node)) return;
41798
+ context.report({
41799
+ node,
41800
+ message: ZOD_ERROR_API_MESSAGE
41801
+ });
41802
+ }
41803
+ };
41804
+ }
41644
41805
  });
41645
41806
  //#endregion
41646
41807
  //#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
@@ -41832,16 +41993,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
41832
41993
  tags: ["migration-hint"],
41833
41994
  severity: "warn",
41834
41995
  recommendation: "Switch to the Zod 4 versions: top-level factories like `z.enum()`, object helpers like `z.strictObject()`, the new `z.function({ input, output })` form, and explicit key/value schemas for `z.record()`.",
41835
- create: (context) => ({
41836
- CallExpression(node) {
41837
- if (isCallToDeprecatedTopLevelFactory(node) || isCallToDroppedCreateFactory(node) || isSingleArgumentRecordCall(node) || isLiteralSymbolCall(node) || isDeprecatedFunctionChainCall(node) || isDirectMethodCallOnZodFactory(node, OBJECT_FACTORY, OBJECT_METHODS) || isDirectMethodCallOnZodFactory(node, NUMBER_FACTORY, NUMBER_METHODS) || isRefineSecondArgumentFunction(node)) reportSchemaMigration(context, node);
41838
- },
41839
- MemberExpression(node) {
41840
- const parent = node.parent;
41841
- if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41842
- if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
41843
- }
41844
- })
41996
+ create: (context) => {
41997
+ let fileImportsZod = false;
41998
+ return {
41999
+ Program(node) {
42000
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
42001
+ },
42002
+ CallExpression(node) {
42003
+ if (!fileImportsZod) return;
42004
+ if (isCallToDeprecatedTopLevelFactory(node) || isCallToDroppedCreateFactory(node) || isSingleArgumentRecordCall(node) || isLiteralSymbolCall(node) || isDeprecatedFunctionChainCall(node) || isDirectMethodCallOnZodFactory(node, OBJECT_FACTORY, OBJECT_METHODS) || isDirectMethodCallOnZodFactory(node, NUMBER_FACTORY, NUMBER_METHODS) || isRefineSecondArgumentFunction(node)) reportSchemaMigration(context, node);
42005
+ },
42006
+ MemberExpression(node) {
42007
+ if (!fileImportsZod) return;
42008
+ const parent = node.parent;
42009
+ if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
42010
+ if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
42011
+ }
42012
+ };
42013
+ }
41845
42014
  });
41846
42015
  //#endregion
41847
42016
  //#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
@@ -41876,13 +42045,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
41876
42045
  tags: ["migration-hint"],
41877
42046
  severity: "warn",
41878
42047
  recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
41879
- create: (context) => ({ CallExpression(node) {
41880
- if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
41881
- context.report({
41882
- node,
41883
- message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
41884
- });
41885
- } })
42048
+ create: (context) => {
42049
+ let fileImportsZod = false;
42050
+ return {
42051
+ Program(node) {
42052
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
42053
+ },
42054
+ CallExpression(node) {
42055
+ if (!fileImportsZod) return;
42056
+ if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
42057
+ context.report({
42058
+ node,
42059
+ message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
42060
+ });
42061
+ }
42062
+ };
42063
+ }
41886
42064
  });
41887
42065
  //#endregion
41888
42066
  //#region src/plugin/rule-registry.ts