oxlint-plugin-react-doctor 0.6.2-dev.fc75a3e → 0.6.3-dev.1571119

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 +956 -555
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -378,13 +378,18 @@ const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
378
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);
379
379
  //#endregion
380
380
  //#region src/plugin/rules/security-scan/utils/is-production-file-path.ts
381
+ const classificationCacheByPattern = /* @__PURE__ */ new Map();
381
382
  const isProductionFilePath = (relativePath, sourceFilePattern) => {
382
- if (!sourceFilePattern.test(relativePath)) return false;
383
- if (TEST_CONTEXT_PATTERN.test(relativePath)) return false;
384
- if (BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath)) return false;
385
- if (DOCUMENTATION_CONTEXT_PATTERN.test(relativePath)) return false;
386
- if (GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath)) return false;
387
- 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;
388
393
  };
389
394
  //#endregion
390
395
  //#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
@@ -583,7 +588,7 @@ const SETTER_PATTERN = /^set[A-Z]/;
583
588
  const RENDER_FUNCTION_PATTERN = /^render[A-Z]/;
584
589
  const UPPERCASE_PATTERN = /^[A-Z]/;
585
590
  const REACT_HANDLER_PROP_PATTERN = /^on[A-Z]/;
586
- const HOOK_NAME_PATTERN = /^use[A-Z]/;
591
+ const HOOK_NAME_PATTERN$1 = /^use[A-Z]/;
587
592
  const HANDLER_FUNCTION_NAME_PATTERN = /^(?:on|handle)[A-Z]/;
588
593
  const EFFECT_HOOK_NAMES$1 = new Set(["useEffect", "useLayoutEffect"]);
589
594
  const HOOKS_WITH_DEPS = new Set([
@@ -783,24 +788,6 @@ const collectChildComponentNames = (element, into) => {
783
788
  into.add(name);
784
789
  });
785
790
  };
786
- const findSameFileComponentBody = (programRoot, componentName) => {
787
- let foundBody = null;
788
- walkAst(programRoot, (node) => {
789
- if (foundBody) return false;
790
- if (isNodeOfType(node, "FunctionDeclaration") && node.id && node.id.name === componentName) {
791
- foundBody = node.body;
792
- return false;
793
- }
794
- if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.id.name === componentName) {
795
- const initializer = node.init;
796
- if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) {
797
- foundBody = initializer.body;
798
- return false;
799
- }
800
- }
801
- });
802
- return foundBody;
803
- };
804
791
  const countEffectHookCalls = (body) => {
805
792
  if (!body) return 0;
806
793
  let count = 0;
@@ -810,6 +797,36 @@ const countEffectHookCalls = (body) => {
810
797
  });
811
798
  return count;
