oxlint-plugin-react-doctor 0.6.2-dev.072d37e → 0.6.2-dev.173cc0a

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 +1067 -627
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -294,15 +294,38 @@ const defineRule = (rule) => {
294
294
  };
295
295
  //#endregion
296
296
  //#region src/plugin/rules/security-scan/utils/get-location-at-index.ts
297
+ let lastContentLineIndex;
298
+ const buildLineStartOffsets = (content) => {
299
+ const lineStartOffsets = [0];
300
+ for (let newlineIndex = content.indexOf("\n"); newlineIndex !== -1; newlineIndex = content.indexOf("\n", newlineIndex + 1)) lineStartOffsets.push(newlineIndex + 1);
301
+ return lineStartOffsets;
302
+ };
303
+ const getLineStartOffsets = (content) => {
304
+ if (lastContentLineIndex?.content === content) return lastContentLineIndex.lineStartOffsets;
305
+ const lineStartOffsets = buildLineStartOffsets(content);
306
+ lastContentLineIndex = {
307
+ content,
308
+ lineStartOffsets
309
+ };
310
+ return lineStartOffsets;
311
+ };
297
312
  const getLocationAtIndex = (content, matchIndex) => {
298
- if (matchIndex < 0) return {
313
+ if (Number.isNaN(matchIndex) || matchIndex < 0) return {
299
314
  line: 1,
300
315
  column: 1
301
316
  };
302
- const lines = content.slice(0, matchIndex).split(/\r?\n/);
317
+ const lineStartOffsets = getLineStartOffsets(content);
318
+ const boundedIndex = Math.min(Math.trunc(matchIndex), content.length);
319
+ let lowLineIndex = 0;
320
+ let highLineIndex = lineStartOffsets.length - 1;
321
+ while (lowLineIndex < highLineIndex) {
322
+ const midLineIndex = lowLineIndex + highLineIndex + 1 >> 1;
323
+ if (lineStartOffsets[midLineIndex] <= boundedIndex) lowLineIndex = midLineIndex;
324
+ else highLineIndex = midLineIndex - 1;
325
+ }
303
326
  return {
304
- line: lines.length,
305
- column: (lines[lines.length - 1]?.length ?? 0) + 1
327
+ line: lowLineIndex + 1,
328
+ column: boundedIndex - lineStartOffsets[lowLineIndex] + 1
306
329
  };
307
330
  };
308
331
  //#endregion
@@ -355,13 +378,18 @@ const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
355
378
  const isConfigOrCiPath = (relativePath) => /(?:^|\/)(?:package\.json|Dockerfile|docker-compose\.ya?ml|\.github\/workflows\/[^/]+\.ya?ml|vercel\.json|next\.config\.[cm]?[jt]s|netlify\.toml)$/i.test(relativePath);
356
379
  //#endregion
357
380
  //#region src/plugin/rules/security-scan/utils/is-production-file-path.ts
381
+ const classificationCacheByPattern = /* @__PURE__ */ new Map();
358
382
  const isProductionFilePath = (relativePath, sourceFilePattern) => {
359
- if (!sourceFilePattern.test(relativePath)) return false;
360
- if (TEST_CONTEXT_PATTERN.test(relativePath)) return false;
361
- if (BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath)) return false;
362
- if (DOCUMENTATION_CONTEXT_PATTERN.test(relativePath)) return false;
363
- if (GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath)) return false;
364
- return true;
383
+ let classificationByPath = classificationCacheByPattern.get(sourceFilePattern);
384
+ if (!classificationByPath) {
385
+ classificationByPath = /* @__PURE__ */ new Map();
386
+ classificationCacheByPattern.set(sourceFilePattern, classificationByPath);
387
+ }
388
+ const cached = classificationByPath.get(relativePath);
389
+ if (cached !== void 0) return cached;
390
+ const isProduction = sourceFilePattern.test(relativePath) && !TEST_CONTEXT_PATTERN.test(relativePath) && !BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(relativePath) && !GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath);
391
+ classificationByPath.set(relativePath, isProduction);
392
+ return isProduction;
365
393
  };
366
394
  //#endregion
367
395
  //#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
@@ -560,7 +588,7 @@ const SETTER_PATTERN = /^set[A-Z]/;
560
588
  const RENDER_FUNCTION_PATTERN = /^render[A-Z]/;
561
589
  const UPPERCASE_PATTERN = /^[A-Z]/;
562
590
  const REACT_HANDLER_PROP_PATTERN = /^on[A-Z]/;
563
- const HOOK_NAME_PATTERN = /^use[A-Z]/;
591
+ const HOOK_NAME_PATTERN$1 = /^use[A-Z]/;
564
592
  const HANDLER_FUNCTION_NAME_PATTERN = /^(?:on|handle)[A-Z]/;
565
593
  const EFFECT_HOOK_NAMES$1 = new Set(["useEffect", "useLayoutEffect"]);
566
594
  const HOOKS_WITH_DEPS = new Set([
@@ -760,24 +788,6 @@ const collectChildComponentNames = (element, into) => {
760
788
  into.add(name);
761
789
  });
762
790
  };
763
- const findSameFileComponentBody = (programRoot, componentName) => {
764
- let foundBody = null;
765
- walkAst(programRoot, (node) => {
766
- if (foundBody) return false;
767
- if (isNodeOfType(node, "FunctionDeclaration") && node.id && node.id.name === componentName) {
768
- foundBody = node.body;
769
- return false;
770
- }
771
- if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.id.name === componentName) {
772
- const initializer = node.init;
773
- if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) {
774
- foundBody = initializer.body;
775
- return false;
776
- }
777
- }
778
- });
779
- return foundBody;
780
- };
781
791
  const countEffectHookCalls = (body) => {
782
792
  if (!body) return 0;
783
793
  let count = 0;
@@ -787,6 +797,36 @@ const countEffectHookCalls = (body) => {
787
797
  });
788
798
  return count;
789
799
  };
800
+ const componentEffectIndexCache = /* @__PURE__ */ new WeakMap();
801
+ const getComponentEffectIndex = (programRoot) => {
802
+ const cached = componentEffectIndexCache.get(programRoot);
803
+ if (cached) return cached;
804
+ const bodyByName = /* @__PURE__ */ new Map();
805
+ walkAst(programRoot, (node) => {
806
+ if (isNodeOfType(node, "FunctionDeclaration") && node.id && !bodyByName.has(node.id.name)) {
807
+ bodyByName.set(node.id.name, node.body);
808
+ return;
809
+ }
810
+ if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && !bodyByName.has(node.id.name)) {
811
+ const initializer = node.init;
812
+ if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) bodyByName.set(node.id.name, initializer.body);
813
+ }
814
+ });
815
+ const index = {
816
+ bodyByName,
817
+ effectCountByName: /* @__PURE__ */ new Map()
818
+ };
819
+ componentEffectIndexCache.set(programRoot, index);
820
+ return index;
821
+ };
822
+ const getSameFileComponentEffectCount = (programRoot, componentName) => {
823
+ const index = getComponentEffectIndex(programRoot);
824
+ const cachedCount = index.effectCountByName.get(componentName);
825
+ if (cachedCount !== void 0) return cachedCount;
826
+ const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
827
+ index.effectCountByName.set(componentName, count);
828
+ return count;
829
+ };
790
830
  const activityWrapsEffectHeavySubtree = defineRule({
791
831
  id: "activity-wraps-effect-heavy-subtree",
792
832
  title: "Activity wraps an effect-heavy subtree",
@@ -843,9 +883,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
843
883
  let totalEffects = 0;
844
884
  const effectfulChildren = [];
845
885
  for (const componentName of childComponentNames) {
846
- const body = findSameFileComponentBody(programRoot, componentName);
847
- if (!body) continue;
848
- const effectCount = countEffectHookCalls(body);
886
+ const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
849
887
  if (effectCount === 0) continue;
850
888
  totalEffects += effectCount;
851
889
  effectfulChildren.push(`<${componentName}>`);
@@ -1394,11 +1432,16 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
1394
1432
  };
1395
1433
  //#endregion
1396
1434
  //#region src/plugin/utils/get-element-type.ts
1435
+ const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
1436
+ const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
1397
1437
  const readJsxA11ySettings = (settings) => {
1398
- if (!settings) return {};
1438
+ if (!settings) return EMPTY_JSX_A11Y_SETTINGS;
1439
+ const cachedSettings = jsxA11ySettingsCache.get(settings);
1440
+ if (cachedSettings) return cachedSettings;
1399
1441
  const block = settings["jsx-a11y"];
1400
- if (!block || typeof block !== "object") return {};
1401
- return block;
1442
+ const a11ySettings = block && typeof block === "object" ? block : EMPTY_JSX_A11Y_SETTINGS;
1443
+ jsxA11ySettingsCache.set(settings, a11ySettings);
1444
+ return a11ySettings;
1402
1445
  };
1403
1446
  const flattenJsxName$2 = (name) => {
1404
1447
  if (isNodeOfType(name, "JSXIdentifier")) return name.name;
@@ -1406,8 +1449,7 @@ const flattenJsxName$2 = (name) => {
1406
1449
  if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
1407
1450
  return "";
1408
1451
  };
1409
- const getElementType = (openingElement, settings) => {
1410
- const a11ySettings = readJsxA11ySettings(settings);
1452
+ const computeElementType = (openingElement, a11ySettings) => {
1411
1453
  const baseName = flattenJsxName$2(openingElement.name);
1412
1454
  if (a11ySettings.polymorphicPropName) {
1413
1455
  const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
@@ -1419,6 +1461,23 @@ const getElementType = (openingElement, settings) => {
1419
1461
  if (a11ySettings.components && baseName in a11ySettings.components) return a11ySettings.components[baseName];
1420
1462
  return baseName;
1421
1463
  };
1464
+ const elementTypeCache = /* @__PURE__ */ new WeakMap();
1465
+ const getElementType = (openingElement, settings) => {
1466
+ const cachedEntry = elementTypeCache.get(openingElement);
1467
+ if (cachedEntry && cachedEntry.settings === settings) return cachedEntry.elementType;
1468
+ const elementType = computeElementType(openingElement, readJsxA11ySettings(settings));
1469
+ elementTypeCache.set(openingElement, {
1470
+ settings,
1471
+ elementType
1472
+ });
1473
+ return elementType;
1474
+ };
1475
+ //#endregion
1476
+ //#region src/plugin/utils/has-jsx-a11y-settings.ts
1477
+ const hasJsxA11ySettings = (settings) => {
1478
+ const block = settings?.["jsx-a11y"];
1479
+ return typeof block === "object" && block !== null;
1480
+ };
1422
1481
  //#endregion
1423
1482
  //#region src/plugin/utils/find-import-source-for-name.ts
1424
1483
  const collectFromProgram = (programRoot) => {
@@ -1623,7 +1682,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
1623
1682
  };
1624
1683
  //#endregion
1625
1684
  //#region src/plugin/utils/normalize-filename.ts
1626
- const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
1685
+ const normalizeFilename = (filename) => filename.includes("\\") ? filename.replaceAll("\\", "/") : filename;
1627
1686
  //#endregion
1628
1687
  //#region src/plugin/utils/is-generated-image-render-context.ts
1629
1688
  const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
@@ -1972,7 +2031,12 @@ const altText = defineRule({
1972
2031
  const objectAliases = new Set(settings.object ?? []);
1973
2032
  const areaAliases = new Set(settings.area ?? []);
1974
2033
  const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
2034
+ const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
1975
2035
  return { JSXOpeningElement(node) {
2036
+ if (!fileHasJsxA11ySettings && isNodeOfType(node.name, "JSXIdentifier")) {
2037
+ const rawName = node.name.name;
2038
+ if (rawName !== "img" && rawName !== "object" && rawName !== "area" && rawName.toLowerCase() !== "input" && !imgAliases.has(rawName) && !objectAliases.has(rawName) && !areaAliases.has(rawName) && !inputImageAliases.has(rawName)) return;
2039
+ }
1976
2040
  if (isGeneratedImageRenderContext(context, node)) return;
1977
2041
  const tag = getElementType(node, context.settings);
1978
2042
  if (checkImg && (tag === "img" || imgAliases.has(tag))) {
@@ -2155,7 +2219,9 @@ const anchorIsValid = defineRule({
2155
2219
  category: "Accessibility",
2156
2220
  create: (context) => {
2157
2221
  const settings = resolveSettings$50(context.settings);
2222
+ const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
2158
2223
  return { JSXOpeningElement(node) {
2224
+ if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
2159
2225
  if (getElementType(node, context.settings) !== "a") return;
2160
2226
  let hrefAttribute;
2161
2227
  for (const attributeName of settings.hrefAttributeNames) {
@@ -3581,8 +3647,9 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
3581
3647
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3582
3648
  //#endregion
3583
3649
  //#region src/plugin/rules/security-scan/utils/find-suspicious-public-env-secret-name.ts
3650
+ const PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN = new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi");
3584
3651
  const findSuspiciousPublicEnvSecretNamePattern = (content) => {
3585
- for (const match of content.matchAll(new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi"))) {
3652
+ for (const match of content.matchAll(PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN)) {
3586
3653
  const value = match[0] ?? "";
3587
3654
  if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
3588
3655
  }
@@ -4049,62 +4116,56 @@ const collectPatternIdentifiers = (pattern, target) => {
4049
4116
  for (const element of pattern.elements ?? []) if (element) collectPatternIdentifiers(element, target);
4050
4117
  } else if (isNodeOfType(pattern, "AssignmentPattern") && pattern.left) collectPatternIdentifiers(pattern.left, target);
4051
4118
  };
4052
- const collectAssignedIdentifiers = (block) => {
4053
- const assigned = /* @__PURE__ */ new Set();
4054
- walkAst(block, (child) => {
4055
- if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
4056
- if (isNodeOfType(child, "AssignmentExpression") && child.left) collectPatternIdentifiers(child.left, assigned);
4057
- });
4058
- return assigned;
4059
- };
4060
- const collectAwaitedArgIdentifiers = (block) => {
4061
- const referenced = /* @__PURE__ */ new Set();
4062
- walkAst(block, (child) => {
4063
- if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
4064
- if (!isNodeOfType(child, "AwaitExpression") || !child.argument) return;
4065
- collectReferenceIdentifierNames(child.argument, referenced);
4066
- });
4067
- return referenced;
4068
- };
4069
4119
  const ARRAY_MUTATION_METHOD_NAMES = new Set([
4070
4120
  "push",
4071
4121
  "unshift",
4072
4122
  "splice"
4073
4123
  ]);
4074
- const collectMutatedArrayNames = (block) => {
4075
- const mutated = /* @__PURE__ */ new Set();
4124
+ const addDerivedBindings = (block, names) => {
4125
+ const declaratorBindings = [];
4076
4126
  walkAst(block, (child) => {
4077
4127
  if (child !== block && isFunctionLike$1(child)) return false;
4078
- if (!isNodeOfType(child, "CallExpression")) return;
4079
- const callee = child.callee;
4080
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_MUTATION_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier")) mutated.add(callee.object.name);
4128
+ if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
4129
+ if (!isNodeOfType(child.id, "Identifier")) return;
4130
+ const referencedNames = /* @__PURE__ */ new Set();
4131
+ collectReferenceIdentifierNames(child.init, referencedNames);
4132
+ declaratorBindings.push({
4133
+ declaredName: child.id.name,
4134
+ referencedNames
4135
+ });
4081
4136
  });
4082
- return mutated;
4083
- };
4084
- const addDerivedBindings = (block, names) => {
4085
4137
  let didGrow = true;
4086
4138
  while (didGrow) {
4087
4139
  didGrow = false;
4088
- walkAst(block, (child) => {
4089
- if (child !== block && isFunctionLike$1(child)) return false;
4090
- if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
4091
- if (!isNodeOfType(child.id, "Identifier") || names.has(child.id.name)) return;
4092
- const initReferences = /* @__PURE__ */ new Set();
4093
- collectReferenceIdentifierNames(child.init, initReferences);
4094
- for (const referenced of initReferences) if (names.has(referenced)) {
4095
- names.add(child.id.name);
4140
+ for (const { declaredName, referencedNames } of declaratorBindings) {
4141
+ if (names.has(declaredName)) continue;
4142
+ for (const referenced of referencedNames) if (names.has(referenced)) {
4143
+ names.add(declaredName);
4096
4144
  didGrow = true;
4097
4145
  break;
4098
4146
  }
4099
- });
4147
+ }
4100
4148
  }
4101
4149
  };
4102
4150
  const hasLoopCarriedDependency = (block) => {
4103
- const carried = collectAssignedIdentifiers(block);
4104
- for (const name of collectMutatedArrayNames(block)) carried.add(name);
4151
+ const carried = /* @__PURE__ */ new Set();
4152
+ const awaitedReferences = /* @__PURE__ */ new Set();
4153
+ walkAst(block, (child) => {
4154
+ if (child !== block && isFunctionLike$1(child)) return false;
4155
+ if (isNodeOfType(child, "AssignmentExpression") && child.left) {
4156
+ collectPatternIdentifiers(child.left, carried);
4157
+ return;
4158
+ }
4159
+ if (isNodeOfType(child, "AwaitExpression") && child.argument) {
4160
+ collectReferenceIdentifierNames(child.argument, awaitedReferences);
4161
+ return;
4162
+ }
4163
+ if (!isNodeOfType(child, "CallExpression")) return;
4164
+ const callee = child.callee;
4165
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_MUTATION_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier")) carried.add(callee.object.name);
4166
+ });
4105
4167
  if (carried.size === 0) return false;
4106
4168
  addDerivedBindings(block, carried);
4107
- const awaitedReferences = collectAwaitedArgIdentifiers(block);
4108
4169
  for (const name of carried) if (awaitedReferences.has(name)) return true;
4109
4170
  return false;
4110
4171
  };
@@ -4782,7 +4843,8 @@ const FORM_CONTROL_TAGS = new Set([
4782
4843
  ]);
4783
4844
  const resolveSettings$48 = (settings) => {
4784
4845
  const reactDoctor = settings?.["react-doctor"];
4785
- 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 ?? []) };
4786
4848
  };
4787
4849
  const autocompleteValid = defineRule({
4788
4850
  id: "autocomplete-valid",
@@ -4795,7 +4857,7 @@ const autocompleteValid = defineRule({
4795
4857
  const settings = resolveSettings$48(context.settings);
4796
4858
  return { JSXOpeningElement: (node) => {
4797
4859
  const tag = getElementType(node, context.settings);
4798
- if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.includes(tag)) return;
4860
+ if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.has(tag)) return;
4799
4861
  const attribute = hasJsxPropIgnoreCase(node.attributes, "autoComplete");
4800
4862
  if (!attribute) return;
4801
4863
  const value = getJsxPropStringValue(attribute);
@@ -4839,9 +4901,8 @@ const isCreateElementCall = (node) => {
4839
4901
  const callee = node.callee;
4840
4902
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createElement";
4841
4903
  if (isNodeOfType(callee, "MemberExpression")) {
4842
- if (memberChainContainsDocument(callee.object)) return false;
4843
- if (callee.computed) return isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement";
4844
- return isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement";
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);
4845
4906
  }
4846
4907
  return false;
4847
4908
  };
@@ -4867,7 +4928,8 @@ const isValidTypeValue = (rawValue, settings) => {
4867
4928
  if (rawValue === "reset") return settings.reset;
4868
4929
  return false;
4869
4930
  };
4870
- const isProvenValidExpression = (expression, settings, resolvedBindings = /* @__PURE__ */ new Set()) => {
4931
+ const isProvenValidExpression = (rawExpression, settings, resolvedBindings = /* @__PURE__ */ new Set()) => {
4932
+ const expression = stripParenExpression(rawExpression);
4871
4933
  if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return isValidTypeValue(expression.value, settings);
4872
4934
  if (isNodeOfType(expression, "TemplateLiteral")) {
4873
4935
  const staticValue = getStaticTemplateLiteralValue(expression);
@@ -5034,11 +5096,20 @@ const resolveSettings$46 = (settings) => {
5034
5096
  ignoreExclusiveCheckedAttribute: ruleSettings.ignoreExclusiveCheckedAttribute ?? false
5035
5097
  };
5036
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
+ };
5037
5107
  const collectFromJsxAttributes = (attributes) => {
5038
5108
  let checkedNode = null;
5039
5109
  let defaultCheckedNode = null;
5040
5110
  let hasOnChangeOrReadOnly = false;
5041
5111
  let hasSpread = false;
5112
+ let hasTruthyDisabled = false;
5042
5113
  for (const attribute of attributes) {
5043
5114
  if (isNodeOfType(attribute, "JSXSpreadAttribute")) {
5044
5115
  hasSpread = true;
@@ -5049,12 +5120,14 @@ const collectFromJsxAttributes = (attributes) => {
5049
5120
  if (name === "checked") checkedNode = attribute;
5050
5121
  else if (name === "defaultChecked" && !defaultCheckedNode) defaultCheckedNode = attribute;
5051
5122
  else if (name === "onChange" || name === "readOnly") hasOnChangeOrReadOnly = true;
5123
+ else if (name === "disabled" && isTruthyDisabledJsxValue(attribute)) hasTruthyDisabled = true;
5052
5124
  }
5053
5125
  return {
5054
5126
  checkedNode,
5055
5127
  defaultCheckedNode,
5056
5128
  hasOnChangeOrReadOnly,
5057
- hasSpread
5129
+ hasSpread,
5130
+ hasTruthyDisabled
5058
5131
  };
5059
5132
  };
5060
5133
  const collectFromObjectProperties = (objectExpression) => {
@@ -5062,6 +5135,7 @@ const collectFromObjectProperties = (objectExpression) => {
5062
5135
  let defaultCheckedNode = null;
5063
5136
  let hasOnChangeOrReadOnly = false;
5064
5137
  let hasSpread = false;
5138
+ let hasTruthyDisabled = false;
5065
5139
  for (const property of objectExpression.properties) {
5066
5140
  if (isNodeOfType(property, "SpreadElement")) {
5067
5141
  hasSpread = true;
@@ -5076,12 +5150,17 @@ const collectFromObjectProperties = (objectExpression) => {
5076
5150
  if (propertyName === "checked") checkedNode = property;
5077
5151
  else if (propertyName === "defaultChecked" && !defaultCheckedNode) defaultCheckedNode = property;
5078
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
+ }
5079
5157
  }
5080
5158
  return {
5081
5159
  checkedNode,
5082
5160
  defaultCheckedNode,
5083
5161
  hasOnChangeOrReadOnly,
5084
- hasSpread
5162
+ hasSpread,
5163
+ hasTruthyDisabled
5085
5164
  };
5086
5165
  };
5087
5166
  const checkedRequiresOnchangeOrReadonly = defineRule({
@@ -5097,7 +5176,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
5097
5176
  node: presence.checkedNode,
5098
5177
  message: EXCLUSIVE_MESSAGE
5099
5178
  });
5100
- 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({
5101
5180
  node: presence.checkedNode,
5102
5181
  message: MISSING_MESSAGE$1
5103
5182
  });
@@ -5658,12 +5737,20 @@ const getTemplateInterpolations = (valueTail) => {
5658
5737
  return interpolations === null ? "" : interpolations.join(" ");
5659
5738
  };
5660
5739
  const isInertParseTarget = (target, fileContent) => {
5740
+ const fileHasCreateElement = fileContent.includes("createElement");
5741
+ const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
5742
+ if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
5661
5743
  const escapedTarget = escapeRegExp(target);
5662
5744
  const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
5663
5745
  if (new RegExp(`\\b${escapedRoot}\\s*=\\s*[^\\n;]*(?:getElementById|querySelector|getElementsBy|\\.current\\b|document\\.(?:body|head|documentElement))`).test(fileContent)) return false;
5664
- if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
5665
- if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
5666
- if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
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;
5667
5754
  if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
5668
5755
  const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
5669
5756
  const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
@@ -5756,10 +5843,9 @@ const dangerousHtmlSink = defineRule({
5756
5843
  const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
5757
5844
  if (valueIdentifier !== void 0) {
5758
5845
  const escapedIdentifier = escapeRegExp(valueIdentifier);
5759
- const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
5760
- const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
5761
- const fromDomContent = new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`);
5762
- if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
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;
5763
5849
  }
5764
5850
  }
5765
5851
  const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
@@ -5908,6 +5994,7 @@ const noRedundantPaddingAxes = defineRule({
5908
5994
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5909
5995
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5910
5996
  if (!classNameLiteral) return;
5997
+ if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
5911
5998
  if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
5912
5999
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
5913
6000
  if (matchedPairs.length === 0) return;
@@ -5932,6 +6019,7 @@ const noRedundantSizeAxes = defineRule({
5932
6019
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5933
6020
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5934
6021
  if (!classNameLiteral) return;
6022
+ if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
5935
6023
  if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
5936
6024
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
5937
6025
  if (matchedPairs.length === 0) return;
@@ -5956,6 +6044,7 @@ const noSpaceOnFlexChildren = defineRule({
5956
6044
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5957
6045
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5958
6046
  if (!classNameLiteral) return;
6047
+ if (!classNameLiteral.includes("space-")) return;
5959
6048
  const tokens = tokenizeClassName(classNameLiteral);
5960
6049
  let hasFlexOrGridLayout = false;
5961
6050
  for (const token of tokens) {
@@ -6147,7 +6236,10 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
6147
6236
  const actualMinor = Number(match[2]);
6148
6237
  return actualMajor > major || actualMajor === major && actualMinor >= minor;
6149
6238
  };
6239
+ const containsJsxCache = /* @__PURE__ */ new WeakMap();
6150
6240
  const containsJsx$1 = (root) => {
6241
+ const cached = containsJsxCache.get(root);
6242
+ if (cached !== void 0) return cached;
6151
6243
  let found = false;
6152
6244
  const visit = (node) => {
6153
6245
  if (found) return;
@@ -6170,6 +6262,7 @@ const containsJsx$1 = (root) => {
6170
6262
  }
6171
6263
  };
6172
6264
  visit(root);
6265
+ containsJsxCache.set(root, found);
6173
6266
  return found;
6174
6267
  };
6175
6268
  const getStaticMemberName = (node) => {
@@ -6261,27 +6354,6 @@ const hasDisplayNameMember = (classNode) => {
6261
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;
6262
6355
  return false;
6263
6356
  };
6264
- const hasDisplayNameAssignment = (className, programRoot) => {
6265
- let found = false;
6266
- const visit = (node) => {
6267
- if (found) return;
6268
- if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && isNodeOfType(node.left.object, "Identifier") && node.left.object.name === className && getStaticMemberName(node.left) === "displayName") {
6269
- found = true;
6270
- return;
6271
- }
6272
- const record = node;
6273
- for (const key of Object.keys(record)) {
6274
- if (key === "parent") continue;
6275
- const child = record[key];
6276
- if (Array.isArray(child)) {
6277
- for (const item of child) if (isAstNode(item)) visit(item);
6278
- } else if (isAstNode(child)) visit(child);
6279
- if (found) return;
6280
- }
6281
- };
6282
- visit(programRoot);
6283
- return found;
6284
- };
6285
6357
  const memberExpressionPath = (node) => {
6286
6358
  if (isNodeOfType(node, "Identifier")) return [node.name];
6287
6359
  if (!isNodeOfType(node, "MemberExpression")) return [];
@@ -6289,15 +6361,16 @@ const memberExpressionPath = (node) => {
6289
6361
  const propertyName = getStaticMemberName(node);
6290
6362
  return propertyName ? [...objectPath, propertyName] : objectPath;
6291
6363
  };
6292
- const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6293
- 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();
6294
6370
  const visit = (node) => {
6295
- if (found) return;
6296
6371
  if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
6297
- if (memberExpressionPath(node.left.object).includes(propertyName)) {
6298
- found = true;
6299
- return;
6300
- }
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);
6301
6374
  }
6302
6375
  const record = node;
6303
6376
  for (const key of Object.keys(record)) {
@@ -6306,12 +6379,18 @@ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6306
6379
  if (Array.isArray(child)) {
6307
6380
  for (const item of child) if (isAstNode(item)) visit(item);
6308
6381
  } else if (isAstNode(child)) visit(child);
6309
- if (found) return;
6310
6382
  }
6311
6383
  };
6312
6384
  visit(programRoot);
6313
- return found;
6385
+ const index = {
6386
+ identifierTargets,
6387
+ memberPathSegments
6388
+ };
6389
+ displayNameAssignmentIndexCache.set(programRoot, index);
6390
+ return index;
6314
6391
  };
6392
+ const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
6393
+ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
6315
6394
  const displayName = defineRule({
6316
6395
  id: "display-name",
6317
6396
  title: "Component missing display name",
@@ -6371,7 +6450,7 @@ const displayName = defineRule({
6371
6450
  },
6372
6451
  ArrowFunctionExpression(node) {
6373
6452
  if (!containsJsx$1(node)) return;
6374
- if (isNodeOfType(node.parent, "ArrowFunctionExpression")) {
6453
+ if (isNodeOfType(node.parent, "ArrowFunctionExpression") || isNodeOfType(node.parent, "ReturnStatement")) {
6375
6454
  reportAt(node);
6376
6455
  return;
6377
6456
  }
@@ -7288,7 +7367,7 @@ const TRANSPARENT_WRAPPER_TYPES = new Set([
7288
7367
  "ParenthesizedExpression",
7289
7368
  "ChainExpression"
7290
7369
  ]);
7291
- const unwrapExpression$2 = (node) => {
7370
+ const unwrapExpression$3 = (node) => {
7292
7371
  let current = node;
7293
7372
  while (TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
7294
7373
  const inner = current.expression;
@@ -7414,7 +7493,7 @@ const symbolHasStableHookOrigin = (symbol) => {
7414
7493
  if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
7415
7494
  const initializerRaw = declarator.init;
7416
7495
  if (!initializerRaw) return false;
7417
- const initializer = unwrapExpression$2(initializerRaw);
7496
+ const initializer = unwrapExpression$3(initializerRaw);
7418
7497
  if (symbol.kind === "const") {
7419
7498
  if (isNodeOfType(initializer, "Literal") && (initializer.value === null || typeof initializer.value === "number" || typeof initializer.value === "string" || typeof initializer.value === "boolean")) return true;
7420
7499
  if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
@@ -7434,13 +7513,13 @@ const symbolHasStableHookOrigin = (symbol) => {
7434
7513
  return false;
7435
7514
  };
7436
7515
  const symbolHasUseEffectEventOrigin = (symbol) => {
7437
- const initializer = symbol.initializer ? unwrapExpression$2(symbol.initializer) : null;
7516
+ const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
7438
7517
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
7439
7518
  return getHookName(initializer.callee) === "useEffectEvent";
7440
7519
  };
7441
7520
  const getFunctionValueNode = (symbol) => {
7442
7521
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
7443
- const initializer = symbol.initializer ? unwrapExpression$2(symbol.initializer) : null;
7522
+ const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
7444
7523
  if (initializer && (isNodeOfType(initializer, "FunctionExpression") || isNodeOfType(initializer, "ArrowFunctionExpression"))) return initializer;
7445
7524
  return null;
7446
7525
  };
@@ -7536,6 +7615,14 @@ const flattenReferenceRootName = (reference) => {
7536
7615
  if (isNodeOfType(referencedIdentifier, "JSXIdentifier")) return referencedIdentifier.name;
7537
7616
  return "";
7538
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
+ };
7539
7626
  const computeDepKey = (reference) => {
7540
7627
  const referencedIdentifier = reference.identifier;
7541
7628
  let parent = referencedIdentifier.parent ?? null;
@@ -7566,21 +7653,22 @@ const computeDepKey = (reference) => {
7566
7653
  const declarator = outermost.parent;
7567
7654
  if (declarator && isNodeOfType(declarator, "VariableDeclarator") && declarator.init === outermost) {
7568
7655
  const destructuredPath = getDestructuredPropertyPath(declarator.id);
7569
- if (destructuredPath) return `${fullName}.${destructuredPath}`;
7656
+ if (destructuredPath) return truncateAtRefCurrent(`${fullName}.${destructuredPath}`);
7570
7657
  }
7658
+ const truncatedName = truncateAtRefCurrent(fullName);
7659
+ if (truncatedName !== fullName) return truncatedName;
7571
7660
  if (reference.flag !== "read") {
7572
7661
  const lastDotIndex = fullName.lastIndexOf(".");
7573
7662
  if (lastDotIndex !== -1) return fullName.slice(0, lastDotIndex);
7574
7663
  }
7575
- if (fullName.endsWith(".current")) return fullName.slice(0, -8);
7576
7664
  return fullName;
7577
7665
  };
7578
7666
  const computeDeclaredDepKey = (entry) => {
7579
- const stripped = unwrapExpression$2(entry);
7667
+ const stripped = unwrapExpression$3(entry);
7580
7668
  if (isNodeOfType(stripped, "Identifier")) return stripped.name;
7581
7669
  if (isNodeOfType(stripped, "MemberExpression")) return stringifyMemberChain(stripped);
7582
7670
  if (isNodeOfType(stripped, "CallExpression") && (stripped.arguments?.length ?? 0) === 0) {
7583
- const callee = unwrapExpression$2(stripped.callee);
7671
+ const callee = unwrapExpression$3(stripped.callee);
7584
7672
  if (isNodeOfType(callee, "Identifier")) return callee.name;
7585
7673
  if (isNodeOfType(callee, "MemberExpression") && !hasComputedMemberExpression(callee)) return stringifyMemberChain(callee);
7586
7674
  }
@@ -7588,16 +7676,16 @@ const computeDeclaredDepKey = (entry) => {
7588
7676
  };
7589
7677
  const depsArrayContainsIdentifier = (depsArgument, identifierName) => {
7590
7678
  if (!depsArgument) return false;
7591
- const strippedDepsArgument = unwrapExpression$2(depsArgument);
7679
+ const strippedDepsArgument = unwrapExpression$3(depsArgument);
7592
7680
  if (!isNodeOfType(strippedDepsArgument, "ArrayExpression")) return false;
7593
7681
  return strippedDepsArgument.elements.some((element) => {
7594
7682
  if (!element) return false;
7595
- const strippedElement = unwrapExpression$2(element);
7683
+ const strippedElement = unwrapExpression$3(element);
7596
7684
  return isNodeOfType(strippedElement, "Identifier") && strippedElement.name === identifierName;
7597
7685
  });
7598
7686
  };
7599
7687
  const stringifyMemberChain = (node) => {
7600
- const stripped = unwrapExpression$2(node);
7688
+ const stripped = unwrapExpression$3(node);
7601
7689
  if (isNodeOfType(stripped, "Identifier")) return stripped.name;
7602
7690
  if (isNodeOfType(stripped, "ThisExpression")) return "this";
7603
7691
  if (isNodeOfType(stripped, "MemberExpression")) {
@@ -7639,13 +7727,13 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
7639
7727
  return false;
7640
7728
  };
7641
7729
  const getMemberRootIdentifier = (node) => {
7642
- const stripped = unwrapExpression$2(node);
7730
+ const stripped = unwrapExpression$3(node);
7643
7731
  if (isNodeOfType(stripped, "Identifier")) return stripped;
7644
7732
  if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier(stripped.object);
7645
7733
  return null;
7646
7734
  };
7647
7735
  const hasComputedMemberExpression = (node) => {
7648
- const stripped = unwrapExpression$2(node);
7736
+ const stripped = unwrapExpression$3(node);
7649
7737
  if (!isNodeOfType(stripped, "MemberExpression")) return false;
7650
7738
  if (stripped.computed) return true;
7651
7739
  return hasComputedMemberExpression(stripped.object);
@@ -7661,7 +7749,7 @@ const getRootSymbol = (node, scopes) => {
7661
7749
  return rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
7662
7750
  };
7663
7751
  const getDeclaredDepSymbolSource = (node) => {
7664
- const stripped = unwrapExpression$2(node);
7752
+ const stripped = unwrapExpression$3(node);
7665
7753
  if (isNodeOfType(stripped, "CallExpression") && (stripped.arguments?.length ?? 0) === 0) return stripped.callee;
7666
7754
  return node;
7667
7755
  };
@@ -7671,7 +7759,7 @@ const isRegExpLiteral = (node) => {
7671
7759
  };
7672
7760
  const isUnstableInitializer = (node) => {
7673
7761
  if (!node) return false;
7674
- const stripped = unwrapExpression$2(node);
7762
+ const stripped = unwrapExpression$3(node);
7675
7763
  if (isRegExpLiteral(stripped)) return true;
7676
7764
  if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
7677
7765
  if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
@@ -7763,9 +7851,9 @@ const findRefCurrentInCleanup = (callback, scopes) => {
7763
7851
  };
7764
7852
  findReturn(callback);
7765
7853
  if (!cleanupFunction) return null;
7766
- let refCurrentName = null;
7854
+ let found = null;
7767
7855
  const visitCleanup = (node) => {
7768
- if (refCurrentName) return;
7856
+ if (found) return;
7769
7857
  if (isNodeOfType(node, "MemberExpression")) {
7770
7858
  const candidateName = getRefCurrentNameFromMemberExpression(node);
7771
7859
  if (candidateName) {
@@ -7773,7 +7861,10 @@ const findRefCurrentInCleanup = (callback, scopes) => {
7773
7861
  const symbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
7774
7862
  const callbackScope = scopes.ownScopeFor(callback) ?? scopes.scopeFor(callback);
7775
7863
  if (!symbol || !isDescendantScope(symbol.scope, callbackScope)) {
7776
- refCurrentName = candidateName;
7864
+ found = {
7865
+ refCurrentName: candidateName,
7866
+ refSymbol: symbol
7867
+ };
7777
7868
  return;
7778
7869
  }
7779
7870
  }
@@ -7788,7 +7879,19 @@ const findRefCurrentInCleanup = (callback, scopes) => {
7788
7879
  }
7789
7880
  };
7790
7881
  visitCleanup(cleanupFunction);
7791
- 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;
7792
7895
  };
7793
7896
  const hasRefCurrentAssignment = (callback, refCurrentName) => {
7794
7897
  let didAssignRefCurrent = false;
@@ -7827,9 +7930,21 @@ const hasMemberCallForRoot = (node, rootName) => {
7827
7930
  const visit = (current) => {
7828
7931
  if (didFindMemberCall) return;
7829
7932
  if (isNodeOfType(current, "CallExpression")) {
7830
- if (getMemberRootIdentifier(unwrapExpression$2(current.callee))?.name === rootName) {
7831
- didFindMemberCall = true;
7832
- 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
+ }
7833
7948
  }
7834
7949
  }
7835
7950
  const record = current;
@@ -7890,7 +8005,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
7890
8005
  return;
7891
8006
  }
7892
8007
  const depsArgumentRaw = node.arguments[depsArgumentIndex];
7893
- const callbackExpression = unwrapExpression$2(callbackArgument);
8008
+ const callbackExpression = unwrapExpression$3(callbackArgument);
7894
8009
  let callbackToAnalyze = null;
7895
8010
  const forcedCaptureKeys = /* @__PURE__ */ new Set();
7896
8011
  if (isNodeOfType(callbackExpression, "ArrowFunctionExpression") || isNodeOfType(callbackExpression, "FunctionExpression")) callbackToAnalyze = callbackExpression;
@@ -7898,7 +8013,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
7898
8013
  const callbackSymbol = context.scopes.symbolFor(callbackExpression);
7899
8014
  const functionValueNode = callbackSymbol ? getFunctionValueNode(callbackSymbol) : null;
7900
8015
  if (functionValueNode) callbackToAnalyze = functionValueNode;
7901
- 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);
7902
8017
  else if (depsArgumentRaw) {
7903
8018
  if (depsArrayContainsIdentifier(depsArgumentRaw, callbackExpression.name)) return;
7904
8019
  context.report({
@@ -7931,11 +8046,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
7931
8046
  message: buildAssignmentMessage(assignment.name)
7932
8047
  });
7933
8048
  if (outerAssignments.length > 0) return;
7934
- const refCurrentName = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
8049
+ const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
7935
8050
  const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
7936
- if (refCurrentName && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentName)) context.report({
8051
+ if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol)) context.report({
7937
8052
  node: callbackToAnalyze,
7938
- message: buildRefCleanupMessage(refCurrentName)
8053
+ message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
7939
8054
  });
7940
8055
  }
7941
8056
  if (!depsArgumentRaw) {
@@ -7955,8 +8070,16 @@ If the missing value is recreated every render, move it inside the hook or stabi
7955
8070
  });
7956
8071
  return;
7957
8072
  }
7958
- const depsArgument = unwrapExpression$2(depsArgumentRaw);
7959
- 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) {
7960
8083
  if (isAutoDependenciesHook(hookName)) return;
7961
8084
  if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) {
7962
8085
  context.report({
@@ -7965,19 +8088,6 @@ If the missing value is recreated every render, move it inside the hook or stabi
7965
8088
  });
7966
8089
  return;
7967
8090
  }
7968
- const nonArrayCaptureKeys = callbackToAnalyze !== null ? new Set(collectCaptureDepKeys(callbackToAnalyze, context.scopes).keys) : /* @__PURE__ */ new Set();
7969
- for (const forcedCaptureKey of forcedCaptureKeys) nonArrayCaptureKeys.add(forcedCaptureKey);
7970
- if (nonArrayCaptureKeys.size > 0) {
7971
- context.report({
7972
- node: depsArgument,
7973
- message: buildNonArrayDepsMessage(hookName)
7974
- });
7975
- context.report({
7976
- node: depsArgument,
7977
- message: buildMissingDepMessage(hookName, [...nonArrayCaptureKeys].join(", "))
7978
- });
7979
- }
7980
- return;
7981
8091
  }
7982
8092
  if (!isNodeOfType(depsArgument, "ArrayExpression")) {
7983
8093
  context.report({
@@ -7996,11 +8106,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
7996
8106
  for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
7997
8107
  const hasLiteralDepElement = depsArgument.elements.some((element) => {
7998
8108
  if (!element) return false;
7999
- return isLiteralOrEmptyTemplate(unwrapExpression$2(element));
8109
+ return isLiteralOrEmptyTemplate(unwrapExpression$3(element));
8000
8110
  });
8001
8111
  const hasNonStringLiteralDep = depsArgument.elements.some((element) => {
8002
8112
  if (!element) return false;
8003
- return isNonStringLiteral(unwrapExpression$2(element));
8113
+ return isNonStringLiteral(unwrapExpression$3(element));
8004
8114
  });
8005
8115
  if (hasNonStringLiteralDep) context.report({
8006
8116
  node: depsArgument,
@@ -8021,7 +8131,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
8021
8131
  });
8022
8132
  continue;
8023
8133
  }
8024
- const stripped = unwrapExpression$2(elementNode);
8134
+ const stripped = unwrapExpression$3(elementNode);
8025
8135
  if (isLiteralOrEmptyTemplate(stripped)) continue;
8026
8136
  if (isNodeOfType(stripped, "Identifier")) {
8027
8137
  const depSymbol = context.scopes.symbolFor(stripped);
@@ -8163,7 +8273,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
8163
8273
  if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
8164
8274
  const rootSymbol = getRootSymbol(reportNode, context.scopes);
8165
8275
  if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
8166
- if (isNodeOfType(unwrapExpression$2(reportNode), "CallExpression")) {
8276
+ if (isNodeOfType(unwrapExpression$3(reportNode), "CallExpression")) {
8167
8277
  context.report({
8168
8278
  node: reportNode,
8169
8279
  message: buildComplexDepMessage(hookName)
@@ -8279,9 +8389,14 @@ const firebaseQueryFilterAsAuth = defineRule({
8279
8389
  * `*Handler`), so the cheap escape-then-replace shape is enough
8280
8390
  * and avoids pulling picomatch into the per-file rule path.
8281
8391
  */
8392
+ const compiledGlobs = /* @__PURE__ */ new Map();
8282
8393
  const compileGlob = (pattern) => {
8394
+ const cached = compiledGlobs.get(pattern);
8395
+ if (cached) return cached;
8283
8396
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
8284
- return new RegExp(`^${escaped}$`);
8397
+ const compiled = new RegExp(`^${escaped}$`);
8398
+ compiledGlobs.set(pattern, compiled);
8399
+ return compiled;
8285
8400
  };
8286
8401
  //#endregion
8287
8402
  //#region src/plugin/rules/react-builtins/forbid-component-props.ts
@@ -8571,7 +8686,7 @@ const DEFAULT_HEADING_TAGS = [
8571
8686
  const resolveSettings$40 = (settings) => {
8572
8687
  const reactDoctor = settings?.["react-doctor"];
8573
8688
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
8574
- return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
8689
+ return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
8575
8690
  };
8576
8691
  const headingHasContent = defineRule({
8577
8692
  id: "heading-has-content",
@@ -8584,7 +8699,7 @@ const headingHasContent = defineRule({
8584
8699
  const settings = resolveSettings$40(context.settings);
8585
8700
  return { JSXOpeningElement(node) {
8586
8701
  const elementType = getElementType(node, context.settings);
8587
- if (!settings.headingTags.includes(elementType)) return;
8702
+ if (!settings.headingTags.has(elementType)) return;
8588
8703
  const parent = node.parent;
8589
8704
  if (parent && isNodeOfType(parent, "JSXElement")) {
8590
8705
  if (objectHasAccessibleChild(parent, context.settings)) return;
@@ -9182,9 +9297,9 @@ const resolveSettings$37 = (settings) => {
9182
9297
  };
9183
9298
  };
9184
9299
  const isWordBoundary = (text, start, end) => {
9185
- const isAlphanumeric = (charCode) => charCode >= 48 && charCode <= 57 || charCode >= 65 && charCode <= 90 || charCode >= 97 && charCode <= 122;
9186
- const startsBoundary = start === 0 || !isAlphanumeric(text.charCodeAt(start - 1));
9187
- 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));
9188
9303
  return startsBoundary && endsBoundary;
9189
9304
  };
9190
9305
  const containsRedundantWord = (altText, words) => {
@@ -9223,10 +9338,10 @@ const imgRedundantAlt = defineRule({
9223
9338
  if (isGeneratedImageRenderContext(context)) return {};
9224
9339
  const settings = resolveSettings$37(context.settings);
9225
9340
  return { JSXOpeningElement(node) {
9226
- if (isGeneratedImageRenderContext(context, node)) return;
9227
9341
  const tag = getElementType(node, context.settings);
9228
9342
  if (!settings.components.includes(tag)) return;
9229
9343
  if (isHiddenFromScreenReader(node, context.settings)) return;
9344
+ if (isGeneratedImageRenderContext(context, node)) return;
9230
9345
  const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
9231
9346
  if (!altAttribute) return;
9232
9347
  if (altValueRedundant(altAttribute, settings.words)) context.report({
@@ -9281,6 +9396,7 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
9281
9396
  const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
9282
9397
  const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
9283
9398
  const SECURITY_CONTEXT_WINDOW_CHARS = 250;
9399
+ const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
9284
9400
  const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
9285
9401
  for (const callMatch of content.matchAll(pattern)) {
9286
9402
  const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
@@ -9310,6 +9426,7 @@ const insecureCryptoRisk = defineRule({
9310
9426
  if (!isProductionSourcePath(file.relativePath)) return [];
9311
9427
  if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
9312
9428
  if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
9429
+ if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
9313
9430
  const content = getScannableContent(file);
9314
9431
  let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
9315
9432
  if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
@@ -9488,6 +9605,7 @@ const KEYBOARD_EVENT_HANDLERS = [
9488
9605
  "onKeyUp"
9489
9606
  ];
9490
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()));
9491
9609
  //#endregion
9492
9610
  //#region src/plugin/utils/is-disabled-element.ts
9493
9611
  const isDisabledElement = (openingElement) => {
@@ -9542,15 +9660,25 @@ const interactiveSupportsFocus = defineRule({
9542
9660
  const settings = resolveSettings$36(context.settings);
9543
9661
  const tabbableSet = new Set(settings.tabbable);
9544
9662
  return { JSXOpeningElement(node) {
9663
+ if (node.attributes.length === 0) return;
9545
9664
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
9546
9665
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
9547
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;
9548
9678
  const elementType = getElementType(node, context.settings);
9549
9679
  if (!HTML_TAGS.has(elementType)) return;
9550
- const hasInteractiveHandler = ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)));
9680
+ if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9551
9681
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
9552
- if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9553
- if (!role) return;
9554
9682
  if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
9555
9683
  const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
9556
9684
  context.report({
@@ -9568,7 +9696,7 @@ const isAtomFromJotai = (callExpression) => {
9568
9696
  if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
9569
9697
  return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
9570
9698
  };
9571
- 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")));
9572
9700
  const getFirstParameterName = (fn) => {
9573
9701
  const parameters = fn.params ?? [];
9574
9702
  if (parameters.length !== 1) return null;
@@ -9701,7 +9829,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
9701
9829
  const args = node.arguments ?? [];
9702
9830
  if (args.length === 0) return;
9703
9831
  const reader = args[0];
9704
- if (!isFunctionExpressionLike$1(reader)) return;
9832
+ if (!isFunctionExpressionLike$2(reader)) return;
9705
9833
  const getParameterName = getFirstParameterName(reader);
9706
9834
  if (!getParameterName) return;
9707
9835
  const freshReturn = getFreshReturnForFunction(reader);
@@ -9799,14 +9927,14 @@ const getHandlerNamedBindingName = (functionNode) => {
9799
9927
  const containingFunctionIsComponentOrHook = (functionNode) => {
9800
9928
  if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
9801
9929
  const declaredName = functionNode.id.name;
9802
- return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN.test(declaredName);
9930
+ return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN$1.test(declaredName);
9803
9931
  }
9804
9932
  let cursor = functionNode.parent ?? null;
9805
9933
  while (cursor && isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
9806
9934
  if (!cursor) return false;
9807
9935
  if (!isNodeOfType(cursor, "VariableDeclarator")) return false;
9808
9936
  if (!isNodeOfType(cursor.id, "Identifier")) return false;
9809
- 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);
9810
9938
  };
9811
9939
  const isBindingInvokedOnRenderPath = (root, bindingName) => {
9812
9940
  let didFindRenderPathInvocation = false;
@@ -10189,8 +10317,6 @@ const jsCachePropertyAccess = defineRule({
10189
10317
  walkAst(loopBody, (child) => {
10190
10318
  if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
10191
10319
  if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
10192
- });
10193
- walkAst(loopBody, (child) => {
10194
10320
  if (!isNodeOfType(child, "MemberExpression")) return;
10195
10321
  if (child.computed) return;
10196
10322
  if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
@@ -10650,7 +10776,6 @@ const jsHoistIntl = defineRule({
10650
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",
10651
10777
  create: (context) => ({ NewExpression(node) {
10652
10778
  if (!isIntlNewExpression(node)) return;
10653
- if (isInsideCacheMemo(node)) return;
10654
10779
  let cursor = node.parent ?? null;
10655
10780
  let inFunctionBody = false;
10656
10781
  while (cursor) {
@@ -10667,6 +10792,7 @@ const jsHoistIntl = defineRule({
10667
10792
  cursor = cursor.parent ?? null;
10668
10793
  }
10669
10794
  if (!inFunctionBody) return;
10795
+ if (isInsideCacheMemo(node)) return;
10670
10796
  const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
10671
10797
  context.report({
10672
10798
  node,
@@ -10741,7 +10867,11 @@ const isSingleFieldEqualityPredicate = (node) => {
10741
10867
  if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
10742
10868
  return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
10743
10869
  };
10744
- 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();
10745
10875
  if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
10746
10876
  if (isNodeOfType(child, "Identifier")) names.add(child.name);
10747
10877
  });
@@ -10758,6 +10888,8 @@ const collectLoopBoundNames = (loop, names) => {
10758
10888
  if (targetRoot) names.add(targetRoot);
10759
10889
  }
10760
10890
  });
10891
+ loopBoundNamesCache.set(loop, names);
10892
+ return names;
10761
10893
  };
10762
10894
  const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
10763
10895
  let cursor = receiver;
@@ -10783,7 +10915,7 @@ const isLoopVariantReceiver = (node) => {
10783
10915
  const loopBoundNames = /* @__PURE__ */ new Set();
10784
10916
  let ancestor = node.parent;
10785
10917
  while (ancestor) {
10786
- 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);
10787
10919
  ancestor = ancestor.parent;
10788
10920
  }
10789
10921
  if (loopBoundNames.has(receiverRoot)) return true;
@@ -14391,14 +14523,14 @@ const keyLifecycleRisk = defineRule({
14391
14523
  //#region src/plugin/rules/a11y/label-has-associated-control.ts
14392
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`.";
14393
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.";
14394
- const DEFAULT_CONTROL_COMPONENTS = [
14526
+ const DEFAULT_CONTROL_COMPONENTS = new Set([
14395
14527
  "input",
14396
14528
  "meter",
14397
14529
  "output",
14398
14530
  "progress",
14399
14531
  "select",
14400
14532
  "textarea"
14401
- ];
14533
+ ]);
14402
14534
  const DEFAULT_LABEL_ATTRIBUTES = [
14403
14535
  "alt",
14404
14536
  "aria-label",
@@ -14410,8 +14542,8 @@ const resolveSettings$24 = (settings) => {
14410
14542
  const jsxA11y = settings?.["jsx-a11y"];
14411
14543
  const forAttributes = (typeof jsxA11y === "object" && jsxA11y !== null ? jsxA11y : {}).attributes?.for ?? ["htmlFor"];
14412
14544
  return {
14413
- labelComponents: ["label", ...ruleSettings.labelComponents ?? []].sort(),
14414
- 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 ?? []]),
14415
14547
  controlComponents: ruleSettings.controlComponents ?? [],
14416
14548
  assert: ruleSettings.assert ?? "either",
14417
14549
  depth: Math.min(ruleSettings.depth ?? 5, 25),
@@ -14419,7 +14551,7 @@ const resolveSettings$24 = (settings) => {
14419
14551
  };
14420
14552
  };
14421
14553
  const isControlComponent = (tagName, controlComponents) => {
14422
- if (DEFAULT_CONTROL_COMPONENTS.includes(tagName)) return true;
14554
+ if (DEFAULT_CONTROL_COMPONENTS.has(tagName)) return true;
14423
14555
  return controlComponents.some((pattern) => compileGlob(pattern).test(tagName));
14424
14556
  };
14425
14557
  const searchForNestedControl = (child, currentDepth, searchContext) => {
@@ -14445,7 +14577,7 @@ const searchForAccessibleLabel = (child, currentDepth, searchContext) => {
14445
14577
  const attributeName = attribute.name;
14446
14578
  if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
14447
14579
  const propName = getJsxAttributeName(attributeName);
14448
- if (!propName || !searchContext.labelAttributes.includes(propName)) continue;
14580
+ if (!propName || !searchContext.labelAttributes.has(propName)) continue;
14449
14581
  const attributeValue = attribute.value;
14450
14582
  if (!attributeValue) continue;
14451
14583
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
@@ -14467,7 +14599,7 @@ const hasAccessibleLabel = (element, searchContext) => {
14467
14599
  const attributeName = attribute.name;
14468
14600
  if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
14469
14601
  const propName = getJsxAttributeName(attributeName);
14470
- if (propName && searchContext.labelAttributes.includes(propName)) return true;
14602
+ if (propName && searchContext.labelAttributes.has(propName)) return true;
14471
14603
  }
14472
14604
  for (const child of element.children) if (searchForAccessibleLabel(child, 1, searchContext)) return true;
14473
14605
  return false;
@@ -14490,7 +14622,7 @@ const labelHasAssociatedControl = defineRule({
14490
14622
  if (isTestlikeFile) return;
14491
14623
  const opening = node.openingElement;
14492
14624
  const tagName = getElementType(opening, context.settings);
14493
- if (!settings.labelComponents.includes(tagName)) return;
14625
+ if (!settings.labelComponents.has(tagName)) return;
14494
14626
  const hasHtmlFor = settings.forAttributes.some((attributeName) => Boolean(hasJsxPropIgnoreCase(opening.attributes, attributeName)));
14495
14627
  const searchContext = {
14496
14628
  depth: settings.depth,
@@ -15079,9 +15211,9 @@ const resolveSettings$23 = (settings) => {
15079
15211
  const reactDoctor = settings?.["react-doctor"];
15080
15212
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.mediaHasCaption ?? {} : {};
15081
15213
  return {
15082
- audio: [...DEFAULT_AUDIO, ...ruleSettings.audio ?? []],
15083
- video: [...DEFAULT_VIDEO, ...ruleSettings.video ?? []],
15084
- 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 ?? []])
15085
15217
  };
15086
15218
  };
15087
15219
  const evaluateMuted = (attribute) => {
@@ -15110,7 +15242,7 @@ const childMayRenderTrack = (child, trackTags, settings) => {
15110
15242
  let rendersCaptionTrack = false;
15111
15243
  walkAst(expression, (inner) => {
15112
15244
  if (rendersCaptionTrack) return false;
15113
- 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)) {
15114
15246
  rendersCaptionTrack = true;
15115
15247
  return false;
15116
15248
  }
@@ -15128,7 +15260,7 @@ const mediaHasCaption = defineRule({
15128
15260
  const settings = resolveSettings$23(context.settings);
15129
15261
  return { JSXOpeningElement(node) {
15130
15262
  const tag = getElementType(node, context.settings);
15131
- if (!(settings.audio.includes(tag) || settings.video.includes(tag))) return;
15263
+ if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
15132
15264
  if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
15133
15265
  const parent = node.parent;
15134
15266
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
@@ -15143,7 +15275,7 @@ const mediaHasCaption = defineRule({
15143
15275
  if (!isNodeOfType(child, "JSXElement")) return false;
15144
15276
  const opening = child.openingElement;
15145
15277
  const childTag = getElementType(opening, context.settings);
15146
- if (!settings.track.includes(childTag)) return false;
15278
+ if (!settings.track.has(childTag)) return false;
15147
15279
  const kindAttribute = hasJsxPropIgnoreCase(opening.attributes, "kind");
15148
15280
  if (!kindAttribute) return false;
15149
15281
  let kindValue = kindAttribute.value;
@@ -15454,16 +15586,30 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
15454
15586
  const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
15455
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;
15456
15588
  const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
15457
- const doesSourceTextExportName = (sourceText, exportedName) => {
15589
+ const collectSourceTextExportNames = (sourceText) => {
15458
15590
  const strippedSource = stripJsComments(sourceText);
15459
- if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
15460
- for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
15461
- 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;
15462
- 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;
15463
15599
  };
15600
+ const exportNamesCache = /* @__PURE__ */ new Map();
15464
15601
  const doesModuleExportName = (filePath, exportedName) => {
15465
15602
  try {
15466
- 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);
15467
15613
  } catch {
15468
15614
  return false;
15469
15615
  }
@@ -15502,6 +15648,7 @@ const nextjsMissingMetadata = defineRule({
15502
15648
  const filename = normalizeFilename(context.filename ?? "");
15503
15649
  if (!PAGE_FILE_PATTERN.test(filename)) return;
15504
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;
15505
15652
  if (programNode.body?.some((statement) => {
15506
15653
  if (!isNodeOfType(statement, "ExportNamedDeclaration")) return false;
15507
15654
  const declaration = statement.declaration;
@@ -15608,8 +15755,12 @@ const containsFetchCall = (node, options) => {
15608
15755
  const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
15609
15756
  let didFindFetchCall = false;
15610
15757
  walkAst(node, (child) => {
15758
+ if (didFindFetchCall) return false;
15611
15759
  if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
15612
- if (isFetchCall$1(child)) didFindFetchCall = true;
15760
+ if (isFetchCall$1(child)) {
15761
+ didFindFetchCall = true;
15762
+ return false;
15763
+ }
15613
15764
  });
15614
15765
  return didFindFetchCall;
15615
15766
  };
@@ -15623,6 +15774,8 @@ const nextjsNoClientFetchForServerData = defineRule({
15623
15774
  severity: "warn",
15624
15775
  recommendation: "Remove 'use client' and fetch directly in the Server Component. No API round-trip, and secrets stay on the server.",
15625
15776
  create: (context) => {
15777
+ const filename = normalizeFilename(context.filename ?? "");
15778
+ if (!(PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages"))) return {};
15626
15779
  let fileHasUseClient = false;
15627
15780
  return {
15628
15781
  Program(programNode) {
@@ -15632,8 +15785,7 @@ const nextjsNoClientFetchForServerData = defineRule({
15632
15785
  if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
15633
15786
  const callback = getEffectCallback(node);
15634
15787
  if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
15635
- const filename = normalizeFilename(context.filename ?? "");
15636
- if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
15788
+ context.report({
15637
15789
  node,
15638
15790
  message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
15639
15791
  });
@@ -16298,16 +16450,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
16298
16450
  recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
16299
16451
  create: (context) => {
16300
16452
  let resolveBinding = () => null;
16453
+ let isRouteHandlerFile = false;
16454
+ let mutatingSegment = null;
16301
16455
  return {
16302
16456
  Program(node) {
16303
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;
16304
16461
  },
16305
16462
  ExportNamedDeclaration(node) {
16306
- const filename = normalizeFilename(context.filename ?? "");
16307
- if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
16308
- if (CRON_ROUTE_PATTERN.test(filename)) return;
16463
+ if (!isRouteHandlerFile) return;
16309
16464
  if (!isExportedGetHandler(node)) return;
16310
- const mutatingSegment = extractMutatingRouteSegment(filename);
16311
16465
  const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
16312
16466
  for (const handlerBody of handlerBodies) {
16313
16467
  const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
@@ -17224,14 +17378,38 @@ const resolvesToRefFactoryCall = (identifier) => {
17224
17378
  });
17225
17379
  return found;
17226
17380
  };
17227
- 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()) => {
17228
17406
  if (!receiver) return false;
17229
- if (isNodeOfType(receiver, "ChainExpression")) return isRefLikeReceiver(receiver.expression);
17230
- if (isNodeOfType(receiver, "TSNonNullExpression")) return isRefLikeReceiver(receiver.expression);
17231
- 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);
17232
17410
  if (isNodeOfType(receiver, "MemberExpression") && isNodeOfType(receiver.property, "Identifier")) {
17233
17411
  if (hasRefLikeName(receiver.property.name)) return true;
17234
- if (receiver.property.name === "current") return isRefLikeReceiver(receiver.object);
17412
+ if (receiver.property.name === "current") return isRefLikeReceiver(receiver.object, visitedAliasNames);
17235
17413
  }
17236
17414
  return false;
17237
17415
  };
@@ -17339,8 +17517,15 @@ const getProgramAnalysis = (anyNode) => {
17339
17517
  programToAnalysis.set(programNode, analysis);
17340
17518
  return analysis;
17341
17519
  };
17520
+ const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
17342
17521
  const getScopeForNode = (node, manager) => {
17343
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;
17344
17529
  let bestScope = null;
17345
17530
  let bestSize = Infinity;
17346
17531
  for (const scope of manager.scopes) {
@@ -17353,11 +17538,25 @@ const getScopeForNode = (node, manager) => {
17353
17538
  bestScope = scope;
17354
17539
  }
17355
17540
  }
17541
+ scopeByNode.set(node, bestScope);
17356
17542
  return bestScope;
17357
17543
  };
17358
17544
  //#endregion
17359
17545
  //#region src/plugin/rules/state-and-effects/utils/effect/ast.ts
17360
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
+ };
17361
17560
  const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
17362
17561
  if (visited.has(ref)) return;
17363
17562
  const result = visit(ref);
@@ -17370,7 +17569,10 @@ const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
17370
17569
  const defNode = def.node;
17371
17570
  const next = defNode.init ?? defNode.body;
17372
17571
  if (!next) continue;
17373
- 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
+ }
17374
17576
  }
17375
17577
  };
17376
17578
  const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
@@ -17387,11 +17589,20 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
17387
17589
  } else if (isAstNode(child)) descend(child, visit, visited);
17388
17590
  }
17389
17591
  };
17592
+ const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
17390
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;
17391
17601
  const refs = [];
17392
17602
  ascend(analysis, ref, (upRef) => {
17393
17603
  refs.push(upRef);
17394
17604
  });
17605
+ upstreamByRef.set(ref, refs);
17395
17606
  return refs;
17396
17607
  };
17397
17608
  const findDownstreamNodes = (topNode, type) => {
@@ -17401,11 +17612,24 @@ const findDownstreamNodes = (topNode, type) => {
17401
17612
  });
17402
17613
  return nodes;
17403
17614
  };
17615
+ const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
17404
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;
17405
17624
  const scope = getScopeForNode(identifier, analysis.scopeManager);
17406
- if (!scope) return null;
17407
- for (const reference of scope.references) if (reference.identifier === identifier) return reference;
17408
- 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;
17409
17633
  };
17410
17634
  const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
17411
17635
  const getDownstreamRefs = (analysis, node) => {
@@ -17480,22 +17704,6 @@ const isEventualCallTo = (analysis, ref, predicate) => {
17480
17704
  };
17481
17705
  //#endregion
17482
17706
  //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
17483
- const getOuterScopeContaining = (analysis, node) => {
17484
- if (!node.range) return null;
17485
- let best = null;
17486
- let bestSize = Infinity;
17487
- for (const scope of analysis.scopeManager.scopes) {
17488
- const block = scope.block;
17489
- if (!block?.range) continue;
17490
- if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
17491
- const size = block.range[1] - block.range[0];
17492
- if (size <= bestSize) {
17493
- bestSize = size;
17494
- best = scope;
17495
- }
17496
- }
17497
- return best;
17498
- };
17499
17707
  const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
17500
17708
  const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
17501
17709
  const isReactFunctionalComponent = (node) => {
@@ -17526,7 +17734,7 @@ const isReactFunctionalHOC = (analysis, node) => {
17526
17734
  const isWrappedSeparately = () => {
17527
17735
  if (!isNodeOfType(node.id, "Identifier")) return false;
17528
17736
  const bindingName = node.id.name;
17529
- const containingScope = getOuterScopeContaining(analysis, node);
17737
+ const containingScope = getScopeForNode(node, analysis.scopeManager);
17530
17738
  if (!containingScope) return false;
17531
17739
  const variable = containingScope.variables.find((v) => v.name === bindingName);
17532
17740
  if (!variable) return false;
@@ -17813,7 +18021,8 @@ const noAriaHiddenOnFocusable = defineRule({
17813
18021
  if (isNodeOfType(value, "Literal") && value.value !== "true") return;
17814
18022
  if (isNodeOfType(value, "JSXExpressionContainer")) {
17815
18023
  const expression = value.expression;
17816
- if (isNodeOfType(expression, "Literal") && !expression.value) return;
18024
+ if (!isNodeOfType(expression, "Literal")) return;
18025
+ if (expression.value !== true && expression.value !== "true") return;
17817
18026
  }
17818
18027
  }
17819
18028
  const tag = getElementType(node, context.settings);
@@ -18068,10 +18277,30 @@ const isArrayFromCall = (node) => {
18068
18277
  *
18069
18278
  * Used both for `<receiver>.map(...)` and for `Array.from(<length>, fn)`.
18070
18279
  */
18071
- 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) => {
18072
18294
  if (isArrayFromCall(receiver)) return true;
18073
18295
  if (isArrayConstructorCallWithNumericLength(receiver)) return true;
18074
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
+ }
18075
18304
  if (isNodeOfType(receiver, "CallExpression")) {
18076
18305
  const callee = receiver.callee;
18077
18306
  if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "fill" && isArrayConstructorCallWithNumericLength(callee.object)) return true;
@@ -18097,6 +18326,7 @@ const isArrayFromLengthObjectCall = (node) => {
18097
18326
  if (!(isNodeOfType(key, "Identifier") && key.name === "length" || isNodeOfType(key, "Literal") && key.value === "length")) continue;
18098
18327
  if (isNumericLiteralOrUndefined(prop.value)) return true;
18099
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;
18100
18330
  }
18101
18331
  return false;
18102
18332
  };
@@ -18205,10 +18435,11 @@ const isInsideStaticPlaceholderMap = (node) => isInsideIteratorMapMatching(node,
18205
18435
  /**
18206
18436
  * Walk up from a JSXAttribute node looking for the enclosing iterator
18207
18437
  * callback (`.map(cb)`, `.flatMap(cb)`, `.forEach(cb)`, `Array.from(_, cb)`)
18208
- * and return the first parameter's name. The first param is the per-item
18209
- * 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) => …)`.
18210
18441
  */
18211
- const findIteratorItemName$1 = (node) => {
18442
+ const findIteratorItemNames = (node) => {
18212
18443
  let current = node;
18213
18444
  while (current.parent) {
18214
18445
  const parent = current.parent;
@@ -18218,18 +18449,20 @@ const findIteratorItemName$1 = (node) => {
18218
18449
  const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
18219
18450
  if (isIteratorMethodCall || isArrayFromCallback) {
18220
18451
  const first = (current.params ?? [])[0];
18221
- if (first && isNodeOfType(first, "Identifier")) return first.name;
18222
- return null;
18452
+ if (!first) return null;
18453
+ const names = /* @__PURE__ */ new Set();
18454
+ collectPatternNames(first, names);
18455
+ return names.size > 0 ? names : null;
18223
18456
  }
18224
18457
  }
18225
18458
  current = parent;
18226
18459
  }
18227
18460
  return null;
18228
18461
  };
18229
- const templateLiteralHasIteratorIdentity = (template, itemName) => {
18462
+ const templateLiteralHasIteratorIdentity = (template, itemNames) => {
18230
18463
  for (const expression of template.expressions ?? []) {
18231
- if (isNodeOfType(expression, "Identifier") && expression.name === itemName) return true;
18232
- 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;
18233
18466
  }
18234
18467
  return false;
18235
18468
  };
@@ -18242,9 +18475,32 @@ const templateLiteralHasIteratorIdentity = (template, itemName) => {
18242
18475
  const isCompositeKeyWithIteratorIdentity = (keyExpression, attributeNode) => {
18243
18476
  if (!isNodeOfType(keyExpression, "TemplateLiteral")) return false;
18244
18477
  if ((keyExpression.expressions ?? []).length < 2) return false;
18245
- const itemName = findIteratorItemName$1(attributeNode);
18246
- if (!itemName) return false;
18247
- 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;
18248
18504
  };
18249
18505
  const noArrayIndexAsKey = defineRule({
18250
18506
  id: "no-array-index-as-key",
@@ -18256,6 +18512,7 @@ const noArrayIndexAsKey = defineRule({
18256
18512
  if (!node.value || !isNodeOfType(node.value, "JSXExpressionContainer")) return;
18257
18513
  const indexName = extractIndexName(node.value.expression);
18258
18514
  if (!indexName) return;
18515
+ if (isNumericForLoopCounter(node, indexName)) return;
18259
18516
  if (isInsideStaticPlaceholderMap(node)) return;
18260
18517
  if (isInsideStringDerivedMap(node)) return;
18261
18518
  if (isCompositeKeyWithIteratorIdentity(node.value.expression, node)) return;
@@ -18876,7 +19133,17 @@ const containsRenderOutput = (node, rootNode, scopes) => {
18876
19133
  }
18877
19134
  return false;
18878
19135
  };
18879
- const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
19136
+ const renderOutputCache = /* @__PURE__ */ new WeakMap();
19137
+ const functionContainsReactRenderOutput = (functionNode, scopes) => {
19138
+ const cachedEntry = renderOutputCache.get(functionNode);
19139
+ if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
19140
+ const hasRenderOutput = containsRenderOutput(functionNode, functionNode, scopes);
19141
+ renderOutputCache.set(functionNode, {
19142
+ scopes,
19143
+ hasRenderOutput
19144
+ });
19145
+ return hasRenderOutput;
19146
+ };
18880
19147
  //#endregion
18881
19148
  //#region src/plugin/utils/is-component-declaration.ts
18882
19149
  const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
@@ -19381,8 +19648,8 @@ const CONTEXT_MODULES = [
19381
19648
  ];
19382
19649
  const isCreateContextCallee = (callee) => {
19383
19650
  if (isNodeOfType(callee, "Identifier")) {
19384
- for (const moduleName of CONTEXT_MODULES) if (getImportedNameFromModule(callee, callee.name, moduleName) === "createContext") return true;
19385
- return false;
19651
+ const binding = getImportBindingForName(callee, callee.name);
19652
+ return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
19386
19653
  }
19387
19654
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
19388
19655
  const namespaceIdentifier = callee.object;
@@ -19392,8 +19659,8 @@ const isCreateContextCallee = (callee) => {
19392
19659
  if (propertyIdentifier.name !== "createContext") return false;
19393
19660
  const namespaceName = namespaceIdentifier.name;
19394
19661
  if (isCanonicalReactNamespaceName(namespaceName)) return true;
19395
- for (const moduleName of CONTEXT_MODULES) if (isImportedFromModule(namespaceIdentifier, namespaceName, moduleName)) return true;
19396
- return false;
19662
+ const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
19663
+ return importSource !== null && CONTEXT_MODULES.includes(importSource);
19397
19664
  }
19398
19665
  return false;
19399
19666
  };
@@ -19795,38 +20062,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
19795
20062
  };
19796
20063
  const parseColorToRgb = (value) => {
19797
20064
  const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
19798
- const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
19799
- if (hex8Match) return {
19800
- red: parseInt(hex8Match[1], 16),
19801
- green: parseInt(hex8Match[2], 16),
19802
- blue: parseInt(hex8Match[3], 16)
19803
- };
19804
- const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
19805
- if (hex6Match) return {
19806
- red: parseInt(hex6Match[1], 16),
19807
- green: parseInt(hex6Match[2], 16),
19808
- blue: parseInt(hex6Match[3], 16)
19809
- };
19810
- const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
19811
- if (hex4Match) return {
19812
- red: parseInt(hex4Match[1] + hex4Match[1], 16),
19813
- green: parseInt(hex4Match[2] + hex4Match[2], 16),
19814
- blue: parseInt(hex4Match[3] + hex4Match[3], 16)
19815
- };
19816
- const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
19817
- if (hex3Match) return {
19818
- red: parseInt(hex3Match[1] + hex3Match[1], 16),
19819
- green: parseInt(hex3Match[2] + hex3Match[2], 16),
19820
- blue: parseInt(hex3Match[3] + hex3Match[3], 16)
19821
- };
19822
- const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
19823
- if (rgbMatch) return {
19824
- red: parseInt(rgbMatch[1], 10),
19825
- green: parseInt(rgbMatch[2], 10),
19826
- blue: parseInt(rgbMatch[3], 10)
19827
- };
19828
- const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
19829
- 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
+ }
19830
20103
  return null;
19831
20104
  };
19832
20105
  //#endregion
@@ -19854,9 +20127,18 @@ const extractColorFromShadowLayer = (layer) => {
19854
20127
  if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
19855
20128
  return null;
19856
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;
19857
20134
  const parseShadowLayerBlur = (layer) => {
19858
- const numericTokens = [...layer.replace(/rgba?\([^)]*\)/g, "").replace(/#[0-9a-f]{3,8}\b/gi, "").matchAll(/(\d+(?:\.\d+)?)(px)?/g)].map((match) => parseFloat(match[1]));
19859
- 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;
19860
20142
  };
19861
20143
  const hasColoredGlowShadow = (shadowValue) => {
19862
20144
  for (const layer of splitShadowLayers(shadowValue)) {
@@ -19976,7 +20258,10 @@ const isIntrinsicJsxAttribute = (node) => {
19976
20258
  };
19977
20259
  //#endregion
19978
20260
  //#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
20261
+ const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
19979
20262
  const collectComponentPropNames = (componentFunction) => {
20263
+ const cached = componentPropNamesCache.get(componentFunction);
20264
+ if (cached) return cached;
19980
20265
  const propNames = /* @__PURE__ */ new Set();
19981
20266
  if (!isFunctionLike$1(componentFunction)) return propNames;
19982
20267
  const propsObjectParamNames = /* @__PURE__ */ new Set();
@@ -19990,27 +20275,23 @@ const collectComponentPropNames = (componentFunction) => {
19990
20275
  if (child !== componentBody && isFunctionLike$1(child)) return false;
19991
20276
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
19992
20277
  });
20278
+ componentPropNamesCache.set(componentFunction, propNames);
19993
20279
  return propNames;
19994
20280
  };
19995
- 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;
19996
20285
  const boundNames = /* @__PURE__ */ new Set();
19997
20286
  if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
19998
- if (boundNames.has(bindingName)) return true;
19999
- let declaresName = false;
20000
20287
  walkAst(functionNode, (child) => {
20001
- if (declaresName) return false;
20002
20288
  if (child !== functionNode && isFunctionLike$1(child)) return false;
20003
- if (isNodeOfType(child, "VariableDeclarator")) {
20004
- const declaratorNames = /* @__PURE__ */ new Set();
20005
- collectPatternNames(child.id, declaratorNames);
20006
- if (declaratorNames.has(bindingName)) {
20007
- declaresName = true;
20008
- return false;
20009
- }
20010
- }
20289
+ if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
20011
20290
  });
20012
- return declaresName;
20291
+ ownScopeBoundNamesCache.set(functionNode, boundNames);
20292
+ return boundNames;
20013
20293
  };
20294
+ const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
20014
20295
  const isPropertyNamePosition = (identifier) => {
20015
20296
  const parent = identifier.parent;
20016
20297
  if (!parent) return false;
@@ -20433,36 +20714,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
20433
20714
  }
20434
20715
  return hasNestedFunction;
20435
20716
  };
20436
- const isReseededDraftBuffer = (useStateCall, isPropName) => {
20437
- const setterName = getStateSetterName(useStateCall);
20438
- if (!setterName) return false;
20439
- const componentFunction = findEnclosingFunction(useStateCall);
20440
- if (!componentFunction) return false;
20441
- let isReseeded = false;
20442
- walkAst(componentFunction, (child) => {
20443
- if (isReseeded) return false;
20444
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
20445
- isReseeded = true;
20446
- return false;
20447
- }
20448
- });
20449
- 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;
20450
20724
  };
20451
- const isAdjustedDuringRender = (useStateCall, isPropName) => {
20725
+ const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
20452
20726
  const setterName = getStateSetterName(useStateCall);
20453
20727
  if (!setterName) return false;
20454
20728
  const componentFunction = findEnclosingFunction(useStateCall);
20455
20729
  if (!componentFunction) return false;
20456
- let isAdjusted = false;
20730
+ let isExempt = false;
20457
20731
  walkAst(componentFunction, (child) => {
20458
- if (isAdjusted) return false;
20459
- if (child !== componentFunction && isFunctionLike$1(child)) return false;
20460
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName)) {
20461
- 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;
20462
20735
  return false;
20463
20736
  }
20464
20737
  });
20465
- return isAdjusted;
20738
+ return isExempt;
20466
20739
  };
20467
20740
  const noDerivedUseState = defineRule({
20468
20741
  id: "no-derived-useState",
@@ -20479,8 +20752,7 @@ const noDerivedUseState = defineRule({
20479
20752
  const initializer = node.arguments[0];
20480
20753
  if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
20481
20754
  if (isInitialOnlyPropName(initializer.name)) return;
20482
- if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20483
- if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20755
+ if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20484
20756
  context.report({
20485
20757
  node,
20486
20758
  message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
@@ -20491,8 +20763,7 @@ const noDerivedUseState = defineRule({
20491
20763
  const rootIdentifierName = getRootIdentifierName(initializer);
20492
20764
  if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
20493
20765
  if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
20494
- if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20495
- if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20766
+ if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20496
20767
  context.report({
20497
20768
  node,
20498
20769
  message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
@@ -20607,7 +20878,7 @@ const isStateMemberExpression = (node) => {
20607
20878
  };
20608
20879
  //#endregion
20609
20880
  //#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
20610
- 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.";
20611
20882
  const shouldIgnoreMutation = (node) => {
20612
20883
  let isConstructor = false;
20613
20884
  let isInsideCallExpression = false;
@@ -20701,6 +20972,18 @@ const initializerMarksPlainState = (initializerArgument) => {
20701
20972
  }
20702
20973
  return producesPlainStateValue(unwrapped);
20703
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
+ };
20704
20987
  const collectFunctionLocalBindings = (functionNode) => {
20705
20988
  const localBindings = /* @__PURE__ */ new Set();
20706
20989
  if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return localBindings;
@@ -20743,8 +21026,10 @@ const noDirectStateMutation = defineRule({
20743
21026
  const bindings = collectUseStateBindings(componentBody);
20744
21027
  if (bindings.length === 0) return;
20745
21028
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
21029
+ const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
20746
21030
  const plainObjectStateValueNames = /* @__PURE__ */ new Set();
20747
21031
  for (const binding of bindings) {
21032
+ if (callbackRefSetterNames.has(binding.setterName)) continue;
20748
21033
  if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
20749
21034
  if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
20750
21035
  }
@@ -20757,7 +21042,7 @@ const noDirectStateMutation = defineRule({
20757
21042
  if (currentlyShadowed.has(rootName)) return;
20758
21043
  context.report({
20759
21044
  node: child,
20760
- 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.`
20761
21046
  });
20762
21047
  return;
20763
21048
  }
@@ -20773,7 +21058,7 @@ const noDirectStateMutation = defineRule({
20773
21058
  if (currentlyShadowed.has(rootName)) return;
20774
21059
  context.report({
20775
21060
  node: child,
20776
- 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.`
20777
21062
  });
20778
21063
  }
20779
21064
  });
@@ -22191,20 +22476,20 @@ const getTriggerGuardRootName = (testNode) => {
22191
22476
  return null;
22192
22477
  };
22193
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
+ });
22194
22491
  const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
22195
- for (const binding of useStateBindings) {
22196
- let didFindAnySetterCall = false;
22197
- let areAllSetterCallsInHandlers = true;
22198
- walkAst(componentBody, (child) => {
22199
- if (!areAllSetterCallsInHandlers) return false;
22200
- if (!isNodeOfType(child, "CallExpression")) return;
22201
- if (!isNodeOfType(child.callee, "Identifier")) return;
22202
- if (child.callee.name !== binding.setterName) return;
22203
- didFindAnySetterCall = true;
22204
- if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
22205
- });
22206
- if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
22207
- }
22492
+ for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
22208
22493
  return handlerOnlyWriteStateNames;
22209
22494
  };
22210
22495
  const noEventTriggerState = defineRule({
@@ -22544,6 +22829,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
22544
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)-)/;
22545
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)-)/;
22546
22831
  const splitVariantScope = (token) => {
22832
+ if (!token.includes(":")) return {
22833
+ scope: "",
22834
+ utility: token.startsWith("!") ? token.slice(1) : token
22835
+ };
22547
22836
  const segments = token.split(":");
22548
22837
  const rawUtility = segments[segments.length - 1];
22549
22838
  return {
@@ -22567,6 +22856,7 @@ const noGrayOnColoredBackground = defineRule({
22567
22856
  const bgColorScopes = /* @__PURE__ */ new Set();
22568
22857
  for (const token of classStr.split(/\s+/)) {
22569
22858
  if (!token) continue;
22859
+ if (!token.includes("text-") && !token.includes("bg-")) continue;
22570
22860
  const { scope, utility } = splitVariantScope(token);
22571
22861
  if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
22572
22862
  if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
@@ -22637,11 +22927,16 @@ const NON_DETERMINISTIC_ID_GENERATOR_NAMES = new Set([
22637
22927
  "ulid",
22638
22928
  "createId"
22639
22929
  ]);
22930
+ const isZeroArgDateConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Date" && (node.arguments?.length ?? 0) === 0;
22640
22931
  const containsNonDeterministicSource = (root) => {
22641
22932
  let found = false;
22642
22933
  walkAst(root, (child) => {
22643
22934
  if (found) return false;
22644
22935
  if (isFunctionLike$1(child)) return false;
22936
+ if (isZeroArgDateConstruction(child)) {
22937
+ found = true;
22938
+ return false;
22939
+ }
22645
22940
  if (!isNodeOfType(child, "CallExpression")) return;
22646
22941
  const callee = child.callee;
22647
22942
  if (isNodeOfType(callee, "Identifier") && NON_DETERMINISTIC_ID_GENERATOR_NAMES.has(callee.name)) {
@@ -22702,6 +22997,53 @@ const argumentReadsPostMountMeasurement = (argument, effectFn, visitedLocalNames
22702
22997
  });
22703
22998
  return found;
22704
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
+ };
22705
23047
  const noInitializeState = defineRule({
22706
23048
  id: "no-initialize-state",
22707
23049
  title: "State initialized from a mount effect",
@@ -22724,6 +23066,7 @@ const noInitializeState = defineRule({
22724
23066
  if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
22725
23067
  if (callExpr.arguments?.some((argument) => Boolean(argument) && containsNonDeterministicSource(argument))) continue;
22726
23068
  if (callExpr.arguments?.some((argument) => Boolean(argument) && argumentReadsPostMountMeasurement(argument, effectFn))) continue;
23069
+ if (callExpr.arguments?.some((argument) => Boolean(argument) && cleanupDisposesArgumentSource(argument, effectFn))) continue;
22727
23070
  const useStateDecl = getUseStateDecl(analysis, ref);
22728
23071
  if (!useStateDecl || !isNodeOfType(useStateDecl, "VariableDeclarator")) continue;
22729
23072
  if (!isNodeOfType(useStateDecl.id, "ArrayPattern")) continue;
@@ -23296,6 +23639,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
23296
23639
  return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
23297
23640
  });
23298
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-])/;
23299
23644
  const noLongTransitionDuration = defineRule({
23300
23645
  id: "no-long-transition-duration",
23301
23646
  title: "Transition duration too long",
@@ -23319,11 +23664,10 @@ const noLongTransitionDuration = defineRule({
23319
23664
  if (key === "transitionDuration" || key === "animationDuration") {
23320
23665
  let longestDurationPropertyMs = 0;
23321
23666
  for (const segment of value.split(",")) {
23322
- const trimmedSegment = segment.trim();
23323
- const msMatch = trimmedSegment.match(/^([\d.]+)ms$/);
23324
- const secondsMatch = trimmedSegment.match(/^([\d.]+)s$/);
23325
- if (msMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(msMatch[1]));
23326
- 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);
23327
23671
  }
23328
23672
  if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
23329
23673
  }
@@ -23331,7 +23675,7 @@ const noLongTransitionDuration = defineRule({
23331
23675
  let longestDurationMs = 0;
23332
23676
  for (const segment of value.split(",")) {
23333
23677
  if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
23334
- const firstTimeMatch = segment.match(/(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/);
23678
+ const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
23335
23679
  if (!firstTimeMatch) continue;
23336
23680
  const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
23337
23681
  longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
@@ -24503,14 +24847,14 @@ const collectRoleBranches = (expression, out) => {
24503
24847
  out.hasOpaqueBranch = true;
24504
24848
  };
24505
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.`;
24506
- const INTERACTIVE_HANDLERS = [
24850
+ const INTERACTIVE_HANDLERS_LOWER = new Set([
24507
24851
  "onClick",
24508
24852
  "onMouseDown",
24509
24853
  "onMouseUp",
24510
24854
  "onKeyDown",
24511
24855
  "onKeyPress",
24512
24856
  "onKeyUp"
24513
- ];
24857
+ ].map((handlerName) => handlerName.toLowerCase()));
24514
24858
  const noNoninteractiveElementInteractions = defineRule({
24515
24859
  id: "no-noninteractive-element-interactions",
24516
24860
  title: "Handler on non-interactive element",
@@ -24525,7 +24869,16 @@ const noNoninteractiveElementInteractions = defineRule({
24525
24869
  const tag = getElementType(node, context.settings);
24526
24870
  if (!NON_INTERACTIVE_ELEMENTS.has(tag)) return;
24527
24871
  if (tag === "label") return;
24528
- 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;
24529
24882
  if (isHiddenFromScreenReader(node, context.settings)) return;
24530
24883
  const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
24531
24884
  if (roleAttr) {
@@ -24631,6 +24984,18 @@ const KEYBOARD_HANDLER_PROP_NAMES = [
24631
24984
  "onKeyPress"
24632
24985
  ];
24633
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
+ };
24634
24999
  const resolveSettings$14 = (settings) => {
24635
25000
  const reactDoctor = settings?.["react-doctor"];
24636
25001
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
@@ -24654,6 +25019,7 @@ const noNoninteractiveTabindex = defineRule({
24654
25019
  if (!tabIndex) return;
24655
25020
  const tabIndexValue = tabIndex.value;
24656
25021
  if (!tabIndexValue) return;
25022
+ if (isRovingTabindexValue(tabIndexValue)) return;
24657
25023
  const numeric = parseJsxValue(tabIndexValue);
24658
25024
  if (numeric === null) {
24659
25025
  if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues && !isKeyboardOperable(node)) context.report({
@@ -25838,16 +26204,22 @@ const ELEMENT_ROLE_PAIRS = [
25838
26204
  ["tr", "row"],
25839
26205
  ["ul", "list"]
25840
26206
  ];
25841
- const getElementImplicitRoles = (tag) => {
25842
- const out = [];
25843
- for (const [element, role] of ELEMENT_ROLE_PAIRS) if (element === tag && !out.includes(role)) out.push(role);
25844
- return out;
25845
- };
25846
- const getTagsForRole = (role) => {
25847
- const out = [];
25848
- for (const [element, r] of ELEMENT_ROLE_PAIRS) if (r === role && !out.includes(element)) out.push(element);
25849
- 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;
25850
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;
25851
26223
  //#endregion
25852
26224
  //#region src/plugin/utils/get-implicit-role.ts
25853
26225
  const getImplicitRole = (node, elementType) => {
@@ -26084,22 +26456,22 @@ const noRenderInRender = defineRule({
26084
26456
  title: "Component rendered by inline function call",
26085
26457
  severity: "warn",
26086
26458
  tags: ["test-noise"],
26087
- 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.",
26088
26460
  create: (context) => ({ JSXExpressionContainer(node) {
26089
26461
  const expression = node.expression;
26090
26462
  if (!isNodeOfType(expression, "CallExpression")) return;
26091
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;
26092
26467
  if (isNodeOfType(expression.callee, "Identifier")) {
26093
26468
  if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
26094
- calleeName = expression.callee.name;
26095
- } else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
26469
+ } else if (isNodeOfType(expression.callee, "MemberExpression")) {
26096
26470
  if (rootsInProps(expression.callee.object, context.scopes)) return;
26097
- calleeName = expression.callee.property.name;
26098
26471
  }
26099
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
26100
26472
  context.report({
26101
26473
  node: expression,
26102
- 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.`
26103
26475
  });
26104
26476
  } })
26105
26477
  });
@@ -26202,8 +26574,8 @@ const countUseStates = (analysis, componentNode) => {
26202
26574
  for (const ref of getDownstreamRefs(analysis, componentNode)) if (isState(analysis, ref)) stateVariables.add(ref.resolved);
26203
26575
  return stateVariables.size;
26204
26576
  };
26205
- const findPropUsedToResetAllState = (analysis, effectFnRefs, depsRefs, useEffectNode) => {
26206
- 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));
26207
26579
  if (stateSetterRefs.length === 0) return null;
26208
26580
  if (!stateSetterRefs.every((ref) => isSetStateToInitialValue(analysis, ref))) return null;
26209
26581
  const containing = findContainingNode(analysis, useEffectNode);
@@ -26226,7 +26598,9 @@ const noResetAllStateOnPropChange = defineRule({
26226
26598
  if (!effectFnRefs || !depsRefs) return;
26227
26599
  const containing = findContainingNode(analysis, node);
26228
26600
  if (containing && isCustomHook(containing)) return;
26229
- 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;
26230
26604
  context.report({
26231
26605
  node,
26232
26606
  message: `Your users briefly see stale state when a prop changes because this useEffect clears all state.`
@@ -26347,6 +26721,7 @@ const TANSTACK_ROUTE_PROPERTY_ORDER = [
26347
26721
  "headers",
26348
26722
  "remountDeps"
26349
26723
  ];
26724
+ const TANSTACK_ROUTE_PROPERTY_INDEX = new Map(TANSTACK_ROUTE_PROPERTY_ORDER.map((propertyName, orderIndex) => [propertyName, orderIndex]));
26350
26725
  const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
26351
26726
  "createFileRoute",
26352
26727
  "createRoute",
@@ -26362,6 +26737,7 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
26362
26737
  "server",
26363
26738
  "handler"
26364
26739
  ];
26740
+ const TANSTACK_MIDDLEWARE_METHOD_INDEX = new Map(TANSTACK_MIDDLEWARE_METHOD_ORDER.map((methodName, orderIndex) => [methodName, orderIndex]));
26365
26741
  const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
26366
26742
  const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
26367
26743
  const TANSTACK_QUERY_HOOKS = new Set([
@@ -27066,14 +27442,21 @@ const noStaticElementInteractions = defineRule({
27066
27442
  category: "Accessibility",
27067
27443
  create: (context) => {
27068
27444
  const settings = resolveSettings$12(context.settings);
27445
+ const handlersLower = new Set(settings.handlers.map((handlerName) => handlerName.toLowerCase()));
27069
27446
  const isTestlikeFile = isTestlikeFilename(context.filename);
27070
27447
  return { JSXOpeningElement(node) {
27071
27448
  if (isTestlikeFile) return;
27072
27449
  let hasNonBlockerHandler = false;
27073
27450
  let hasAnyHandler = false;
27074
- for (const handler of settings.handlers) {
27075
- const attribute = hasJsxPropIgnoreCase(node.attributes, handler);
27076
- 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);
27077
27460
  if (isNullValue(attribute)) continue;
27078
27461
  hasAnyHandler = true;
27079
27462
  if (!isPureEventBlockerHandler(attribute)) {
@@ -27085,6 +27468,7 @@ const noStaticElementInteractions = defineRule({
27085
27468
  if (!hasNonBlockerHandler) return;
27086
27469
  const elementType = getElementType(node, context.settings);
27087
27470
  if (!HTML_TAGS.has(elementType)) return;
27471
+ if (elementType === "svg") return;
27088
27472
  if (isHiddenFromScreenReader(node, context.settings)) return;
27089
27473
  if (isPresentationRole(node)) return;
27090
27474
  if (isInteractiveElement(elementType, node)) return;
@@ -27098,7 +27482,8 @@ const noStaticElementInteractions = defineRule({
27098
27482
  });
27099
27483
  return;
27100
27484
  }
27101
- 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;
27102
27487
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
27103
27488
  const firstRole = attributeValue.value.toLowerCase().trim().split(/\s+/)[0];
27104
27489
  if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
@@ -28254,13 +28639,13 @@ const DOM_ATTRIBUTES_TO_CAMEL = new Map([
28254
28639
  ["xml:lang", "xmlLang"],
28255
28640
  ["xml:space", "xmlSpace"]
28256
28641
  ]);
28257
- const DOM_PROPERTIES_IGNORE_CASE = [
28642
+ const DOM_PROPERTIES_IGNORE_CASE_BY_LOWER = new Map([
28258
28643
  "charset",
28259
28644
  "allowFullScreen",
28260
28645
  "webkitAllowFullScreen",
28261
28646
  "mozAllowFullScreen",
28262
28647
  "webkitDirectory"
28263
- ];
28648
+ ].map((name) => [name.toLowerCase(), name]));
28264
28649
  //#endregion
28265
28650
  //#region src/plugin/constants/dom-property-tags.ts
28266
28651
  const DOM_PROPERTY_TO_ALLOWED_TAGS = new Map([
@@ -28464,10 +28849,7 @@ const matchesHtmlTagConventions = (tagName) => {
28464
28849
  if (!(firstCharacter >= 97 && firstCharacter <= 122)) return false;
28465
28850
  return !tagName.includes("-");
28466
28851
  };
28467
- const normalizeAttributeCase = (name) => {
28468
- for (const ignoreCaseName of DOM_PROPERTIES_IGNORE_CASE) if (ignoreCaseName.toLowerCase() === name.toLowerCase()) return ignoreCaseName;
28469
- return name;
28470
- };
28852
+ const normalizeAttributeCase = (name) => DOM_PROPERTIES_IGNORE_CASE_BY_LOWER.get(name.toLowerCase()) ?? name;
28471
28853
  const hasUppercaseChar = (input) => /[A-Z]/.test(input);
28472
28854
  const INVALID_PROP_ON_TAG = (propName, allowedTags) => `React ignores \`${propName}\` here because it only works on these tags: ${allowedTags}.`;
28473
28855
  const DATA_LOWERCASE_REQUIRED = () => `React drops this \`data-*\` prop because of its capital letters.`;
@@ -28811,32 +29193,26 @@ const isFirstArgumentOfHocCall = (node) => {
28811
29193
  if (!isHocCallee$1(parent)) return false;
28812
29194
  return parent.arguments[0] === node;
28813
29195
  };
29196
+ const MAP_LIKE_METHOD_NAMES = new Set([
29197
+ "map",
29198
+ "forEach",
29199
+ "filter",
29200
+ "flatMap",
29201
+ "reduce",
29202
+ "reduceRight"
29203
+ ]);
28814
29204
  const isReturnOfMapCallback = (node) => {
28815
29205
  const parent = node.parent;
28816
29206
  if (!parent) return false;
28817
29207
  if (isNodeOfType(parent, "CallExpression")) {
28818
29208
  const callee = parent.callee;
28819
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return [
28820
- "map",
28821
- "forEach",
28822
- "filter",
28823
- "flatMap",
28824
- "reduce",
28825
- "reduceRight"
28826
- ].includes(callee.property.name);
29209
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28827
29210
  }
28828
29211
  if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
28829
29212
  const callbackParent = parent.parent;
28830
29213
  if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
28831
29214
  const callee = callbackParent.callee;
28832
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return [
28833
- "map",
28834
- "forEach",
28835
- "filter",
28836
- "flatMap",
28837
- "reduce",
28838
- "reduceRight"
28839
- ].includes(callee.property.name);
29215
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28840
29216
  }
28841
29217
  }
28842
29218
  return false;
@@ -28872,10 +29248,10 @@ const isRenderFlowingReadReference = (identifier) => {
28872
29248
  valueNode = parent;
28873
29249
  parent = parent.parent;
28874
29250
  continue;
28875
- 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);
28876
29252
  case "AssignmentExpression": {
28877
29253
  const assignmentTarget = parent.left;
28878
- return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isUppercaseName(assignmentTarget.name);
29254
+ return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
28879
29255
  }
28880
29256
  case "Property":
28881
29257
  if (parent.value !== valueNode) return false;
@@ -28968,14 +29344,14 @@ const noUnstableNestedComponents = defineRule({
28968
29344
  };
28969
29345
  return {
28970
29346
  JSXOpeningElement(node) {
28971
- if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) {
29347
+ if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
28972
29348
  recordInstantiation(node.name, node.name.name);
28973
29349
  return;
28974
29350
  }
28975
29351
  if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
28976
29352
  },
28977
29353
  Identifier(node) {
28978
- if (!isUppercaseName(node.name)) return;
29354
+ if (!isReactComponentName(node.name)) return;
28979
29355
  if (!isRenderFlowingReadReference(node)) return;
28980
29356
  recordInstantiation(node, node.name);
28981
29357
  },
@@ -29906,7 +30282,7 @@ const containsVnodeFactoryCall = (root) => {
29906
30282
  };
29907
30283
  const isComponentLikeFunction = (functionNode) => {
29908
30284
  const bindingName = getFunctionBindingName(functionNode);
29909
- if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN.test(bindingName))) return true;
30285
+ if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN$1.test(bindingName))) return true;
29910
30286
  const body = "body" in functionNode ? functionNode.body : null;
29911
30287
  if (!body || !isAstNode(body)) return false;
29912
30288
  return containsJsxElement(body) || containsVnodeFactoryCall(body);
@@ -30785,13 +31161,13 @@ const preferStableEmptyFallback = defineRule({
30785
31161
  memoRegistry = buildSameFileMemoRegistry(node);
30786
31162
  },
30787
31163
  JSXAttribute(node) {
30788
- if (!isInsideFunctionScope(node)) return;
30789
- if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30790
31164
  if (!node.value) return;
30791
31165
  if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
30792
31166
  const innerExpression = node.value.expression;
30793
31167
  if (!innerExpression) return;
30794
31168
  if (innerExpression.type === "JSXEmptyExpression") return;
31169
+ if (!isInsideFunctionScope(node)) return;
31170
+ if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30795
31171
  const parentJsxOpening = node.parent;
30796
31172
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
30797
31173
  if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
@@ -31363,7 +31739,7 @@ const queryMutationMissingInvalidation = defineRule({
31363
31739
  });
31364
31740
  if (!hasCacheUpdate) context.report({
31365
31741
  node,
31366
- 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."
31367
31743
  });
31368
31744
  } })
31369
31745
  });
@@ -32012,6 +32388,7 @@ const renderingHydrationMismatchTime = defineRule({
32012
32388
  return { JSXExpressionContainer(node) {
32013
32389
  if (isTestlikeFile) return;
32014
32390
  if (!node.expression) return;
32391
+ if (isGeneratedImageRenderContext(context, findOpeningElementOfChild(node) ?? node)) return;
32015
32392
  const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
32016
32393
  if (matched) {
32017
32394
  if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
@@ -32163,41 +32540,11 @@ const callsIdentifier = (root, identifierName) => {
32163
32540
  });
32164
32541
  return found;
32165
32542
  };
32166
- const setterIsCalledInAsyncContext = (componentBody, setterName) => {
32167
- if (!componentBody) return false;
32168
- let found = false;
32169
- walkAst(componentBody, (child) => {
32170
- if (found) return;
32171
- if (!isFunctionLike$1(child)) return;
32172
- const functionBody = child.body;
32173
- if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
32174
- if (callsIdentifier(functionBody, setterName)) found = true;
32175
- });
32176
- return found;
32177
- };
32178
32543
  const PROMISE_CHAIN_METHOD_NAMES = new Set([
32179
32544
  "then",
32180
32545
  "catch",
32181
32546
  "finally"
32182
32547
  ]);
32183
- const setterIsCalledInPromiseChain = (componentBody, setterName) => {
32184
- if (!componentBody) return false;
32185
- let found = false;
32186
- walkAst(componentBody, (child) => {
32187
- if (found) return;
32188
- if (!isNodeOfType(child, "CallExpression")) return;
32189
- const callee = child.callee;
32190
- if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
32191
- for (const argument of child.arguments ?? []) {
32192
- if (!isFunctionLike$1(argument)) continue;
32193
- if (callsIdentifier(argument.body, setterName)) {
32194
- found = true;
32195
- return;
32196
- }
32197
- }
32198
- });
32199
- return found;
32200
- };
32201
32548
  const ASYNC_DATA_CALLEE_NAMES = new Set([
32202
32549
  "useApolloClient",
32203
32550
  "useMutation",
@@ -32210,20 +32557,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
32210
32557
  "fetch",
32211
32558
  "axios"
32212
32559
  ]);
32213
- const referencesAsyncDataApi = (body) => {
32214
- if (!body) return false;
32560
+ const hasAsyncLoadingWork = (fnBody, setterName) => {
32215
32561
  let found = false;
32216
- walkAst(body, (child) => {
32217
- if (found) return;
32562
+ walkAst(fnBody, (child) => {
32563
+ if (found) return false;
32218
32564
  if (isNodeOfType(child, "CallExpression")) {
32219
32565
  const callee = child.callee;
32220
32566
  if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
32221
32567
  found = true;
32222
- return;
32568
+ return false;
32223
32569
  }
32224
- 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)) {
32225
32588
  found = true;
32226
- return;
32589
+ return false;
32227
32590
  }
32228
32591
  }
32229
32592
  });
@@ -32248,7 +32611,7 @@ const renderingUsetransitionLoading = defineRule({
32248
32611
  const secondBinding = node.id.elements[1];
32249
32612
  const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
32250
32613
  const fnBody = enclosingFunctionBody(node);
32251
- if (fnBody && (setterName && setterIsCalledInAsyncContext(fnBody, setterName) || setterName && setterIsCalledInPromiseChain(fnBody, setterName) || referencesAsyncDataApi(fnBody))) return;
32614
+ if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
32252
32615
  context.report({
32253
32616
  node: node.init,
32254
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`
@@ -33042,17 +33405,17 @@ const rerenderStateOnlyInHandlers = defineRule({
33042
33405
  const effectTriggerNames = /* @__PURE__ */ new Set();
33043
33406
  for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
33044
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
+ });
33045
33413
  for (const binding of bindings) {
33046
33414
  if (renderReachableNames.has(binding.valueName)) continue;
33047
33415
  if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
33048
33416
  const setterSuffix = binding.setterName.slice(3);
33049
33417
  if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
33050
- let setterCalled = false;
33051
- walkAst(componentBody, (child) => {
33052
- if (setterCalled) return;
33053
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
33054
- });
33055
- if (!setterCalled) continue;
33418
+ if (!calledSetterNames.has(binding.setterName)) continue;
33056
33419
  if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
33057
33420
  context.report({
33058
33421
  node: binding.declarator,
@@ -33384,6 +33747,7 @@ const RECYCLABLE_LIST_PACKAGES = {
33384
33747
  FlashList: ["@shopify/flash-list"],
33385
33748
  LegendList: ["@legendapp/list"]
33386
33749
  };
33750
+ const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
33387
33751
  const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
33388
33752
  const RENDER_ITEM_PROP_NAMES = new Set([
33389
33753
  "renderItem",
@@ -33626,33 +33990,42 @@ const rnListMissingEstimatedItemSize = defineRule({
33626
33990
  requires: ["react-native"],
33627
33991
  severity: "warn",
33628
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.",
33629
- create: (context) => ({ JSXOpeningElement(node) {
33630
- const localElementName = resolveJsxElementName(node);
33631
- if (!localElementName) return;
33632
- const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
33633
- if (canonicalRecyclerName === null) return;
33634
- if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
33635
- let hasSizingHint = false;
33636
- let dataIsEmptyLiteral = false;
33637
- let hasDataProp = false;
33638
- for (const attribute of node.attributes ?? []) {
33639
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
33640
- if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
33641
- const attributeName = attribute.name.name;
33642
- if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
33643
- if (attributeName === "data") {
33644
- hasDataProp = true;
33645
- 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
+ });
33646
34026
  }
33647
- }
33648
- if (hasSizingHint) return;
33649
- if (dataIsEmptyLiteral) return;
33650
- if (!hasDataProp) return;
33651
- context.report({
33652
- node,
33653
- message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
33654
- });
33655
- } })
34027
+ };
34028
+ }
33656
34029
  });
33657
34030
  //#endregion
33658
34031
  //#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
@@ -33663,25 +34036,34 @@ const rnListRecyclableWithoutTypes = defineRule({
33663
34036
  requires: ["react-native"],
33664
34037
  severity: "warn",
33665
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.",
33666
- create: (context) => ({ JSXOpeningElement(node) {
33667
- const elementName = resolveJsxElementName(node);
33668
- if (!elementName) return;
33669
- if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
33670
- let hasRecycleItemsEnabled = false;
33671
- let hasGetItemType = false;
33672
- for (const attr of node.attributes ?? []) {
33673
- if (!isNodeOfType(attr, "JSXAttribute")) continue;
33674
- if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
33675
- if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
33676
- else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
33677
- else hasRecycleItemsEnabled = true;
33678
- if (attr.name.name === "getItemType") hasGetItemType = true;
33679
- }
33680
- if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
33681
- node,
33682
- message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
33683
- });
33684
- } })
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
+ }
33685
34067
  });
33686
34068
  //#endregion
33687
34069
  //#region src/plugin/rules/react-native/rn-no-deep-imports.ts
@@ -33822,7 +34204,7 @@ const rnNoDimensionsGet = defineRule({
33822
34204
  if (binding !== null && !isBindingReactNativeDimensions(node, binding)) return;
33823
34205
  if (isMemberProperty(node.callee, "get")) context.report({
33824
34206
  node,
33825
- 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."
33826
34208
  });
33827
34209
  if (isMemberProperty(node.callee, "addEventListener")) context.report({
33828
34210
  node,
@@ -34644,7 +35026,10 @@ const isExpoUiComponentElement = (openingElement, contextNode, componentName) =>
34644
35026
  };
34645
35027
  //#endregion
34646
35028
  //#region src/plugin/rules/react-native/rn-no-raw-text.ts
34647
- 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
+ };
34648
35033
  const isRawTextContent = (child) => {
34649
35034
  if (isNodeOfType(child, "JSXText")) return Boolean(child.value?.trim());
34650
35035
  if (!isNodeOfType(child, "JSXExpressionContainer") || !child.expression) return false;
@@ -34665,9 +35050,10 @@ const resolveTextBoundaryName = (openingElement) => {
34665
35050
  if (isNodeOfType(openingElement.name, "JSXNamespacedName")) return openingElement.name.namespace.name;
34666
35051
  return resolveJsxElementName(openingElement);
34667
35052
  };
35053
+ const TEXT_COMPONENT_KEYWORDS = [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS];
34668
35054
  const isTextHandlingComponent = (elementName) => {
34669
35055
  if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
34670
- return [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS].some((keyword) => elementName.includes(keyword));
35056
+ return TEXT_COMPONENT_KEYWORDS.some((keyword) => elementName.includes(keyword));
34671
35057
  };
34672
35058
  const isTransparentTextWrapper = (elementName) => elementName !== null && REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS.has(elementName);
34673
35059
  const isInsideTextHandlingComponent = (node) => {
@@ -34711,6 +35097,7 @@ const rnNoRawText = defineRule({
34711
35097
  return {
34712
35098
  Program(programNode) {
34713
35099
  isDomComponentFile = hasDirective(programNode, "use dom");
35100
+ if (!containsJsxElement(programNode)) return;
34714
35101
  const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
34715
35102
  autoDetectedTextWrappers = childrenForwarding.textWrappers;
34716
35103
  autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
@@ -38665,14 +39052,7 @@ const roleSupportsAriaProps = defineRule({
38665
39052
  recommendation: "Only use `aria-*` attributes that the element's role supports.",
38666
39053
  category: "Accessibility",
38667
39054
  create: (context) => ({ JSXOpeningElement(node) {
38668
- const elementType = getElementType(node, context.settings);
38669
- const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
38670
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
38671
- if (!role) return;
38672
- if (!VALID_ARIA_ROLES.has(role)) return;
38673
- const isImplicit = !roleAttribute;
38674
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
38675
- if (!supported) return;
39055
+ let ariaAttributes = null;
38676
39056
  for (const attribute of node.attributes) {
38677
39057
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
38678
39058
  const attributeNode = attribute;
@@ -38682,6 +39062,21 @@ const roleSupportsAriaProps = defineRule({
38682
39062
  const propName = propRawName.toLowerCase();
38683
39063
  if (!propName.startsWith("aria-")) continue;
38684
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) {
38685
39080
  if (supported.has(propName)) continue;
38686
39081
  context.report({
38687
39082
  node: attribute,
@@ -38903,7 +39298,7 @@ const isInsideClassComponent = (node) => {
38903
39298
  const hasShortCircuitAncestor = (descendant, ancestor) => {
38904
39299
  let current = descendant.parent;
38905
39300
  while (current && current !== ancestor) {
38906
- if (isNodeOfType(current, "ConditionalExpression")) return true;
39301
+ if (isNodeOfType(current, "ConditionalExpression") && (isWithinRange(descendant, current.consequent) || isWithinRange(descendant, current.alternate))) return true;
38907
39302
  if (isNodeOfType(current, "LogicalExpression") && (current.operator === "&&" || current.operator === "||" || current.operator === "??") && isWithinRange(descendant, current.right)) return true;
38908
39303
  current = current.parent ?? null;
38909
39304
  }
@@ -39029,6 +39424,7 @@ const rulesOfHooks = defineRule({
39029
39424
  if (parentInfo.isComponentOrHook) isInsideComponentOrHook = true;
39030
39425
  }
39031
39426
  if (!isInsideComponentOrHook) {
39427
+ if (!enclosing.hasResolvedName) return;
39032
39428
  if (isLocalNonHookFunctionCallee(node, context.scopes, settings)) return;
39033
39429
  context.report({
39034
39430
  node: node.callee,
@@ -39519,6 +39915,7 @@ const serverCacheWithObjectLiteral = defineRule({
39519
39915
  cachedFunctionNames.add(node.id.name);
39520
39916
  },
39521
39917
  CallExpression(node) {
39918
+ if (cachedFunctionNames.size === 0) return;
39522
39919
  if (!isNodeOfType(node.callee, "Identifier")) return;
39523
39920
  if (!cachedFunctionNames.has(node.callee.name)) return;
39524
39921
  const firstArg = node.arguments?.[0];
@@ -39602,6 +39999,14 @@ const objectExpressionHasCachingConfig = (objectExpression) => (objectExpression
39602
39999
  const objectExpressionHasSpread = (objectExpression) => (objectExpression.properties ?? []).some((property) => isNodeOfType(property, "SpreadElement"));
39603
40000
  const APP_ROUTER_FILE_PATTERN = new RegExp(`/app/(?:[^/]+/)*(?:route|page|layout|template|loading|error|default)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
39604
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
+ };
39605
40010
  const serverFetchWithoutRevalidate = defineRule({
39606
40011
  id: "server-fetch-without-revalidate",
39607
40012
  title: "Fetch without revalidate",
@@ -39621,7 +40026,7 @@ const serverFetchWithoutRevalidate = defineRule({
39621
40026
  isServerSideFile = false;
39622
40027
  return;
39623
40028
  }
39624
- 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);
39625
40030
  },
39626
40031
  CallExpression(node) {
39627
40032
  if (!isServerSideFile) return;
@@ -39634,6 +40039,7 @@ const serverFetchWithoutRevalidate = defineRule({
39634
40039
  if (objectExpressionHasSpread(optionsArg)) return;
39635
40040
  }
39636
40041
  const urlArg = node.arguments?.[0];
40042
+ if (isImportMetaUrlAssetArgument(urlArg)) return;
39637
40043
  const urlText = isNodeOfType(urlArg, "Literal") && typeof urlArg.value === "string" ? `"${urlArg.value}"` : "url";
39638
40044
  context.report({
39639
40045
  node,
@@ -39862,7 +40268,7 @@ const serverNoMutableModuleState = defineRule({
39862
40268
  if (node.kind === "let" || node.kind === "var") {
39863
40269
  context.report({
39864
40270
  node: declarator,
39865
- 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.`
39866
40272
  });
39867
40273
  continue;
39868
40274
  }
@@ -39923,25 +40329,29 @@ const GATE_LEADING_VERBS = new Set([
39923
40329
  "authorize",
39924
40330
  "authenticate"
39925
40331
  ]);
39926
- const isStartedPromiseBinding = (name, statements, beforeIndex) => {
39927
- for (let index = 0; index < beforeIndex; index++) {
39928
- const statement = statements[index];
39929
- if (!isNodeOfType(statement, "VariableDeclaration")) continue;
39930
- for (const declarator of statement.declarations ?? []) {
39931
- if (!isNodeOfType(declarator.id, "Identifier")) continue;
39932
- if (declarator.id.name !== name) continue;
39933
- if (declarator.init && !isNodeOfType(declarator.init, "AwaitExpression")) return true;
39934
- }
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;
39935
40339
  }
39936
40340
  return false;
39937
40341
  };
39938
- 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) => {
39939
40349
  if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
39940
40350
  for (const declarator of declaration.declarations ?? []) {
39941
40351
  const init = declarator.init;
39942
40352
  if (!isNodeOfType(init, "AwaitExpression")) continue;
39943
40353
  const argument = init.argument;
39944
- if (isNodeOfType(argument, "Identifier") && isStartedPromiseBinding(argument.name, statements, declarationIndex)) return true;
40354
+ if (isNodeOfType(argument, "CallExpression") && isRequestScopedCallee(argument.callee)) return true;
39945
40355
  }
39946
40356
  return false;
39947
40357
  };
@@ -39976,7 +40386,8 @@ const serverSequentialIndependentAwait = defineRule({
39976
40386
  if (!isNodeOfType(nextStatement, "VariableDeclaration")) continue;
39977
40387
  if (!declarationStartsWithAwait(nextStatement)) continue;
39978
40388
  if (declarationReadsAnyName(nextStatement, declaredNames)) continue;
39979
- if (declarationAwaitsStartedPromise(nextStatement, statements, statementIndex + 1)) continue;
40389
+ if (declarationAwaitsExistingPromise(nextStatement)) continue;
40390
+ if (declarationAwaitsRequestScopedCall(currentStatement) || declarationAwaitsRequestScopedCall(nextStatement)) continue;
39980
40391
  if (declarationAwaitsGate(currentStatement)) continue;
39981
40392
  context.report({
39982
40393
  node: nextStatement,
@@ -40445,7 +40856,16 @@ const supabaseRlsPolicyRisk = defineRule({
40445
40856
  //#endregion
40446
40857
  //#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
40447
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;
40448
- 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
+ };
40449
40869
  const supabaseTableMissingRls = defineRule({
40450
40870
  id: "supabase-table-missing-rls",
40451
40871
  title: "Supabase table created without Row Level Security",
@@ -40456,11 +40876,13 @@ const supabaseTableMissingRls = defineRule({
40456
40876
  const content = sanitizeSqlForScan(file.content);
40457
40877
  if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
40458
40878
  const findings = [];
40879
+ const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
40459
40880
  CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
40460
40881
  for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
40461
40882
  const tableName = match[1];
40462
40883
  if (tableName === void 0) continue;
40463
- 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;
40464
40886
  const location = getLocationAtIndex(content, match.index);
40465
40887
  findings.push({
40466
40888
  message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
@@ -40736,6 +41158,7 @@ const tanstackStartMissingHeadContent = defineRule({
40736
41158
  severity: "warn",
40737
41159
  recommendation: "Add `<HeadContent />` inside `<head>` in your __root route. Without it, route `head()` meta tags are dropped.",
40738
41160
  create: (context) => {
41161
+ if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(context.filename ?? "")) return {};
40739
41162
  let hasHeadContentElement = false;
40740
41163
  let hasDocumentHeadElement = false;
40741
41164
  let hasCustomHeadChildElement = false;
@@ -40772,8 +41195,6 @@ const tanstackStartMissingHeadContent = defineRule({
40772
41195
  };
40773
41196
  return {
40774
41197
  Program(node) {
40775
- const filename = context.filename ?? "";
40776
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40777
41198
  const statements = node.body ?? [];
40778
41199
  for (const statement of statements) collectImportBindings(statement);
40779
41200
  for (const statement of statements) {
@@ -40782,18 +41203,12 @@ const tanstackStartMissingHeadContent = defineRule({
40782
41203
  }
40783
41204
  },
40784
41205
  ImportDeclaration(node) {
40785
- const filename = context.filename ?? "";
40786
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40787
41206
  collectImportBindings(node);
40788
41207
  },
40789
41208
  VariableDeclarator(node) {
40790
- const filename = context.filename ?? "";
40791
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40792
41209
  collectVariableAlias(node);
40793
41210
  },
40794
41211
  JSXOpeningElement(node) {
40795
- const filename = normalizeFilename(context.filename ?? "");
40796
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40797
41212
  if (isNodeOfType(node.name, "JSXIdentifier")) {
40798
41213
  if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
40799
41214
  if (headContentComponentNames.has(node.name.name)) hasHeadContentElement = true;
@@ -40807,8 +41222,6 @@ const tanstackStartMissingHeadContent = defineRule({
40807
41222
  if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
40808
41223
  },
40809
41224
  "Program:exit"(programNode) {
40810
- const filename = normalizeFilename(context.filename ?? "");
40811
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40812
41225
  if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
40813
41226
  node: programNode,
40814
41227
  message: "Without <HeadContent /> in the __root route, your route head() meta tags never render."
@@ -40832,27 +41245,29 @@ const tanstackStartNoAnchorElement = defineRule({
40832
41245
  requires: ["tanstack-start"],
40833
41246
  severity: "warn",
40834
41247
  recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
40835
- create: (context) => ({ JSXOpeningElement(node) {
40836
- if (!isInProjectDirectory(context, "routes")) return;
40837
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
40838
- const attributes = node.attributes ?? [];
40839
- const hrefAttribute = findJsxAttribute(attributes, "href");
40840
- if (!hrefAttribute?.value) return;
40841
- let hrefValue = null;
40842
- if (isNodeOfType(hrefAttribute.value, "Literal")) hrefValue = hrefAttribute.value.value;
40843
- else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
40844
- if (typeof hrefValue !== "string" || !hrefValue.startsWith("/")) return;
40845
- if (hrefValue.startsWith("//")) return;
40846
- const pathname = hrefValue.split(/[?#]/)[0] ?? hrefValue;
40847
- if (pathname.startsWith("/api/")) return;
40848
- if (/\.[a-z0-9]{1,8}$/i.test(pathname)) return;
40849
- if (findJsxAttribute(attributes, "download")) return;
40850
- if (getAttributeStringValue(findJsxAttribute(attributes, "target")) === "_blank") return;
40851
- context.report({
40852
- node,
40853
- message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
40854
- });
40855
- } })
41248
+ create: (context) => {
41249
+ if (!isInProjectDirectory(context, "routes")) return {};
41250
+ return { JSXOpeningElement(node) {
41251
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
41252
+ const attributes = node.attributes ?? [];
41253
+ const hrefAttribute = findJsxAttribute(attributes, "href");
41254
+ if (!hrefAttribute?.value) return;
41255
+ let hrefValue = null;
41256
+ if (isNodeOfType(hrefAttribute.value, "Literal")) hrefValue = hrefAttribute.value.value;
41257
+ else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
41258
+ if (typeof hrefValue !== "string" || !hrefValue.startsWith("/")) return;
41259
+ if (hrefValue.startsWith("//")) return;
41260
+ const pathname = hrefValue.split(/[?#]/)[0] ?? hrefValue;
41261
+ if (pathname.startsWith("/api/")) return;
41262
+ if (/\.[a-z0-9]{1,8}$/i.test(pathname)) return;
41263
+ if (findJsxAttribute(attributes, "download")) return;
41264
+ if (getAttributeStringValue(findJsxAttribute(attributes, "target")) === "_blank") return;
41265
+ context.report({
41266
+ node,
41267
+ message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
41268
+ });
41269
+ } };
41270
+ }
40856
41271
  });
40857
41272
  //#endregion
40858
41273
  //#region src/plugin/rules/tanstack-start/tanstack-start-no-direct-fetch-in-loader.ts
@@ -40916,7 +41331,7 @@ const tanstackStartNoNavigateInRender = defineRule({
40916
41331
  severity: "warn",
40917
41332
  recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
40918
41333
  create: (context) => {
40919
- const isRouteFile = isInProjectDirectory(context, "routes");
41334
+ if (!isInProjectDirectory(context, "routes")) return {};
40920
41335
  let deferredCallbackDepth = 0;
40921
41336
  let eventHandlerDepth = 0;
40922
41337
  const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
@@ -40925,7 +41340,7 @@ const tanstackStartNoNavigateInRender = defineRule({
40925
41340
  if (!isNodeOfType(callParent, "CallExpression")) return false;
40926
41341
  if (callParent.callee === functionNode) return false;
40927
41342
  if (isNodeOfType(callParent.callee, "MemberExpression") && !callParent.callee.computed && isNodeOfType(callParent.callee.property, "Identifier") && PROMISE_CONTINUATION_METHODS.has(callParent.callee.property.name)) return true;
40928
- 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;
40929
41344
  };
40930
41345
  const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && REACT_HANDLER_PROP_PATTERN.test(node.name.name);
40931
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));
@@ -40953,11 +41368,11 @@ const tanstackStartNoNavigateInRender = defineRule({
40953
41368
  if (isNodeOfType(parent, "ReturnStatement")) {
40954
41369
  const outerFunction = findEnclosingFunction(parent);
40955
41370
  const hookName = outerFunction ? getFunctionBindingName(outerFunction) : null;
40956
- return Boolean(hookName && HOOK_NAME_PATTERN.test(hookName));
41371
+ return Boolean(hookName && HOOK_NAME_PATTERN$1.test(hookName));
40957
41372
  }
40958
41373
  if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === functionNode) {
40959
41374
  const hookName = getFunctionBindingName(parent);
40960
- return Boolean(hookName && HOOK_NAME_PATTERN.test(hookName));
41375
+ return Boolean(hookName && HOOK_NAME_PATTERN$1.test(hookName));
40961
41376
  }
40962
41377
  return false;
40963
41378
  };
@@ -40980,7 +41395,6 @@ const tanstackStartNoNavigateInRender = defineRule({
40980
41395
  };
40981
41396
  return {
40982
41397
  CallExpression(node) {
40983
- if (!isRouteFile) return;
40984
41398
  if (isDeferredHookCall(node)) deferredCallbackDepth++;
40985
41399
  if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
40986
41400
  if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
@@ -40992,39 +41406,30 @@ const tanstackStartNoNavigateInRender = defineRule({
40992
41406
  }
40993
41407
  },
40994
41408
  "CallExpression:exit"(node) {
40995
- if (!isRouteFile) return;
40996
41409
  if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
40997
41410
  },
40998
41411
  JSXAttribute(node) {
40999
- if (!isRouteFile) return;
41000
41412
  if (isEventHandlerAttribute(node)) eventHandlerDepth++;
41001
41413
  },
41002
41414
  "JSXAttribute:exit"(node) {
41003
- if (!isRouteFile) return;
41004
41415
  if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
41005
41416
  },
41006
41417
  Property(node) {
41007
- if (!isRouteFile) return;
41008
41418
  if (isEventHandlerProperty(node)) eventHandlerDepth++;
41009
41419
  },
41010
41420
  "Property:exit"(node) {
41011
- if (!isRouteFile) return;
41012
41421
  if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
41013
41422
  },
41014
41423
  VariableDeclarator(node) {
41015
- if (!isRouteFile) return;
41016
41424
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
41017
41425
  },
41018
41426
  "VariableDeclarator:exit"(node) {
41019
- if (!isRouteFile) return;
41020
41427
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
41021
41428
  },
41022
41429
  FunctionDeclaration(node) {
41023
- if (!isRouteFile) return;
41024
41430
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
41025
41431
  },
41026
41432
  "FunctionDeclaration:exit"(node) {
41027
- if (!isRouteFile) return;
41028
41433
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
41029
41434
  }
41030
41435
  };
@@ -41108,23 +41513,25 @@ const tanstackStartNoUseEffectFetch = defineRule({
41108
41513
  requires: ["tanstack-start"],
41109
41514
  severity: "warn",
41110
41515
  recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
41111
- create: (context) => ({ CallExpression(node) {
41112
- if (!isInProjectDirectory(context, "routes")) return;
41113
- if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
41114
- const callback = node.arguments?.[0];
41115
- if (!callback) return;
41116
- let hasFetchCall = false;
41117
- const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
41118
- walkAst(callback, (child) => {
41119
- if (hasFetchCall) return false;
41120
- if (child !== callback && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
41121
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === "fetch") hasFetchCall = true;
41122
- });
41123
- if (hasFetchCall) context.report({
41124
- node,
41125
- message: "fetch() inside useEffect makes your users wait through a loading spinner after render."
41126
- });
41127
- } })
41516
+ create: (context) => {
41517
+ if (!isInProjectDirectory(context, "routes")) return {};
41518
+ return { CallExpression(node) {
41519
+ if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
41520
+ const callback = node.arguments?.[0];
41521
+ if (!callback) return;
41522
+ let hasFetchCall = false;
41523
+ const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
41524
+ walkAst(callback, (child) => {
41525
+ if (hasFetchCall) return false;
41526
+ if (child !== callback && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
41527
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === "fetch") hasFetchCall = true;
41528
+ });
41529
+ if (hasFetchCall) context.report({
41530
+ node,
41531
+ message: "fetch() inside useEffect makes your users wait through a loading spinner after render."
41532
+ });
41533
+ } };
41534
+ }
41128
41535
  });
41129
41536
  //#endregion
41130
41537
  //#region src/plugin/rules/tanstack-start/tanstack-start-redirect-in-try-catch.ts
@@ -41165,10 +41572,10 @@ const tanstackStartRoutePropertyOrder = defineRule({
41165
41572
  const propertyName = getPropertyKeyName(property);
41166
41573
  if (propertyName !== null) orderedPropertyNames.push(propertyName);
41167
41574
  }
41168
- const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_ORDER.includes(propertyName));
41575
+ const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_INDEX.has(propertyName));
41169
41576
  let lastIndex = -1;
41170
41577
  for (const propertyName of sensitiveProperties) {
41171
- const currentIndex = TANSTACK_ROUTE_PROPERTY_ORDER.indexOf(propertyName);
41578
+ const currentIndex = TANSTACK_ROUTE_PROPERTY_INDEX.get(propertyName) ?? -1;
41172
41579
  if (currentIndex < lastIndex) {
41173
41580
  const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
41174
41581
  context.report({
@@ -41205,10 +41612,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
41205
41612
  } else return;
41206
41613
  const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
41207
41614
  if (methodNames[methodNames.length - 1] !== ownMethodName) return;
41208
- 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)));
41209
41616
  let lastIndex = -1;
41210
41617
  for (const methodName of orderSensitiveMethods) {
41211
- const currentIndex = TANSTACK_MIDDLEWARE_METHOD_ORDER.indexOf(toMethodOrderToken(methodName));
41618
+ const currentIndex = TANSTACK_MIDDLEWARE_METHOD_INDEX.get(toMethodOrderToken(methodName)) ?? -1;
41212
41619
  if (currentIndex < lastIndex) {
41213
41620
  const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
41214
41621
  context.report({
@@ -41492,13 +41899,21 @@ const webhookSignatureRisk = defineRule({
41492
41899
  //#endregion
41493
41900
  //#region src/plugin/rules/zod/utils/zod-ast.ts
41494
41901
  const ZOD_MODULE = "zod";
41902
+ const ZOD_MODULE_SOURCES = [ZOD_MODULE];
41495
41903
  const getStaticPropertyName = (member) => {
41496
41904
  const property = member.property;
41497
41905
  if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
41498
41906
  if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
41499
41907
  return null;
41500
41908
  };
41909
+ const importInfoCache = /* @__PURE__ */ new WeakMap();
41501
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) => {
41502
41917
  const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
41503
41918
  if (!specifier) return null;
41504
41919
  const declaration = specifier.parent;
@@ -41635,25 +42050,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
41635
42050
  tags: ["migration-hint"],
41636
42051
  severity: "warn",
41637
42052
  recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
41638
- create: (context) => ({
41639
- CallExpression(node) {
41640
- if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
41641
- if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
41642
- context.report({
41643
- node,
41644
- message: ZOD_ERROR_API_MESSAGE
41645
- });
41646
- },
41647
- MemberExpression(node) {
41648
- const parent = node.parent;
41649
- if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41650
- if (!isDeprecatedZodErrorMemberAccess(node)) return;
41651
- context.report({
41652
- node,
41653
- message: ZOD_ERROR_API_MESSAGE
41654
- });
41655
- }
41656
- })
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
+ }
41657
42080
  });
41658
42081
  //#endregion
41659
42082
  //#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
@@ -41845,16 +42268,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
41845
42268
  tags: ["migration-hint"],
41846
42269
  severity: "warn",
41847
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()`.",
41848
- create: (context) => ({
41849
- CallExpression(node) {
41850
- 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);
41851
- },
41852
- MemberExpression(node) {
41853
- const parent = node.parent;
41854
- if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41855
- if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
41856
- }
41857
- })
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
+ }
41858
42289
  });
41859
42290
  //#endregion
41860
42291
  //#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
@@ -41889,13 +42320,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
41889
42320
  tags: ["migration-hint"],
41890
42321
  severity: "warn",
41891
42322
  recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
41892
- create: (context) => ({ CallExpression(node) {
41893
- if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
41894
- context.report({
41895
- node,
41896
- message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
41897
- });
41898
- } })
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
+ }
41899
42339
  });
41900
42340
  //#endregion
41901
42341
  //#region src/plugin/rule-registry.ts