812
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
+ };
813
830
  const activityWrapsEffectHeavySubtree = defineRule({
814
831
  id: "activity-wraps-effect-heavy-subtree",
815
832
  title: "Activity wraps an effect-heavy subtree",
@@ -866,9 +883,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
866
883
  let totalEffects = 0;
867
884
  const effectfulChildren = [];
868
885
  for (const componentName of childComponentNames) {
869
- const body = findSameFileComponentBody(programRoot, componentName);
870
- if (!body) continue;
871
- const effectCount = countEffectHookCalls(body);
886
+ const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
872
887
  if (effectCount === 0) continue;
873
888
  totalEffects += effectCount;
874
889
  effectfulChildren.push(`<${componentName}>`);
@@ -1458,6 +1473,12 @@ const getElementType = (openingElement, settings) => {
1458
1473
  return elementType;
1459
1474
  };
1460
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
+ };
1481
+ //#endregion
1461
1482
  //#region src/plugin/utils/find-import-source-for-name.ts
1462
1483
  const collectFromProgram = (programRoot) => {
1463
1484
  const lookup = /* @__PURE__ */ new Map();
@@ -1661,7 +1682,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
1661
1682
  };
1662
1683
  //#endregion
1663
1684
  //#region src/plugin/utils/normalize-filename.ts
1664
- const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
1685
+ const normalizeFilename = (filename) => filename.includes("\\") ? filename.replaceAll("\\", "/") : filename;
1665
1686
  //#endregion
1666
1687
  //#region src/plugin/utils/is-generated-image-render-context.ts
1667
1688
  const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
@@ -2010,7 +2031,12 @@ const altText = defineRule({
2010
2031
  const objectAliases = new Set(settings.object ?? []);
2011
2032
  const areaAliases = new Set(settings.area ?? []);
2012
2033
  const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
2034
+ const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
2013
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
+ }
2014
2040
  if (isGeneratedImageRenderContext(context, node)) return;
2015
2041
  const tag = getElementType(node, context.settings);
2016
2042
  if (checkImg && (tag === "img" || imgAliases.has(tag))) {
@@ -2193,7 +2219,9 @@ const anchorIsValid = defineRule({
2193
2219
  category: "Accessibility",
2194
2220
  create: (context) => {
2195
2221
  const settings = resolveSettings$50(context.settings);
2222
+ const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
2196
2223
  return { JSXOpeningElement(node) {
2224
+ if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
2197
2225
  if (getElementType(node, context.settings) !== "a") return;
2198
2226
  let hrefAttribute;
2199
2227
  for (const attributeName of settings.hrefAttributeNames) {
@@ -3619,8 +3647,9 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
3619
3647
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3620
3648
  //#endregion
3621
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");
3622
3651
  const findSuspiciousPublicEnvSecretNamePattern = (content) => {
3623
- 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)) {
3624
3653
  const value = match[0] ?? "";
3625
3654
  if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
3626
3655
  }
@@ -4087,62 +4116,56 @@ const collectPatternIdentifiers = (pattern, target) => {
4087
4116
  for (const element of pattern.elements ?? []) if (element) collectPatternIdentifiers(element, target);
4088
4117
  } else if (isNodeOfType(pattern, "AssignmentPattern") && pattern.left) collectPatternIdentifiers(pattern.left, target);
4089
4118
  };
4090
- const collectAssignedIdentifiers = (block) => {
4091
- const assigned = /* @__PURE__ */ new Set();
4092
- walkAst(block, (child) => {
4093
- if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
4094
- if (isNodeOfType(child, "AssignmentExpression") && child.left) collectPatternIdentifiers(child.left, assigned);
4095
- });
4096
- return assigned;
4097
- };
4098
- const collectAwaitedArgIdentifiers = (block) => {
4099
- const referenced = /* @__PURE__ */ new Set();
4100
- walkAst(block, (child) => {
4101
- if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
4102
- if (!isNodeOfType(child, "AwaitExpression") || !child.argument) return;
4103
- collectReferenceIdentifierNames(child.argument, referenced);
4104
- });
4105
- return referenced;
4106
- };
4107
4119
  const ARRAY_MUTATION_METHOD_NAMES = new Set([
4108
4120
  "push",
4109
4121
  "unshift",
4110
4122
  "splice"
4111
4123
  ]);
4112
- const collectMutatedArrayNames = (block) => {
4113
- const mutated = /* @__PURE__ */ new Set();
4124
+ const addDerivedBindings = (block, names) => {
4125
+ const declaratorBindings = [];
4114
4126
  walkAst(block, (child) => {
4115
4127
  if (child !== block && isFunctionLike$1(child)) return false;
4116
- if (!isNodeOfType(child, "CallExpression")) return;
4117
- const callee = child.callee;
4118
- 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
+ });
4119
4136
  });
4120
- return mutated;
4121
- };
4122
- const addDerivedBindings = (block, names) => {
4123
4137
  let didGrow = true;
4124
4138
  while (didGrow) {
4125
4139
  didGrow = false;
4126
- walkAst(block, (child) => {
4127
- if (child !== block && isFunctionLike$1(child)) return false;
4128
- if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
4129
- if (!isNodeOfType(child.id, "Identifier") || names.has(child.id.name)) return;
4130
- const initReferences = /* @__PURE__ */ new Set();
4131
- collectReferenceIdentifierNames(child.init, initReferences);
4132
- for (const referenced of initReferences) if (names.has(referenced)) {
4133
- 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);
4134
4144
  didGrow = true;
4135
4145
  break;
4136
4146
  }
4137
- });
4147
+ }
4138
4148
  }
4139
4149
  };
4140
4150
  const hasLoopCarriedDependency = (block) => {
4141
- const carried = collectAssignedIdentifiers(block);
4142
- 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
+ });
4143
4167
  if (carried.size === 0) return false;
4144
4168
  addDerivedBindings(block, carried);
4145
- const awaitedReferences = collectAwaitedArgIdentifiers(block);
4146
4169
  for (const name of carried) if (awaitedReferences.has(name)) return true;
4147
4170
  return false;
4148
4171
  };
@@ -4820,7 +4843,8 @@ const FORM_CONTROL_TAGS = new Set([
4820
4843
  ]);
4821
4844
  const resolveSettings$48 = (settings) => {
4822
4845
  const reactDoctor = settings?.["react-doctor"];
4823
- return { inputComponents: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {}).inputComponents ?? [] };
4846
+ const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {};
4847
+ return { inputComponents: new Set(ruleSettings.inputComponents ?? []) };
4824
4848
  };
4825
4849
  const autocompleteValid = defineRule({
4826
4850
  id: "autocomplete-valid",
@@ -4833,7 +4857,7 @@ const autocompleteValid = defineRule({
4833
4857
  const settings = resolveSettings$48(context.settings);
4834
4858
  return { JSXOpeningElement: (node) => {
4835
4859
  const tag = getElementType(node, context.settings);
4836
- if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.includes(tag)) return;
4860
+ if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.has(tag)) return;
4837
4861
  const attribute = hasJsxPropIgnoreCase(node.attributes, "autoComplete");
4838
4862
  if (!attribute) return;
4839
4863
  const value = getJsxPropStringValue(attribute);
@@ -4877,9 +4901,8 @@ const isCreateElementCall = (node) => {
4877
4901
  const callee = node.callee;
4878
4902
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createElement";
4879
4903
  if (isNodeOfType(callee, "MemberExpression")) {
4880
- if (memberChainContainsDocument(callee.object)) return false;
4881
- if (callee.computed) return isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement";
4882
- return isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement";
4904
+ if (!(callee.computed ? isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement" : isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement")) return false;
4905
+ return !memberChainContainsDocument(callee.object);
4883
4906
  }
4884
4907
  return false;
4885
4908
  };
@@ -4905,7 +4928,8 @@ const isValidTypeValue = (rawValue, settings) => {
4905
4928
  if (rawValue === "reset") return settings.reset;
4906
4929
  return false;
4907
4930
  };
4908
- const isProvenValidExpression = (expression, settings, resolvedBindings = /* @__PURE__ */ new Set()) => {
4931
+ const isProvenValidExpression = (rawExpression, settings, resolvedBindings = /* @__PURE__ */ new Set()) => {
4932
+ const expression = stripParenExpression(rawExpression);
4909
4933
  if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return isValidTypeValue(expression.value, settings);
4910
4934
  if (isNodeOfType(expression, "TemplateLiteral")) {
4911
4935
  const staticValue = getStaticTemplateLiteralValue(expression);
@@ -5072,11 +5096,20 @@ const resolveSettings$46 = (settings) => {
5072
5096
  ignoreExclusiveCheckedAttribute: ruleSettings.ignoreExclusiveCheckedAttribute ?? false
5073
5097
  };
5074
5098
  };
5099
+ const isTruthyDisabledJsxValue = (attribute) => {
5100
+ if (!attribute.value) return true;
5101
+ if (isNodeOfType(attribute.value, "JSXExpressionContainer")) {
5102
+ const expression = attribute.value.expression;
5103
+ return isNodeOfType(expression, "Literal") && expression.value === true;
5104
+ }
5105
+ return false;
5106
+ };
5075
5107
  const collectFromJsxAttributes = (attributes) => {
5076
5108
  let checkedNode = null;
5077
5109
  let defaultCheckedNode = null;
5078
5110
  let hasOnChangeOrReadOnly = false;
5079
5111
  let hasSpread = false;
5112
+ let hasTruthyDisabled = false;
5080
5113
  for (const attribute of attributes) {
5081
5114
  if (isNodeOfType(attribute, "JSXSpreadAttribute")) {
5082
5115
  hasSpread = true;
@@ -5087,12 +5120,14 @@ const collectFromJsxAttributes = (attributes) => {
5087
5120
  if (name === "checked") checkedNode = attribute;
5088
5121
  else if (name === "defaultChecked" && !defaultCheckedNode) defaultCheckedNode = attribute;
5089
5122
  else if (name === "onChange" || name === "readOnly") hasOnChangeOrReadOnly = true;
5123
+ else if (name === "disabled" && isTruthyDisabledJsxValue(attribute)) hasTruthyDisabled = true;
5090
5124
  }
5091
5125
  return {
5092
5126
  checkedNode,
5093
5127
  defaultCheckedNode,
5094
5128
  hasOnChangeOrReadOnly,
5095
- hasSpread
5129
+ hasSpread,
5130
+ hasTruthyDisabled
5096
5131
  };
5097
5132
  };
5098
5133
  const collectFromObjectProperties = (objectExpression) => {
@@ -5100,6 +5135,7 @@ const collectFromObjectProperties = (objectExpression) => {
5100
5135
  let defaultCheckedNode = null;
5101
5136
  let hasOnChangeOrReadOnly = false;
5102
5137
  let hasSpread = false;
5138
+ let hasTruthyDisabled = false;
5103
5139
  for (const property of objectExpression.properties) {
5104
5140
  if (isNodeOfType(property, "SpreadElement")) {
5105
5141
  hasSpread = true;
@@ -5114,12 +5150,17 @@ const collectFromObjectProperties = (objectExpression) => {
5114
5150
  if (propertyName === "checked") checkedNode = property;
5115
5151
  else if (propertyName === "defaultChecked" && !defaultCheckedNode) defaultCheckedNode = property;
5116
5152
  else if (propertyName === "onChange" || propertyName === "readOnly") hasOnChangeOrReadOnly = true;
5153
+ else if (propertyName === "disabled") {
5154
+ const propertyValue = property.value;
5155
+ if (isNodeOfType(propertyValue, "Literal") && propertyValue.value === true) hasTruthyDisabled = true;
5156
+ }
5117
5157
  }
5118
5158
  return {
5119
5159
  checkedNode,
5120
5160
  defaultCheckedNode,
5121
5161
  hasOnChangeOrReadOnly,
5122
- hasSpread
5162
+ hasSpread,
5163
+ hasTruthyDisabled
5123
5164
  };
5124
5165
  };
5125
5166
  const checkedRequiresOnchangeOrReadonly = defineRule({
@@ -5135,7 +5176,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
5135
5176
  node: presence.checkedNode,
5136
5177
  message: EXCLUSIVE_MESSAGE
5137
5178
  });
5138
- if (presence.checkedNode && !presence.hasOnChangeOrReadOnly && !presence.hasSpread && !settings.ignoreMissingProperties) context.report({
5179
+ if (presence.checkedNode && !presence.hasOnChangeOrReadOnly && !presence.hasSpread && !presence.hasTruthyDisabled && !settings.ignoreMissingProperties) context.report({
5139
5180
  node: presence.checkedNode,
5140
5181
  message: MISSING_MESSAGE$1
5141
5182
  });
@@ -5696,12 +5737,20 @@ const getTemplateInterpolations = (valueTail) => {
5696
5737
  return interpolations === null ? "" : interpolations.join(" ");
5697
5738
  };
5698
5739
  const isInertParseTarget = (target, fileContent) => {
5740
+ const fileHasCreateElement = fileContent.includes("createElement");
5741
+ const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
5742
+ if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
5699
5743
  const escapedTarget = escapeRegExp(target);
5700
5744
  const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
5701
5745
  if (new RegExp(`\\b${escapedRoot}\\s*=\\s*[^\\n;]*(?:getElementById|querySelector|getElementsBy|\\.current\\b|document\\.(?:body|head|documentElement))`).test(fileContent)) return false;
5702
- if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
5703
- if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
5704
- if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
5746
+ if (fileHasCreateElement) {
5747
+ if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
5748
+ if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
5749
+ }
5750
+ if (fileHasIsolatedDocument) {
5751
+ if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
5752
+ }
5753
+ if (!fileHasCreateElement) return false;
5705
5754
  if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
5706
5755
  const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
5707
5756
  const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
@@ -5794,10 +5843,9 @@ const dangerousHtmlSink = defineRule({
5794
5843
  const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
5795
5844
  if (valueIdentifier !== void 0) {
5796
5845
  const escapedIdentifier = escapeRegExp(valueIdentifier);
5797
- const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
5798
- const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
5799
- const fromDomContent = new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`);
5800
- if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
5846
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5847
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5848
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
5801
5849
  }
5802
5850
  }
5803
5851
  const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
@@ -5946,6 +5994,7 @@ const noRedundantPaddingAxes = defineRule({
5946
5994
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5947
5995
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5948
5996
  if (!classNameLiteral) return;
5997
+ if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
5949
5998
  if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
5950
5999
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
5951
6000
  if (matchedPairs.length === 0) return;
@@ -5970,6 +6019,7 @@ const noRedundantSizeAxes = defineRule({
5970
6019
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5971
6020
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5972
6021
  if (!classNameLiteral) return;
6022
+ if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
5973
6023
  if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
5974
6024
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
5975
6025
  if (matchedPairs.length === 0) return;
@@ -5994,6 +6044,7 @@ const noSpaceOnFlexChildren = defineRule({
5994
6044
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5995
6045
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5996
6046
  if (!classNameLiteral) return;
6047
+ if (!classNameLiteral.includes("space-")) return;
5997
6048
  const tokens = tokenizeClassName(classNameLiteral);
5998
6049
  let hasFlexOrGridLayout = false;
5999
6050
  for (const token of tokens) {
@@ -6185,7 +6236,10 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
6185
6236
  const actualMinor = Number(match[2]);
6186
6237
  return actualMajor > major || actualMajor === major && actualMinor >= minor;
6187
6238
  };
6239
+ const containsJsxCache = /* @__PURE__ */ new WeakMap();
6188
6240
  const containsJsx$1 = (root) => {
6241
+ const cached = containsJsxCache.get(root);
6242
+ if (cached !== void 0) return cached;
6189
6243
  let found = false;
6190
6244
  const visit = (node) => {
6191
6245
  if (found) return;
@@ -6208,6 +6262,7 @@ const containsJsx$1 = (root) => {
6208
6262
  }
6209
6263
  };
6210
6264
  visit(root);
6265
+ containsJsxCache.set(root, found);
6211
6266
  return found;
6212
6267
  };
6213
6268
  const getStaticMemberName = (node) => {
@@ -6299,27 +6354,6 @@ const hasDisplayNameMember = (classNode) => {
6299
6354
  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;
6300
6355
  return false;
6301
6356
  };
6302
- const hasDisplayNameAssignment = (className, programRoot) => {
6303
- let found = false;
6304
- const visit = (node) => {
6305
- if (found) return;
6306
- if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && isNodeOfType(node.left.object, "Identifier") && node.left.object.name === className && getStaticMemberName(node.left) === "displayName") {
6307
- found = true;
6308
- return;
6309
- }
6310
- const record = node;
6311
- for (const key of Object.keys(record)) {
6312
- if (key === "parent") continue;
6313
- const child = record[key];
6314
- if (Array.isArray(child)) {
6315
- for (const item of child) if (isAstNode(item)) visit(item);
6316
- } else if (isAstNode(child)) visit(child);
6317
- if (found) return;
6318
- }
6319
- };
6320
- visit(programRoot);
6321
- return found;
6322
- };
6323
6357
  const memberExpressionPath = (node) => {
6324
6358
  if (isNodeOfType(node, "Identifier")) return [node.name];
6325
6359
  if (!isNodeOfType(node, "MemberExpression")) return [];
@@ -6327,15 +6361,16 @@ const memberExpressionPath = (node) => {
6327
6361
  const propertyName = getStaticMemberName(node);
6328
6362
  return propertyName ? [...objectPath, propertyName] : objectPath;
6329
6363
  };
6330
- const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6331
- let found = false;
6364
+ const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
6365
+ const getDisplayNameAssignmentIndex = (programRoot) => {
6366
+ const cached = displayNameAssignmentIndexCache.get(programRoot);
6367
+ if (cached) return cached;
6368
+ const identifierTargets = /* @__PURE__ */ new Set();
6369
+ const memberPathSegments = /* @__PURE__ */ new Set();
6332
6370
  const visit = (node) => {
6333
- if (found) return;
6334
6371
  if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
6335
- if (memberExpressionPath(node.left.object).includes(propertyName)) {
6336
- found = true;
6337
- return;
6338
- }
6372
+ if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
6373
+ for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
6339
6374
  }
6340
6375
  const record = node;
6341
6376
  for (const key of Object.keys(record)) {
@@ -6344,12 +6379,18 @@ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6344
6379
  if (Array.isArray(child)) {
6345
6380
  for (const item of child) if (isAstNode(item)) visit(item);
6346
6381
  } else if (isAstNode(child)) visit(child);
6347
- if (found) return;
6348
6382
  }
6349
6383
  };
6350
6384
  visit(programRoot);
6351
- return found;
6385
+ const index = {
6386
+ identifierTargets,
6387
+ memberPathSegments
6388
+ };
6389
+ displayNameAssignmentIndexCache.set(programRoot, index);
6390
+ return index;
6352
6391
  };
6392
+ const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
6393
+ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
6353
6394
  const displayName = defineRule({
6354
6395
  id: "display-name",
6355
6396
  title: "Component missing display name",
@@ -6409,7 +6450,7 @@ const displayName = defineRule({
6409
6450
  },
6410
6451
  ArrowFunctionExpression(node) {
6411
6452
  if (!containsJsx$1(node)) return;
6412
- if (isNodeOfType(node.parent, "ArrowFunctionExpression")) {
6453
+ if (isNodeOfType(node.parent, "ArrowFunctionExpression") || isNodeOfType(node.parent, "ReturnStatement")) {
6413
6454
  reportAt(node);
6414
6455
  return;
6415
6456
  }
@@ -7326,7 +7367,7 @@ const TRANSPARENT_WRAPPER_TYPES = new Set([
7326
7367
  "ParenthesizedExpression",
7327
7368
  "ChainExpression"
7328
7369
  ]);
7329
- const unwrapExpression$2 = (node) => {
7370
+ const unwrapExpression$3 = (node) => {
7330
7371
  let current = node;
7331
7372
  while (TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
7332
7373
  const inner = current.expression;
@@ -7452,7 +7493,7 @@ const symbolHasStableHookOrigin = (symbol) => {
7452
7493
  if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
7453
7494
  const initializerRaw = declarator.init;
7454
7495
  if (!initializerRaw) return false;
7455
- const initializer = unwrapExpression$2(initializerRaw);
7496
+ const initializer = unwrapExpression$3(initializerRaw);
7456
7497
  if (symbol.kind === "const") {
7457
7498
  if (isNodeOfType(initializer, "Literal") && (initializer.value === null || typeof initializer.value === "number" || typeof initializer.value === "string" || typeof initializer.value === "boolean")) return true;
7458
7499
  if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
@@ -7472,13 +7513,13 @@ const symbolHasStableHookOrigin = (symbol) => {
7472
7513
  return false;
7473
7514
  };
7474
7515
  const symbolHasUseEffectEventOrigin = (symbol) => {
7475
- const initializer = symbol.initializer ? unwrapExpression$2(symbol.initializer) : null;
7516
+ const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
7476
7517
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
7477
7518
  return getHookName(initializer.callee) === "useEffectEvent";
7478
7519
  };
7479
7520
  const getFunctionValueNode = (symbol) => {
7480
7521
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
7481
- const initializer = symbol.initializer ? unwrapExpression$2(symbol.initializer) : null;
7522
+ const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
7482
7523
  if (initializer && (isNodeOfType(initializer, "FunctionExpression") || isNodeOfType(initializer, "ArrowFunctionExpression"))) return initializer;
7483
7524
  return null;
7484
7525
  };
@@ -7574,6 +7615,14 @@ const flattenReferenceRootName = (reference) => {
7574
7615
  if (isNodeOfType(referencedIdentifier, "JSXIdentifier")) return referencedIdentifier.name;
7575
7616
  return "";
7576
7617
  };
7618
+ const REF_CURRENT_SEGMENT = ".current";
7619
+ const truncateAtRefCurrent = (chain) => {
7620
+ const refCurrentIndex = chain.indexOf(REF_CURRENT_SEGMENT);
7621
+ if (refCurrentIndex === -1) return chain;
7622
+ const segmentEndIndex = refCurrentIndex + 8;
7623
+ if (segmentEndIndex === chain.length || chain[segmentEndIndex] === ".") return chain.slice(0, refCurrentIndex);
7624
+ return chain;
7625
+ };
7577
7626
  const computeDepKey = (reference) => {
7578
7627
  const referencedIdentifier = reference.identifier;
7579
7628
  let parent = referencedIdentifier.parent ?? null;
@@ -7604,21 +7653,22 @@ const computeDepKey = (reference) => {
7604
7653
  const declarator = outermost.parent;
7605
7654
  if (declarator && isNodeOfType(declarator, "VariableDeclarator") && declarator.init === outermost) {
7606
7655
  const destructuredPath = getDestructuredPropertyPath(declarator.id);
7607
- if (destructuredPath) return `${fullName}.${destructuredPath}`;
7656
+ if (destructuredPath) return truncateAtRefCurrent(`${fullName}.${destructuredPath}`);
7608
7657
  }
7658
+ const truncatedName = truncateAtRefCurrent(fullName);
7659
+ if (truncatedName !== fullName) return truncatedName;
7609
7660
  if (reference.flag !== "read") {
7610
7661
  const lastDotIndex = fullName.lastIndexOf(".");
7611
7662
  if (lastDotIndex !== -1) return fullName.slice(0, lastDotIndex);
7612
7663
  }
7613
- if (fullName.endsWith(".current")) return fullName.slice(0, -8);
7614
7664
  return fullName;
7615
7665
  };
7616
7666
  const computeDeclaredDepKey = (entry) => {
7617
- const stripped = unwrapExpression$2(entry);
7667
+ const stripped = unwrapExpression$3(entry);
7618
7668
  if (isNodeOfType(stripped, "Identifier")) return stripped.name;
7619
7669
  if (isNodeOfType(stripped, "MemberExpression")) return stringifyMemberChain(stripped);
7620
7670
  if (isNodeOfType(stripped, "CallExpression") && (stripped.arguments?.length ?? 0) === 0) {
7621
- const callee = unwrapExpression$2(stripped.callee);
7671
+ const callee = unwrapExpression$3(stripped.callee);
7622
7672
  if (isNodeOfType(callee, "Identifier")) return callee.name;
7623
7673
  if (isNodeOfType(callee, "MemberExpression") && !hasComputedMemberExpression(callee)) return stringifyMemberChain(callee);
7624
7674
  }
@@ -7626,16 +7676,16 @@ const computeDeclaredDepKey = (entry) => {
7626
7676
  };
7627
7677
  const depsArrayContainsIdentifier = (depsArgument, identifierName) => {
7628
7678
  if (!depsArgument) return false;
7629
- const strippedDepsArgument = unwrapExpression$2(depsArgument);
7679
+ const strippedDepsArgument = unwrapExpression$3(depsArgument);
7630
7680
  if (!isNodeOfType(strippedDepsArgument, "ArrayExpression")) return false;
7631
7681
  return strippedDepsArgument.elements.some((element) => {
7632
7682
  if (!element) return false;
7633
- const strippedElement = unwrapExpression$2(element);
7683
+ const strippedElement = unwrapExpression$3(element);
7634
7684
  return isNodeOfType(strippedElement, "Identifier") && strippedElement.name === identifierName;
7635
7685
  });
7636
7686
  };
7637
7687
  const stringifyMemberChain = (node) => {
7638
- const stripped = unwrapExpression$2(node);
7688
+ const stripped = unwrapExpression$3(node);
7639
7689
  if (isNodeOfType(stripped, "Identifier")) return stripped.name;
7640
7690
  if (isNodeOfType(stripped, "ThisExpression")) return "this";
7641
7691
  if (isNodeOfType(stripped, "MemberExpression")) {
@@ -7677,13 +7727,13 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
7677
7727
  return false;
7678
7728
  };
7679
7729
  const getMemberRootIdentifier = (node) => {
7680
- const stripped = unwrapExpression$2(node);
7730
+ const stripped = unwrapExpression$3(node);
7681
7731
  if (isNodeOfType(stripped, "Identifier")) return stripped;
7682
7732
  if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier(stripped.object);
7683
7733
  return null;
7684
7734
  };
7685
7735
  const hasComputedMemberExpression = (node) => {
7686
- const stripped = unwrapExpression$2(node);
7736
+ const stripped = unwrapExpression$3(node);
7687
7737
  if (!isNodeOfType(stripped, "MemberExpression")) return false;
7688
7738
  if (stripped.computed) return true;
7689
7739
  return hasComputedMemberExpression(stripped.object);
@@ -7699,7 +7749,7 @@ const getRootSymbol = (node, scopes) => {
7699
7749
  return rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
7700
7750
  };
7701
7751
  const getDeclaredDepSymbolSource = (node) => {
7702
- const stripped = unwrapExpression$2(node);
7752
+ const stripped = unwrapExpression$3(node);
7703
7753
  if (isNodeOfType(stripped, "CallExpression") && (stripped.arguments?.length ?? 0) === 0) return stripped.callee;
7704
7754
  return node;
7705
7755
  };
@@ -7709,7 +7759,7 @@ const isRegExpLiteral = (node) => {
7709
7759
  };
7710
7760
  const isUnstableInitializer = (node) => {
7711
7761
  if (!node) return false;
7712
- const stripped = unwrapExpression$2(node);
7762
+ const stripped = unwrapExpression$3(node);
7713
7763
  if (isRegExpLiteral(stripped)) return true;
7714
7764
  if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
7715
7765
  if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
@@ -7801,9 +7851,9 @@ const findRefCurrentInCleanup = (callback, scopes) => {
7801
7851
  };
7802
7852
  findReturn(callback);
7803
7853
  if (!cleanupFunction) return null;
7804
- let refCurrentName = null;
7854
+ let found = null;
7805
7855
  const visitCleanup = (node) => {
7806
- if (refCurrentName) return;
7856
+ if (found) return;
7807
7857
  if (isNodeOfType(node, "MemberExpression")) {
7808
7858
  const candidateName = getRefCurrentNameFromMemberExpression(node);
7809
7859
  if (candidateName) {
@@ -7811,7 +7861,10 @@ const findRefCurrentInCleanup = (callback, scopes) => {
7811
7861
  const symbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
7812
7862
  const callbackScope = scopes.ownScopeFor(callback) ?? scopes.scopeFor(callback);
7813
7863
  if (!symbol || !isDescendantScope(symbol.scope, callbackScope)) {
7814
- refCurrentName = candidateName;
7864
+ found = {
7865
+ refCurrentName: candidateName,
7866
+ refSymbol: symbol
7867
+ };
7815
7868
  return;
7816
7869
  }
7817
7870
  }
@@ -7826,7 +7879,19 @@ const findRefCurrentInCleanup = (callback, scopes) => {
7826
7879
  }
7827
7880
  };
7828
7881
  visitCleanup(cleanupFunction);
7829
- return refCurrentName;
7882
+ return found;
7883
+ };
7884
+ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
7885
+ if (!refSymbol) return false;
7886
+ for (const reference of refSymbol.references) {
7887
+ const memberParent = reference.identifier.parent;
7888
+ if (!memberParent || !isNodeOfType(memberParent, "MemberExpression")) continue;
7889
+ if (memberParent.object !== reference.identifier) continue;
7890
+ if (getRefCurrentNameFromMemberExpression(memberParent) === null) continue;
7891
+ const assignmentParent = memberParent.parent;
7892
+ if (assignmentParent && isNodeOfType(assignmentParent, "AssignmentExpression") && assignmentParent.left === memberParent) return true;
7893
+ }
7894
+ return false;
7830
7895
  };
7831
7896
  const hasRefCurrentAssignment = (callback, refCurrentName) => {
7832
7897
  let didAssignRefCurrent = false;
@@ -7865,9 +7930,21 @@ const hasMemberCallForRoot = (node, rootName) => {
7865
7930
  const visit = (current) => {
7866
7931
  if (didFindMemberCall) return;
7867
7932
  if (isNodeOfType(current, "CallExpression")) {
7868
- if (getMemberRootIdentifier(unwrapExpression$2(current.callee))?.name === rootName) {
7869
- didFindMemberCall = true;
7870
- return;
7933
+ const callee = unwrapExpression$3(current.callee);
7934
+ if (isNodeOfType(callee, "MemberExpression")) {
7935
+ let chainObject = unwrapExpression$3(callee.object);
7936
+ let doesChainPassThroughCurrent = false;
7937
+ while (chainObject && isNodeOfType(chainObject, "MemberExpression")) {
7938
+ if (isNodeOfType(chainObject.property, "Identifier") && chainObject.property.name === "current") {
7939
+ doesChainPassThroughCurrent = true;
7940
+ break;
7941
+ }
7942
+ chainObject = unwrapExpression$3(chainObject.object);
7943
+ }
7944
+ if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
7945
+ didFindMemberCall = true;
7946
+ return;
7947
+ }
7871
7948
  }
7872
7949
  }
7873
7950
  const record = current;
@@ -7928,7 +8005,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
7928
8005
  return;
7929
8006
  }
7930
8007
  const depsArgumentRaw = node.arguments[depsArgumentIndex];
7931
- const callbackExpression = unwrapExpression$2(callbackArgument);
8008
+ const callbackExpression = unwrapExpression$3(callbackArgument);
7932
8009
  let callbackToAnalyze = null;
7933
8010
  const forcedCaptureKeys = /* @__PURE__ */ new Set();
7934
8011
  if (isNodeOfType(callbackExpression, "ArrowFunctionExpression") || isNodeOfType(callbackExpression, "FunctionExpression")) callbackToAnalyze = callbackExpression;
@@ -7936,7 +8013,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
7936
8013
  const callbackSymbol = context.scopes.symbolFor(callbackExpression);
7937
8014
  const functionValueNode = callbackSymbol ? getFunctionValueNode(callbackSymbol) : null;
7938
8015
  if (functionValueNode) callbackToAnalyze = functionValueNode;
7939
- else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$2(callbackSymbol.initializer), "CallExpression")) forcedCaptureKeys.add(callbackExpression.name);
8016
+ else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$3(callbackSymbol.initializer), "CallExpression")) forcedCaptureKeys.add(callbackExpression.name);
7940
8017
  else if (depsArgumentRaw) {
7941
8018
  if (depsArrayContainsIdentifier(depsArgumentRaw, callbackExpression.name)) return;
7942
8019
  context.report({
@@ -7969,11 +8046,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
7969
8046
  message: buildAssignmentMessage(assignment.name)
7970
8047
  });
7971
8048
  if (outerAssignments.length > 0) return;
7972
- const refCurrentName = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
8049
+ const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
7973
8050
  const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
7974
- if (refCurrentName && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentName)) context.report({
8051
+ if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol)) context.report({
7975
8052
  node: callbackToAnalyze,
7976
- message: buildRefCleanupMessage(refCurrentName)
8053
+ message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
7977
8054
  });
7978
8055
  }
7979
8056
  if (!depsArgumentRaw) {
@@ -7993,8 +8070,16 @@ If the missing value is recreated every render, move it inside the hook or stabi
7993
8070
  });
7994
8071
  return;
7995
8072
  }
7996
- const depsArgument = unwrapExpression$2(depsArgumentRaw);
7997
- if (isNodeOfType(depsArgument, "Literal") && depsArgument.value === null || isNodeOfType(depsArgument, "Identifier") && depsArgument.name === "undefined") {
8073
+ const depsArgument = unwrapExpression$3(depsArgumentRaw);
8074
+ if (isNodeOfType(depsArgument, "Identifier") && depsArgument.name === "undefined") {
8075
+ if (isAutoDependenciesHook(hookName)) return;
8076
+ if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) context.report({
8077
+ node: depsArgument,
8078
+ message: buildMissingDepArrayMessage(hookName)
8079
+ });
8080
+ return;
8081
+ }
8082
+ if (isNodeOfType(depsArgument, "Literal") && depsArgument.value === null) {
7998
8083
  if (isAutoDependenciesHook(hookName)) return;
7999
8084
  if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) {
8000
8085
  context.report({
@@ -8003,19 +8088,6 @@ If the missing value is recreated every render, move it inside the hook or stabi
8003
8088
  });
8004
8089
  return;
8005
8090
  }
8006
- const nonArrayCaptureKeys = callbackToAnalyze !== null ? new Set(collectCaptureDepKeys(callbackToAnalyze, context.scopes).keys) : /* @__PURE__ */ new Set();
8007
- for (const forcedCaptureKey of forcedCaptureKeys) nonArrayCaptureKeys.add(forcedCaptureKey);
8008
- if (nonArrayCaptureKeys.size > 0) {
8009
- context.report({
8010
- node: depsArgument,
8011
- message: buildNonArrayDepsMessage(hookName)
8012
- });
8013
- context.report({
8014
- node: depsArgument,
8015
- message: buildMissingDepMessage(hookName, [...nonArrayCaptureKeys].join(", "))
8016
- });
8017
- }
8018
- return;
8019
8091
  }
8020
8092
  if (!isNodeOfType(depsArgument, "ArrayExpression")) {
8021
8093
  context.report({
@@ -8034,11 +8106,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
8034
8106
  for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
8035
8107
  const hasLiteralDepElement = depsArgument.elements.some((element) => {
8036
8108
  if (!element) return false;
8037
- return isLiteralOrEmptyTemplate(unwrapExpression$2(element));
8109
+ return isLiteralOrEmptyTemplate(unwrapExpression$3(element));
8038
8110
  });
8039
8111
  const hasNonStringLiteralDep = depsArgument.elements.some((element) => {
8040
8112
  if (!element) return false;
8041
- return isNonStringLiteral(unwrapExpression$2(element));
8113
+ return isNonStringLiteral(unwrapExpression$3(element));
8042
8114
  });
8043
8115
  if (hasNonStringLiteralDep) context.report({
8044
8116
  node: depsArgument,
@@ -8059,7 +8131,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
8059
8131
  });
8060
8132
  continue;
8061
8133
  }
8062
- const stripped = unwrapExpression$2(elementNode);
8134
+ const stripped = unwrapExpression$3(elementNode);
8063
8135
  if (isLiteralOrEmptyTemplate(stripped)) continue;
8064
8136
  if (isNodeOfType(stripped, "Identifier")) {
8065
8137
  const depSymbol = context.scopes.symbolFor(stripped);
@@ -8201,7 +8273,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
8201
8273
  if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
8202
8274
  const rootSymbol = getRootSymbol(reportNode, context.scopes);
8203
8275
  if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
8204
- if (isNodeOfType(unwrapExpression$2(reportNode), "CallExpression")) {
8276
+ if (isNodeOfType(unwrapExpression$3(reportNode), "CallExpression")) {
8205
8277
  context.report({
8206
8278
  node: reportNode,
8207
8279
  message: buildComplexDepMessage(hookName)
@@ -8614,7 +8686,7 @@ const DEFAULT_HEADING_TAGS = [
8614
8686
  const resolveSettings$40 = (settings) => {
8615
8687
  const reactDoctor = settings?.["react-doctor"];
8616
8688
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
8617
- return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
8689
+ return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
8618
8690
  };
8619
8691
  const headingHasContent = defineRule({
8620
8692
  id: "heading-has-content",
@@ -8627,7 +8699,7 @@ const headingHasContent = defineRule({
8627
8699
  const settings = resolveSettings$40(context.settings);
8628
8700
  return { JSXOpeningElement(node) {
8629
8701
  const elementType = getElementType(node, context.settings);
8630
- if (!settings.headingTags.includes(elementType)) return;
8702
+ if (!settings.headingTags.has(elementType)) return;
8631
8703
  const parent = node.parent;
8632
8704
  if (parent && isNodeOfType(parent, "JSXElement")) {
8633
8705
  if (objectHasAccessibleChild(parent, context.settings)) return;
@@ -9225,9 +9297,9 @@ const resolveSettings$37 = (settings) => {
9225
9297
  };
9226
9298
  };
9227
9299
  const isWordBoundary = (text, start, end) => {
9228
- const isAlphanumeric = (charCode) => charCode >= 48 && charCode <= 57 || charCode >= 65 && charCode <= 90 || charCode >= 97 && charCode <= 122;
9229
- const startsBoundary = start === 0 || !isAlphanumeric(text.charCodeAt(start - 1));
9230
- const endsBoundary = end === text.length || !isAlphanumeric(text.charCodeAt(end));
9300
+ const isWordCharacter = (charCode) => charCode >= 48 && charCode <= 57 || charCode >= 65 && charCode <= 90 || charCode >= 97 && charCode <= 122 || charCode === 45 || charCode === 95;
9301
+ const startsBoundary = start === 0 || !isWordCharacter(text.charCodeAt(start - 1));
9302
+ const endsBoundary = end === text.length || !isWordCharacter(text.charCodeAt(end));
9231
9303
  return startsBoundary && endsBoundary;
9232
9304
  };
9233
9305
  const containsRedundantWord = (altText, words) => {
@@ -9266,10 +9338,10 @@ const imgRedundantAlt = defineRule({
9266
9338
  if (isGeneratedImageRenderContext(context)) return {};
9267
9339
  const settings = resolveSettings$37(context.settings);
9268
9340
  return { JSXOpeningElement(node) {
9269
- if (isGeneratedImageRenderContext(context, node)) return;
9270
9341
  const tag = getElementType(node, context.settings);
9271
9342
  if (!settings.components.includes(tag)) return;
9272
9343
  if (isHiddenFromScreenReader(node, context.settings)) return;
9344
+ if (isGeneratedImageRenderContext(context, node)) return;
9273
9345
  const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
9274
9346
  if (!altAttribute) return;
9275
9347
  if (altValueRedundant(altAttribute, settings.words)) context.report({
@@ -9324,6 +9396,7 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
9324
9396
  const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
9325
9397
  const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
9326
9398
  const SECURITY_CONTEXT_WINDOW_CHARS = 250;
9399
+ const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
9327
9400
  const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
9328
9401
  for (const callMatch of content.matchAll(pattern)) {
9329
9402
  const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
@@ -9353,6 +9426,7 @@ const insecureCryptoRisk = defineRule({
9353
9426
  if (!isProductionSourcePath(file.relativePath)) return [];
9354
9427
  if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
9355
9428
  if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
9429
+ if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
9356
9430
  const content = getScannableContent(file);
9357
9431
  let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
9358
9432
  if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
@@ -9531,6 +9605,7 @@ const KEYBOARD_EVENT_HANDLERS = [
9531
9605
  "onKeyUp"
9532
9606
  ];
9533
9607
  const ALL_EVENT_HANDLERS = [...MOUSE_EVENT_HANDLERS, ...KEYBOARD_EVENT_HANDLERS];
9608
+ const ALL_EVENT_HANDLERS_LOWER = new Set(ALL_EVENT_HANDLERS.map((handlerName) => handlerName.toLowerCase()));
9534
9609
  //#endregion
9535
9610
  //#region src/plugin/utils/is-disabled-element.ts
9536
9611
  const isDisabledElement = (openingElement) => {
@@ -9585,15 +9660,25 @@ const interactiveSupportsFocus = defineRule({
9585
9660
  const settings = resolveSettings$36(context.settings);
9586
9661
  const tabbableSet = new Set(settings.tabbable);
9587
9662
  return { JSXOpeningElement(node) {
9663
+ if (node.attributes.length === 0) return;
9588
9664
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
9589
9665
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
9590
9666
  const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
9667
+ if (!role) return;
9668
+ let hasInteractiveHandler = false;
9669
+ for (const attribute of node.attributes) {
9670
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
9671
+ const attributeName = getJsxAttributeName(attribute.name);
9672
+ if (attributeName && ALL_EVENT_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
9673
+ hasInteractiveHandler = true;
9674
+ break;
9675
+ }
9676
+ }
9677
+ if (!hasInteractiveHandler) return;
9591
9678
  const elementType = getElementType(node, context.settings);
9592
9679
  if (!HTML_TAGS.has(elementType)) return;
9593
- const hasInteractiveHandler = ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)));
9680
+ if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9594
9681
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
9595
- if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9596
- if (!role) return;
9597
9682
  if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
9598
9683
  const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
9599
9684
  context.report({
@@ -9611,7 +9696,7 @@ const isAtomFromJotai = (callExpression) => {
9611
9696
  if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
9612
9697
  return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
9613
9698
  };
9614
- const isFunctionExpressionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
9699
+ const isFunctionExpressionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
9615
9700
  const getFirstParameterName = (fn) => {
9616
9701
  const parameters = fn.params ?? [];
9617
9702
  if (parameters.length !== 1) return null;
@@ -9744,7 +9829,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
9744
9829
  const args = node.arguments ?? [];
9745
9830
  if (args.length === 0) return;
9746
9831
  const reader = args[0];
9747
- if (!isFunctionExpressionLike$1(reader)) return;
9832
+ if (!isFunctionExpressionLike$2(reader)) return;
9748
9833
  const getParameterName = getFirstParameterName(reader);
9749
9834
  if (!getParameterName) return;
9750
9835
  const freshReturn = getFreshReturnForFunction(reader);
@@ -9842,14 +9927,14 @@ const getHandlerNamedBindingName = (functionNode) => {
9842
9927
  const containingFunctionIsComponentOrHook = (functionNode) => {
9843
9928
  if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
9844
9929
  const declaredName = functionNode.id.name;
9845
- return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN.test(declaredName);
9930
+ return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN$1.test(declaredName);
9846
9931
  }
9847
9932
  let cursor = functionNode.parent ?? null;
9848
9933
  while (cursor && isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
9849
9934
  if (!cursor) return false;
9850
9935
  if (!isNodeOfType(cursor, "VariableDeclarator")) return false;
9851
9936
  if (!isNodeOfType(cursor.id, "Identifier")) return false;
9852
- return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN.test(cursor.id.name);
9937
+ return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN$1.test(cursor.id.name);
9853
9938
  };
9854
9939
  const isBindingInvokedOnRenderPath = (root, bindingName) => {
9855
9940
  let didFindRenderPathInvocation = false;
@@ -10232,8 +10317,6 @@ const jsCachePropertyAccess = defineRule({
10232
10317
  walkAst(loopBody, (child) => {
10233
10318
  if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
10234
10319
  if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
10235
- });
10236
- walkAst(loopBody, (child) => {
10237
10320
  if (!isNodeOfType(child, "MemberExpression")) return;
10238
10321
  if (child.computed) return;
10239
10322
  if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
@@ -10693,7 +10776,6 @@ const jsHoistIntl = defineRule({
10693
10776
  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",
10694
10777
  create: (context) => ({ NewExpression(node) {
10695
10778
  if (!isIntlNewExpression(node)) return;
10696
- if (isInsideCacheMemo(node)) return;
10697
10779
  let cursor = node.parent ?? null;
10698
10780
  let inFunctionBody = false;
10699
10781
  while (cursor) {
@@ -10710,6 +10792,7 @@ const jsHoistIntl = defineRule({
10710
10792
  cursor = cursor.parent ?? null;
10711
10793
  }
10712
10794
  if (!inFunctionBody) return;
10795
+ if (isInsideCacheMemo(node)) return;
10713
10796
  const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
10714
10797
  context.report({
10715
10798
  node,
@@ -10784,7 +10867,11 @@ const isSingleFieldEqualityPredicate = (node) => {
10784
10867
  if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
10785
10868
  return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
10786
10869
  };
10787
- const collectLoopBoundNames = (loop, names) => {
10870
+ const loopBoundNamesCache = /* @__PURE__ */ new WeakMap();
10871
+ const getLoopBoundNames = (loop) => {
10872
+ const cached = loopBoundNamesCache.get(loop);
10873
+ if (cached) return cached;
10874
+ const names = /* @__PURE__ */ new Set();
10788
10875
  if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
10789
10876
  if (isNodeOfType(child, "Identifier")) names.add(child.name);
10790
10877
  });
@@ -10801,6 +10888,8 @@ const collectLoopBoundNames = (loop, names) => {
10801
10888
  if (targetRoot) names.add(targetRoot);
10802
10889
  }
10803
10890
  });
10891
+ loopBoundNamesCache.set(loop, names);
10892
+ return names;
10804
10893
  };
10805
10894
  const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
10806
10895
  let cursor = receiver;
@@ -10826,7 +10915,7 @@ const isLoopVariantReceiver = (node) => {
10826
10915
  const loopBoundNames = /* @__PURE__ */ new Set();
10827
10916
  let ancestor = node.parent;
10828
10917
  while (ancestor) {
10829
- if (LOOP_TYPES.includes(ancestor.type)) collectLoopBoundNames(ancestor, loopBoundNames);
10918
+ if (LOOP_TYPES.includes(ancestor.type)) for (const name of getLoopBoundNames(ancestor)) loopBoundNames.add(name);
10830
10919
  ancestor = ancestor.parent;
10831
10920
  }
10832
10921
  if (loopBoundNames.has(receiverRoot)) return true;
@@ -14434,14 +14523,14 @@ const keyLifecycleRisk = defineRule({
14434
14523
  //#region src/plugin/rules/a11y/label-has-associated-control.ts
14435
14524
  const MESSAGE_NO_LABEL = "Blind users can't identify this field because screen readers find no label text, so add visible text, `aria-label`, or `aria-labelledby`.";
14436
14525
  const MESSAGE_NO_CONTROL = "Screen reader users can't tell which input this label names because it's tied to none, so add `htmlFor` or wrap the input inside it.";
14437
- const DEFAULT_CONTROL_COMPONENTS = [
14526
+ const DEFAULT_CONTROL_COMPONENTS = new Set([
14438
14527
  "input",
14439
14528
  "meter",
14440
14529
  "output",
14441
14530
  "progress",
14442
14531
  "select",
14443
14532
  "textarea"
14444
- ];
14533
+ ]);
14445
14534
  const DEFAULT_LABEL_ATTRIBUTES = [
14446
14535
  "alt",
14447
14536
  "aria-label",
@@ -14453,8 +14542,8 @@ const resolveSettings$24 = (settings) => {
14453
14542
  const jsxA11y = settings?.["jsx-a11y"];
14454
14543
  const forAttributes = (typeof jsxA11y === "object" && jsxA11y !== null ? jsxA11y : {}).attributes?.for ?? ["htmlFor"];
14455
14544
  return {
14456
- labelComponents: ["label", ...ruleSettings.labelComponents ?? []].sort(),
14457
- labelAttributes: [...new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []])].sort(),
14545
+ labelComponents: new Set(["label", ...ruleSettings.labelComponents ?? []]),
14546
+ labelAttributes: new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []]),
14458
14547
  controlComponents: ruleSettings.controlComponents ?? [],
14459
14548
  assert: ruleSettings.assert ?? "either",
14460
14549
  depth: Math.min(ruleSettings.depth ?? 5, 25),
@@ -14462,7 +14551,7 @@ const resolveSettings$24 = (settings) => {
14462
14551
  };
14463
14552
  };
14464
14553
  const isControlComponent = (tagName, controlComponents) => {
14465
- if (DEFAULT_CONTROL_COMPONENTS.includes(tagName)) return true;
14554
+ if (DEFAULT_CONTROL_COMPONENTS.has(tagName)) return true;
14466
14555
  return controlComponents.some((pattern) => compileGlob(pattern).test(tagName));
14467
14556
  };
14468
14557
  const searchForNestedControl = (child, currentDepth, searchContext) => {
@@ -14488,7 +14577,7 @@ const searchForAccessibleLabel = (child, currentDepth, searchContext) => {
14488
14577
  const attributeName = attribute.name;
14489
14578
  if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
14490
14579
  const propName = getJsxAttributeName(attributeName);
14491
- if (!propName || !searchContext.labelAttributes.includes(propName)) continue;
14580
+ if (!propName || !searchContext.labelAttributes.has(propName)) continue;
14492
14581
  const attributeValue = attribute.value;
14493
14582
  if (!attributeValue) continue;
14494
14583
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
@@ -14510,7 +14599,7 @@ const hasAccessibleLabel = (element, searchContext) => {
14510
14599
  const attributeName = attribute.name;
14511
14600
  if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
14512
14601
  const propName = getJsxAttributeName(attributeName);
14513
- if (propName && searchContext.labelAttributes.includes(propName)) return true;
14602
+ if (propName && searchContext.labelAttributes.has(propName)) return true;
14514
14603
  }
14515
14604
  for (const child of element.children) if (searchForAccessibleLabel(child, 1, searchContext)) return true;
14516
14605
  return false;
@@ -14533,7 +14622,7 @@ const labelHasAssociatedControl = defineRule({
14533
14622
  if (isTestlikeFile) return;
14534
14623
  const opening = node.openingElement;
14535
14624
  const tagName = getElementType(opening, context.settings);
14536
- if (!settings.labelComponents.includes(tagName)) return;
14625
+ if (!settings.labelComponents.has(tagName)) return;
14537
14626
  const hasHtmlFor = settings.forAttributes.some((attributeName) => Boolean(hasJsxPropIgnoreCase(opening.attributes, attributeName)));
14538
14627
  const searchContext = {
14539
14628
  depth: settings.depth,
@@ -15122,9 +15211,9 @@ const resolveSettings$23 = (settings) => {
15122
15211
  const reactDoctor = settings?.["react-doctor"];
15123
15212
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.mediaHasCaption ?? {} : {};
15124
15213
  return {
15125
- audio: [...DEFAULT_AUDIO, ...ruleSettings.audio ?? []],
15126
- video: [...DEFAULT_VIDEO, ...ruleSettings.video ?? []],
15127
- track: [...DEFAULT_TRACK, ...ruleSettings.track ?? []]
15214
+ audio: new Set([...DEFAULT_AUDIO, ...ruleSettings.audio ?? []]),
15215
+ video: new Set([...DEFAULT_VIDEO, ...ruleSettings.video ?? []]),
15216
+ track: new Set([...DEFAULT_TRACK, ...ruleSettings.track ?? []])
15128
15217
  };
15129
15218
  };
15130
15219
  const evaluateMuted = (attribute) => {
@@ -15153,7 +15242,7 @@ const childMayRenderTrack = (child, trackTags, settings) => {
15153
15242
  let rendersCaptionTrack = false;
15154
15243
  walkAst(expression, (inner) => {
15155
15244
  if (rendersCaptionTrack) return false;
15156
- if (isNodeOfType(inner, "JSXElement") && trackTags.includes(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
15245
+ if (isNodeOfType(inner, "JSXElement") && trackTags.has(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
15157
15246
  rendersCaptionTrack = true;
15158
15247
  return false;
15159
15248
  }
@@ -15171,7 +15260,7 @@ const mediaHasCaption = defineRule({
15171
15260
  const settings = resolveSettings$23(context.settings);
15172
15261
  return { JSXOpeningElement(node) {
15173
15262
  const tag = getElementType(node, context.settings);
15174
- if (!(settings.audio.includes(tag) || settings.video.includes(tag))) return;
15263
+ if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
15175
15264
  if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
15176
15265
  const parent = node.parent;
15177
15266
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
@@ -15186,7 +15275,7 @@ const mediaHasCaption = defineRule({
15186
15275
  if (!isNodeOfType(child, "JSXElement")) return false;
15187
15276
  const opening = child.openingElement;
15188
15277
  const childTag = getElementType(opening, context.settings);
15189
- if (!settings.track.includes(childTag)) return false;
15278
+ if (!settings.track.has(childTag)) return false;
15190
15279
  const kindAttribute = hasJsxPropIgnoreCase(opening.attributes, "kind");
15191
15280
  if (!kindAttribute) return false;
15192
15281
  let kindValue = kindAttribute.value;
@@ -15497,16 +15586,30 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
15497
15586
  const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
15498
15587
  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;
15499
15588
  const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
15500
- const doesSourceTextExportName = (sourceText, exportedName) => {
15589
+ const collectSourceTextExportNames = (sourceText) => {
15501
15590
  const strippedSource = stripJsComments(sourceText);
15502
- if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
15503
- for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
15504
- 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;
15505
- return false;
15591
+ const exportedNames = /* @__PURE__ */ new Set();
15592
+ if (DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) exportedNames.add("default");
15593
+ for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1]) exportedNames.add(match[1]);
15594
+ for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) {
15595
+ const specifiersText = match[1] ?? "";
15596
+ for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
15597
+ }
15598
+ return exportedNames;
15506
15599
  };
15600
+ const exportNamesCache = /* @__PURE__ */ new Map();
15507
15601
  const doesModuleExportName = (filePath, exportedName) => {
15508
15602
  try {
15509
- return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
15603
+ const fileStat = fs.statSync(filePath);
15604
+ const cached = exportNamesCache.get(filePath);
15605
+ if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
15606
+ const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
15607
+ exportNamesCache.set(filePath, {
15608
+ mtimeMs: fileStat.mtimeMs,
15609
+ size: fileStat.size,
15610
+ exportedNames
15611
+ });
15612
+ return exportedNames.has(exportedName);
15510
15613
  } catch {
15511
15614
  return false;
15512
15615
  }
@@ -15545,6 +15648,7 @@ const nextjsMissingMetadata = defineRule({
15545
15648
  const filename = normalizeFilename(context.filename ?? "");
15546
15649
  if (!PAGE_FILE_PATTERN.test(filename)) return;
15547
15650
  if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
15651
+ if ((programNode.body ?? []).some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use client")) return;
15548
15652
  if (programNode.body?.some((statement) => {
15549
15653
  if (!isNodeOfType(statement, "ExportNamedDeclaration")) return false;
15550
15654
  const declaration = statement.declaration;
@@ -15651,8 +15755,12 @@ const containsFetchCall = (node, options) => {
15651
15755
  const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
15652
15756
  let didFindFetchCall = false;
15653
15757
  walkAst(node, (child) => {
15758
+ if (didFindFetchCall) return false;
15654
15759
  if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
15655
- if (isFetchCall$1(child)) didFindFetchCall = true;
15760
+ if (isFetchCall$1(child)) {
15761
+ didFindFetchCall = true;
15762
+ return false;
15763
+ }
15656
15764
  });
15657
15765
  return didFindFetchCall;
15658
15766
  };
@@ -16342,16 +16450,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
16342
16450
  recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
16343
16451
  create: (context) => {
16344
16452
  let resolveBinding = () => null;
16453
+ let isRouteHandlerFile = false;
16454
+ let mutatingSegment = null;
16345
16455
  return {
16346
16456
  Program(node) {
16347
16457
  resolveBinding = buildProgramBindingLookup(node);
16458
+ const filename = normalizeFilename(context.filename ?? "");
16459
+ isRouteHandlerFile = ROUTE_HANDLER_FILE_PATTERN.test(filename) && !CRON_ROUTE_PATTERN.test(filename);
16460
+ mutatingSegment = isRouteHandlerFile ? extractMutatingRouteSegment(filename) : null;
16348
16461
  },
16349
16462
  ExportNamedDeclaration(node) {
16350
- const filename = normalizeFilename(context.filename ?? "");
16351
- if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
16352
- if (CRON_ROUTE_PATTERN.test(filename)) return;
16463
+ if (!isRouteHandlerFile) return;
16353
16464
  if (!isExportedGetHandler(node)) return;
16354
- const mutatingSegment = extractMutatingRouteSegment(filename);
16355
16465
  const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
16356
16466
  for (const handlerBody of handlerBodies) {
16357
16467
  const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
@@ -17268,14 +17378,38 @@ const resolvesToRefFactoryCall = (identifier) => {
17268
17378
  });
17269
17379
  return found;
17270
17380
  };
17271
- const isRefLikeReceiver = (receiver) => {
17381
+ const unwrapExpression$2 = (node) => {
17382
+ if (!node) return null;
17383
+ if (isNodeOfType(node, "ChainExpression")) return unwrapExpression$2(node.expression);
17384
+ if (isNodeOfType(node, "TSNonNullExpression")) return unwrapExpression$2(node.expression);
17385
+ return node;
17386
+ };
17387
+ const resolvesToRefCurrentAlias = (identifier, visitedAliasNames) => {
17388
+ if (visitedAliasNames.has(identifier.name)) return false;
17389
+ const root = findProgramRoot(identifier);
17390
+ if (!root) return false;
17391
+ const nextVisited = new Set([...visitedAliasNames, identifier.name]);
17392
+ let found = false;
17393
+ walkAst(root, (child) => {
17394
+ if (found) return false;
17395
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier") && child.id.name === identifier.name) {
17396
+ const init = unwrapExpression$2(child.init);
17397
+ if (init && isNodeOfType(init, "MemberExpression") && isNodeOfType(init.property, "Identifier") && init.property.name === "current" && isRefLikeReceiver(init.object, nextVisited)) {
17398
+ found = true;
17399
+ return false;
17400
+ }
17401
+ }
17402
+ });
17403
+ return found;
17404
+ };
17405
+ const isRefLikeReceiver = (receiver, visitedAliasNames = /* @__PURE__ */ new Set()) => {
17272
17406
  if (!receiver) return false;
17273
- if (isNodeOfType(receiver, "ChainExpression")) return isRefLikeReceiver(receiver.expression);
17274
- if (isNodeOfType(receiver, "TSNonNullExpression")) return isRefLikeReceiver(receiver.expression);
17275
- if (isNodeOfType(receiver, "Identifier")) return hasRefLikeName(receiver.name) || resolvesToRefFactoryCall(receiver);
17407
+ if (isNodeOfType(receiver, "ChainExpression")) return isRefLikeReceiver(receiver.expression, visitedAliasNames);
17408
+ if (isNodeOfType(receiver, "TSNonNullExpression")) return isRefLikeReceiver(receiver.expression, visitedAliasNames);
17409
+ if (isNodeOfType(receiver, "Identifier")) return hasRefLikeName(receiver.name) || resolvesToRefFactoryCall(receiver) || resolvesToRefCurrentAlias(receiver, visitedAliasNames);
17276
17410
  if (isNodeOfType(receiver, "MemberExpression") && isNodeOfType(receiver.property, "Identifier")) {
17277
17411
  if (hasRefLikeName(receiver.property.name)) return true;
17278
- if (receiver.property.name === "current") return isRefLikeReceiver(receiver.object);
17412
+ if (receiver.property.name === "current") return isRefLikeReceiver(receiver.object, visitedAliasNames);
17279
17413
  }
17280
17414
  return false;
17281
17415
  };
@@ -17383,8 +17517,15 @@ const getProgramAnalysis = (anyNode) => {
17383
17517
  programToAnalysis.set(programNode, analysis);
17384
17518
  return analysis;
17385
17519
  };
17520
+ const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
17386
17521
  const getScopeForNode = (node, manager) => {
17387
17522
  if (!node.range) return null;
17523
+ let scopeByNode = scopeByNodeCache.get(manager);
17524
+ if (!scopeByNode) {
17525
+ scopeByNode = /* @__PURE__ */ new WeakMap();
17526
+ scopeByNodeCache.set(manager, scopeByNode);
17527
+ }
17528
+ if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
17388
17529
  let bestScope = null;
17389
17530
  let bestSize = Infinity;
17390
17531
  for (const scope of manager.scopes) {
@@ -17397,11 +17538,25 @@ const getScopeForNode = (node, manager) => {
17397
17538
  bestScope = scope;
17398
17539
  }
17399
17540
  }
17541
+ scopeByNode.set(node, bestScope);
17400
17542
  return bestScope;
17401
17543
  };
17402
17544
  //#endregion
17403
17545
  //#region src/plugin/rules/state-and-effects/utils/effect/ast.ts
17404
17546
  const getChildKeys = (node) => VISITOR_KEYS[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
17547
+ const HOOK_NAME_PATTERN = /^use[A-Z0-9]/;
17548
+ const isInsideCallbackArgumentOf = (identifier, initializer) => {
17549
+ if (!isNodeOfType(initializer, "CallExpression") && !isNodeOfType(initializer, "NewExpression")) return false;
17550
+ if (isNodeOfType(initializer, "CallExpression") && isNodeOfType(initializer.callee, "Identifier") && HOOK_NAME_PATTERN.test(initializer.callee.name)) return false;
17551
+ const callbackArguments = (initializer.arguments ?? []).filter((argument) => isFunctionLike$1(argument));
17552
+ if (callbackArguments.length === 0) return false;
17553
+ let node = identifier;
17554
+ while (node && node !== initializer) {
17555
+ if (callbackArguments.includes(node)) return true;
17556
+ node = node.parent;
17557
+ }
17558
+ return false;
17559
+ };
17405
17560
  const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
17406
17561
  if (visited.has(ref)) return;
17407
17562
  const result = visit(ref);
@@ -17414,7 +17569,10 @@ const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
17414
17569
  const defNode = def.node;
17415
17570
  const next = defNode.init ?? defNode.body;
17416
17571
  if (!next) continue;
17417
- for (const innerRef of getDownstreamRefs(analysis, next)) ascend(analysis, innerRef, visit, visited);
17572
+ for (const innerRef of getDownstreamRefs(analysis, next)) {
17573
+ if (isInsideCallbackArgumentOf(innerRef.identifier, next)) continue;
17574
+ ascend(analysis, innerRef, visit, visited);
17575
+ }
17418
17576
  }
17419
17577
  };
17420
17578
  const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
@@ -17431,11 +17589,20 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
17431
17589
  } else if (isAstNode(child)) descend(child, visit, visited);
17432
17590
  }
17433
17591
  };
17592
+ const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
17434
17593
  const getUpstreamRefs = (analysis, ref) => {
17594
+ let upstreamByRef = upstreamRefsCache.get(analysis);
17595
+ if (!upstreamByRef) {
17596
+ upstreamByRef = /* @__PURE__ */ new WeakMap();
17597
+ upstreamRefsCache.set(analysis, upstreamByRef);
17598
+ }
17599
+ const cached = upstreamByRef.get(ref);
17600
+ if (cached) return cached;
17435
17601
  const refs = [];
17436
17602
  ascend(analysis, ref, (upRef) => {
17437
17603
  refs.push(upRef);
17438
17604
  });
17605
+ upstreamByRef.set(ref, refs);
17439
17606
  return refs;
17440
17607
  };
17441
17608
  const findDownstreamNodes = (topNode, type) => {
@@ -17445,11 +17612,24 @@ const findDownstreamNodes = (topNode, type) => {
17445
17612
  });
17446
17613
  return nodes;
17447
17614
  };
17615
+ const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
17448
17616
  const getRef = (analysis, identifier) => {
17617
+ let refByIdentifier = refByIdentifierCache.get(analysis);
17618
+ if (!refByIdentifier) {
17619
+ refByIdentifier = /* @__PURE__ */ new WeakMap();
17620
+ refByIdentifierCache.set(analysis, refByIdentifier);
17621
+ }
17622
+ if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
17623
+ let resolvedReference = null;
17449
17624
  const scope = getScopeForNode(identifier, analysis.scopeManager);
17450
- if (!scope) return null;
17451
- for (const reference of scope.references) if (reference.identifier === identifier) return reference;
17452
- return null;
17625
+ if (scope) {
17626
+ for (const reference of scope.references) if (reference.identifier === identifier) {
17627
+ resolvedReference = reference;
17628
+ break;
17629
+ }
17630
+ }
17631
+ refByIdentifier.set(identifier, resolvedReference);
17632
+ return resolvedReference;
17453
17633
  };
17454
17634
  const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
17455
17635
  const getDownstreamRefs = (analysis, node) => {
@@ -17524,22 +17704,6 @@ const isEventualCallTo = (analysis, ref, predicate) => {
17524
17704
  };
17525
17705
  //#endregion
17526
17706
  //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
17527
- const getOuterScopeContaining = (analysis, node) => {
17528
- if (!node.range) return null;
17529
- let best = null;
17530
- let bestSize = Infinity;
17531
- for (const scope of analysis.scopeManager.scopes) {
17532
- const block = scope.block;
17533
- if (!block?.range) continue;
17534
- if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
17535
- const size = block.range[1] - block.range[0];
17536
- if (size <= bestSize) {
17537
- bestSize = size;
17538
- best = scope;
17539
- }
17540
- }
17541
- return best;
17542
- };
17543
17707
  const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
17544
17708
  const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
17545
17709
  const isReactFunctionalComponent = (node) => {
@@ -17570,7 +17734,7 @@ const isReactFunctionalHOC = (analysis, node) => {
17570
17734
  const isWrappedSeparately = () => {
17571
17735
  if (!isNodeOfType(node.id, "Identifier")) return false;
17572
17736
  const bindingName = node.id.name;
17573
- const containingScope = getOuterScopeContaining(analysis, node);
17737
+ const containingScope = getScopeForNode(node, analysis.scopeManager);
17574
17738
  if (!containingScope) return false;
17575
17739
  const variable = containingScope.variables.find((v) => v.name === bindingName);
17576
17740
  if (!variable) return false;
@@ -17857,7 +18021,8 @@ const noAriaHiddenOnFocusable = defineRule({
17857
18021
  if (isNodeOfType(value, "Literal") && value.value !== "true") return;
17858
18022
  if (isNodeOfType(value, "JSXExpressionContainer")) {
17859
18023
  const expression = value.expression;
17860
- if (isNodeOfType(expression, "Literal") && !expression.value) return;
18024
+ if (!isNodeOfType(expression, "Literal")) return;
18025
+ if (expression.value !== true && expression.value !== "true") return;
17861
18026
  }
17862
18027
  }
17863
18028
  const tag = getElementType(node, context.settings);
@@ -18112,10 +18277,30 @@ const isArrayFromCall = (node) => {
18112
18277
  *
18113
18278
  * Used both for `<receiver>.map(...)` and for `Array.from(<length>, fn)`.
18114
18279
  */
18115
- const isStaticPlaceholderReceiver = (receiver) => {
18280
+ const isBindingReassigned = (referenceNode, bindingName) => {
18281
+ const programRoot = findProgramRoot(referenceNode);
18282
+ if (!programRoot) return false;
18283
+ let didFindReassignment = false;
18284
+ walkAst(programRoot, (child) => {
18285
+ if (didFindReassignment) return false;
18286
+ if (isNodeOfType(child, "AssignmentExpression") && isNodeOfType(child.left, "Identifier") && child.left.name === bindingName) {
18287
+ didFindReassignment = true;
18288
+ return false;
18289
+ }
18290
+ });
18291
+ return didFindReassignment;
18292
+ };
18293
+ const isStaticPlaceholderReceiver = (receiver, depth = 0) => {
18116
18294
  if (isArrayFromCall(receiver)) return true;
18117
18295
  if (isArrayConstructorCallWithNumericLength(receiver)) return true;
18118
18296
  if (isAllLiteralArrayExpression(receiver)) return true;
18297
+ if (isNodeOfType(receiver, "Identifier")) {
18298
+ if (depth >= TYPE_RESOLUTION_DEPTH_LIMIT) return false;
18299
+ const binding = findVariableInitializer(receiver, receiver.name);
18300
+ if (!binding?.initializer) return false;
18301
+ if (isBindingReassigned(receiver, receiver.name)) return false;
18302
+ return isStaticPlaceholderReceiver(binding.initializer, depth + 1);
18303
+ }
18119
18304
  if (isNodeOfType(receiver, "CallExpression")) {
18120
18305
  const callee = receiver.callee;
18121
18306
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "fill" && isArrayConstructorCallWithNumericLength(callee.object)) return true;
@@ -18141,6 +18326,7 @@ const isArrayFromLengthObjectCall = (node) => {
18141
18326
  if (!(isNodeOfType(key, "Identifier") && key.name === "length" || isNodeOfType(key, "Literal") && key.value === "length")) continue;
18142
18327
  if (isNumericLiteralOrUndefined(prop.value)) return true;
18143
18328
  if (isNodeOfType(prop.value, "Identifier")) return true;
18329
+ if (isNodeOfType(prop.value, "MemberExpression") && isNodeOfType(prop.value.property, "Identifier") && prop.value.property.name === "length") return true;
18144
18330
  }
18145
18331
  return false;
18146
18332
  };
@@ -18249,10 +18435,11 @@ const isInsideStaticPlaceholderMap = (node) => isInsideIteratorMapMatching(node,
18249
18435
  /**
18250
18436
  * Walk up from a JSXAttribute node looking for the enclosing iterator
18251
18437
  * callback (`.map(cb)`, `.flatMap(cb)`, `.forEach(cb)`, `Array.from(_, cb)`)
18252
- * and return the first parameter's name. The first param is the per-item
18253
- * value, e.g. `item` in `arr.map((item, index) => …)`.
18438
+ * and return the names bound by the first parameter `item` in
18439
+ * `arr.map((item, index) => …)`, or every destructured field in
18440
+ * `arr.map(({ id, label }, index) => …)`.
18254
18441
  */
18255
- const findIteratorItemName$1 = (node) => {
18442
+ const findIteratorItemNames = (node) => {
18256
18443
  let current = node;
18257
18444
  while (current.parent) {
18258
18445
  const parent = current.parent;
@@ -18262,18 +18449,20 @@ const findIteratorItemName$1 = (node) => {
18262
18449
  const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
18263
18450
  if (isIteratorMethodCall || isArrayFromCallback) {
18264
18451
  const first = (current.params ?? [])[0];
18265
- if (first && isNodeOfType(first, "Identifier")) return first.name;
18266
- return null;
18452
+ if (!first) return null;
18453
+ const names = /* @__PURE__ */ new Set();
18454
+ collectPatternNames(first, names);
18455
+ return names.size > 0 ? names : null;
18267
18456
  }
18268
18457
  }
18269
18458
  current = parent;
18270
18459
  }
18271
18460
  return null;
18272
18461
  };
18273
- const templateLiteralHasIteratorIdentity = (template, itemName) => {
18462
+ const templateLiteralHasIteratorIdentity = (template, itemNames) => {
18274
18463
  for (const expression of template.expressions ?? []) {
18275
- if (isNodeOfType(expression, "Identifier") && expression.name === itemName) return true;
18276
- if (isNodeOfType(expression, "MemberExpression") && isNodeOfType(expression.object, "Identifier") && expression.object.name === itemName) return true;
18464
+ const rootName = getRootIdentifierName(expression, { followCallChains: true });
18465
+ if (rootName !== null && itemNames.has(rootName)) return true;
18277
18466
  }
18278
18467
  return false;
18279
18468
  };
@@ -18286,9 +18475,32 @@ const templateLiteralHasIteratorIdentity = (template, itemName) => {
18286
18475
  const isCompositeKeyWithIteratorIdentity = (keyExpression, attributeNode) => {
18287
18476
  if (!isNodeOfType(keyExpression, "TemplateLiteral")) return false;
18288
18477
  if ((keyExpression.expressions ?? []).length < 2) return false;
18289
- const itemName = findIteratorItemName$1(attributeNode);
18290
- if (!itemName) return false;
18291
- return templateLiteralHasIteratorIdentity(keyExpression, itemName);
18478
+ const itemNames = findIteratorItemNames(attributeNode);
18479
+ if (!itemNames) return false;
18480
+ return templateLiteralHasIteratorIdentity(keyExpression, itemNames);
18481
+ };
18482
+ const forLoopTestReadsDataLength = (test) => {
18483
+ let didFindLengthRead = false;
18484
+ walkAst(test, (child) => {
18485
+ if (didFindLengthRead) return false;
18486
+ if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.property, "Identifier") && child.property.name === "length") {
18487
+ didFindLengthRead = true;
18488
+ return false;
18489
+ }
18490
+ });
18491
+ return didFindLengthRead;
18492
+ };
18493
+ const isNumericForLoopCounter = (attributeNode, indexName) => {
18494
+ const binding = findVariableInitializer(attributeNode, indexName);
18495
+ if (!binding) return false;
18496
+ const declarator = binding.bindingIdentifier.parent;
18497
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
18498
+ const declaration = declarator.parent;
18499
+ if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return false;
18500
+ const forStatement = declaration.parent;
18501
+ if (!forStatement || !isNodeOfType(forStatement, "ForStatement") || forStatement.init !== declaration || !declarator.init || !isNodeOfType(declarator.init, "Literal") || typeof declarator.init.value !== "number") return false;
18502
+ if (forStatement.test && forLoopTestReadsDataLength(forStatement.test)) return false;
18503
+ return true;
18292
18504
  };
18293
18505
  const noArrayIndexAsKey = defineRule({
18294
18506
  id: "no-array-index-as-key",
@@ -18300,6 +18512,7 @@ const noArrayIndexAsKey = defineRule({
18300
18512
  if (!node.value || !isNodeOfType(node.value, "JSXExpressionContainer")) return;
18301
18513
  const indexName = extractIndexName(node.value.expression);
18302
18514
  if (!indexName) return;
18515
+ if (isNumericForLoopCounter(node, indexName)) return;
18303
18516
  if (isInsideStaticPlaceholderMap(node)) return;
18304
18517
  if (isInsideStringDerivedMap(node)) return;
18305
18518
  if (isCompositeKeyWithIteratorIdentity(node.value.expression, node)) return;
@@ -19435,8 +19648,8 @@ const CONTEXT_MODULES = [
19435
19648
  ];
19436
19649
  const isCreateContextCallee = (callee) => {
19437
19650
  if (isNodeOfType(callee, "Identifier")) {
19438
- for (const moduleName of CONTEXT_MODULES) if (getImportedNameFromModule(callee, callee.name, moduleName) === "createContext") return true;
19439
- return false;
19651
+ const binding = getImportBindingForName(callee, callee.name);
19652
+ return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
19440
19653
  }
19441
19654
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
19442
19655
  const namespaceIdentifier = callee.object;
@@ -19446,8 +19659,8 @@ const isCreateContextCallee = (callee) => {
19446
19659
  if (propertyIdentifier.name !== "createContext") return false;
19447
19660
  const namespaceName = namespaceIdentifier.name;
19448
19661
  if (isCanonicalReactNamespaceName(namespaceName)) return true;
19449
- for (const moduleName of CONTEXT_MODULES) if (isImportedFromModule(namespaceIdentifier, namespaceName, moduleName)) return true;
19450
- return false;
19662
+ const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
19663
+ return importSource !== null && CONTEXT_MODULES.includes(importSource);
19451
19664
  }
19452
19665
  return false;
19453
19666
  };
@@ -19849,38 +20062,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
19849
20062
  };
19850
20063
  const parseColorToRgb = (value) => {
19851
20064
  const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
19852
- const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
19853
- if (hex8Match) return {
19854
- red: parseInt(hex8Match[1], 16),
19855
- green: parseInt(hex8Match[2], 16),
19856
- blue: parseInt(hex8Match[3], 16)
19857
- };
19858
- const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
19859
- if (hex6Match) return {
19860
- red: parseInt(hex6Match[1], 16),
19861
- green: parseInt(hex6Match[2], 16),
19862
- blue: parseInt(hex6Match[3], 16)
19863
- };
19864
- const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
19865
- if (hex4Match) return {
19866
- red: parseInt(hex4Match[1] + hex4Match[1], 16),
19867
- green: parseInt(hex4Match[2] + hex4Match[2], 16),
19868
- blue: parseInt(hex4Match[3] + hex4Match[3], 16)
19869
- };
19870
- const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
19871
- if (hex3Match) return {
19872
- red: parseInt(hex3Match[1] + hex3Match[1], 16),
19873
- green: parseInt(hex3Match[2] + hex3Match[2], 16),
19874
- blue: parseInt(hex3Match[3] + hex3Match[3], 16)
19875
- };
19876
- const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
19877
- if (rgbMatch) return {
19878
- red: parseInt(rgbMatch[1], 10),
19879
- green: parseInt(rgbMatch[2], 10),
19880
- blue: parseInt(rgbMatch[3], 10)
19881
- };
19882
- const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
19883
- if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
20065
+ if (trimmed.startsWith("#")) {
20066
+ const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
20067
+ if (hex8Match) return {
20068
+ red: parseInt(hex8Match[1], 16),
20069
+ green: parseInt(hex8Match[2], 16),
20070
+ blue: parseInt(hex8Match[3], 16)
20071
+ };
20072
+ const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
20073
+ if (hex6Match) return {
20074
+ red: parseInt(hex6Match[1], 16),
20075
+ green: parseInt(hex6Match[2], 16),
20076
+ blue: parseInt(hex6Match[3], 16)
20077
+ };
20078
+ const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
20079
+ if (hex4Match) return {
20080
+ red: parseInt(hex4Match[1] + hex4Match[1], 16),
20081
+ green: parseInt(hex4Match[2] + hex4Match[2], 16),
20082
+ blue: parseInt(hex4Match[3] + hex4Match[3], 16)
20083
+ };
20084
+ const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
20085
+ if (hex3Match) return {
20086
+ red: parseInt(hex3Match[1] + hex3Match[1], 16),
20087
+ green: parseInt(hex3Match[2] + hex3Match[2], 16),
20088
+ blue: parseInt(hex3Match[3] + hex3Match[3], 16)
20089
+ };
20090
+ }
20091
+ if (trimmed.includes("rgb")) {
20092
+ const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
20093
+ if (rgbMatch) return {
20094
+ red: parseInt(rgbMatch[1], 10),
20095
+ green: parseInt(rgbMatch[2], 10),
20096
+ blue: parseInt(rgbMatch[3], 10)
20097
+ };
20098
+ }
20099
+ if (trimmed.includes("hsl")) {
20100
+ const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
20101
+ if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
20102
+ }
19884
20103
  return null;
19885
20104
  };
19886
20105
  //#endregion
@@ -19908,9 +20127,18 @@ const extractColorFromShadowLayer = (layer) => {
19908
20127
  if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
19909
20128
  return null;
19910
20129
  };
20130
+ const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
20131
+ const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
20132
+ const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
20133
+ const SHADOW_BLUR_TOKEN_INDEX = 2;
19911
20134
  const parseShadowLayerBlur = (layer) => {
19912
- const numericTokens = [...layer.replace(/rgba?\([^)]*\)/g, "").replace(/#[0-9a-f]{3,8}\b/gi, "").matchAll(/(\d+(?:\.\d+)?)(px)?/g)].map((match) => parseFloat(match[1]));
19913
- return numericTokens.length >= 3 ? numericTokens[2] : 0;
20135
+ const withoutColors = layer.replace(RGB_FUNCTION_PATTERN, "").replace(HEX_COLOR_PATTERN, "");
20136
+ let tokenIndex = 0;
20137
+ for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
20138
+ if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
20139
+ tokenIndex += 1;
20140
+ }
20141
+ return 0;
19914
20142
  };
19915
20143
  const hasColoredGlowShadow = (shadowValue) => {
19916
20144
  for (const layer of splitShadowLayers(shadowValue)) {
@@ -20030,7 +20258,10 @@ const isIntrinsicJsxAttribute = (node) => {
20030
20258
  };
20031
20259
  //#endregion
20032
20260
  //#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
20261
+ const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
20033
20262
  const collectComponentPropNames = (componentFunction) => {
20263
+ const cached = componentPropNamesCache.get(componentFunction);
20264
+ if (cached) return cached;
20034
20265
  const propNames = /* @__PURE__ */ new Set();
20035
20266
  if (!isFunctionLike$1(componentFunction)) return propNames;
20036
20267
  const propsObjectParamNames = /* @__PURE__ */ new Set();
@@ -20044,27 +20275,23 @@ const collectComponentPropNames = (componentFunction) => {
20044
20275
  if (child !== componentBody && isFunctionLike$1(child)) return false;
20045
20276
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
20046
20277
  });
20278
+ componentPropNamesCache.set(componentFunction, propNames);
20047
20279
  return propNames;
20048
20280
  };
20049
- const declaresBindingNamed = (functionNode, bindingName) => {
20281
+ const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
20282
+ const getOwnScopeBoundNames = (functionNode) => {
20283
+ const cached = ownScopeBoundNamesCache.get(functionNode);
20284
+ if (cached) return cached;
20050
20285
  const boundNames = /* @__PURE__ */ new Set();
20051
20286
  if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
20052
- if (boundNames.has(bindingName)) return true;
20053
- let declaresName = false;
20054
20287
  walkAst(functionNode, (child) => {
20055
- if (declaresName) return false;
20056
20288
  if (child !== functionNode && isFunctionLike$1(child)) return false;
20057
- if (isNodeOfType(child, "VariableDeclarator")) {
20058
- const declaratorNames = /* @__PURE__ */ new Set();
20059
- collectPatternNames(child.id, declaratorNames);
20060
- if (declaratorNames.has(bindingName)) {
20061
- declaresName = true;
20062
- return false;
20063
- }
20064
- }
20289
+ if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
20065
20290
  });
20066
- return declaresName;
20291
+ ownScopeBoundNamesCache.set(functionNode, boundNames);
20292
+ return boundNames;
20067
20293
  };
20294
+ const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
20068
20295
  const isPropertyNamePosition = (identifier) => {
20069
20296
  const parent = identifier.parent;
20070
20297
  if (!parent) return false;
@@ -20487,36 +20714,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
20487
20714
  }
20488
20715
  return hasNestedFunction;
20489
20716
  };
20490
- const isReseededDraftBuffer = (useStateCall, isPropName) => {
20491
- const setterName = getStateSetterName(useStateCall);
20492
- if (!setterName) return false;
20493
- const componentFunction = findEnclosingFunction(useStateCall);
20494
- if (!componentFunction) return false;
20495
- let isReseeded = false;
20496
- walkAst(componentFunction, (child) => {
20497
- if (isReseeded) return false;
20498
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
20499
- isReseeded = true;
20500
- return false;
20501
- }
20502
- });
20503
- return isReseeded;
20717
+ const isInRenderScope = (node, componentFunction) => {
20718
+ let cursor = node.parent ?? null;
20719
+ while (cursor && cursor !== componentFunction) {
20720
+ if (isFunctionLike$1(cursor)) return false;
20721
+ cursor = cursor.parent ?? null;
20722
+ }
20723
+ return true;
20504
20724
  };
20505
- const isAdjustedDuringRender = (useStateCall, isPropName) => {
20725
+ const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
20506
20726
  const setterName = getStateSetterName(useStateCall);
20507
20727
  if (!setterName) return false;
20508
20728
  const componentFunction = findEnclosingFunction(useStateCall);
20509
20729
  if (!componentFunction) return false;
20510
- let isAdjusted = false;
20730
+ let isExempt = false;
20511
20731
  walkAst(componentFunction, (child) => {
20512
- if (isAdjusted) return false;
20513
- if (child !== componentFunction && isFunctionLike$1(child)) return false;
20514
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName)) {
20515
- isAdjusted = true;
20732
+ if (isExempt) return false;
20733
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && (isHandlerShapedReseed(child, componentFunction) || isInRenderScope(child, componentFunction))) {
20734
+ isExempt = true;
20516
20735
  return false;
20517
20736
  }
20518
20737
  });
20519
- return isAdjusted;
20738
+ return isExempt;
20520
20739
  };
20521
20740
  const noDerivedUseState = defineRule({
20522
20741
  id: "no-derived-useState",
@@ -20533,8 +20752,7 @@ const noDerivedUseState = defineRule({
20533
20752
  const initializer = node.arguments[0];
20534
20753
  if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
20535
20754
  if (isInitialOnlyPropName(initializer.name)) return;
20536
- if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20537
- if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20755
+ if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20538
20756
  context.report({
20539
20757
  node,
20540
20758
  message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
@@ -20545,8 +20763,7 @@ const noDerivedUseState = defineRule({
20545
20763
  const rootIdentifierName = getRootIdentifierName(initializer);
20546
20764
  if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
20547
20765
  if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
20548
- if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20549
- if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20766
+ if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20550
20767
  context.report({
20551
20768
  node,
20552
20769
  message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
@@ -20661,7 +20878,7 @@ const isStateMemberExpression = (node) => {
20661
20878
  };
20662
20879
  //#endregion
20663
20880
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
20664
- const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
20881
+ const MESSAGE$24 = "Mutating `this.state` by hand never triggers a redraw on its own & a later setState can overwrite it, so use `this.setState` instead.";
20665
20882
  const shouldIgnoreMutation = (node) => {
20666
20883
  let isConstructor = false;
20667
20884
  let isInsideCallExpression = false;
@@ -20755,6 +20972,18 @@ const initializerMarksPlainState = (initializerArgument) => {
20755
20972
  }
20756
20973
  return producesPlainStateValue(unwrapped);
20757
20974
  };
20975
+ const collectCallbackRefSetterNames = (componentBody) => {
20976
+ const callbackRefSetterNames = /* @__PURE__ */ new Set();
20977
+ walkAst(componentBody, (node) => {
20978
+ if (!isNodeOfType(node, "JSXAttribute")) return;
20979
+ const attributeName = node.name;
20980
+ if (isNodeOfType(attributeName, "JSXIdentifier") && attributeName.name === "ref" && node.value && isNodeOfType(node.value, "JSXExpressionContainer")) {
20981
+ const expression = stripParenExpression(node.value.expression);
20982
+ if (isNodeOfType(expression, "Identifier")) callbackRefSetterNames.add(expression.name);
20983
+ }
20984
+ });
20985
+ return callbackRefSetterNames;
20986
+ };
20758
20987
  const collectFunctionLocalBindings = (functionNode) => {
20759
20988
  const localBindings = /* @__PURE__ */ new Set();
20760
20989
  if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return localBindings;
@@ -20797,8 +21026,10 @@ const noDirectStateMutation = defineRule({
20797
21026
  const bindings = collectUseStateBindings(componentBody);
20798
21027
  if (bindings.length === 0) return;
20799
21028
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
21029
+ const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
20800
21030
  const plainObjectStateValueNames = /* @__PURE__ */ new Set();
20801
21031
  for (const binding of bindings) {
21032
+ if (callbackRefSetterNames.has(binding.setterName)) continue;
20802
21033
  if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
20803
21034
  if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
20804
21035
  }
@@ -20811,7 +21042,7 @@ const noDirectStateMutation = defineRule({
20811
21042
  if (currentlyShadowed.has(rootName)) return;
20812
21043
  context.report({
20813
21044
  node: child,
20814
- message: `Your screen won't update because you change "${rootName}" in place.`
21045
+ message: `React can't tell you changed "${rootName}" in place, so this update can be skipped or lost.`
20815
21046
  });
20816
21047
  return;
20817
21048
  }
@@ -20827,7 +21058,7 @@ const noDirectStateMutation = defineRule({
20827
21058
  if (currentlyShadowed.has(rootName)) return;
20828
21059
  context.report({
20829
21060
  node: child,
20830
- message: `Your screen won't update because .${methodName}() changes "${rootName}" in place.`
21061
+ message: `React can't tell .${methodName}() changed "${rootName}" in place, so this update can be skipped or lost.`
20831
21062
  });
20832
21063
  }
20833
21064
  });
@@ -22245,20 +22476,20 @@ const getTriggerGuardRootName = (testNode) => {
22245
22476
  return null;
22246
22477
  };
22247
22478
  const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
22479
+ const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
22480
+ const settersWithAnyCall = /* @__PURE__ */ new Set();
22481
+ const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
22482
+ walkAst(componentBody, (child) => {
22483
+ if (!isNodeOfType(child, "CallExpression")) return;
22484
+ if (!isNodeOfType(child.callee, "Identifier")) return;
22485
+ const setterName = child.callee.name;
22486
+ if (!setterNames.has(setterName)) return;
22487
+ settersWithAnyCall.add(setterName);
22488
+ if (settersWithNonHandlerCall.has(setterName)) return;
22489
+ if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
22490
+ });
22248
22491
  const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
22249
- for (const binding of useStateBindings) {
22250
- let didFindAnySetterCall = false;
22251
- let areAllSetterCallsInHandlers = true;
22252
- walkAst(componentBody, (child) => {
22253
- if (!areAllSetterCallsInHandlers) return false;
22254
- if (!isNodeOfType(child, "CallExpression")) return;
22255
- if (!isNodeOfType(child.callee, "Identifier")) return;
22256
- if (child.callee.name !== binding.setterName) return;
22257
- didFindAnySetterCall = true;
22258
- if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
22259
- });
22260
- if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
22261
- }
22492
+ for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
22262
22493
  return handlerOnlyWriteStateNames;
22263
22494
  };
22264
22495
  const noEventTriggerState = defineRule({
@@ -22598,6 +22829,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
22598
22829
  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)-)/;
22599
22830
  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)-)/;
22600
22831
  const splitVariantScope = (token) => {
22832
+ if (!token.includes(":")) return {
22833
+ scope: "",
22834
+ utility: token.startsWith("!") ? token.slice(1) : token
22835
+ };
22601
22836
  const segments = token.split(":");
22602
22837
  const rawUtility = segments[segments.length - 1];
22603
22838
  return {
@@ -22621,6 +22856,7 @@ const noGrayOnColoredBackground = defineRule({
22621
22856
  const bgColorScopes = /* @__PURE__ */ new Set();
22622
22857
  for (const token of classStr.split(/\s+/)) {
22623
22858
  if (!token) continue;
22859
+ if (!token.includes("text-") && !token.includes("bg-")) continue;
22624
22860
  const { scope, utility } = splitVariantScope(token);
22625
22861
  if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
22626
22862
  if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
@@ -22691,11 +22927,16 @@ const NON_DETERMINISTIC_ID_GENERATOR_NAMES = new Set([
22691
22927
  "ulid",
22692
22928
  "createId"
22693
22929
  ]);
22930
+ const isZeroArgDateConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Date" && (node.arguments?.length ?? 0) === 0;
22694
22931
  const containsNonDeterministicSource = (root) => {
22695
22932
  let found = false;
22696
22933
  walkAst(root, (child) => {
22697
22934
  if (found) return false;
22698
22935
  if (isFunctionLike$1(child)) return false;
22936
+ if (isZeroArgDateConstruction(child)) {
22937
+ found = true;
22938
+ return false;
22939
+ }
22699
22940
  if (!isNodeOfType(child, "CallExpression")) return;
22700
22941
  const callee = child.callee;
22701
22942
  if (isNodeOfType(callee, "Identifier") && NON_DETERMINISTIC_ID_GENERATOR_NAMES.has(callee.name)) {
@@ -22756,6 +22997,53 @@ const argumentReadsPostMountMeasurement = (argument, effectFn, visitedLocalNames
22756
22997
  });
22757
22998
  return found;
22758
22999
  };
23000
+ const isResourceLikeInitializer = (initializer) => {
23001
+ if (isNodeOfType(initializer, "AwaitExpression")) return isResourceLikeInitializer(initializer.argument);
23002
+ return isNodeOfType(initializer, "NewExpression") || isNodeOfType(initializer, "CallExpression");
23003
+ };
23004
+ const collectArgumentSourceLocalNames = (argument, effectFn, sourceLocalNames = /* @__PURE__ */ new Set()) => {
23005
+ walkAst(argument, (child) => {
23006
+ if (!isNodeOfType(child, "Identifier")) return;
23007
+ if (sourceLocalNames.has(child.name)) return;
23008
+ const localInitializer = findEffectLocalInitializer(effectFn, child.name);
23009
+ if (!localInitializer || !isResourceLikeInitializer(localInitializer)) return;
23010
+ sourceLocalNames.add(child.name);
23011
+ collectArgumentSourceLocalNames(localInitializer, effectFn, sourceLocalNames);
23012
+ });
23013
+ return sourceLocalNames;
23014
+ };
23015
+ const isFunctionExpressionLike$1 = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression");
23016
+ const findCleanupFunction = (effectFn) => {
23017
+ if (!isNodeOfType(effectFn, "ArrowFunctionExpression") && !isNodeOfType(effectFn, "FunctionExpression")) return null;
23018
+ const body = effectFn.body;
23019
+ if (!isNodeOfType(body, "BlockStatement")) return null;
23020
+ let cleanupFunction = null;
23021
+ walkAst(body, (child) => {
23022
+ if (cleanupFunction) return false;
23023
+ if (isNodeOfType(child, "ReturnStatement")) {
23024
+ if (child.argument && isFunctionExpressionLike$1(child.argument)) cleanupFunction = child.argument;
23025
+ return false;
23026
+ }
23027
+ if (child !== body && isFunctionExpressionLike$1(child)) return false;
23028
+ if (isNodeOfType(child, "FunctionDeclaration")) return false;
23029
+ });
23030
+ return cleanupFunction;
23031
+ };
23032
+ const cleanupDisposesArgumentSource = (argument, effectFn) => {
23033
+ const cleanupFunction = findCleanupFunction(effectFn);
23034
+ if (!cleanupFunction) return false;
23035
+ const sourceLocalNames = collectArgumentSourceLocalNames(argument, effectFn);
23036
+ if (sourceLocalNames.size === 0) return false;
23037
+ let referencesSource = false;
23038
+ walkAst(cleanupFunction, (child) => {
23039
+ if (referencesSource) return false;
23040
+ if (isNodeOfType(child, "Identifier") && sourceLocalNames.has(child.name)) {
23041
+ referencesSource = true;
23042
+ return false;
23043
+ }
23044
+ });
23045
+ return referencesSource;
23046
+ };
22759
23047
  const noInitializeState = defineRule({
22760
23048
  id: "no-initialize-state",
22761
23049
  title: "State initialized from a mount effect",
@@ -22778,6 +23066,7 @@ const noInitializeState = defineRule({
22778
23066
  if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
22779
23067
  if (callExpr.arguments?.some((argument) => Boolean(argument) && containsNonDeterministicSource(argument))) continue;
22780
23068
  if (callExpr.arguments?.some((argument) => Boolean(argument) && argumentReadsPostMountMeasurement(argument, effectFn))) continue;
23069
+ if (callExpr.arguments?.some((argument) => Boolean(argument) && cleanupDisposesArgumentSource(argument, effectFn))) continue;
22781
23070
  const useStateDecl = getUseStateDecl(analysis, ref);
22782
23071
  if (!useStateDecl || !isNodeOfType(useStateDecl, "VariableDeclarator")) continue;
22783
23072
  if (!isNodeOfType(useStateDecl.id, "ArrayPattern")) continue;
@@ -23350,6 +23639,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
23350
23639
  return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
23351
23640
  });
23352
23641
  const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
23642
+ const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
23643
+ const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
23353
23644
  const noLongTransitionDuration = defineRule({
23354
23645
  id: "no-long-transition-duration",
23355
23646
  title: "Transition duration too long",
@@ -23373,11 +23664,10 @@ const noLongTransitionDuration = defineRule({
23373
23664
  if (key === "transitionDuration" || key === "animationDuration") {
23374
23665
  let longestDurationPropertyMs = 0;
23375
23666
  for (const segment of value.split(",")) {
23376
- const trimmedSegment = segment.trim();
23377
- const msMatch = trimmedSegment.match(/^([\d.]+)ms$/);
23378
- const secondsMatch = trimmedSegment.match(/^([\d.]+)s$/);
23379
- if (msMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(msMatch[1]));
23380
- else if (secondsMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(secondsMatch[1]) * 1e3);
23667
+ const durationMatch = segment.trim().match(DURATION_SEGMENT_PATTERN);
23668
+ if (!durationMatch) continue;
23669
+ const segmentDurationMs = durationMatch[2] === "ms" ? parseFloat(durationMatch[1]) : parseFloat(durationMatch[1]) * 1e3;
23670
+ longestDurationPropertyMs = Math.max(longestDurationPropertyMs, segmentDurationMs);
23381
23671
  }
23382
23672
  if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
23383
23673
  }
@@ -23385,7 +23675,7 @@ const noLongTransitionDuration = defineRule({
23385
23675
  let longestDurationMs = 0;
23386
23676
  for (const segment of value.split(",")) {
23387
23677
  if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
23388
- const firstTimeMatch = segment.match(/(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/);
23678
+ const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
23389
23679
  if (!firstTimeMatch) continue;
23390
23680
  const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
23391
23681
  longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
@@ -24557,14 +24847,14 @@ const collectRoleBranches = (expression, out) => {
24557
24847
  out.hasOpaqueBranch = true;
24558
24848
  };
24559
24849
  const buildMessage$12 = (tag) => `Keyboard & screen reader users can't trigger this \`<${tag}>\` because it isn't interactive, so use a button or link or add an interactive role.`;
24560
- const INTERACTIVE_HANDLERS = [
24850
+ const INTERACTIVE_HANDLERS_LOWER = new Set([
24561
24851
  "onClick",
24562
24852
  "onMouseDown",
24563
24853
  "onMouseUp",
24564
24854
  "onKeyDown",
24565
24855
  "onKeyPress",
24566
24856
  "onKeyUp"
24567
- ];
24857
+ ].map((handlerName) => handlerName.toLowerCase()));
24568
24858
  const noNoninteractiveElementInteractions = defineRule({
24569
24859
  id: "no-noninteractive-element-interactions",
24570
24860
  title: "Handler on non-interactive element",
@@ -24579,7 +24869,16 @@ const noNoninteractiveElementInteractions = defineRule({
24579
24869
  const tag = getElementType(node, context.settings);
24580
24870
  if (!NON_INTERACTIVE_ELEMENTS.has(tag)) return;
24581
24871
  if (tag === "label") return;
24582
- if (!INTERACTIVE_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
24872
+ let hasHandler = false;
24873
+ for (const attribute of node.attributes) {
24874
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
24875
+ const attributeName = getJsxAttributeName(attribute.name);
24876
+ if (attributeName && INTERACTIVE_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
24877
+ hasHandler = true;
24878
+ break;
24879
+ }
24880
+ }
24881
+ if (!hasHandler) return;
24583
24882
  if (isHiddenFromScreenReader(node, context.settings)) return;
24584
24883
  const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
24585
24884
  if (roleAttr) {
@@ -24685,6 +24984,18 @@ const KEYBOARD_HANDLER_PROP_NAMES = [
24685
24984
  "onKeyPress"
24686
24985
  ];
24687
24986
  const isKeyboardOperable = (node) => KEYBOARD_HANDLER_PROP_NAMES.some((propName) => Boolean(hasJsxPropIgnoreCase(node.attributes, propName)));
24987
+ const parseNumericBranch = (expression) => {
24988
+ if (isNodeOfType(expression, "Literal") && typeof expression.value === "number") return expression.value;
24989
+ if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-" && isNodeOfType(expression.argument, "Literal") && typeof expression.argument.value === "number") return -expression.argument.value;
24990
+ return null;
24991
+ };
24992
+ const isRovingTabindexValue = (value) => {
24993
+ if (!isNodeOfType(value, "JSXExpressionContainer")) return false;
24994
+ const expression = value.expression;
24995
+ if (!isNodeOfType(expression, "ConditionalExpression")) return false;
24996
+ if (isNodeOfType(expression.test, "Literal")) return false;
24997
+ return [parseNumericBranch(expression.consequent), parseNumericBranch(expression.alternate)].some((branchValue) => branchValue !== null && branchValue < 0);
24998
+ };
24688
24999
  const resolveSettings$14 = (settings) => {
24689
25000
  const reactDoctor = settings?.["react-doctor"];
24690
25001
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -24708,6 +25019,7 @@ const noNoninteractiveTabindex = defineRule({
24708
25019
  if (!tabIndex) return;
24709
25020
  const tabIndexValue = tabIndex.value;
24710
25021
  if (!tabIndexValue) return;
25022
+ if (isRovingTabindexValue(tabIndexValue)) return;
24711
25023
  const numeric = parseJsxValue(tabIndexValue);
24712
25024
  if (numeric === null) {
24713
25025
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues && !isKeyboardOperable(node)) context.report({
@@ -25892,16 +26204,22 @@ const ELEMENT_ROLE_PAIRS = [
25892
26204
  ["tr", "row"],
25893
26205
  ["ul", "list"]
25894
26206
  ];
25895
- const getElementImplicitRoles = (tag) => {
25896
- const out = [];
25897
- for (const [element, role] of ELEMENT_ROLE_PAIRS) if (element === tag && !out.includes(role)) out.push(role);
25898
- return out;
25899
- };
25900
- const getTagsForRole = (role) => {
25901
- const out = [];
25902
- for (const [element, r] of ELEMENT_ROLE_PAIRS) if (r === role && !out.includes(element)) out.push(element);
25903
- return out;
26207
+ const EMPTY_ROLE_LIST = [];
26208
+ const buildLookup = (pairs, keyIndex) => {
26209
+ const lookup = /* @__PURE__ */ new Map();
26210
+ for (const pair of pairs) {
26211
+ const key = pair[keyIndex];
26212
+ const value = pair[keyIndex === 0 ? 1 : 0];
26213
+ const values = lookup.get(key);
26214
+ if (!values) lookup.set(key, [value]);
26215
+ else if (!values.includes(value)) values.push(value);
26216
+ }
26217
+ return lookup;
25904
26218
  };
26219
+ const IMPLICIT_ROLES_BY_TAG = buildLookup(ELEMENT_ROLE_PAIRS, 0);
26220
+ const TAGS_BY_ROLE = buildLookup(ELEMENT_ROLE_PAIRS, 1);
26221
+ const getElementImplicitRoles = (tag) => IMPLICIT_ROLES_BY_TAG.get(tag) ?? EMPTY_ROLE_LIST;
26222
+ const getTagsForRole = (role) => TAGS_BY_ROLE.get(role) ?? EMPTY_ROLE_LIST;
25905
26223
  //#endregion
25906
26224
  //#region src/plugin/utils/get-implicit-role.ts
25907
26225
  const getImplicitRole = (node, elementType) => {
@@ -26138,22 +26456,22 @@ const noRenderInRender = defineRule({
26138
26456
  title: "Component rendered by inline function call",
26139
26457
  severity: "warn",
26140
26458
  tags: ["test-noise"],
26141
- recommendation: "Make it a named component so React preserves its identity and does not remount its state.",
26459
+ recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
26142
26460
  create: (context) => ({ JSXExpressionContainer(node) {
26143
26461
  const expression = node.expression;
26144
26462
  if (!isNodeOfType(expression, "CallExpression")) return;
26145
26463
  let calleeName = null;
26464
+ if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
26465
+ else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
26466
+ if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
26146
26467
  if (isNodeOfType(expression.callee, "Identifier")) {
26147
26468
  if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
26148
- calleeName = expression.callee.name;
26149
- } else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
26469
+ } else if (isNodeOfType(expression.callee, "MemberExpression")) {
26150
26470
  if (rootsInProps(expression.callee.object, context.scopes)) return;
26151
- calleeName = expression.callee.property.name;
26152
26471
  }
26153
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
26154
26472
  context.report({
26155
26473
  node: expression,
26156
- message: `Your users lose state because "${calleeName}()" builds UI from an inline call that React remounts, so pull it into its own component instead.`
26474
+ message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
26157
26475
  });
26158
26476
  } })
26159
26477
  });
@@ -26256,8 +26574,8 @@ const countUseStates = (analysis, componentNode) => {
26256
26574
  for (const ref of getDownstreamRefs(analysis, componentNode)) if (isState(analysis, ref)) stateVariables.add(ref.resolved);
26257
26575
  return stateVariables.size;
26258
26576
  };
26259
- const findPropUsedToResetAllState = (analysis, effectFnRefs, depsRefs, useEffectNode) => {
26260
- const stateSetterRefs = effectFnRefs.filter((ref) => isStateSetterCall(analysis, ref));
26577
+ const findPropUsedToResetAllState = (analysis, effectFnRefs, depsRefs, useEffectNode, effectFn) => {
26578
+ const stateSetterRefs = effectFnRefs.filter((ref) => isSyncStateSetterCall(analysis, ref, effectFn));
26261
26579
  if (stateSetterRefs.length === 0) return null;
26262
26580
  if (!stateSetterRefs.every((ref) => isSetStateToInitialValue(analysis, ref))) return null;
26263
26581
  const containing = findContainingNode(analysis, useEffectNode);
@@ -26280,7 +26598,9 @@ const noResetAllStateOnPropChange = defineRule({
26280
26598
  if (!effectFnRefs || !depsRefs) return;
26281
26599
  const containing = findContainingNode(analysis, node);
26282
26600
  if (containing && isCustomHook(containing)) return;
26283
- if (!findPropUsedToResetAllState(analysis, effectFnRefs, depsRefs, node)) return;
26601
+ const effectFn = getEffectFn(analysis, node);
26602
+ if (!effectFn) return;
26603
+ if (!findPropUsedToResetAllState(analysis, effectFnRefs, depsRefs, node, effectFn)) return;
26284
26604
  context.report({
26285
26605
  node,
26286
26606
  message: `Your users briefly see stale state when a prop changes because this useEffect clears all state.`
@@ -26401,6 +26721,7 @@ const TANSTACK_ROUTE_PROPERTY_ORDER = [
26401
26721
  "headers",
26402
26722
  "remountDeps"
26403
26723
  ];
26724
+ const TANSTACK_ROUTE_PROPERTY_INDEX = new Map(TANSTACK_ROUTE_PROPERTY_ORDER.map((propertyName, orderIndex) => [propertyName, orderIndex]));
26404
26725
  const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
26405
26726
  "createFileRoute",
26406
26727
  "createRoute",
@@ -26416,6 +26737,7 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
26416
26737
  "server",
26417
26738
  "handler"
26418
26739
  ];
26740
+ const TANSTACK_MIDDLEWARE_METHOD_INDEX = new Map(TANSTACK_MIDDLEWARE_METHOD_ORDER.map((methodName, orderIndex) => [methodName, orderIndex]));
26419
26741
  const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
26420
26742
  const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
26421
26743
  const TANSTACK_QUERY_HOOKS = new Set([
@@ -27120,14 +27442,21 @@ const noStaticElementInteractions = defineRule({
27120
27442
  category: "Accessibility",
27121
27443
  create: (context) => {
27122
27444
  const settings = resolveSettings$12(context.settings);
27445
+ const handlersLower = new Set(settings.handlers.map((handlerName) => handlerName.toLowerCase()));
27123
27446
  const isTestlikeFile = isTestlikeFilename(context.filename);
27124
27447
  return { JSXOpeningElement(node) {
27125
27448
  if (isTestlikeFile) return;
27126
27449
  let hasNonBlockerHandler = false;
27127
27450
  let hasAnyHandler = false;
27128
- for (const handler of settings.handlers) {
27129
- const attribute = hasJsxPropIgnoreCase(node.attributes, handler);
27130
- if (!attribute) continue;
27451
+ let seenHandlerNames = null;
27452
+ for (const attribute of node.attributes) {
27453
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
27454
+ const attributeName = getJsxAttributeName(attribute.name);
27455
+ if (!attributeName) continue;
27456
+ const handlerNameLower = attributeName.toLowerCase();
27457
+ if (!handlersLower.has(handlerNameLower)) continue;
27458
+ if (seenHandlerNames?.has(handlerNameLower)) continue;
27459
+ (seenHandlerNames ??= /* @__PURE__ */ new Set()).add(handlerNameLower);
27131
27460
  if (isNullValue(attribute)) continue;
27132
27461
  hasAnyHandler = true;
27133
27462
  if (!isPureEventBlockerHandler(attribute)) {
@@ -27139,6 +27468,7 @@ const noStaticElementInteractions = defineRule({
27139
27468
  if (!hasNonBlockerHandler) return;
27140
27469
  const elementType = getElementType(node, context.settings);
27141
27470
  if (!HTML_TAGS.has(elementType)) return;
27471
+ if (elementType === "svg") return;
27142
27472
  if (isHiddenFromScreenReader(node, context.settings)) return;
27143
27473
  if (isPresentationRole(node)) return;
27144
27474
  if (isInteractiveElement(elementType, node)) return;
@@ -27152,7 +27482,8 @@ const noStaticElementInteractions = defineRule({
27152
27482
  });
27153
27483
  return;
27154
27484
  }
27155
- const attributeValue = roleAttribute.value;
27485
+ let attributeValue = roleAttribute.value;
27486
+ if (isNodeOfType(attributeValue, "JSXExpressionContainer") && isNodeOfType(attributeValue.expression, "Literal") && typeof attributeValue.expression.value === "string") attributeValue = attributeValue.expression;
27156
27487
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
27157
27488
  const firstRole = attributeValue.value.toLowerCase().trim().split(/\s+/)[0];
27158
27489
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
@@ -28308,13 +28639,13 @@ const DOM_ATTRIBUTES_TO_CAMEL = new Map([
28308
28639
  ["xml:lang", "xmlLang"],
28309
28640
  ["xml:space", "xmlSpace"]
28310
28641
  ]);
28311
- const DOM_PROPERTIES_IGNORE_CASE = [
28642
+ const DOM_PROPERTIES_IGNORE_CASE_BY_LOWER = new Map([
28312
28643
  "charset",
28313
28644
  "allowFullScreen",
28314
28645
  "webkitAllowFullScreen",
28315
28646
  "mozAllowFullScreen",
28316
28647
  "webkitDirectory"
28317
- ];
28648
+ ].map((name) => [name.toLowerCase(), name]));
28318
28649
  //#endregion
28319
28650
  //#region src/plugin/constants/dom-property-tags.ts
28320
28651
  const DOM_PROPERTY_TO_ALLOWED_TAGS = new Map([
@@ -28518,10 +28849,7 @@ const matchesHtmlTagConventions = (tagName) => {
28518
28849
  if (!(firstCharacter >= 97 && firstCharacter <= 122)) return false;
28519
28850
  return !tagName.includes("-");
28520
28851
  };
28521
- const normalizeAttributeCase = (name) => {
28522
- for (const ignoreCaseName of DOM_PROPERTIES_IGNORE_CASE) if (ignoreCaseName.toLowerCase() === name.toLowerCase()) return ignoreCaseName;
28523
- return name;
28524
- };
28852
+ const normalizeAttributeCase = (name) => DOM_PROPERTIES_IGNORE_CASE_BY_LOWER.get(name.toLowerCase()) ?? name;
28525
28853
  const hasUppercaseChar = (input) => /[A-Z]/.test(input);
28526
28854
  const INVALID_PROP_ON_TAG = (propName, allowedTags) => `React ignores \`${propName}\` here because it only works on these tags: ${allowedTags}.`;
28527
28855
  const DATA_LOWERCASE_REQUIRED = () => `React drops this \`data-*\` prop because of its capital letters.`;
@@ -28865,32 +29193,26 @@ const isFirstArgumentOfHocCall = (node) => {
28865
29193
  if (!isHocCallee$1(parent)) return false;
28866
29194
  return parent.arguments[0] === node;
28867
29195
  };
29196
+ const MAP_LIKE_METHOD_NAMES = new Set([
29197
+ "map",
29198
+ "forEach",
29199
+ "filter",
29200
+ "flatMap",
29201
+ "reduce",
29202
+ "reduceRight"
29203
+ ]);
28868
29204
  const isReturnOfMapCallback = (node) => {
28869
29205
  const parent = node.parent;
28870
29206
  if (!parent) return false;
28871
29207
  if (isNodeOfType(parent, "CallExpression")) {
28872
29208
  const callee = parent.callee;
28873
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return [
28874
- "map",
28875
- "forEach",
28876
- "filter",
28877
- "flatMap",
28878
- "reduce",
28879
- "reduceRight"
28880
- ].includes(callee.property.name);
29209
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28881
29210
  }
28882
29211
  if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
28883
29212
  const callbackParent = parent.parent;
28884
29213
  if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
28885
29214
  const callee = callbackParent.callee;
28886
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return [
28887
- "map",
28888
- "forEach",
28889
- "filter",
28890
- "flatMap",
28891
- "reduce",
28892
- "reduceRight"
28893
- ].includes(callee.property.name);
29215
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28894
29216
  }
28895
29217
  }
28896
29218
  return false;
@@ -28926,10 +29248,10 @@ const isRenderFlowingReadReference = (identifier) => {
28926
29248
  valueNode = parent;
28927
29249
  parent = parent.parent;
28928
29250
  continue;
28929
- case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isUppercaseName(parent.id.name);
29251
+ case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isReactComponentName(parent.id.name);
28930
29252
  case "AssignmentExpression": {
28931
29253
  const assignmentTarget = parent.left;
28932
- return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isUppercaseName(assignmentTarget.name);
29254
+ return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
28933
29255
  }
28934
29256
  case "Property":
28935
29257
  if (parent.value !== valueNode) return false;
@@ -29022,14 +29344,14 @@ const noUnstableNestedComponents = defineRule({
29022
29344
  };
29023
29345
  return {
29024
29346
  JSXOpeningElement(node) {
29025
- if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) {
29347
+ if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
29026
29348
  recordInstantiation(node.name, node.name.name);
29027
29349
  return;
29028
29350
  }
29029
29351
  if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
29030
29352
  },
29031
29353
  Identifier(node) {
29032
- if (!isUppercaseName(node.name)) return;
29354
+ if (!isReactComponentName(node.name)) return;
29033
29355
  if (!isRenderFlowingReadReference(node)) return;
29034
29356
  recordInstantiation(node, node.name);
29035
29357
  },
@@ -29960,7 +30282,7 @@ const containsVnodeFactoryCall = (root) => {
29960
30282
  };
29961
30283
  const isComponentLikeFunction = (functionNode) => {
29962
30284
  const bindingName = getFunctionBindingName(functionNode);
29963
- if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN.test(bindingName))) return true;
30285
+ if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN$1.test(bindingName))) return true;
29964
30286
  const body = "body" in functionNode ? functionNode.body : null;
29965
30287
  if (!body || !isAstNode(body)) return false;
29966
30288
  return containsJsxElement(body) || containsVnodeFactoryCall(body);
@@ -30839,13 +31161,13 @@ const preferStableEmptyFallback = defineRule({
30839
31161
  memoRegistry = buildSameFileMemoRegistry(node);
30840
31162
  },
30841
31163
  JSXAttribute(node) {
30842
- if (!isInsideFunctionScope(node)) return;
30843
- if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30844
31164
  if (!node.value) return;
30845
31165
  if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
30846
31166
  const innerExpression = node.value.expression;
30847
31167
  if (!innerExpression) return;
30848
31168
  if (innerExpression.type === "JSXEmptyExpression") return;
31169
+ if (!isInsideFunctionScope(node)) return;
31170
+ if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30849
31171
  const parentJsxOpening = node.parent;
30850
31172
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
30851
31173
  if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
@@ -31417,7 +31739,7 @@ const queryMutationMissingInvalidation = defineRule({
31417
31739
  });
31418
31740
  if (!hasCacheUpdate) context.report({
31419
31741
  node,
31420
- message: "useMutation with no cache update leaves your users looking at stale data after it runs."
31742
+ message: "useMutation with no cache update here can leave your users looking at stale data after it runs."
31421
31743
  });
31422
31744
  } })
31423
31745
  });
@@ -32066,6 +32388,7 @@ const renderingHydrationMismatchTime = defineRule({
32066
32388
  return { JSXExpressionContainer(node) {
32067
32389
  if (isTestlikeFile) return;
32068
32390
  if (!node.expression) return;
32391
+ if (isGeneratedImageRenderContext(context, findOpeningElementOfChild(node) ?? node)) return;
32069
32392
  const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
32070
32393
  if (matched) {
32071
32394
  if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
@@ -32217,41 +32540,11 @@ const callsIdentifier = (root, identifierName) => {
32217
32540
  });
32218
32541
  return found;
32219
32542
  };
32220
- const setterIsCalledInAsyncContext = (componentBody, setterName) => {
32221
- if (!componentBody) return false;
32222
- let found = false;
32223
- walkAst(componentBody, (child) => {
32224
- if (found) return;
32225
- if (!isFunctionLike$1(child)) return;
32226
- const functionBody = child.body;
32227
- if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
32228
- if (callsIdentifier(functionBody, setterName)) found = true;
32229
- });
32230
- return found;
32231
- };
32232
32543
  const PROMISE_CHAIN_METHOD_NAMES = new Set([
32233
32544
  "then",
32234
32545
  "catch",
32235
32546
  "finally"
32236
32547
  ]);
32237
- const setterIsCalledInPromiseChain = (componentBody, setterName) => {
32238
- if (!componentBody) return false;
32239
- let found = false;
32240
- walkAst(componentBody, (child) => {
32241
- if (found) return;
32242
- if (!isNodeOfType(child, "CallExpression")) return;
32243
- const callee = child.callee;
32244
- if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
32245
- for (const argument of child.arguments ?? []) {
32246
- if (!isFunctionLike$1(argument)) continue;
32247
- if (callsIdentifier(argument.body, setterName)) {
32248
- found = true;
32249
- return;
32250
- }
32251
- }
32252
- });
32253
- return found;
32254
- };
32255
32548
  const ASYNC_DATA_CALLEE_NAMES = new Set([
32256
32549
  "useApolloClient",
32257
32550
  "useMutation",
@@ -32264,20 +32557,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
32264
32557
  "fetch",
32265
32558
  "axios"
32266
32559
  ]);
32267
- const referencesAsyncDataApi = (body) => {
32268
- if (!body) return false;
32560
+ const hasAsyncLoadingWork = (fnBody, setterName) => {
32269
32561
  let found = false;
32270
- walkAst(body, (child) => {
32271
- if (found) return;
32562
+ walkAst(fnBody, (child) => {
32563
+ if (found) return false;
32272
32564
  if (isNodeOfType(child, "CallExpression")) {
32273
32565
  const callee = child.callee;
32274
32566
  if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
32275
32567
  found = true;
32276
- return;
32568
+ return false;
32277
32569
  }
32278
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
32570
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
32571
+ if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
32572
+ found = true;
32573
+ return false;
32574
+ }
32575
+ if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
32576
+ if (!isFunctionLike$1(argument)) continue;
32577
+ if (callsIdentifier(argument.body, setterName)) {
32578
+ found = true;
32579
+ return false;
32580
+ }
32581
+ }
32582
+ }
32583
+ return;
32584
+ }
32585
+ if (setterName !== null && isFunctionLike$1(child)) {
32586
+ const functionBody = child.body;
32587
+ if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
32279
32588
  found = true;
32280
- return;
32589
+ return false;
32281
32590
  }
32282
32591
  }
32283
32592
  });
@@ -32302,7 +32611,7 @@ const renderingUsetransitionLoading = defineRule({
32302
32611
  const secondBinding = node.id.elements[1];
32303
32612
  const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
32304
32613
  const fnBody = enclosingFunctionBody(node);
32305
- if (fnBody && (setterName && setterIsCalledInAsyncContext(fnBody, setterName) || setterName && setterIsCalledInPromiseChain(fnBody, setterName) || referencesAsyncDataApi(fnBody))) return;
32614
+ if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
32306
32615
  context.report({
32307
32616
  node: node.init,
32308
32617
  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`
@@ -33096,17 +33405,17 @@ const rerenderStateOnlyInHandlers = defineRule({
33096
33405
  const effectTriggerNames = /* @__PURE__ */ new Set();
33097
33406
  for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
33098
33407
  for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
33408
+ const setterNames = new Set(bindings.map((binding) => binding.setterName));
33409
+ const calledSetterNames = /* @__PURE__ */ new Set();
33410
+ walkAst(componentBody, (child) => {
33411
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
33412
+ });
33099
33413
  for (const binding of bindings) {
33100
33414
  if (renderReachableNames.has(binding.valueName)) continue;
33101
33415
  if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
33102
33416
  const setterSuffix = binding.setterName.slice(3);
33103
33417
  if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
33104
- let setterCalled = false;
33105
- walkAst(componentBody, (child) => {
33106
- if (setterCalled) return;
33107
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
33108
- });
33109
- if (!setterCalled) continue;
33418
+ if (!calledSetterNames.has(binding.setterName)) continue;
33110
33419
  if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
33111
33420
  context.report({
33112
33421
  node: binding.declarator,
@@ -33438,6 +33747,7 @@ const RECYCLABLE_LIST_PACKAGES = {
33438
33747
  FlashList: ["@shopify/flash-list"],
33439
33748
  LegendList: ["@legendapp/list"]
33440
33749
  };
33750
+ const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
33441
33751
  const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
33442
33752
  const RENDER_ITEM_PROP_NAMES = new Set([
33443
33753
  "renderItem",
@@ -33680,33 +33990,42 @@ const rnListMissingEstimatedItemSize = defineRule({
33680
33990
  requires: ["react-native"],
33681
33991
  severity: "warn",
33682
33992
  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.",
33683
- create: (context) => ({ JSXOpeningElement(node) {
33684
- const localElementName = resolveJsxElementName(node);
33685
- if (!localElementName) return;
33686
- const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
33687
- if (canonicalRecyclerName === null) return;
33688
- if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
33689
- let hasSizingHint = false;
33690
- let dataIsEmptyLiteral = false;
33691
- let hasDataProp = false;
33692
- for (const attribute of node.attributes ?? []) {
33693
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
33694
- if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
33695
- const attributeName = attribute.name.name;
33696
- if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
33697
- if (attributeName === "data") {
33698
- hasDataProp = true;
33699
- if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
33993
+ create: (context) => {
33994
+ let fileImportsRecycler = false;
33995
+ return {
33996
+ Program(node) {
33997
+ fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
33998
+ },
33999
+ JSXOpeningElement(node) {
34000
+ if (!fileImportsRecycler) return;
34001
+ const localElementName = resolveJsxElementName(node);
34002
+ if (!localElementName) return;
34003
+ const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
34004
+ if (canonicalRecyclerName === null) return;
34005
+ if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
34006
+ let hasSizingHint = false;
34007
+ let dataIsEmptyLiteral = false;
34008
+ let hasDataProp = false;
34009
+ for (const attribute of node.attributes ?? []) {
34010
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
34011
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
34012
+ const attributeName = attribute.name.name;
34013
+ if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
34014
+ if (attributeName === "data") {
34015
+ hasDataProp = true;
34016
+ if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
34017
+ }
34018
+ }
34019
+ if (hasSizingHint) return;
34020
+ if (dataIsEmptyLiteral) return;
34021
+ if (!hasDataProp) return;
34022
+ context.report({
34023
+ node,
34024
+ message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
34025
+ });
33700
34026
  }
33701
- }
33702
- if (hasSizingHint) return;
33703
- if (dataIsEmptyLiteral) return;
33704
- if (!hasDataProp) return;
33705
- context.report({
33706
- node,
33707
- message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
33708
- });
33709
- } })
34027
+ };
34028
+ }
33710
34029
  });
33711
34030
  //#endregion
33712
34031
  //#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
@@ -33717,25 +34036,34 @@ const rnListRecyclableWithoutTypes = defineRule({
33717
34036
  requires: ["react-native"],
33718
34037
  severity: "warn",
33719
34038
  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.",
33720
- create: (context) => ({ JSXOpeningElement(node) {
33721
- const elementName = resolveJsxElementName(node);
33722
- if (!elementName) return;
33723
- if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
33724
- let hasRecycleItemsEnabled = false;
33725
- let hasGetItemType = false;
33726
- for (const attr of node.attributes ?? []) {
33727
- if (!isNodeOfType(attr, "JSXAttribute")) continue;
33728
- if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
33729
- if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
33730
- else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
33731
- else hasRecycleItemsEnabled = true;
33732
- if (attr.name.name === "getItemType") hasGetItemType = true;
33733
- }
33734
- if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
33735
- node,
33736
- message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
33737
- });
33738
- } })
34039
+ create: (context) => {
34040
+ let fileImportsRecycler = false;
34041
+ return {
34042
+ Program(node) {
34043
+ fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
34044
+ },
34045
+ JSXOpeningElement(node) {
34046
+ if (!fileImportsRecycler) return;
34047
+ const elementName = resolveJsxElementName(node);
34048
+ if (!elementName) return;
34049
+ if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
34050
+ let hasRecycleItemsEnabled = false;
34051
+ let hasGetItemType = false;
34052
+ for (const attr of node.attributes ?? []) {
34053
+ if (!isNodeOfType(attr, "JSXAttribute")) continue;
34054
+ if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
34055
+ if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
34056
+ else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
34057
+ else hasRecycleItemsEnabled = true;
34058
+ if (attr.name.name === "getItemType") hasGetItemType = true;
34059
+ }
34060
+ if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
34061
+ node,
34062
+ message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
34063
+ });
34064
+ }
34065
+ };
34066
+ }
33739
34067
  });
33740
34068
  //#endregion
33741
34069
  //#region src/plugin/rules/react-native/rn-no-deep-imports.ts
@@ -33876,7 +34204,7 @@ const rnNoDimensionsGet = defineRule({
33876
34204
  if (binding !== null && !isBindingReactNativeDimensions(node, binding)) return;
33877
34205
  if (isMemberProperty(node.callee, "get")) context.report({
33878
34206
  node,
33879
- message: "Your users see a stale layout on rotation or resize because Dimensions.get() does not update."
34207
+ message: "Dimensions.get() reads the size once and never updates, so layouts built from it go stale on rotation or resize."
33880
34208
  });
33881
34209
  if (isMemberProperty(node.callee, "addEventListener")) context.report({
33882
34210
  node,
@@ -34698,7 +35026,10 @@ const isExpoUiComponentElement = (openingElement, contextNode, componentName) =>
34698
35026
  };
34699
35027
  //#endregion
34700
35028
  //#region src/plugin/rules/react-native/rn-no-raw-text.ts
34701
- const truncateText = (text) => text.length > 30 ? `${text.slice(0, 30)}...` : text;
35029
+ const truncateText = (text) => {
35030
+ const collapsedText = text.replace(/\s+/g, " ");
35031
+ return collapsedText.length > 30 ? `${collapsedText.slice(0, 30)}...` : collapsedText;
35032
+ };
34702
35033
  const isRawTextContent = (child) => {
34703
35034
  if (isNodeOfType(child, "JSXText")) return Boolean(child.value?.trim());
34704
35035
  if (!isNodeOfType(child, "JSXExpressionContainer") || !child.expression) return false;
@@ -34719,9 +35050,10 @@ const resolveTextBoundaryName = (openingElement) => {
34719
35050
  if (isNodeOfType(openingElement.name, "JSXNamespacedName")) return openingElement.name.namespace.name;
34720
35051
  return resolveJsxElementName(openingElement);
34721
35052
  };
35053
+ const TEXT_COMPONENT_KEYWORDS = [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS];
34722
35054
  const isTextHandlingComponent = (elementName) => {
34723
35055
  if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
34724
- return [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS].some((keyword) => elementName.includes(keyword));
35056
+ return TEXT_COMPONENT_KEYWORDS.some((keyword) => elementName.includes(keyword));
34725
35057
  };
34726
35058
  const isTransparentTextWrapper = (elementName) => elementName !== null && REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS.has(elementName);
34727
35059
  const isInsideTextHandlingComponent = (node) => {
@@ -34765,6 +35097,7 @@ const rnNoRawText = defineRule({
34765
35097
  return {
34766
35098
  Program(programNode) {
34767
35099
  isDomComponentFile = hasDirective(programNode, "use dom");
35100
+ if (!containsJsxElement(programNode)) return;
34768
35101
  const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
34769
35102
  autoDetectedTextWrappers = childrenForwarding.textWrappers;
34770
35103
  autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
@@ -38719,14 +39052,7 @@ const roleSupportsAriaProps = defineRule({
38719
39052
  recommendation: "Only use `aria-*` attributes that the element's role supports.",
38720
39053
  category: "Accessibility",
38721
39054
  create: (context) => ({ JSXOpeningElement(node) {
38722
- const elementType = getElementType(node, context.settings);
38723
- const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
38724
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
38725
- if (!role) return;
38726
- if (!VALID_ARIA_ROLES.has(role)) return;
38727
- const isImplicit = !roleAttribute;
38728
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
38729
- if (!supported) return;
39055
+ let ariaAttributes = null;
38730
39056
  for (const attribute of node.attributes) {
38731
39057
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
38732
39058
  const attributeNode = attribute;
@@ -38736,6 +39062,21 @@ const roleSupportsAriaProps = defineRule({
38736
39062
  const propName = propRawName.toLowerCase();
38737
39063
  if (!propName.startsWith("aria-")) continue;
38738
39064
  if (!ARIA_PROPERTIES.has(propName)) continue;
39065
+ (ariaAttributes ??= []).push({
39066
+ attribute,
39067
+ propName
39068
+ });
39069
+ }
39070
+ if (!ariaAttributes) return;
39071
+ const elementType = getElementType(node, context.settings);
39072
+ const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
39073
+ const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
39074
+ if (!role) return;
39075
+ if (!VALID_ARIA_ROLES.has(role)) return;
39076
+ const isImplicit = !roleAttribute;
39077
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
39078
+ if (!supported) return;
39079
+ for (const { attribute, propName } of ariaAttributes) {
38739
39080
  if (supported.has(propName)) continue;
38740
39081
  context.report({
38741
39082
  node: attribute,
@@ -38957,7 +39298,7 @@ const isInsideClassComponent = (node) => {
38957
39298
  const hasShortCircuitAncestor = (descendant, ancestor) => {
38958
39299
  let current = descendant.parent;
38959
39300
  while (current && current !== ancestor) {
38960
- if (isNodeOfType(current, "ConditionalExpression")) return true;
39301
+ if (isNodeOfType(current, "ConditionalExpression") && (isWithinRange(descendant, current.consequent) || isWithinRange(descendant, current.alternate))) return true;
38961
39302
  if (isNodeOfType(current, "LogicalExpression") && (current.operator === "&&" || current.operator === "||" || current.operator === "??") && isWithinRange(descendant, current.right)) return true;
38962
39303
  current = current.parent ?? null;
38963
39304
  }
@@ -39083,6 +39424,7 @@ const rulesOfHooks = defineRule({
39083
39424
  if (parentInfo.isComponentOrHook) isInsideComponentOrHook = true;
39084
39425
  }
39085
39426
  if (!isInsideComponentOrHook) {
39427
+ if (!enclosing.hasResolvedName) return;
39086
39428
  if (isLocalNonHookFunctionCallee(node, context.scopes, settings)) return;
39087
39429
  context.report({
39088
39430
  node: node.callee,
@@ -39573,6 +39915,7 @@ const serverCacheWithObjectLiteral = defineRule({
39573
39915
  cachedFunctionNames.add(node.id.name);
39574
39916
  },
39575
39917
  CallExpression(node) {
39918
+ if (cachedFunctionNames.size === 0) return;
39576
39919
  if (!isNodeOfType(node.callee, "Identifier")) return;
39577
39920
  if (!cachedFunctionNames.has(node.callee.name)) return;
39578
39921
  const firstArg = node.arguments?.[0];
@@ -39656,6 +39999,14 @@ const objectExpressionHasCachingConfig = (objectExpression) => (objectExpression
39656
39999
  const objectExpressionHasSpread = (objectExpression) => (objectExpression.properties ?? []).some((property) => isNodeOfType(property, "SpreadElement"));
39657
40000
  const APP_ROUTER_FILE_PATTERN = new RegExp(`/app/(?:[^/]+/)*(?:route|page|layout|template|loading|error|default)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
39658
40001
  const NON_PROJECT_PATH_PATTERN = /\/(?:node_modules|dist|build|\.next)\//;
40002
+ const REMIX_IMPORT_SOURCE_PATTERN = /^(?:@remix-run\/|@react-router\/|react-router(?:-dom)?$)/;
40003
+ const programImportsRemixRouter = (programNode) => (programNode.body ?? []).some((statement) => isNodeOfType(statement, "ImportDeclaration") && !isTypeOnlyImport(statement) && typeof statement.source?.value === "string" && REMIX_IMPORT_SOURCE_PATTERN.test(statement.source.value));
40004
+ const isImportMetaUrlAssetArgument = (urlArg) => {
40005
+ if (!isNodeOfType(urlArg, "NewExpression")) return false;
40006
+ if (!isNodeOfType(urlArg.callee, "Identifier") || urlArg.callee.name !== "URL") return false;
40007
+ const baseArg = urlArg.arguments?.[1];
40008
+ return isNodeOfType(baseArg, "MemberExpression") && isNodeOfType(baseArg.object, "MetaProperty") && isNodeOfType(baseArg.property, "Identifier") && baseArg.property.name === "url";
40009
+ };
39659
40010
  const serverFetchWithoutRevalidate = defineRule({
39660
40011
  id: "server-fetch-without-revalidate",
39661
40012
  title: "Fetch without revalidate",
@@ -39675,7 +40026,7 @@ const serverFetchWithoutRevalidate = defineRule({
39675
40026
  isServerSideFile = false;
39676
40027
  return;
39677
40028
  }
39678
- isServerSideFile = !(node.body ?? []).some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use client");
40029
+ isServerSideFile = !(node.body ?? []).some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use client") && !programImportsRemixRouter(node);
39679
40030
  },
39680
40031
  CallExpression(node) {
39681
40032
  if (!isServerSideFile) return;
@@ -39688,6 +40039,7 @@ const serverFetchWithoutRevalidate = defineRule({
39688
40039
  if (objectExpressionHasSpread(optionsArg)) return;
39689
40040
  }
39690
40041
  const urlArg = node.arguments?.[0];
40042
+ if (isImportMetaUrlAssetArgument(urlArg)) return;
39691
40043
  const urlText = isNodeOfType(urlArg, "Literal") && typeof urlArg.value === "string" ? `"${urlArg.value}"` : "url";
39692
40044
  context.report({
39693
40045
  node,
@@ -39916,7 +40268,7 @@ const serverNoMutableModuleState = defineRule({
39916
40268
  if (node.kind === "let" || node.kind === "var") {
39917
40269
  context.report({
39918
40270
  node: declarator,
39919
- message: `Module-scoped ${node.kind} "${variableName}" leaks state between your users, since every request shares it.`
40271
+ message: `Module-scoped ${node.kind} "${variableName}" is shared by every request, so any write to it leaks state between your users.`
39920
40272
  });
39921
40273
  continue;
39922
40274
  }
@@ -39977,25 +40329,29 @@ const GATE_LEADING_VERBS = new Set([
39977
40329
  "authorize",
39978
40330
  "authenticate"
39979
40331
  ]);
39980
- const isStartedPromiseBinding = (name, statements, beforeIndex) => {
39981
- for (let index = 0; index < beforeIndex; index++) {
39982
- const statement = statements[index];
39983
- if (!isNodeOfType(statement, "VariableDeclaration")) continue;
39984
- for (const declarator of statement.declarations ?? []) {
39985
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
39986
- if (declarator.id.name !== name) continue;
39987
- if (declarator.init && !isNodeOfType(declarator.init, "AwaitExpression")) return true;
39988
- }
40332
+ const declarationAwaitsExistingPromise = (declaration) => {
40333
+ if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
40334
+ for (const declarator of declaration.declarations ?? []) {
40335
+ const init = declarator.init;
40336
+ if (!isNodeOfType(init, "AwaitExpression")) continue;
40337
+ const argument = init.argument;
40338
+ if (isNodeOfType(argument, "Identifier") || isNodeOfType(argument, "MemberExpression")) return true;
39989
40339
  }
39990
40340
  return false;
39991
40341
  };
39992
- const declarationAwaitsStartedPromise = (declaration, statements, declarationIndex) => {
40342
+ const REQUEST_SCOPED_IMPORT_SOURCES = ["next/headers", "next-intl/server"];
40343
+ const isRequestScopedCallee = (callee) => {
40344
+ if (!isNodeOfType(callee, "Identifier")) return false;
40345
+ if (REQUEST_SCOPED_IMPORT_SOURCES.some((moduleSource) => getImportedNameFromModule(callee, callee.name, moduleSource) !== null)) return true;
40346
+ return getImportedNameFromModule(callee, callee.name, "next/server") === "connection";
40347
+ };
40348
+ const declarationAwaitsRequestScopedCall = (declaration) => {
39993
40349
  if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
39994
40350
  for (const declarator of declaration.declarations ?? []) {
39995
40351
  const init = declarator.init;
39996
40352
  if (!isNodeOfType(init, "AwaitExpression")) continue;
39997
40353
  const argument = init.argument;
39998
- if (isNodeOfType(argument, "Identifier") && isStartedPromiseBinding(argument.name, statements, declarationIndex)) return true;
40354
+ if (isNodeOfType(argument, "CallExpression") && isRequestScopedCallee(argument.callee)) return true;
39999
40355
  }
40000
40356
  return false;
40001
40357
  };
@@ -40030,7 +40386,8 @@ const serverSequentialIndependentAwait = defineRule({
40030
40386
  if (!isNodeOfType(nextStatement, "VariableDeclaration")) continue;
40031
40387
  if (!declarationStartsWithAwait(nextStatement)) continue;
40032
40388
  if (declarationReadsAnyName(nextStatement, declaredNames)) continue;
40033
- if (declarationAwaitsStartedPromise(nextStatement, statements, statementIndex + 1)) continue;
40389
+ if (declarationAwaitsExistingPromise(nextStatement)) continue;
40390
+ if (declarationAwaitsRequestScopedCall(currentStatement) || declarationAwaitsRequestScopedCall(nextStatement)) continue;
40034
40391
  if (declarationAwaitsGate(currentStatement)) continue;
40035
40392
  context.report({
40036
40393
  node: nextStatement,
@@ -40499,7 +40856,16 @@ const supabaseRlsPolicyRisk = defineRule({
40499
40856
  //#endregion
40500
40857
  //#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
40501
40858
  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;
40502
- 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");
40859
+ 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;
40860
+ const collectLastEnableRlsIndexByTable = (content) => {
40861
+ const lastEnableIndexByTable = /* @__PURE__ */ new Map();
40862
+ for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
40863
+ const tableName = match[1];
40864
+ if (tableName === void 0) continue;
40865
+ lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
40866
+ }
40867
+ return lastEnableIndexByTable;
40868
+ };
40503
40869
  const supabaseTableMissingRls = defineRule({
40504
40870
  id: "supabase-table-missing-rls",
40505
40871
  title: "Supabase table created without Row Level Security",
@@ -40510,11 +40876,13 @@ const supabaseTableMissingRls = defineRule({
40510
40876
  const content = sanitizeSqlForScan(file.content);
40511
40877
  if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
40512
40878
  const findings = [];
40879
+ const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
40513
40880
  CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
40514
40881
  for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
40515
40882
  const tableName = match[1];
40516
40883
  if (tableName === void 0) continue;
40517
- if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
40884
+ const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
40885
+ if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
40518
40886
  const location = getLocationAtIndex(content, match.index);
40519
40887
  findings.push({
40520
40888
  message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
@@ -40972,7 +41340,7 @@ const tanstackStartNoNavigateInRender = defineRule({
40972
41340
  if (!isNodeOfType(callParent, "CallExpression")) return false;
40973
41341
  if (callParent.callee === functionNode) return false;
40974
41342
  if (isNodeOfType(callParent.callee, "MemberExpression") && !callParent.callee.computed && isNodeOfType(callParent.callee.property, "Identifier") && PROMISE_CONTINUATION_METHODS.has(callParent.callee.property.name)) return true;
40975
- return isNodeOfType(callParent.callee, "Identifier") && HOOK_NAME_PATTERN.test(callParent.callee.name) && !RENDER_SYNCHRONOUS_HOOK_NAMES.has(callParent.callee.name) && callParent.arguments?.[0] === functionNode;
41343
+ return isNodeOfType(callParent.callee, "Identifier") && HOOK_NAME_PATTERN$1.test(callParent.callee.name) && !RENDER_SYNCHRONOUS_HOOK_NAMES.has(callParent.callee.name) && callParent.arguments?.[0] === functionNode;
40976
41344
  };
40977
41345
  const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && REACT_HANDLER_PROP_PATTERN.test(node.name.name);
40978
41346
  const isEventHandlerNamedProperty = (node) => isNodeOfType(node, "Property") && (isNodeOfType(node.key, "Identifier") && typeof node.key.name === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.name) || isNodeOfType(node.key, "Literal") && typeof node.key.value === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.value));
@@ -41000,11 +41368,11 @@ const tanstackStartNoNavigateInRender = defineRule({
41000
41368
  if (isNodeOfType(parent, "ReturnStatement")) {
41001
41369
  const outerFunction = findEnclosingFunction(parent);
41002
41370
  const hookName = outerFunction ? getFunctionBindingName(outerFunction) : null;
41003
- return Boolean(hookName && HOOK_NAME_PATTERN.test(hookName));
41371
+ return Boolean(hookName && HOOK_NAME_PATTERN$1.test(hookName));
41004
41372
  }
41005
41373
  if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === functionNode) {
41006
41374
  const hookName = getFunctionBindingName(parent);
41007
- return Boolean(hookName && HOOK_NAME_PATTERN.test(hookName));
41375
+ return Boolean(hookName && HOOK_NAME_PATTERN$1.test(hookName));
41008
41376
  }
41009
41377
  return false;
41010
41378
  };
@@ -41204,10 +41572,10 @@ const tanstackStartRoutePropertyOrder = defineRule({
41204
41572
  const propertyName = getPropertyKeyName(property);
41205
41573
  if (propertyName !== null) orderedPropertyNames.push(propertyName);
41206
41574
  }
41207
- const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_ORDER.includes(propertyName));
41575
+ const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_INDEX.has(propertyName));
41208
41576
  let lastIndex = -1;
41209
41577
  for (const propertyName of sensitiveProperties) {
41210
- const currentIndex = TANSTACK_ROUTE_PROPERTY_ORDER.indexOf(propertyName);
41578
+ const currentIndex = TANSTACK_ROUTE_PROPERTY_INDEX.get(propertyName) ?? -1;
41211
41579
  if (currentIndex < lastIndex) {
41212
41580
  const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
41213
41581
  context.report({
@@ -41244,10 +41612,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
41244
41612
  } else return;
41245
41613
  const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
41246
41614
  if (methodNames[methodNames.length - 1] !== ownMethodName) return;
41247
- const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_ORDER.includes(toMethodOrderToken(name)));
41615
+ const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_INDEX.has(toMethodOrderToken(name)));
41248
41616
  let lastIndex = -1;
41249
41617
  for (const methodName of orderSensitiveMethods) {
41250
- const currentIndex = TANSTACK_MIDDLEWARE_METHOD_ORDER.indexOf(toMethodOrderToken(methodName));
41618
+ const currentIndex = TANSTACK_MIDDLEWARE_METHOD_INDEX.get(toMethodOrderToken(methodName)) ?? -1;
41251
41619
  if (currentIndex < lastIndex) {
41252
41620
  const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
41253
41621
  context.report({
@@ -41531,13 +41899,21 @@ const webhookSignatureRisk = defineRule({
41531
41899
  //#endregion
41532
41900
  //#region src/plugin/rules/zod/utils/zod-ast.ts
41533
41901
  const ZOD_MODULE = "zod";
41902
+ const ZOD_MODULE_SOURCES = [ZOD_MODULE];
41534
41903
  const getStaticPropertyName = (member) => {
41535
41904
  const property = member.property;
41536
41905
  if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
41537
41906
  if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
41538
41907
  return null;
41539
41908
  };
41909
+ const importInfoCache = /* @__PURE__ */ new WeakMap();
41540
41910
  const getImportInfoForIdentifier = (identifier) => {
41911
+ if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
41912
+ const importInfo = computeImportInfoForIdentifier(identifier);
41913
+ importInfoCache.set(identifier, importInfo);
41914
+ return importInfo;
41915
+ };
41916
+ const computeImportInfoForIdentifier = (identifier) => {
41541
41917
  const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
41542
41918
  if (!specifier) return null;
41543
41919
  const declaration = specifier.parent;
@@ -41674,25 +42050,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
41674
42050
  tags: ["migration-hint"],
41675
42051
  severity: "warn",
41676
42052
  recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
41677
- create: (context) => ({
41678
- CallExpression(node) {
41679
- if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
41680
- if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
41681
- context.report({
41682
- node,
41683
- message: ZOD_ERROR_API_MESSAGE
41684
- });
41685
- },
41686
- MemberExpression(node) {
41687
- const parent = node.parent;
41688
- if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41689
- if (!isDeprecatedZodErrorMemberAccess(node)) return;
41690
- context.report({
41691
- node,
41692
- message: ZOD_ERROR_API_MESSAGE
41693
- });
41694
- }
41695
- })
42053
+ create: (context) => {
42054
+ let fileImportsZod = false;
42055
+ return {
42056
+ Program(node) {
42057
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
42058
+ },
42059
+ CallExpression(node) {
42060
+ if (!fileImportsZod) return;
42061
+ if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
42062
+ if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
42063
+ context.report({
42064
+ node,
42065
+ message: ZOD_ERROR_API_MESSAGE
42066
+ });
42067
+ },
42068
+ MemberExpression(node) {
42069
+ if (!fileImportsZod) return;
42070
+ const parent = node.parent;
42071
+ if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
42072
+ if (!isDeprecatedZodErrorMemberAccess(node)) return;
42073
+ context.report({
42074
+ node,
42075
+ message: ZOD_ERROR_API_MESSAGE
42076
+ });
42077
+ }
42078
+ };
42079
+ }
41696
42080
  });
41697
42081
  //#endregion
41698
42082
  //#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
@@ -41884,16 +42268,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
41884
42268
  tags: ["migration-hint"],
41885
42269
  severity: "warn",
41886
42270
  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()`.",
41887
- create: (context) => ({
41888
- CallExpression(node) {
41889
- 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);
41890
- },
41891
- MemberExpression(node) {
41892
- const parent = node.parent;
41893
- if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41894
- if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
41895
- }
41896
- })
42271
+ create: (context) => {
42272
+ let fileImportsZod = false;
42273
+ return {
42274
+ Program(node) {
42275
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
42276
+ },
42277
+ CallExpression(node) {
42278
+ if (!fileImportsZod) return;
42279
+ 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);
42280
+ },
42281
+ MemberExpression(node) {
42282
+ if (!fileImportsZod) return;
42283
+ const parent = node.parent;
42284
+ if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
42285
+ if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
42286
+ }
42287
+ };
42288
+ }
41897
42289
  });
41898
42290
  //#endregion
41899
42291
  //#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
@@ -41928,13 +42320,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
41928
42320
  tags: ["migration-hint"],
41929
42321
  severity: "warn",
41930
42322
  recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
41931
- create: (context) => ({ CallExpression(node) {
41932
- if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
41933
- context.report({
41934
- node,
41935
- message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
41936
- });
41937
- } })
42323
+ create: (context) => {
42324
+ let fileImportsZod = false;
42325
+ return {
42326
+ Program(node) {
42327
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
42328
+ },
42329
+ CallExpression(node) {
42330
+ if (!fileImportsZod) return;
42331
+ if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
42332
+ context.report({
42333
+ node,
42334
+ message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
42335
+ });
42336
+ }
42337
+ };
42338
+ }
41938
42339
  });
41939
42340
  //#endregion
41940
42341
  //#region src/plugin/rule-registry.ts