oxlint-plugin-react-doctor 0.6.2-dev.397816a → 0.6.2-dev.59e8178

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 +760 -542
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -294,15 +294,38 @@ const defineRule = (rule) => {
294
294
  };
295
295
  //#endregion
296
296
  //#region src/plugin/rules/security-scan/utils/get-location-at-index.ts
297
+ let lastContentLineIndex;
298
+ const buildLineStartOffsets = (content) => {
299
+ const lineStartOffsets = [0];
300
+ for (let newlineIndex = content.indexOf("\n"); newlineIndex !== -1; newlineIndex = content.indexOf("\n", newlineIndex + 1)) lineStartOffsets.push(newlineIndex + 1);
301
+ return lineStartOffsets;
302
+ };
303
+ const getLineStartOffsets = (content) => {
304
+ if (lastContentLineIndex?.content === content) return lastContentLineIndex.lineStartOffsets;
305
+ const lineStartOffsets = buildLineStartOffsets(content);
306
+ lastContentLineIndex = {
307
+ content,
308
+ lineStartOffsets
309
+ };
310
+ return lineStartOffsets;
311
+ };
297
312
  const getLocationAtIndex = (content, matchIndex) => {
298
- if (matchIndex < 0) return {
313
+ if (Number.isNaN(matchIndex) || matchIndex < 0) return {
299
314
  line: 1,
300
315
  column: 1
301
316
  };
302
- const lines = content.slice(0, matchIndex).split(/\r?\n/);
317
+ const lineStartOffsets = getLineStartOffsets(content);
318
+ const boundedIndex = Math.min(Math.trunc(matchIndex), content.length);
319
+ let lowLineIndex = 0;
320
+ let highLineIndex = lineStartOffsets.length - 1;
321
+ while (lowLineIndex < highLineIndex) {
322
+ const midLineIndex = lowLineIndex + highLineIndex + 1 >> 1;
323
+ if (lineStartOffsets[midLineIndex] <= boundedIndex) lowLineIndex = midLineIndex;
324
+ else highLineIndex = midLineIndex - 1;
325
+ }
303
326
  return {
304
- line: lines.length,
305
- column: (lines[lines.length - 1]?.length ?? 0) + 1
327
+ line: lowLineIndex + 1,
328
+ column: boundedIndex - lineStartOffsets[lowLineIndex] + 1
306
329
  };
307
330
  };
308
331
  //#endregion
@@ -355,13 +378,18 @@ const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
355
378
  const isConfigOrCiPath = (relativePath) => /(?:^|\/)(?:package\.json|Dockerfile|docker-compose\.ya?ml|\.github\/workflows\/[^/]+\.ya?ml|vercel\.json|next\.config\.[cm]?[jt]s|netlify\.toml)$/i.test(relativePath);
356
379
  //#endregion
357
380
  //#region src/plugin/rules/security-scan/utils/is-production-file-path.ts
381
+ const classificationCacheByPattern = /* @__PURE__ */ new Map();
358
382
  const isProductionFilePath = (relativePath, sourceFilePattern) => {
359
- if (!sourceFilePattern.test(relativePath)) return false;
360
- if (TEST_CONTEXT_PATTERN.test(relativePath)) return false;
361
- if (BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath)) return false;
362
- if (DOCUMENTATION_CONTEXT_PATTERN.test(relativePath)) return false;
363
- if (GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath)) return false;
364
- return true;
383
+ let classificationByPath = classificationCacheByPattern.get(sourceFilePattern);
384
+ if (!classificationByPath) {
385
+ classificationByPath = /* @__PURE__ */ new Map();
386
+ classificationCacheByPattern.set(sourceFilePattern, classificationByPath);
387
+ }
388
+ const cached = classificationByPath.get(relativePath);
389
+ if (cached !== void 0) return cached;
390
+ const isProduction = sourceFilePattern.test(relativePath) && !TEST_CONTEXT_PATTERN.test(relativePath) && !BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(relativePath) && !GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath);
391
+ classificationByPath.set(relativePath, isProduction);
392
+ return isProduction;
365
393
  };
366
394
  //#endregion
367
395
  //#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
@@ -760,24 +788,6 @@ const collectChildComponentNames = (element, into) => {
760
788
  into.add(name);
761
789
  });
762
790
  };
763
- const findSameFileComponentBody = (programRoot, componentName) => {
764
- let foundBody = null;
765
- walkAst(programRoot, (node) => {
766
- if (foundBody) return false;
767
- if (isNodeOfType(node, "FunctionDeclaration") && node.id && node.id.name === componentName) {
768
- foundBody = node.body;
769
- return false;
770
- }
771
- if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.id.name === componentName) {
772
- const initializer = node.init;
773
- if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) {
774
- foundBody = initializer.body;
775
- return false;
776
- }
777
- }
778
- });
779
- return foundBody;
780
- };
781
791
  const countEffectHookCalls = (body) => {
782
792
  if (!body) return 0;
783
793
  let count = 0;
@@ -787,6 +797,36 @@ const countEffectHookCalls = (body) => {
787
797
  });
788
798
  return count;
789
799
  };
800
+ const componentEffectIndexCache = /* @__PURE__ */ new WeakMap();
801
+ const getComponentEffectIndex = (programRoot) => {
802
+ const cached = componentEffectIndexCache.get(programRoot);
803
+ if (cached) return cached;
804
+ const bodyByName = /* @__PURE__ */ new Map();
805
+ walkAst(programRoot, (node) => {
806
+ if (isNodeOfType(node, "FunctionDeclaration") && node.id && !bodyByName.has(node.id.name)) {
807
+ bodyByName.set(node.id.name, node.body);
808
+ return;
809
+ }
810
+ if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && !bodyByName.has(node.id.name)) {
811
+ const initializer = node.init;
812
+ if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) bodyByName.set(node.id.name, initializer.body);
813
+ }
814
+ });
815
+ const index = {
816
+ bodyByName,
817
+ effectCountByName: /* @__PURE__ */ new Map()
818
+ };
819
+ componentEffectIndexCache.set(programRoot, index);
820
+ return index;
821
+ };
822
+ const getSameFileComponentEffectCount = (programRoot, componentName) => {
823
+ const index = getComponentEffectIndex(programRoot);
824
+ const cachedCount = index.effectCountByName.get(componentName);
825
+ if (cachedCount !== void 0) return cachedCount;
826
+ const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
827
+ index.effectCountByName.set(componentName, count);
828
+ return count;
829
+ };
790
830
  const activityWrapsEffectHeavySubtree = defineRule({
791
831
  id: "activity-wraps-effect-heavy-subtree",
792
832
  title: "Activity wraps an effect-heavy subtree",
@@ -843,9 +883,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
843
883
  let totalEffects = 0;
844
884
  const effectfulChildren = [];
845
885
  for (const componentName of childComponentNames) {
846
- const body = findSameFileComponentBody(programRoot, componentName);
847
- if (!body) continue;
848
- const effectCount = countEffectHookCalls(body);
886
+ const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
849
887
  if (effectCount === 0) continue;
850
888
  totalEffects += effectCount;
851
889
  effectfulChildren.push(`<${componentName}>`);
@@ -1394,11 +1432,16 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
1394
1432
  };
1395
1433
  //#endregion
1396
1434
  //#region src/plugin/utils/get-element-type.ts
1435
+ const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
1436
+ const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
1397
1437
  const readJsxA11ySettings = (settings) => {
1398
- if (!settings) return {};
1438
+ if (!settings) return EMPTY_JSX_A11Y_SETTINGS;
1439
+ const cachedSettings = jsxA11ySettingsCache.get(settings);
1440
+ if (cachedSettings) return cachedSettings;
1399
1441
  const block = settings["jsx-a11y"];
1400
- if (!block || typeof block !== "object") return {};
1401
- return block;
1442
+ const a11ySettings = block && typeof block === "object" ? block : EMPTY_JSX_A11Y_SETTINGS;
1443
+ jsxA11ySettingsCache.set(settings, a11ySettings);
1444
+ return a11ySettings;
1402
1445
  };
1403
1446
  const flattenJsxName$2 = (name) => {
1404
1447
  if (isNodeOfType(name, "JSXIdentifier")) return name.name;
@@ -1406,8 +1449,7 @@ const flattenJsxName$2 = (name) => {
1406
1449
  if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
1407
1450
  return "";
1408
1451
  };
1409
- const getElementType = (openingElement, settings) => {
1410
- const a11ySettings = readJsxA11ySettings(settings);
1452
+ const computeElementType = (openingElement, a11ySettings) => {
1411
1453
  const baseName = flattenJsxName$2(openingElement.name);
1412
1454
  if (a11ySettings.polymorphicPropName) {
1413
1455
  const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
@@ -1419,6 +1461,23 @@ const getElementType = (openingElement, settings) => {
1419
1461
  if (a11ySettings.components && baseName in a11ySettings.components) return a11ySettings.components[baseName];
1420
1462
  return baseName;
1421
1463
  };
1464
+ const elementTypeCache = /* @__PURE__ */ new WeakMap();
1465
+ const getElementType = (openingElement, settings) => {
1466
+ const cachedEntry = elementTypeCache.get(openingElement);
1467
+ if (cachedEntry && cachedEntry.settings === settings) return cachedEntry.elementType;
1468
+ const elementType = computeElementType(openingElement, readJsxA11ySettings(settings));
1469
+ elementTypeCache.set(openingElement, {
1470
+ settings,
1471
+ elementType
1472
+ });
1473
+ return elementType;
1474
+ };
1475
+ //#endregion
1476
+ //#region src/plugin/utils/has-jsx-a11y-settings.ts
1477
+ const hasJsxA11ySettings = (settings) => {
1478
+ const block = settings?.["jsx-a11y"];
1479
+ return typeof block === "object" && block !== null;
1480
+ };
1422
1481
  //#endregion
1423
1482
  //#region src/plugin/utils/find-import-source-for-name.ts
1424
1483
  const collectFromProgram = (programRoot) => {
@@ -1623,7 +1682,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
1623
1682
  };
1624
1683
  //#endregion
1625
1684
  //#region src/plugin/utils/normalize-filename.ts
1626
- const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
1685
+ const normalizeFilename = (filename) => filename.includes("\\") ? filename.replaceAll("\\", "/") : filename;
1627
1686
  //#endregion
1628
1687
  //#region src/plugin/utils/is-generated-image-render-context.ts
1629
1688
  const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
@@ -1972,7 +2031,12 @@ const altText = defineRule({
1972
2031
  const objectAliases = new Set(settings.object ?? []);
1973
2032
  const areaAliases = new Set(settings.area ?? []);
1974
2033
  const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
2034
+ const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
1975
2035
  return { JSXOpeningElement(node) {
2036
+ if (!fileHasJsxA11ySettings && isNodeOfType(node.name, "JSXIdentifier")) {
2037
+ const rawName = node.name.name;
2038
+ if (rawName !== "img" && rawName !== "object" && rawName !== "area" && rawName.toLowerCase() !== "input" && !imgAliases.has(rawName) && !objectAliases.has(rawName) && !areaAliases.has(rawName) && !inputImageAliases.has(rawName)) return;
2039
+ }
1976
2040
  if (isGeneratedImageRenderContext(context, node)) return;
1977
2041
  const tag = getElementType(node, context.settings);
1978
2042
  if (checkImg && (tag === "img" || imgAliases.has(tag))) {
@@ -2155,7 +2219,9 @@ const anchorIsValid = defineRule({
2155
2219
  category: "Accessibility",
2156
2220
  create: (context) => {
2157
2221
  const settings = resolveSettings$50(context.settings);
2222
+ const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
2158
2223
  return { JSXOpeningElement(node) {
2224
+ if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
2159
2225
  if (getElementType(node, context.settings) !== "a") return;
2160
2226
  let hrefAttribute;
2161
2227
  for (const attributeName of settings.hrefAttributeNames) {
@@ -3458,7 +3524,7 @@ const SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS = [
3458
3524
  ];
3459
3525
  const SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS = [/(?:^|[_\-\s])(?:example|sample|dummy)(?:$|[_\-\s])/i];
3460
3526
  const SECRET_PLACEHOLDER_CONTEXT_PATTERN = /(?:placeholder|example|sample|dummy|masked|redacted|mask)/i;
3461
- const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth)/i;
3527
+ const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth(?!or(?!i[sz])))/i;
3462
3528
  const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
3463
3529
  const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
3464
3530
  const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
@@ -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
  };
@@ -5658,12 +5719,20 @@ const getTemplateInterpolations = (valueTail) => {
5658
5719
  return interpolations === null ? "" : interpolations.join(" ");
5659
5720
  };
5660
5721
  const isInertParseTarget = (target, fileContent) => {
5722
+ const fileHasCreateElement = fileContent.includes("createElement");
5723
+ const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
5724
+ if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
5661
5725
  const escapedTarget = escapeRegExp(target);
5662
5726
  const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
5663
5727
  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;
5728
+ if (fileHasCreateElement) {
5729
+ if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
5730
+ if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
5731
+ }
5732
+ if (fileHasIsolatedDocument) {
5733
+ if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
5734
+ }
5735
+ if (!fileHasCreateElement) return false;
5667
5736
  if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
5668
5737
  const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
5669
5738
  const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
@@ -5756,10 +5825,9 @@ const dangerousHtmlSink = defineRule({
5756
5825
  const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
5757
5826
  if (valueIdentifier !== void 0) {
5758
5827
  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;
5828
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5829
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5830
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
5763
5831
  }
5764
5832
  }
5765
5833
  const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
@@ -5908,6 +5976,7 @@ const noRedundantPaddingAxes = defineRule({
5908
5976
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5909
5977
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5910
5978
  if (!classNameLiteral) return;
5979
+ if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
5911
5980
  if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
5912
5981
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
5913
5982
  if (matchedPairs.length === 0) return;
@@ -5932,6 +6001,7 @@ const noRedundantSizeAxes = defineRule({
5932
6001
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5933
6002
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5934
6003
  if (!classNameLiteral) return;
6004
+ if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
5935
6005
  if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
5936
6006
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
5937
6007
  if (matchedPairs.length === 0) return;
@@ -5956,6 +6026,7 @@ const noSpaceOnFlexChildren = defineRule({
5956
6026
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5957
6027
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5958
6028
  if (!classNameLiteral) return;
6029
+ if (!classNameLiteral.includes("space-")) return;
5959
6030
  const tokens = tokenizeClassName(classNameLiteral);
5960
6031
  let hasFlexOrGridLayout = false;
5961
6032
  for (const token of tokens) {
@@ -6147,7 +6218,10 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
6147
6218
  const actualMinor = Number(match[2]);
6148
6219
  return actualMajor > major || actualMajor === major && actualMinor >= minor;
6149
6220
  };
6221
+ const containsJsxCache = /* @__PURE__ */ new WeakMap();
6150
6222
  const containsJsx$1 = (root) => {
6223
+ const cached = containsJsxCache.get(root);
6224
+ if (cached !== void 0) return cached;
6151
6225
  let found = false;
6152
6226
  const visit = (node) => {
6153
6227
  if (found) return;
@@ -6170,6 +6244,7 @@ const containsJsx$1 = (root) => {
6170
6244
  }
6171
6245
  };
6172
6246
  visit(root);
6247
+ containsJsxCache.set(root, found);
6173
6248
  return found;
6174
6249
  };
6175
6250
  const getStaticMemberName = (node) => {
@@ -6261,27 +6336,6 @@ const hasDisplayNameMember = (classNode) => {
6261
6336
  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
6337
  return false;
6263
6338
  };
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
6339
  const memberExpressionPath = (node) => {
6286
6340
  if (isNodeOfType(node, "Identifier")) return [node.name];
6287
6341
  if (!isNodeOfType(node, "MemberExpression")) return [];
@@ -6289,15 +6343,16 @@ const memberExpressionPath = (node) => {
6289
6343
  const propertyName = getStaticMemberName(node);
6290
6344
  return propertyName ? [...objectPath, propertyName] : objectPath;
6291
6345
  };
6292
- const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6293
- let found = false;
6346
+ const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
6347
+ const getDisplayNameAssignmentIndex = (programRoot) => {
6348
+ const cached = displayNameAssignmentIndexCache.get(programRoot);
6349
+ if (cached) return cached;
6350
+ const identifierTargets = /* @__PURE__ */ new Set();
6351
+ const memberPathSegments = /* @__PURE__ */ new Set();
6294
6352
  const visit = (node) => {
6295
- if (found) return;
6296
6353
  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
- }
6354
+ if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
6355
+ for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
6301
6356
  }
6302
6357
  const record = node;
6303
6358
  for (const key of Object.keys(record)) {
@@ -6306,12 +6361,18 @@ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6306
6361
  if (Array.isArray(child)) {
6307
6362
  for (const item of child) if (isAstNode(item)) visit(item);
6308
6363
  } else if (isAstNode(child)) visit(child);
6309
- if (found) return;
6310
6364
  }
6311
6365
  };
6312
6366
  visit(programRoot);
6313
- return found;
6367
+ const index = {
6368
+ identifierTargets,
6369
+ memberPathSegments
6370
+ };
6371
+ displayNameAssignmentIndexCache.set(programRoot, index);
6372
+ return index;
6314
6373
  };
6374
+ const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
6375
+ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
6315
6376
  const displayName = defineRule({
6316
6377
  id: "display-name",
6317
6378
  title: "Component missing display name",
@@ -7183,27 +7244,8 @@ const isDescendantScope = (inner, outer) => {
7183
7244
  return false;
7184
7245
  };
7185
7246
  //#endregion
7186
- //#region src/plugin/utils/is-ast-descendant.ts
7187
- /**
7188
- * True when `inner` is `outer` itself or any descendant in the AST
7189
- * `parent` chain. Walks `inner.parent` upward and stops at either a
7190
- * match (`true`) or the chain's root (`false`).
7191
- *
7192
- * Was duplicated byte-identical in:
7193
- * - exhaustive-deps-symbol-stability.ts
7194
- * - semantic/closure-captures.ts
7195
- */
7196
- const isAstDescendant = (inner, outer) => {
7197
- let current = inner;
7198
- while (current) {
7199
- if (current === outer) return true;
7200
- current = current.parent ?? null;
7201
- }
7202
- return false;
7203
- };
7204
- //#endregion
7205
7247
  //#region src/plugin/semantic/closure-captures.ts
7206
- const closureCaptures = (functionNode, scopes) => {
7248
+ const computeClosureCaptures = (functionNode, scopes) => {
7207
7249
  const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
7208
7250
  const out = [];
7209
7251
  const seen = /* @__PURE__ */ new Set();
@@ -7238,7 +7280,20 @@ const closureCaptures = (functionNode, scopes) => {
7238
7280
  }
7239
7281
  };
7240
7282
  visit(functionNode);
7241
- return out.filter((reference) => isAstDescendant(reference.identifier, functionNode));
7283
+ return out;
7284
+ };
7285
+ const capturesByAnalysis = /* @__PURE__ */ new WeakMap();
7286
+ const closureCaptures = (functionNode, scopes) => {
7287
+ let capturesByFunction = capturesByAnalysis.get(scopes);
7288
+ if (!capturesByFunction) {
7289
+ capturesByFunction = /* @__PURE__ */ new WeakMap();
7290
+ capturesByAnalysis.set(scopes, capturesByFunction);
7291
+ }
7292
+ const memoizedCaptures = capturesByFunction.get(functionNode);
7293
+ if (memoizedCaptures) return memoizedCaptures;
7294
+ const computedCaptures = computeClosureCaptures(functionNode, scopes);
7295
+ capturesByFunction.set(functionNode, computedCaptures);
7296
+ return computedCaptures;
7242
7297
  };
7243
7298
  //#endregion
7244
7299
  //#region src/plugin/utils/is-react-hook-name.ts
@@ -7367,6 +7422,25 @@ const resolveExhaustiveDepsSettings = (settings) => {
7367
7422
  };
7368
7423
  };
7369
7424
  //#endregion
7425
+ //#region src/plugin/utils/is-ast-descendant.ts
7426
+ /**
7427
+ * True when `inner` is `outer` itself or any descendant in the AST
7428
+ * `parent` chain. Walks `inner.parent` upward and stops at either a
7429
+ * match (`true`) or the chain's root (`false`).
7430
+ *
7431
+ * Was duplicated byte-identical in:
7432
+ * - exhaustive-deps-symbol-stability.ts
7433
+ * - semantic/closure-captures.ts
7434
+ */
7435
+ const isAstDescendant = (inner, outer) => {
7436
+ let current = inner;
7437
+ while (current) {
7438
+ if (current === outer) return true;
7439
+ current = current.parent ?? null;
7440
+ }
7441
+ return false;
7442
+ };
7443
+ //#endregion
7370
7444
  //#region src/plugin/rules/react-builtins/exhaustive-deps-symbol-stability.ts
7371
7445
  /**
7372
7446
  * Symbol-stability helpers consumed by the `exhaustive-deps` rule.
@@ -8266,9 +8340,14 @@ const firebaseQueryFilterAsAuth = defineRule({
8266
8340
  * `*Handler`), so the cheap escape-then-replace shape is enough
8267
8341
  * and avoids pulling picomatch into the per-file rule path.
8268
8342
  */
8343
+ const compiledGlobs = /* @__PURE__ */ new Map();
8269
8344
  const compileGlob = (pattern) => {
8345
+ const cached = compiledGlobs.get(pattern);
8346
+ if (cached) return cached;
8270
8347
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
8271
- return new RegExp(`^${escaped}$`);
8348
+ const compiled = new RegExp(`^${escaped}$`);
8349
+ compiledGlobs.set(pattern, compiled);
8350
+ return compiled;
8272
8351
  };
8273
8352
  //#endregion
8274
8353
  //#region src/plugin/rules/react-builtins/forbid-component-props.ts
@@ -8558,7 +8637,7 @@ const DEFAULT_HEADING_TAGS = [
8558
8637
  const resolveSettings$40 = (settings) => {
8559
8638
  const reactDoctor = settings?.["react-doctor"];
8560
8639
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
8561
- return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
8640
+ return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
8562
8641
  };
8563
8642
  const headingHasContent = defineRule({
8564
8643
  id: "heading-has-content",
@@ -8571,7 +8650,7 @@ const headingHasContent = defineRule({
8571
8650
  const settings = resolveSettings$40(context.settings);
8572
8651
  return { JSXOpeningElement(node) {
8573
8652
  const elementType = getElementType(node, context.settings);
8574
- if (!settings.headingTags.includes(elementType)) return;
8653
+ if (!settings.headingTags.has(elementType)) return;
8575
8654
  const parent = node.parent;
8576
8655
  if (parent && isNodeOfType(parent, "JSXElement")) {
8577
8656
  if (objectHasAccessibleChild(parent, context.settings)) return;
@@ -9210,10 +9289,10 @@ const imgRedundantAlt = defineRule({
9210
9289
  if (isGeneratedImageRenderContext(context)) return {};
9211
9290
  const settings = resolveSettings$37(context.settings);
9212
9291
  return { JSXOpeningElement(node) {
9213
- if (isGeneratedImageRenderContext(context, node)) return;
9214
9292
  const tag = getElementType(node, context.settings);
9215
9293
  if (!settings.components.includes(tag)) return;
9216
9294
  if (isHiddenFromScreenReader(node, context.settings)) return;
9295
+ if (isGeneratedImageRenderContext(context, node)) return;
9217
9296
  const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
9218
9297
  if (!altAttribute) return;
9219
9298
  if (altValueRedundant(altAttribute, settings.words)) context.report({
@@ -9268,6 +9347,7 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
9268
9347
  const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
9269
9348
  const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
9270
9349
  const SECURITY_CONTEXT_WINDOW_CHARS = 250;
9350
+ const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
9271
9351
  const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
9272
9352
  for (const callMatch of content.matchAll(pattern)) {
9273
9353
  const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
@@ -9297,6 +9377,7 @@ const insecureCryptoRisk = defineRule({
9297
9377
  if (!isProductionSourcePath(file.relativePath)) return [];
9298
9378
  if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
9299
9379
  if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
9380
+ if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
9300
9381
  const content = getScannableContent(file);
9301
9382
  let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
9302
9383
  if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
@@ -9475,6 +9556,7 @@ const KEYBOARD_EVENT_HANDLERS = [
9475
9556
  "onKeyUp"
9476
9557
  ];
9477
9558
  const ALL_EVENT_HANDLERS = [...MOUSE_EVENT_HANDLERS, ...KEYBOARD_EVENT_HANDLERS];
9559
+ const ALL_EVENT_HANDLERS_LOWER = new Set(ALL_EVENT_HANDLERS.map((handlerName) => handlerName.toLowerCase()));
9478
9560
  //#endregion
9479
9561
  //#region src/plugin/utils/is-disabled-element.ts
9480
9562
  const isDisabledElement = (openingElement) => {
@@ -9529,15 +9611,25 @@ const interactiveSupportsFocus = defineRule({
9529
9611
  const settings = resolveSettings$36(context.settings);
9530
9612
  const tabbableSet = new Set(settings.tabbable);
9531
9613
  return { JSXOpeningElement(node) {
9614
+ if (node.attributes.length === 0) return;
9532
9615
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
9533
9616
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
9534
9617
  const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
9618
+ if (!role) return;
9619
+ let hasInteractiveHandler = false;
9620
+ for (const attribute of node.attributes) {
9621
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
9622
+ const attributeName = getJsxAttributeName(attribute.name);
9623
+ if (attributeName && ALL_EVENT_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
9624
+ hasInteractiveHandler = true;
9625
+ break;
9626
+ }
9627
+ }
9628
+ if (!hasInteractiveHandler) return;
9535
9629
  const elementType = getElementType(node, context.settings);
9536
9630
  if (!HTML_TAGS.has(elementType)) return;
9537
- const hasInteractiveHandler = ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)));
9631
+ if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9538
9632
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
9539
- if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9540
- if (!role) return;
9541
9633
  if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
9542
9634
  const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
9543
9635
  context.report({
@@ -10176,8 +10268,6 @@ const jsCachePropertyAccess = defineRule({
10176
10268
  walkAst(loopBody, (child) => {
10177
10269
  if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
10178
10270
  if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
10179
- });
10180
- walkAst(loopBody, (child) => {
10181
10271
  if (!isNodeOfType(child, "MemberExpression")) return;
10182
10272
  if (child.computed) return;
10183
10273
  if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
@@ -10637,7 +10727,6 @@ const jsHoistIntl = defineRule({
10637
10727
  recommendation: "Move `new Intl.NumberFormat(...)` to the top of the file or wrap it in `useMemo`. Building one is slow, so don't redo it on every call",
10638
10728
  create: (context) => ({ NewExpression(node) {
10639
10729
  if (!isIntlNewExpression(node)) return;
10640
- if (isInsideCacheMemo(node)) return;
10641
10730
  let cursor = node.parent ?? null;
10642
10731
  let inFunctionBody = false;
10643
10732
  while (cursor) {
@@ -10654,6 +10743,7 @@ const jsHoistIntl = defineRule({
10654
10743
  cursor = cursor.parent ?? null;
10655
10744
  }
10656
10745
  if (!inFunctionBody) return;
10746
+ if (isInsideCacheMemo(node)) return;
10657
10747
  const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
10658
10748
  context.report({
10659
10749
  node,
@@ -10728,7 +10818,11 @@ const isSingleFieldEqualityPredicate = (node) => {
10728
10818
  if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
10729
10819
  return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
10730
10820
  };
10731
- const collectLoopBoundNames = (loop, names) => {
10821
+ const loopBoundNamesCache = /* @__PURE__ */ new WeakMap();
10822
+ const getLoopBoundNames = (loop) => {
10823
+ const cached = loopBoundNamesCache.get(loop);
10824
+ if (cached) return cached;
10825
+ const names = /* @__PURE__ */ new Set();
10732
10826
  if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
10733
10827
  if (isNodeOfType(child, "Identifier")) names.add(child.name);
10734
10828
  });
@@ -10745,6 +10839,8 @@ const collectLoopBoundNames = (loop, names) => {
10745
10839
  if (targetRoot) names.add(targetRoot);
10746
10840
  }
10747
10841
  });
10842
+ loopBoundNamesCache.set(loop, names);
10843
+ return names;
10748
10844
  };
10749
10845
  const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
10750
10846
  let cursor = receiver;
@@ -10770,7 +10866,7 @@ const isLoopVariantReceiver = (node) => {
10770
10866
  const loopBoundNames = /* @__PURE__ */ new Set();
10771
10867
  let ancestor = node.parent;
10772
10868
  while (ancestor) {
10773
- if (LOOP_TYPES.includes(ancestor.type)) collectLoopBoundNames(ancestor, loopBoundNames);
10869
+ if (LOOP_TYPES.includes(ancestor.type)) for (const name of getLoopBoundNames(ancestor)) loopBoundNames.add(name);
10774
10870
  ancestor = ancestor.parent;
10775
10871
  }
10776
10872
  if (loopBoundNames.has(receiverRoot)) return true;
@@ -12529,6 +12625,7 @@ const KNOWN_SLOT_PROP_NAMES = new Set([
12529
12625
  "bottomContent",
12530
12626
  "leftContent",
12531
12627
  "rightContent",
12628
+ "config",
12532
12629
  "value",
12533
12630
  "currentValue",
12534
12631
  "form",
@@ -12667,6 +12764,10 @@ const SLOT_PROP_SUFFIXES = [
12667
12764
  "Panel",
12668
12765
  "Overlay",
12669
12766
  "Shape",
12767
+ "Avatar",
12768
+ "Text",
12769
+ "State",
12770
+ "Zone",
12670
12771
  "Override",
12671
12772
  "Overrides",
12672
12773
  "Items",
@@ -12683,6 +12784,8 @@ const SLOT_PROP_SUFFIXES = [
12683
12784
  ];
12684
12785
  const isSlotPropName = (propName) => {
12685
12786
  if (KNOWN_SLOT_PROP_NAMES.has(propName)) return true;
12787
+ const decapitalized = propName.charAt(0).toLowerCase() + propName.slice(1);
12788
+ if (decapitalized !== propName && KNOWN_SLOT_PROP_NAMES.has(decapitalized)) return true;
12686
12789
  for (const suffix of SLOT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
12687
12790
  return false;
12688
12791
  };
@@ -14371,14 +14474,14 @@ const keyLifecycleRisk = defineRule({
14371
14474
  //#region src/plugin/rules/a11y/label-has-associated-control.ts
14372
14475
  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`.";
14373
14476
  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.";
14374
- const DEFAULT_CONTROL_COMPONENTS = [
14477
+ const DEFAULT_CONTROL_COMPONENTS = new Set([
14375
14478
  "input",
14376
14479
  "meter",
14377
14480
  "output",
14378
14481
  "progress",
14379
14482
  "select",
14380
14483
  "textarea"
14381
- ];
14484
+ ]);
14382
14485
  const DEFAULT_LABEL_ATTRIBUTES = [
14383
14486
  "alt",
14384
14487
  "aria-label",
@@ -14390,8 +14493,8 @@ const resolveSettings$24 = (settings) => {
14390
14493
  const jsxA11y = settings?.["jsx-a11y"];
14391
14494
  const forAttributes = (typeof jsxA11y === "object" && jsxA11y !== null ? jsxA11y : {}).attributes?.for ?? ["htmlFor"];
14392
14495
  return {
14393
- labelComponents: ["label", ...ruleSettings.labelComponents ?? []].sort(),
14394
- labelAttributes: [...new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []])].sort(),
14496
+ labelComponents: new Set(["label", ...ruleSettings.labelComponents ?? []]),
14497
+ labelAttributes: new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []]),
14395
14498
  controlComponents: ruleSettings.controlComponents ?? [],
14396
14499
  assert: ruleSettings.assert ?? "either",
14397
14500
  depth: Math.min(ruleSettings.depth ?? 5, 25),
@@ -14399,7 +14502,7 @@ const resolveSettings$24 = (settings) => {
14399
14502
  };
14400
14503
  };
14401
14504
  const isControlComponent = (tagName, controlComponents) => {
14402
- if (DEFAULT_CONTROL_COMPONENTS.includes(tagName)) return true;
14505
+ if (DEFAULT_CONTROL_COMPONENTS.has(tagName)) return true;
14403
14506
  return controlComponents.some((pattern) => compileGlob(pattern).test(tagName));
14404
14507
  };
14405
14508
  const searchForNestedControl = (child, currentDepth, searchContext) => {
@@ -14425,7 +14528,7 @@ const searchForAccessibleLabel = (child, currentDepth, searchContext) => {
14425
14528
  const attributeName = attribute.name;
14426
14529
  if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
14427
14530
  const propName = getJsxAttributeName(attributeName);
14428
- if (!propName || !searchContext.labelAttributes.includes(propName)) continue;
14531
+ if (!propName || !searchContext.labelAttributes.has(propName)) continue;
14429
14532
  const attributeValue = attribute.value;
14430
14533
  if (!attributeValue) continue;
14431
14534
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
@@ -14447,7 +14550,7 @@ const hasAccessibleLabel = (element, searchContext) => {
14447
14550
  const attributeName = attribute.name;
14448
14551
  if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
14449
14552
  const propName = getJsxAttributeName(attributeName);
14450
- if (propName && searchContext.labelAttributes.includes(propName)) return true;
14553
+ if (propName && searchContext.labelAttributes.has(propName)) return true;
14451
14554
  }
14452
14555
  for (const child of element.children) if (searchForAccessibleLabel(child, 1, searchContext)) return true;
14453
14556
  return false;
@@ -14470,7 +14573,7 @@ const labelHasAssociatedControl = defineRule({
14470
14573
  if (isTestlikeFile) return;
14471
14574
  const opening = node.openingElement;
14472
14575
  const tagName = getElementType(opening, context.settings);
14473
- if (!settings.labelComponents.includes(tagName)) return;
14576
+ if (!settings.labelComponents.has(tagName)) return;
14474
14577
  const hasHtmlFor = settings.forAttributes.some((attributeName) => Boolean(hasJsxPropIgnoreCase(opening.attributes, attributeName)));
14475
14578
  const searchContext = {
14476
14579
  depth: settings.depth,
@@ -15059,9 +15162,9 @@ const resolveSettings$23 = (settings) => {
15059
15162
  const reactDoctor = settings?.["react-doctor"];
15060
15163
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.mediaHasCaption ?? {} : {};
15061
15164
  return {
15062
- audio: [...DEFAULT_AUDIO, ...ruleSettings.audio ?? []],
15063
- video: [...DEFAULT_VIDEO, ...ruleSettings.video ?? []],
15064
- track: [...DEFAULT_TRACK, ...ruleSettings.track ?? []]
15165
+ audio: new Set([...DEFAULT_AUDIO, ...ruleSettings.audio ?? []]),
15166
+ video: new Set([...DEFAULT_VIDEO, ...ruleSettings.video ?? []]),
15167
+ track: new Set([...DEFAULT_TRACK, ...ruleSettings.track ?? []])
15065
15168
  };
15066
15169
  };
15067
15170
  const evaluateMuted = (attribute) => {
@@ -15090,7 +15193,7 @@ const childMayRenderTrack = (child, trackTags, settings) => {
15090
15193
  let rendersCaptionTrack = false;
15091
15194
  walkAst(expression, (inner) => {
15092
15195
  if (rendersCaptionTrack) return false;
15093
- if (isNodeOfType(inner, "JSXElement") && trackTags.includes(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
15196
+ if (isNodeOfType(inner, "JSXElement") && trackTags.has(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
15094
15197
  rendersCaptionTrack = true;
15095
15198
  return false;
15096
15199
  }
@@ -15108,7 +15211,7 @@ const mediaHasCaption = defineRule({
15108
15211
  const settings = resolveSettings$23(context.settings);
15109
15212
  return { JSXOpeningElement(node) {
15110
15213
  const tag = getElementType(node, context.settings);
15111
- if (!(settings.audio.includes(tag) || settings.video.includes(tag))) return;
15214
+ if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
15112
15215
  if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
15113
15216
  const parent = node.parent;
15114
15217
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
@@ -15123,7 +15226,7 @@ const mediaHasCaption = defineRule({
15123
15226
  if (!isNodeOfType(child, "JSXElement")) return false;
15124
15227
  const opening = child.openingElement;
15125
15228
  const childTag = getElementType(opening, context.settings);
15126
- if (!settings.track.includes(childTag)) return false;
15229
+ if (!settings.track.has(childTag)) return false;
15127
15230
  const kindAttribute = hasJsxPropIgnoreCase(opening.attributes, "kind");
15128
15231
  if (!kindAttribute) return false;
15129
15232
  let kindValue = kindAttribute.value;
@@ -15434,16 +15537,30 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
15434
15537
  const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
15435
15538
  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;
15436
15539
  const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
15437
- const doesSourceTextExportName = (sourceText, exportedName) => {
15540
+ const collectSourceTextExportNames = (sourceText) => {
15438
15541
  const strippedSource = stripJsComments(sourceText);
15439
- if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
15440
- for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
15441
- 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;
15442
- return false;
15542
+ const exportedNames = /* @__PURE__ */ new Set();
15543
+ if (DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) exportedNames.add("default");
15544
+ for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1]) exportedNames.add(match[1]);
15545
+ for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) {
15546
+ const specifiersText = match[1] ?? "";
15547
+ for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
15548
+ }
15549
+ return exportedNames;
15443
15550
  };
15551
+ const exportNamesCache = /* @__PURE__ */ new Map();
15444
15552
  const doesModuleExportName = (filePath, exportedName) => {
15445
15553
  try {
15446
- return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
15554
+ const fileStat = fs.statSync(filePath);
15555
+ const cached = exportNamesCache.get(filePath);
15556
+ if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
15557
+ const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
15558
+ exportNamesCache.set(filePath, {
15559
+ mtimeMs: fileStat.mtimeMs,
15560
+ size: fileStat.size,
15561
+ exportedNames
15562
+ });
15563
+ return exportedNames.has(exportedName);
15447
15564
  } catch {
15448
15565
  return false;
15449
15566
  }
@@ -15588,8 +15705,12 @@ const containsFetchCall = (node, options) => {
15588
15705
  const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
15589
15706
  let didFindFetchCall = false;
15590
15707
  walkAst(node, (child) => {
15708
+ if (didFindFetchCall) return false;
15591
15709
  if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
15592
- if (isFetchCall$1(child)) didFindFetchCall = true;
15710
+ if (isFetchCall$1(child)) {
15711
+ didFindFetchCall = true;
15712
+ return false;
15713
+ }
15593
15714
  });
15594
15715
  return didFindFetchCall;
15595
15716
  };
@@ -15603,6 +15724,8 @@ const nextjsNoClientFetchForServerData = defineRule({
15603
15724
  severity: "warn",
15604
15725
  recommendation: "Remove 'use client' and fetch directly in the Server Component. No API round-trip, and secrets stay on the server.",
15605
15726
  create: (context) => {
15727
+ const filename = normalizeFilename(context.filename ?? "");
15728
+ if (!(PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages"))) return {};
15606
15729
  let fileHasUseClient = false;
15607
15730
  return {
15608
15731
  Program(programNode) {
@@ -15612,8 +15735,7 @@ const nextjsNoClientFetchForServerData = defineRule({
15612
15735
  if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
15613
15736
  const callback = getEffectCallback(node);
15614
15737
  if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
15615
- const filename = normalizeFilename(context.filename ?? "");
15616
- if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
15738
+ context.report({
15617
15739
  node,
15618
15740
  message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
15619
15741
  });
@@ -16278,16 +16400,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
16278
16400
  recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
16279
16401
  create: (context) => {
16280
16402
  let resolveBinding = () => null;
16403
+ let isRouteHandlerFile = false;
16404
+ let mutatingSegment = null;
16281
16405
  return {
16282
16406
  Program(node) {
16283
16407
  resolveBinding = buildProgramBindingLookup(node);
16408
+ const filename = normalizeFilename(context.filename ?? "");
16409
+ isRouteHandlerFile = ROUTE_HANDLER_FILE_PATTERN.test(filename) && !CRON_ROUTE_PATTERN.test(filename);
16410
+ mutatingSegment = isRouteHandlerFile ? extractMutatingRouteSegment(filename) : null;
16284
16411
  },
16285
16412
  ExportNamedDeclaration(node) {
16286
- const filename = normalizeFilename(context.filename ?? "");
16287
- if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
16288
- if (CRON_ROUTE_PATTERN.test(filename)) return;
16413
+ if (!isRouteHandlerFile) return;
16289
16414
  if (!isExportedGetHandler(node)) return;
16290
- const mutatingSegment = extractMutatingRouteSegment(filename);
16291
16415
  const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
16292
16416
  for (const handlerBody of handlerBodies) {
16293
16417
  const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
@@ -17319,8 +17443,15 @@ const getProgramAnalysis = (anyNode) => {
17319
17443
  programToAnalysis.set(programNode, analysis);
17320
17444
  return analysis;
17321
17445
  };
17446
+ const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
17322
17447
  const getScopeForNode = (node, manager) => {
17323
17448
  if (!node.range) return null;
17449
+ let scopeByNode = scopeByNodeCache.get(manager);
17450
+ if (!scopeByNode) {
17451
+ scopeByNode = /* @__PURE__ */ new WeakMap();
17452
+ scopeByNodeCache.set(manager, scopeByNode);
17453
+ }
17454
+ if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
17324
17455
  let bestScope = null;
17325
17456
  let bestSize = Infinity;
17326
17457
  for (const scope of manager.scopes) {
@@ -17333,6 +17464,7 @@ const getScopeForNode = (node, manager) => {
17333
17464
  bestScope = scope;
17334
17465
  }
17335
17466
  }
17467
+ scopeByNode.set(node, bestScope);
17336
17468
  return bestScope;
17337
17469
  };
17338
17470
  //#endregion
@@ -17367,11 +17499,20 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
17367
17499
  } else if (isAstNode(child)) descend(child, visit, visited);
17368
17500
  }
17369
17501
  };
17502
+ const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
17370
17503
  const getUpstreamRefs = (analysis, ref) => {
17504
+ let upstreamByRef = upstreamRefsCache.get(analysis);
17505
+ if (!upstreamByRef) {
17506
+ upstreamByRef = /* @__PURE__ */ new WeakMap();
17507
+ upstreamRefsCache.set(analysis, upstreamByRef);
17508
+ }
17509
+ const cached = upstreamByRef.get(ref);
17510
+ if (cached) return cached;
17371
17511
  const refs = [];
17372
17512
  ascend(analysis, ref, (upRef) => {
17373
17513
  refs.push(upRef);
17374
17514
  });
17515
+ upstreamByRef.set(ref, refs);
17375
17516
  return refs;
17376
17517
  };
17377
17518
  const findDownstreamNodes = (topNode, type) => {
@@ -17381,11 +17522,24 @@ const findDownstreamNodes = (topNode, type) => {
17381
17522
  });
17382
17523
  return nodes;
17383
17524
  };
17525
+ const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
17384
17526
  const getRef = (analysis, identifier) => {
17527
+ let refByIdentifier = refByIdentifierCache.get(analysis);
17528
+ if (!refByIdentifier) {
17529
+ refByIdentifier = /* @__PURE__ */ new WeakMap();
17530
+ refByIdentifierCache.set(analysis, refByIdentifier);
17531
+ }
17532
+ if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
17533
+ let resolvedReference = null;
17385
17534
  const scope = getScopeForNode(identifier, analysis.scopeManager);
17386
- if (!scope) return null;
17387
- for (const reference of scope.references) if (reference.identifier === identifier) return reference;
17388
- return null;
17535
+ if (scope) {
17536
+ for (const reference of scope.references) if (reference.identifier === identifier) {
17537
+ resolvedReference = reference;
17538
+ break;
17539
+ }
17540
+ }
17541
+ refByIdentifier.set(identifier, resolvedReference);
17542
+ return resolvedReference;
17389
17543
  };
17390
17544
  const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
17391
17545
  const getDownstreamRefs = (analysis, node) => {
@@ -17460,22 +17614,6 @@ const isEventualCallTo = (analysis, ref, predicate) => {
17460
17614
  };
17461
17615
  //#endregion
17462
17616
  //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
17463
- const getOuterScopeContaining = (analysis, node) => {
17464
- if (!node.range) return null;
17465
- let best = null;
17466
- let bestSize = Infinity;
17467
- for (const scope of analysis.scopeManager.scopes) {
17468
- const block = scope.block;
17469
- if (!block?.range) continue;
17470
- if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
17471
- const size = block.range[1] - block.range[0];
17472
- if (size <= bestSize) {
17473
- bestSize = size;
17474
- best = scope;
17475
- }
17476
- }
17477
- return best;
17478
- };
17479
17617
  const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
17480
17618
  const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
17481
17619
  const isReactFunctionalComponent = (node) => {
@@ -17506,7 +17644,7 @@ const isReactFunctionalHOC = (analysis, node) => {
17506
17644
  const isWrappedSeparately = () => {
17507
17645
  if (!isNodeOfType(node.id, "Identifier")) return false;
17508
17646
  const bindingName = node.id.name;
17509
- const containingScope = getOuterScopeContaining(analysis, node);
17647
+ const containingScope = getScopeForNode(node, analysis.scopeManager);
17510
17648
  if (!containingScope) return false;
17511
17649
  const variable = containingScope.variables.find((v) => v.name === bindingName);
17512
17650
  if (!variable) return false;
@@ -18856,7 +18994,17 @@ const containsRenderOutput = (node, rootNode, scopes) => {
18856
18994
  }
18857
18995
  return false;
18858
18996
  };
18859
- const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
18997
+ const renderOutputCache = /* @__PURE__ */ new WeakMap();
18998
+ const functionContainsReactRenderOutput = (functionNode, scopes) => {
18999
+ const cachedEntry = renderOutputCache.get(functionNode);
19000
+ if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
19001
+ const hasRenderOutput = containsRenderOutput(functionNode, functionNode, scopes);
19002
+ renderOutputCache.set(functionNode, {
19003
+ scopes,
19004
+ hasRenderOutput
19005
+ });
19006
+ return hasRenderOutput;
19007
+ };
18860
19008
  //#endregion
18861
19009
  //#region src/plugin/utils/is-component-declaration.ts
18862
19010
  const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
@@ -19361,8 +19509,8 @@ const CONTEXT_MODULES = [
19361
19509
  ];
19362
19510
  const isCreateContextCallee = (callee) => {
19363
19511
  if (isNodeOfType(callee, "Identifier")) {
19364
- for (const moduleName of CONTEXT_MODULES) if (getImportedNameFromModule(callee, callee.name, moduleName) === "createContext") return true;
19365
- return false;
19512
+ const binding = getImportBindingForName(callee, callee.name);
19513
+ return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
19366
19514
  }
19367
19515
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
19368
19516
  const namespaceIdentifier = callee.object;
@@ -19372,8 +19520,8 @@ const isCreateContextCallee = (callee) => {
19372
19520
  if (propertyIdentifier.name !== "createContext") return false;
19373
19521
  const namespaceName = namespaceIdentifier.name;
19374
19522
  if (isCanonicalReactNamespaceName(namespaceName)) return true;
19375
- for (const moduleName of CONTEXT_MODULES) if (isImportedFromModule(namespaceIdentifier, namespaceName, moduleName)) return true;
19376
- return false;
19523
+ const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
19524
+ return importSource !== null && CONTEXT_MODULES.includes(importSource);
19377
19525
  }
19378
19526
  return false;
19379
19527
  };
@@ -19775,38 +19923,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
19775
19923
  };
19776
19924
  const parseColorToRgb = (value) => {
19777
19925
  const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
19778
- const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
19779
- if (hex8Match) return {
19780
- red: parseInt(hex8Match[1], 16),
19781
- green: parseInt(hex8Match[2], 16),
19782
- blue: parseInt(hex8Match[3], 16)
19783
- };
19784
- const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
19785
- if (hex6Match) return {
19786
- red: parseInt(hex6Match[1], 16),
19787
- green: parseInt(hex6Match[2], 16),
19788
- blue: parseInt(hex6Match[3], 16)
19789
- };
19790
- const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
19791
- if (hex4Match) return {
19792
- red: parseInt(hex4Match[1] + hex4Match[1], 16),
19793
- green: parseInt(hex4Match[2] + hex4Match[2], 16),
19794
- blue: parseInt(hex4Match[3] + hex4Match[3], 16)
19795
- };
19796
- const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
19797
- if (hex3Match) return {
19798
- red: parseInt(hex3Match[1] + hex3Match[1], 16),
19799
- green: parseInt(hex3Match[2] + hex3Match[2], 16),
19800
- blue: parseInt(hex3Match[3] + hex3Match[3], 16)
19801
- };
19802
- const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
19803
- if (rgbMatch) return {
19804
- red: parseInt(rgbMatch[1], 10),
19805
- green: parseInt(rgbMatch[2], 10),
19806
- blue: parseInt(rgbMatch[3], 10)
19807
- };
19808
- const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
19809
- if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
19926
+ if (trimmed.startsWith("#")) {
19927
+ const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
19928
+ if (hex8Match) return {
19929
+ red: parseInt(hex8Match[1], 16),
19930
+ green: parseInt(hex8Match[2], 16),
19931
+ blue: parseInt(hex8Match[3], 16)
19932
+ };
19933
+ const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
19934
+ if (hex6Match) return {
19935
+ red: parseInt(hex6Match[1], 16),
19936
+ green: parseInt(hex6Match[2], 16),
19937
+ blue: parseInt(hex6Match[3], 16)
19938
+ };
19939
+ const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
19940
+ if (hex4Match) return {
19941
+ red: parseInt(hex4Match[1] + hex4Match[1], 16),
19942
+ green: parseInt(hex4Match[2] + hex4Match[2], 16),
19943
+ blue: parseInt(hex4Match[3] + hex4Match[3], 16)
19944
+ };
19945
+ const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
19946
+ if (hex3Match) return {
19947
+ red: parseInt(hex3Match[1] + hex3Match[1], 16),
19948
+ green: parseInt(hex3Match[2] + hex3Match[2], 16),
19949
+ blue: parseInt(hex3Match[3] + hex3Match[3], 16)
19950
+ };
19951
+ }
19952
+ if (trimmed.includes("rgb")) {
19953
+ const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
19954
+ if (rgbMatch) return {
19955
+ red: parseInt(rgbMatch[1], 10),
19956
+ green: parseInt(rgbMatch[2], 10),
19957
+ blue: parseInt(rgbMatch[3], 10)
19958
+ };
19959
+ }
19960
+ if (trimmed.includes("hsl")) {
19961
+ const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
19962
+ if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
19963
+ }
19810
19964
  return null;
19811
19965
  };
19812
19966
  //#endregion
@@ -19834,9 +19988,18 @@ const extractColorFromShadowLayer = (layer) => {
19834
19988
  if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
19835
19989
  return null;
19836
19990
  };
19991
+ const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
19992
+ const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
19993
+ const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
19994
+ const SHADOW_BLUR_TOKEN_INDEX = 2;
19837
19995
  const parseShadowLayerBlur = (layer) => {
19838
- const numericTokens = [...layer.replace(/rgba?\([^)]*\)/g, "").replace(/#[0-9a-f]{3,8}\b/gi, "").matchAll(/(\d+(?:\.\d+)?)(px)?/g)].map((match) => parseFloat(match[1]));
19839
- return numericTokens.length >= 3 ? numericTokens[2] : 0;
19996
+ const withoutColors = layer.replace(RGB_FUNCTION_PATTERN, "").replace(HEX_COLOR_PATTERN, "");
19997
+ let tokenIndex = 0;
19998
+ for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
19999
+ if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
20000
+ tokenIndex += 1;
20001
+ }
20002
+ return 0;
19840
20003
  };
19841
20004
  const hasColoredGlowShadow = (shadowValue) => {
19842
20005
  for (const layer of splitShadowLayers(shadowValue)) {
@@ -19956,7 +20119,10 @@ const isIntrinsicJsxAttribute = (node) => {
19956
20119
  };
19957
20120
  //#endregion
19958
20121
  //#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
20122
+ const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
19959
20123
  const collectComponentPropNames = (componentFunction) => {
20124
+ const cached = componentPropNamesCache.get(componentFunction);
20125
+ if (cached) return cached;
19960
20126
  const propNames = /* @__PURE__ */ new Set();
19961
20127
  if (!isFunctionLike$1(componentFunction)) return propNames;
19962
20128
  const propsObjectParamNames = /* @__PURE__ */ new Set();
@@ -19970,27 +20136,23 @@ const collectComponentPropNames = (componentFunction) => {
19970
20136
  if (child !== componentBody && isFunctionLike$1(child)) return false;
19971
20137
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
19972
20138
  });
20139
+ componentPropNamesCache.set(componentFunction, propNames);
19973
20140
  return propNames;
19974
20141
  };
19975
- const declaresBindingNamed = (functionNode, bindingName) => {
20142
+ const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
20143
+ const getOwnScopeBoundNames = (functionNode) => {
20144
+ const cached = ownScopeBoundNamesCache.get(functionNode);
20145
+ if (cached) return cached;
19976
20146
  const boundNames = /* @__PURE__ */ new Set();
19977
20147
  if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
19978
- if (boundNames.has(bindingName)) return true;
19979
- let declaresName = false;
19980
20148
  walkAst(functionNode, (child) => {
19981
- if (declaresName) return false;
19982
20149
  if (child !== functionNode && isFunctionLike$1(child)) return false;
19983
- if (isNodeOfType(child, "VariableDeclarator")) {
19984
- const declaratorNames = /* @__PURE__ */ new Set();
19985
- collectPatternNames(child.id, declaratorNames);
19986
- if (declaratorNames.has(bindingName)) {
19987
- declaresName = true;
19988
- return false;
19989
- }
19990
- }
20150
+ if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
19991
20151
  });
19992
- return declaresName;
20152
+ ownScopeBoundNamesCache.set(functionNode, boundNames);
20153
+ return boundNames;
19993
20154
  };
20155
+ const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
19994
20156
  const isPropertyNamePosition = (identifier) => {
19995
20157
  const parent = identifier.parent;
19996
20158
  if (!parent) return false;
@@ -20413,36 +20575,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
20413
20575
  }
20414
20576
  return hasNestedFunction;
20415
20577
  };
20416
- const isReseededDraftBuffer = (useStateCall, isPropName) => {
20417
- const setterName = getStateSetterName(useStateCall);
20418
- if (!setterName) return false;
20419
- const componentFunction = findEnclosingFunction(useStateCall);
20420
- if (!componentFunction) return false;
20421
- let isReseeded = false;
20422
- walkAst(componentFunction, (child) => {
20423
- if (isReseeded) return false;
20424
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
20425
- isReseeded = true;
20426
- return false;
20427
- }
20428
- });
20429
- return isReseeded;
20578
+ const isInRenderScope = (node, componentFunction) => {
20579
+ let cursor = node.parent ?? null;
20580
+ while (cursor && cursor !== componentFunction) {
20581
+ if (isFunctionLike$1(cursor)) return false;
20582
+ cursor = cursor.parent ?? null;
20583
+ }
20584
+ return true;
20430
20585
  };
20431
- const isAdjustedDuringRender = (useStateCall, isPropName) => {
20586
+ const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
20432
20587
  const setterName = getStateSetterName(useStateCall);
20433
20588
  if (!setterName) return false;
20434
20589
  const componentFunction = findEnclosingFunction(useStateCall);
20435
20590
  if (!componentFunction) return false;
20436
- let isAdjusted = false;
20591
+ let isExempt = false;
20437
20592
  walkAst(componentFunction, (child) => {
20438
- if (isAdjusted) return false;
20439
- if (child !== componentFunction && isFunctionLike$1(child)) return false;
20440
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName)) {
20441
- isAdjusted = true;
20593
+ if (isExempt) return false;
20594
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && (isHandlerShapedReseed(child, componentFunction) || isInRenderScope(child, componentFunction))) {
20595
+ isExempt = true;
20442
20596
  return false;
20443
20597
  }
20444
20598
  });
20445
- return isAdjusted;
20599
+ return isExempt;
20446
20600
  };
20447
20601
  const noDerivedUseState = defineRule({
20448
20602
  id: "no-derived-useState",
@@ -20459,8 +20613,7 @@ const noDerivedUseState = defineRule({
20459
20613
  const initializer = node.arguments[0];
20460
20614
  if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
20461
20615
  if (isInitialOnlyPropName(initializer.name)) return;
20462
- if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20463
- if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20616
+ if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20464
20617
  context.report({
20465
20618
  node,
20466
20619
  message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
@@ -20471,8 +20624,7 @@ const noDerivedUseState = defineRule({
20471
20624
  const rootIdentifierName = getRootIdentifierName(initializer);
20472
20625
  if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
20473
20626
  if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
20474
- if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20475
- if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20627
+ if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20476
20628
  context.report({
20477
20629
  node,
20478
20630
  message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
@@ -22171,20 +22323,20 @@ const getTriggerGuardRootName = (testNode) => {
22171
22323
  return null;
22172
22324
  };
22173
22325
  const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
22326
+ const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
22327
+ const settersWithAnyCall = /* @__PURE__ */ new Set();
22328
+ const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
22329
+ walkAst(componentBody, (child) => {
22330
+ if (!isNodeOfType(child, "CallExpression")) return;
22331
+ if (!isNodeOfType(child.callee, "Identifier")) return;
22332
+ const setterName = child.callee.name;
22333
+ if (!setterNames.has(setterName)) return;
22334
+ settersWithAnyCall.add(setterName);
22335
+ if (settersWithNonHandlerCall.has(setterName)) return;
22336
+ if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
22337
+ });
22174
22338
  const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
22175
- for (const binding of useStateBindings) {
22176
- let didFindAnySetterCall = false;
22177
- let areAllSetterCallsInHandlers = true;
22178
- walkAst(componentBody, (child) => {
22179
- if (!areAllSetterCallsInHandlers) return false;
22180
- if (!isNodeOfType(child, "CallExpression")) return;
22181
- if (!isNodeOfType(child.callee, "Identifier")) return;
22182
- if (child.callee.name !== binding.setterName) return;
22183
- didFindAnySetterCall = true;
22184
- if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
22185
- });
22186
- if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
22187
- }
22339
+ for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
22188
22340
  return handlerOnlyWriteStateNames;
22189
22341
  };
22190
22342
  const noEventTriggerState = defineRule({
@@ -22524,6 +22676,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
22524
22676
  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)-)/;
22525
22677
  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)-)/;
22526
22678
  const splitVariantScope = (token) => {
22679
+ if (!token.includes(":")) return {
22680
+ scope: "",
22681
+ utility: token.startsWith("!") ? token.slice(1) : token
22682
+ };
22527
22683
  const segments = token.split(":");
22528
22684
  const rawUtility = segments[segments.length - 1];
22529
22685
  return {
@@ -22547,6 +22703,7 @@ const noGrayOnColoredBackground = defineRule({
22547
22703
  const bgColorScopes = /* @__PURE__ */ new Set();
22548
22704
  for (const token of classStr.split(/\s+/)) {
22549
22705
  if (!token) continue;
22706
+ if (!token.includes("text-") && !token.includes("bg-")) continue;
22550
22707
  const { scope, utility } = splitVariantScope(token);
22551
22708
  if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
22552
22709
  if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
@@ -23276,6 +23433,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
23276
23433
  return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
23277
23434
  });
23278
23435
  const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
23436
+ const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
23437
+ const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
23279
23438
  const noLongTransitionDuration = defineRule({
23280
23439
  id: "no-long-transition-duration",
23281
23440
  title: "Transition duration too long",
@@ -23299,11 +23458,10 @@ const noLongTransitionDuration = defineRule({
23299
23458
  if (key === "transitionDuration" || key === "animationDuration") {
23300
23459
  let longestDurationPropertyMs = 0;
23301
23460
  for (const segment of value.split(",")) {
23302
- const trimmedSegment = segment.trim();
23303
- const msMatch = trimmedSegment.match(/^([\d.]+)ms$/);
23304
- const secondsMatch = trimmedSegment.match(/^([\d.]+)s$/);
23305
- if (msMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(msMatch[1]));
23306
- else if (secondsMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(secondsMatch[1]) * 1e3);
23461
+ const durationMatch = segment.trim().match(DURATION_SEGMENT_PATTERN);
23462
+ if (!durationMatch) continue;
23463
+ const segmentDurationMs = durationMatch[2] === "ms" ? parseFloat(durationMatch[1]) : parseFloat(durationMatch[1]) * 1e3;
23464
+ longestDurationPropertyMs = Math.max(longestDurationPropertyMs, segmentDurationMs);
23307
23465
  }
23308
23466
  if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
23309
23467
  }
@@ -23311,7 +23469,7 @@ const noLongTransitionDuration = defineRule({
23311
23469
  let longestDurationMs = 0;
23312
23470
  for (const segment of value.split(",")) {
23313
23471
  if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
23314
- const firstTimeMatch = segment.match(/(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/);
23472
+ const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
23315
23473
  if (!firstTimeMatch) continue;
23316
23474
  const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
23317
23475
  longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
@@ -24483,14 +24641,14 @@ const collectRoleBranches = (expression, out) => {
24483
24641
  out.hasOpaqueBranch = true;
24484
24642
  };
24485
24643
  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.`;
24486
- const INTERACTIVE_HANDLERS = [
24644
+ const INTERACTIVE_HANDLERS_LOWER = new Set([
24487
24645
  "onClick",
24488
24646
  "onMouseDown",
24489
24647
  "onMouseUp",
24490
24648
  "onKeyDown",
24491
24649
  "onKeyPress",
24492
24650
  "onKeyUp"
24493
- ];
24651
+ ].map((handlerName) => handlerName.toLowerCase()));
24494
24652
  const noNoninteractiveElementInteractions = defineRule({
24495
24653
  id: "no-noninteractive-element-interactions",
24496
24654
  title: "Handler on non-interactive element",
@@ -24505,7 +24663,16 @@ const noNoninteractiveElementInteractions = defineRule({
24505
24663
  const tag = getElementType(node, context.settings);
24506
24664
  if (!NON_INTERACTIVE_ELEMENTS.has(tag)) return;
24507
24665
  if (tag === "label") return;
24508
- if (!INTERACTIVE_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
24666
+ let hasHandler = false;
24667
+ for (const attribute of node.attributes) {
24668
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
24669
+ const attributeName = getJsxAttributeName(attribute.name);
24670
+ if (attributeName && INTERACTIVE_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
24671
+ hasHandler = true;
24672
+ break;
24673
+ }
24674
+ }
24675
+ if (!hasHandler) return;
24509
24676
  if (isHiddenFromScreenReader(node, context.settings)) return;
24510
24677
  const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
24511
24678
  if (roleAttr) {
@@ -25818,16 +25985,22 @@ const ELEMENT_ROLE_PAIRS = [
25818
25985
  ["tr", "row"],
25819
25986
  ["ul", "list"]
25820
25987
  ];
25821
- const getElementImplicitRoles = (tag) => {
25822
- const out = [];
25823
- for (const [element, role] of ELEMENT_ROLE_PAIRS) if (element === tag && !out.includes(role)) out.push(role);
25824
- return out;
25825
- };
25826
- const getTagsForRole = (role) => {
25827
- const out = [];
25828
- for (const [element, r] of ELEMENT_ROLE_PAIRS) if (r === role && !out.includes(element)) out.push(element);
25829
- return out;
25988
+ const EMPTY_ROLE_LIST = [];
25989
+ const buildLookup = (pairs, keyIndex) => {
25990
+ const lookup = /* @__PURE__ */ new Map();
25991
+ for (const pair of pairs) {
25992
+ const key = pair[keyIndex];
25993
+ const value = pair[keyIndex === 0 ? 1 : 0];
25994
+ const values = lookup.get(key);
25995
+ if (!values) lookup.set(key, [value]);
25996
+ else if (!values.includes(value)) values.push(value);
25997
+ }
25998
+ return lookup;
25830
25999
  };
26000
+ const IMPLICIT_ROLES_BY_TAG = buildLookup(ELEMENT_ROLE_PAIRS, 0);
26001
+ const TAGS_BY_ROLE = buildLookup(ELEMENT_ROLE_PAIRS, 1);
26002
+ const getElementImplicitRoles = (tag) => IMPLICIT_ROLES_BY_TAG.get(tag) ?? EMPTY_ROLE_LIST;
26003
+ const getTagsForRole = (role) => TAGS_BY_ROLE.get(role) ?? EMPTY_ROLE_LIST;
25831
26004
  //#endregion
25832
26005
  //#region src/plugin/utils/get-implicit-role.ts
25833
26006
  const getImplicitRole = (node, elementType) => {
@@ -26069,14 +26242,14 @@ const noRenderInRender = defineRule({
26069
26242
  const expression = node.expression;
26070
26243
  if (!isNodeOfType(expression, "CallExpression")) return;
26071
26244
  let calleeName = null;
26245
+ if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
26246
+ else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
26247
+ if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
26072
26248
  if (isNodeOfType(expression.callee, "Identifier")) {
26073
26249
  if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
26074
- calleeName = expression.callee.name;
26075
- } else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
26250
+ } else if (isNodeOfType(expression.callee, "MemberExpression")) {
26076
26251
  if (rootsInProps(expression.callee.object, context.scopes)) return;
26077
- calleeName = expression.callee.property.name;
26078
26252
  }
26079
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
26080
26253
  context.report({
26081
26254
  node: expression,
26082
26255
  message: `Your users lose state because "${calleeName}()" builds UI from an inline call that React remounts, so pull it into its own component instead.`
@@ -26327,6 +26500,7 @@ const TANSTACK_ROUTE_PROPERTY_ORDER = [
26327
26500
  "headers",
26328
26501
  "remountDeps"
26329
26502
  ];
26503
+ const TANSTACK_ROUTE_PROPERTY_INDEX = new Map(TANSTACK_ROUTE_PROPERTY_ORDER.map((propertyName, orderIndex) => [propertyName, orderIndex]));
26330
26504
  const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
26331
26505
  "createFileRoute",
26332
26506
  "createRoute",
@@ -26342,6 +26516,7 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
26342
26516
  "server",
26343
26517
  "handler"
26344
26518
  ];
26519
+ const TANSTACK_MIDDLEWARE_METHOD_INDEX = new Map(TANSTACK_MIDDLEWARE_METHOD_ORDER.map((methodName, orderIndex) => [methodName, orderIndex]));
26345
26520
  const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
26346
26521
  const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
26347
26522
  const TANSTACK_QUERY_HOOKS = new Set([
@@ -27046,14 +27221,21 @@ const noStaticElementInteractions = defineRule({
27046
27221
  category: "Accessibility",
27047
27222
  create: (context) => {
27048
27223
  const settings = resolveSettings$12(context.settings);
27224
+ const handlersLower = new Set(settings.handlers.map((handlerName) => handlerName.toLowerCase()));
27049
27225
  const isTestlikeFile = isTestlikeFilename(context.filename);
27050
27226
  return { JSXOpeningElement(node) {
27051
27227
  if (isTestlikeFile) return;
27052
27228
  let hasNonBlockerHandler = false;
27053
27229
  let hasAnyHandler = false;
27054
- for (const handler of settings.handlers) {
27055
- const attribute = hasJsxPropIgnoreCase(node.attributes, handler);
27056
- if (!attribute) continue;
27230
+ let seenHandlerNames = null;
27231
+ for (const attribute of node.attributes) {
27232
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
27233
+ const attributeName = getJsxAttributeName(attribute.name);
27234
+ if (!attributeName) continue;
27235
+ const handlerNameLower = attributeName.toLowerCase();
27236
+ if (!handlersLower.has(handlerNameLower)) continue;
27237
+ if (seenHandlerNames?.has(handlerNameLower)) continue;
27238
+ (seenHandlerNames ??= /* @__PURE__ */ new Set()).add(handlerNameLower);
27057
27239
  if (isNullValue(attribute)) continue;
27058
27240
  hasAnyHandler = true;
27059
27241
  if (!isPureEventBlockerHandler(attribute)) {
@@ -28234,13 +28416,13 @@ const DOM_ATTRIBUTES_TO_CAMEL = new Map([
28234
28416
  ["xml:lang", "xmlLang"],
28235
28417
  ["xml:space", "xmlSpace"]
28236
28418
  ]);
28237
- const DOM_PROPERTIES_IGNORE_CASE = [
28419
+ const DOM_PROPERTIES_IGNORE_CASE_BY_LOWER = new Map([
28238
28420
  "charset",
28239
28421
  "allowFullScreen",
28240
28422
  "webkitAllowFullScreen",
28241
28423
  "mozAllowFullScreen",
28242
28424
  "webkitDirectory"
28243
- ];
28425
+ ].map((name) => [name.toLowerCase(), name]));
28244
28426
  //#endregion
28245
28427
  //#region src/plugin/constants/dom-property-tags.ts
28246
28428
  const DOM_PROPERTY_TO_ALLOWED_TAGS = new Map([
@@ -28444,10 +28626,7 @@ const matchesHtmlTagConventions = (tagName) => {
28444
28626
  if (!(firstCharacter >= 97 && firstCharacter <= 122)) return false;
28445
28627
  return !tagName.includes("-");
28446
28628
  };
28447
- const normalizeAttributeCase = (name) => {
28448
- for (const ignoreCaseName of DOM_PROPERTIES_IGNORE_CASE) if (ignoreCaseName.toLowerCase() === name.toLowerCase()) return ignoreCaseName;
28449
- return name;
28450
- };
28629
+ const normalizeAttributeCase = (name) => DOM_PROPERTIES_IGNORE_CASE_BY_LOWER.get(name.toLowerCase()) ?? name;
28451
28630
  const hasUppercaseChar = (input) => /[A-Z]/.test(input);
28452
28631
  const INVALID_PROP_ON_TAG = (propName, allowedTags) => `React ignores \`${propName}\` here because it only works on these tags: ${allowedTags}.`;
28453
28632
  const DATA_LOWERCASE_REQUIRED = () => `React drops this \`data-*\` prop because of its capital letters.`;
@@ -28791,32 +28970,26 @@ const isFirstArgumentOfHocCall = (node) => {
28791
28970
  if (!isHocCallee$1(parent)) return false;
28792
28971
  return parent.arguments[0] === node;
28793
28972
  };
28973
+ const MAP_LIKE_METHOD_NAMES = new Set([
28974
+ "map",
28975
+ "forEach",
28976
+ "filter",
28977
+ "flatMap",
28978
+ "reduce",
28979
+ "reduceRight"
28980
+ ]);
28794
28981
  const isReturnOfMapCallback = (node) => {
28795
28982
  const parent = node.parent;
28796
28983
  if (!parent) return false;
28797
28984
  if (isNodeOfType(parent, "CallExpression")) {
28798
28985
  const callee = parent.callee;
28799
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return [
28800
- "map",
28801
- "forEach",
28802
- "filter",
28803
- "flatMap",
28804
- "reduce",
28805
- "reduceRight"
28806
- ].includes(callee.property.name);
28986
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28807
28987
  }
28808
28988
  if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
28809
28989
  const callbackParent = parent.parent;
28810
28990
  if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
28811
28991
  const callee = callbackParent.callee;
28812
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return [
28813
- "map",
28814
- "forEach",
28815
- "filter",
28816
- "flatMap",
28817
- "reduce",
28818
- "reduceRight"
28819
- ].includes(callee.property.name);
28992
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28820
28993
  }
28821
28994
  }
28822
28995
  return false;
@@ -28852,10 +29025,10 @@ const isRenderFlowingReadReference = (identifier) => {
28852
29025
  valueNode = parent;
28853
29026
  parent = parent.parent;
28854
29027
  continue;
28855
- case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isUppercaseName(parent.id.name);
29028
+ case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isReactComponentName(parent.id.name);
28856
29029
  case "AssignmentExpression": {
28857
29030
  const assignmentTarget = parent.left;
28858
- return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isUppercaseName(assignmentTarget.name);
29031
+ return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
28859
29032
  }
28860
29033
  case "Property":
28861
29034
  if (parent.value !== valueNode) return false;
@@ -28948,14 +29121,14 @@ const noUnstableNestedComponents = defineRule({
28948
29121
  };
28949
29122
  return {
28950
29123
  JSXOpeningElement(node) {
28951
- if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) {
29124
+ if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
28952
29125
  recordInstantiation(node.name, node.name.name);
28953
29126
  return;
28954
29127
  }
28955
29128
  if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
28956
29129
  },
28957
29130
  Identifier(node) {
28958
- if (!isUppercaseName(node.name)) return;
29131
+ if (!isReactComponentName(node.name)) return;
28959
29132
  if (!isRenderFlowingReadReference(node)) return;
28960
29133
  recordInstantiation(node, node.name);
28961
29134
  },
@@ -29728,10 +29901,10 @@ const onlyExportComponents = defineRule({
29728
29901
  };
29729
29902
  for (const child of componentCandidates) {
29730
29903
  if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
29731
- if (isReactComponentName(child.id.name) && !isInsideExport(child)) localComponents.push(child.id);
29904
+ if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
29732
29905
  }
29733
29906
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
29734
- if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child)) localComponents.push(child.id);
29907
+ if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
29735
29908
  }
29736
29909
  }
29737
29910
  if (hasAnyExports && hasReactExport) for (const entry of exports) {
@@ -30765,13 +30938,13 @@ const preferStableEmptyFallback = defineRule({
30765
30938
  memoRegistry = buildSameFileMemoRegistry(node);
30766
30939
  },
30767
30940
  JSXAttribute(node) {
30768
- if (!isInsideFunctionScope(node)) return;
30769
- if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30770
30941
  if (!node.value) return;
30771
30942
  if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
30772
30943
  const innerExpression = node.value.expression;
30773
30944
  if (!innerExpression) return;
30774
30945
  if (innerExpression.type === "JSXEmptyExpression") return;
30946
+ if (!isInsideFunctionScope(node)) return;
30947
+ if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30775
30948
  const parentJsxOpening = node.parent;
30776
30949
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
30777
30950
  if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
@@ -32143,41 +32316,11 @@ const callsIdentifier = (root, identifierName) => {
32143
32316
  });
32144
32317
  return found;
32145
32318
  };
32146
- const setterIsCalledInAsyncContext = (componentBody, setterName) => {
32147
- if (!componentBody) return false;
32148
- let found = false;
32149
- walkAst(componentBody, (child) => {
32150
- if (found) return;
32151
- if (!isFunctionLike$1(child)) return;
32152
- const functionBody = child.body;
32153
- if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
32154
- if (callsIdentifier(functionBody, setterName)) found = true;
32155
- });
32156
- return found;
32157
- };
32158
32319
  const PROMISE_CHAIN_METHOD_NAMES = new Set([
32159
32320
  "then",
32160
32321
  "catch",
32161
32322
  "finally"
32162
32323
  ]);
32163
- const setterIsCalledInPromiseChain = (componentBody, setterName) => {
32164
- if (!componentBody) return false;
32165
- let found = false;
32166
- walkAst(componentBody, (child) => {
32167
- if (found) return;
32168
- if (!isNodeOfType(child, "CallExpression")) return;
32169
- const callee = child.callee;
32170
- if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
32171
- for (const argument of child.arguments ?? []) {
32172
- if (!isFunctionLike$1(argument)) continue;
32173
- if (callsIdentifier(argument.body, setterName)) {
32174
- found = true;
32175
- return;
32176
- }
32177
- }
32178
- });
32179
- return found;
32180
- };
32181
32324
  const ASYNC_DATA_CALLEE_NAMES = new Set([
32182
32325
  "useApolloClient",
32183
32326
  "useMutation",
@@ -32190,20 +32333,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
32190
32333
  "fetch",
32191
32334
  "axios"
32192
32335
  ]);
32193
- const referencesAsyncDataApi = (body) => {
32194
- if (!body) return false;
32336
+ const hasAsyncLoadingWork = (fnBody, setterName) => {
32195
32337
  let found = false;
32196
- walkAst(body, (child) => {
32197
- if (found) return;
32338
+ walkAst(fnBody, (child) => {
32339
+ if (found) return false;
32198
32340
  if (isNodeOfType(child, "CallExpression")) {
32199
32341
  const callee = child.callee;
32200
32342
  if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
32201
32343
  found = true;
32202
- return;
32344
+ return false;
32345
+ }
32346
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
32347
+ if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
32348
+ found = true;
32349
+ return false;
32350
+ }
32351
+ if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
32352
+ if (!isFunctionLike$1(argument)) continue;
32353
+ if (callsIdentifier(argument.body, setterName)) {
32354
+ found = true;
32355
+ return false;
32356
+ }
32357
+ }
32203
32358
  }
32204
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
32359
+ return;
32360
+ }
32361
+ if (setterName !== null && isFunctionLike$1(child)) {
32362
+ const functionBody = child.body;
32363
+ if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
32205
32364
  found = true;
32206
- return;
32365
+ return false;
32207
32366
  }
32208
32367
  }
32209
32368
  });
@@ -32228,7 +32387,7 @@ const renderingUsetransitionLoading = defineRule({
32228
32387
  const secondBinding = node.id.elements[1];
32229
32388
  const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
32230
32389
  const fnBody = enclosingFunctionBody(node);
32231
- if (fnBody && (setterName && setterIsCalledInAsyncContext(fnBody, setterName) || setterName && setterIsCalledInPromiseChain(fnBody, setterName) || referencesAsyncDataApi(fnBody))) return;
32390
+ if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
32232
32391
  context.report({
32233
32392
  node: node.init,
32234
32393
  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`
@@ -33022,17 +33181,17 @@ const rerenderStateOnlyInHandlers = defineRule({
33022
33181
  const effectTriggerNames = /* @__PURE__ */ new Set();
33023
33182
  for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
33024
33183
  for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
33184
+ const setterNames = new Set(bindings.map((binding) => binding.setterName));
33185
+ const calledSetterNames = /* @__PURE__ */ new Set();
33186
+ walkAst(componentBody, (child) => {
33187
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
33188
+ });
33025
33189
  for (const binding of bindings) {
33026
33190
  if (renderReachableNames.has(binding.valueName)) continue;
33027
33191
  if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
33028
33192
  const setterSuffix = binding.setterName.slice(3);
33029
33193
  if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
33030
- let setterCalled = false;
33031
- walkAst(componentBody, (child) => {
33032
- if (setterCalled) return;
33033
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
33034
- });
33035
- if (!setterCalled) continue;
33194
+ if (!calledSetterNames.has(binding.setterName)) continue;
33036
33195
  if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
33037
33196
  context.report({
33038
33197
  node: binding.declarator,
@@ -33364,6 +33523,7 @@ const RECYCLABLE_LIST_PACKAGES = {
33364
33523
  FlashList: ["@shopify/flash-list"],
33365
33524
  LegendList: ["@legendapp/list"]
33366
33525
  };
33526
+ const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
33367
33527
  const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
33368
33528
  const RENDER_ITEM_PROP_NAMES = new Set([
33369
33529
  "renderItem",
@@ -33606,33 +33766,42 @@ const rnListMissingEstimatedItemSize = defineRule({
33606
33766
  requires: ["react-native"],
33607
33767
  severity: "warn",
33608
33768
  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.",
33609
- create: (context) => ({ JSXOpeningElement(node) {
33610
- const localElementName = resolveJsxElementName(node);
33611
- if (!localElementName) return;
33612
- const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
33613
- if (canonicalRecyclerName === null) return;
33614
- if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
33615
- let hasSizingHint = false;
33616
- let dataIsEmptyLiteral = false;
33617
- let hasDataProp = false;
33618
- for (const attribute of node.attributes ?? []) {
33619
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
33620
- if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
33621
- const attributeName = attribute.name.name;
33622
- if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
33623
- if (attributeName === "data") {
33624
- hasDataProp = true;
33625
- if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
33769
+ create: (context) => {
33770
+ let fileImportsRecycler = false;
33771
+ return {
33772
+ Program(node) {
33773
+ fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
33774
+ },
33775
+ JSXOpeningElement(node) {
33776
+ if (!fileImportsRecycler) return;
33777
+ const localElementName = resolveJsxElementName(node);
33778
+ if (!localElementName) return;
33779
+ const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
33780
+ if (canonicalRecyclerName === null) return;
33781
+ if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
33782
+ let hasSizingHint = false;
33783
+ let dataIsEmptyLiteral = false;
33784
+ let hasDataProp = false;
33785
+ for (const attribute of node.attributes ?? []) {
33786
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
33787
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
33788
+ const attributeName = attribute.name.name;
33789
+ if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
33790
+ if (attributeName === "data") {
33791
+ hasDataProp = true;
33792
+ if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
33793
+ }
33794
+ }
33795
+ if (hasSizingHint) return;
33796
+ if (dataIsEmptyLiteral) return;
33797
+ if (!hasDataProp) return;
33798
+ context.report({
33799
+ node,
33800
+ message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
33801
+ });
33626
33802
  }
33627
- }
33628
- if (hasSizingHint) return;
33629
- if (dataIsEmptyLiteral) return;
33630
- if (!hasDataProp) return;
33631
- context.report({
33632
- node,
33633
- message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
33634
- });
33635
- } })
33803
+ };
33804
+ }
33636
33805
  });
33637
33806
  //#endregion
33638
33807
  //#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
@@ -33643,25 +33812,34 @@ const rnListRecyclableWithoutTypes = defineRule({
33643
33812
  requires: ["react-native"],
33644
33813
  severity: "warn",
33645
33814
  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.",
33646
- create: (context) => ({ JSXOpeningElement(node) {
33647
- const elementName = resolveJsxElementName(node);
33648
- if (!elementName) return;
33649
- if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
33650
- let hasRecycleItemsEnabled = false;
33651
- let hasGetItemType = false;
33652
- for (const attr of node.attributes ?? []) {
33653
- if (!isNodeOfType(attr, "JSXAttribute")) continue;
33654
- if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
33655
- if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
33656
- else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
33657
- else hasRecycleItemsEnabled = true;
33658
- if (attr.name.name === "getItemType") hasGetItemType = true;
33659
- }
33660
- if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
33661
- node,
33662
- message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
33663
- });
33664
- } })
33815
+ create: (context) => {
33816
+ let fileImportsRecycler = false;
33817
+ return {
33818
+ Program(node) {
33819
+ fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
33820
+ },
33821
+ JSXOpeningElement(node) {
33822
+ if (!fileImportsRecycler) return;
33823
+ const elementName = resolveJsxElementName(node);
33824
+ if (!elementName) return;
33825
+ if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
33826
+ let hasRecycleItemsEnabled = false;
33827
+ let hasGetItemType = false;
33828
+ for (const attr of node.attributes ?? []) {
33829
+ if (!isNodeOfType(attr, "JSXAttribute")) continue;
33830
+ if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
33831
+ if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
33832
+ else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
33833
+ else hasRecycleItemsEnabled = true;
33834
+ if (attr.name.name === "getItemType") hasGetItemType = true;
33835
+ }
33836
+ if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
33837
+ node,
33838
+ message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
33839
+ });
33840
+ }
33841
+ };
33842
+ }
33665
33843
  });
33666
33844
  //#endregion
33667
33845
  //#region src/plugin/rules/react-native/rn-no-deep-imports.ts
@@ -34645,9 +34823,10 @@ const resolveTextBoundaryName = (openingElement) => {
34645
34823
  if (isNodeOfType(openingElement.name, "JSXNamespacedName")) return openingElement.name.namespace.name;
34646
34824
  return resolveJsxElementName(openingElement);
34647
34825
  };
34826
+ const TEXT_COMPONENT_KEYWORDS = [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS];
34648
34827
  const isTextHandlingComponent = (elementName) => {
34649
34828
  if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
34650
- return [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS].some((keyword) => elementName.includes(keyword));
34829
+ return TEXT_COMPONENT_KEYWORDS.some((keyword) => elementName.includes(keyword));
34651
34830
  };
34652
34831
  const isTransparentTextWrapper = (elementName) => elementName !== null && REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS.has(elementName);
34653
34832
  const isInsideTextHandlingComponent = (node) => {
@@ -34691,6 +34870,7 @@ const rnNoRawText = defineRule({
34691
34870
  return {
34692
34871
  Program(programNode) {
34693
34872
  isDomComponentFile = hasDirective(programNode, "use dom");
34873
+ if (!containsJsxElement(programNode)) return;
34694
34874
  const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
34695
34875
  autoDetectedTextWrappers = childrenForwarding.textWrappers;
34696
34876
  autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
@@ -38645,14 +38825,7 @@ const roleSupportsAriaProps = defineRule({
38645
38825
  recommendation: "Only use `aria-*` attributes that the element's role supports.",
38646
38826
  category: "Accessibility",
38647
38827
  create: (context) => ({ JSXOpeningElement(node) {
38648
- const elementType = getElementType(node, context.settings);
38649
- const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
38650
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
38651
- if (!role) return;
38652
- if (!VALID_ARIA_ROLES.has(role)) return;
38653
- const isImplicit = !roleAttribute;
38654
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
38655
- if (!supported) return;
38828
+ let ariaAttributes = null;
38656
38829
  for (const attribute of node.attributes) {
38657
38830
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
38658
38831
  const attributeNode = attribute;
@@ -38662,6 +38835,21 @@ const roleSupportsAriaProps = defineRule({
38662
38835
  const propName = propRawName.toLowerCase();
38663
38836
  if (!propName.startsWith("aria-")) continue;
38664
38837
  if (!ARIA_PROPERTIES.has(propName)) continue;
38838
+ (ariaAttributes ??= []).push({
38839
+ attribute,
38840
+ propName
38841
+ });
38842
+ }
38843
+ if (!ariaAttributes) return;
38844
+ const elementType = getElementType(node, context.settings);
38845
+ const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
38846
+ const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
38847
+ if (!role) return;
38848
+ if (!VALID_ARIA_ROLES.has(role)) return;
38849
+ const isImplicit = !roleAttribute;
38850
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
38851
+ if (!supported) return;
38852
+ for (const { attribute, propName } of ariaAttributes) {
38665
38853
  if (supported.has(propName)) continue;
38666
38854
  context.report({
38667
38855
  node: attribute,
@@ -39499,6 +39687,7 @@ const serverCacheWithObjectLiteral = defineRule({
39499
39687
  cachedFunctionNames.add(node.id.name);
39500
39688
  },
39501
39689
  CallExpression(node) {
39690
+ if (cachedFunctionNames.size === 0) return;
39502
39691
  if (!isNodeOfType(node.callee, "Identifier")) return;
39503
39692
  if (!cachedFunctionNames.has(node.callee.name)) return;
39504
39693
  const firstArg = node.arguments?.[0];
@@ -40425,7 +40614,16 @@ const supabaseRlsPolicyRisk = defineRule({
40425
40614
  //#endregion
40426
40615
  //#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
40427
40616
  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;
40428
- 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");
40617
+ 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;
40618
+ const collectLastEnableRlsIndexByTable = (content) => {
40619
+ const lastEnableIndexByTable = /* @__PURE__ */ new Map();
40620
+ for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
40621
+ const tableName = match[1];
40622
+ if (tableName === void 0) continue;
40623
+ lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
40624
+ }
40625
+ return lastEnableIndexByTable;
40626
+ };
40429
40627
  const supabaseTableMissingRls = defineRule({
40430
40628
  id: "supabase-table-missing-rls",
40431
40629
  title: "Supabase table created without Row Level Security",
@@ -40436,11 +40634,13 @@ const supabaseTableMissingRls = defineRule({
40436
40634
  const content = sanitizeSqlForScan(file.content);
40437
40635
  if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
40438
40636
  const findings = [];
40637
+ const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
40439
40638
  CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
40440
40639
  for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
40441
40640
  const tableName = match[1];
40442
40641
  if (tableName === void 0) continue;
40443
- if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
40642
+ const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
40643
+ if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
40444
40644
  const location = getLocationAtIndex(content, match.index);
40445
40645
  findings.push({
40446
40646
  message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
@@ -40716,6 +40916,7 @@ const tanstackStartMissingHeadContent = defineRule({
40716
40916
  severity: "warn",
40717
40917
  recommendation: "Add `<HeadContent />` inside `<head>` in your __root route. Without it, route `head()` meta tags are dropped.",
40718
40918
  create: (context) => {
40919
+ if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(context.filename ?? "")) return {};
40719
40920
  let hasHeadContentElement = false;
40720
40921
  let hasDocumentHeadElement = false;
40721
40922
  let hasCustomHeadChildElement = false;
@@ -40752,8 +40953,6 @@ const tanstackStartMissingHeadContent = defineRule({
40752
40953
  };
40753
40954
  return {
40754
40955
  Program(node) {
40755
- const filename = context.filename ?? "";
40756
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40757
40956
  const statements = node.body ?? [];
40758
40957
  for (const statement of statements) collectImportBindings(statement);
40759
40958
  for (const statement of statements) {
@@ -40762,18 +40961,12 @@ const tanstackStartMissingHeadContent = defineRule({
40762
40961
  }
40763
40962
  },
40764
40963
  ImportDeclaration(node) {
40765
- const filename = context.filename ?? "";
40766
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40767
40964
  collectImportBindings(node);
40768
40965
  },
40769
40966
  VariableDeclarator(node) {
40770
- const filename = context.filename ?? "";
40771
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40772
40967
  collectVariableAlias(node);
40773
40968
  },
40774
40969
  JSXOpeningElement(node) {
40775
- const filename = normalizeFilename(context.filename ?? "");
40776
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40777
40970
  if (isNodeOfType(node.name, "JSXIdentifier")) {
40778
40971
  if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
40779
40972
  if (headContentComponentNames.has(node.name.name)) hasHeadContentElement = true;
@@ -40787,8 +40980,6 @@ const tanstackStartMissingHeadContent = defineRule({
40787
40980
  if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
40788
40981
  },
40789
40982
  "Program:exit"(programNode) {
40790
- const filename = normalizeFilename(context.filename ?? "");
40791
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40792
40983
  if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
40793
40984
  node: programNode,
40794
40985
  message: "Without <HeadContent /> in the __root route, your route head() meta tags never render."
@@ -40812,27 +41003,29 @@ const tanstackStartNoAnchorElement = defineRule({
40812
41003
  requires: ["tanstack-start"],
40813
41004
  severity: "warn",
40814
41005
  recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
40815
- create: (context) => ({ JSXOpeningElement(node) {
40816
- if (!isInProjectDirectory(context, "routes")) return;
40817
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
40818
- const attributes = node.attributes ?? [];
40819
- const hrefAttribute = findJsxAttribute(attributes, "href");
40820
- if (!hrefAttribute?.value) return;
40821
- let hrefValue = null;
40822
- if (isNodeOfType(hrefAttribute.value, "Literal")) hrefValue = hrefAttribute.value.value;
40823
- else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
40824
- if (typeof hrefValue !== "string" || !hrefValue.startsWith("/")) return;
40825
- if (hrefValue.startsWith("//")) return;
40826
- const pathname = hrefValue.split(/[?#]/)[0] ?? hrefValue;
40827
- if (pathname.startsWith("/api/")) return;
40828
- if (/\.[a-z0-9]{1,8}$/i.test(pathname)) return;
40829
- if (findJsxAttribute(attributes, "download")) return;
40830
- if (getAttributeStringValue(findJsxAttribute(attributes, "target")) === "_blank") return;
40831
- context.report({
40832
- node,
40833
- message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
40834
- });
40835
- } })
41006
+ create: (context) => {
41007
+ if (!isInProjectDirectory(context, "routes")) return {};
41008
+ return { JSXOpeningElement(node) {
41009
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
41010
+ const attributes = node.attributes ?? [];
41011
+ const hrefAttribute = findJsxAttribute(attributes, "href");
41012
+ if (!hrefAttribute?.value) return;
41013
+ let hrefValue = null;
41014
+ if (isNodeOfType(hrefAttribute.value, "Literal")) hrefValue = hrefAttribute.value.value;
41015
+ else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
41016
+ if (typeof hrefValue !== "string" || !hrefValue.startsWith("/")) return;
41017
+ if (hrefValue.startsWith("//")) return;
41018
+ const pathname = hrefValue.split(/[?#]/)[0] ?? hrefValue;
41019
+ if (pathname.startsWith("/api/")) return;
41020
+ if (/\.[a-z0-9]{1,8}$/i.test(pathname)) return;
41021
+ if (findJsxAttribute(attributes, "download")) return;
41022
+ if (getAttributeStringValue(findJsxAttribute(attributes, "target")) === "_blank") return;
41023
+ context.report({
41024
+ node,
41025
+ message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
41026
+ });
41027
+ } };
41028
+ }
40836
41029
  });
40837
41030
  //#endregion
40838
41031
  //#region src/plugin/rules/tanstack-start/tanstack-start-no-direct-fetch-in-loader.ts
@@ -40896,7 +41089,7 @@ const tanstackStartNoNavigateInRender = defineRule({
40896
41089
  severity: "warn",
40897
41090
  recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
40898
41091
  create: (context) => {
40899
- const isRouteFile = isInProjectDirectory(context, "routes");
41092
+ if (!isInProjectDirectory(context, "routes")) return {};
40900
41093
  let deferredCallbackDepth = 0;
40901
41094
  let eventHandlerDepth = 0;
40902
41095
  const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
@@ -40960,7 +41153,6 @@ const tanstackStartNoNavigateInRender = defineRule({
40960
41153
  };
40961
41154
  return {
40962
41155
  CallExpression(node) {
40963
- if (!isRouteFile) return;
40964
41156
  if (isDeferredHookCall(node)) deferredCallbackDepth++;
40965
41157
  if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
40966
41158
  if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
@@ -40972,39 +41164,30 @@ const tanstackStartNoNavigateInRender = defineRule({
40972
41164
  }
40973
41165
  },
40974
41166
  "CallExpression:exit"(node) {
40975
- if (!isRouteFile) return;
40976
41167
  if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
40977
41168
  },
40978
41169
  JSXAttribute(node) {
40979
- if (!isRouteFile) return;
40980
41170
  if (isEventHandlerAttribute(node)) eventHandlerDepth++;
40981
41171
  },
40982
41172
  "JSXAttribute:exit"(node) {
40983
- if (!isRouteFile) return;
40984
41173
  if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40985
41174
  },
40986
41175
  Property(node) {
40987
- if (!isRouteFile) return;
40988
41176
  if (isEventHandlerProperty(node)) eventHandlerDepth++;
40989
41177
  },
40990
41178
  "Property:exit"(node) {
40991
- if (!isRouteFile) return;
40992
41179
  if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40993
41180
  },
40994
41181
  VariableDeclarator(node) {
40995
- if (!isRouteFile) return;
40996
41182
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
40997
41183
  },
40998
41184
  "VariableDeclarator:exit"(node) {
40999
- if (!isRouteFile) return;
41000
41185
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
41001
41186
  },
41002
41187
  FunctionDeclaration(node) {
41003
- if (!isRouteFile) return;
41004
41188
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
41005
41189
  },
41006
41190
  "FunctionDeclaration:exit"(node) {
41007
- if (!isRouteFile) return;
41008
41191
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
41009
41192
  }
41010
41193
  };
@@ -41088,23 +41271,25 @@ const tanstackStartNoUseEffectFetch = defineRule({
41088
41271
  requires: ["tanstack-start"],
41089
41272
  severity: "warn",
41090
41273
  recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
41091
- create: (context) => ({ CallExpression(node) {
41092
- if (!isInProjectDirectory(context, "routes")) return;
41093
- if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
41094
- const callback = node.arguments?.[0];
41095
- if (!callback) return;
41096
- let hasFetchCall = false;
41097
- const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
41098
- walkAst(callback, (child) => {
41099
- if (hasFetchCall) return false;
41100
- if (child !== callback && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
41101
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === "fetch") hasFetchCall = true;
41102
- });
41103
- if (hasFetchCall) context.report({
41104
- node,
41105
- message: "fetch() inside useEffect makes your users wait through a loading spinner after render."
41106
- });
41107
- } })
41274
+ create: (context) => {
41275
+ if (!isInProjectDirectory(context, "routes")) return {};
41276
+ return { CallExpression(node) {
41277
+ if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
41278
+ const callback = node.arguments?.[0];
41279
+ if (!callback) return;
41280
+ let hasFetchCall = false;
41281
+ const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
41282
+ walkAst(callback, (child) => {
41283
+ if (hasFetchCall) return false;
41284
+ if (child !== callback && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
41285
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === "fetch") hasFetchCall = true;
41286
+ });
41287
+ if (hasFetchCall) context.report({
41288
+ node,
41289
+ message: "fetch() inside useEffect makes your users wait through a loading spinner after render."
41290
+ });
41291
+ } };
41292
+ }
41108
41293
  });
41109
41294
  //#endregion
41110
41295
  //#region src/plugin/rules/tanstack-start/tanstack-start-redirect-in-try-catch.ts
@@ -41145,10 +41330,10 @@ const tanstackStartRoutePropertyOrder = defineRule({
41145
41330
  const propertyName = getPropertyKeyName(property);
41146
41331
  if (propertyName !== null) orderedPropertyNames.push(propertyName);
41147
41332
  }
41148
- const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_ORDER.includes(propertyName));
41333
+ const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_INDEX.has(propertyName));
41149
41334
  let lastIndex = -1;
41150
41335
  for (const propertyName of sensitiveProperties) {
41151
- const currentIndex = TANSTACK_ROUTE_PROPERTY_ORDER.indexOf(propertyName);
41336
+ const currentIndex = TANSTACK_ROUTE_PROPERTY_INDEX.get(propertyName) ?? -1;
41152
41337
  if (currentIndex < lastIndex) {
41153
41338
  const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
41154
41339
  context.report({
@@ -41185,10 +41370,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
41185
41370
  } else return;
41186
41371
  const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
41187
41372
  if (methodNames[methodNames.length - 1] !== ownMethodName) return;
41188
- const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_ORDER.includes(toMethodOrderToken(name)));
41373
+ const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_INDEX.has(toMethodOrderToken(name)));
41189
41374
  let lastIndex = -1;
41190
41375
  for (const methodName of orderSensitiveMethods) {
41191
- const currentIndex = TANSTACK_MIDDLEWARE_METHOD_ORDER.indexOf(toMethodOrderToken(methodName));
41376
+ const currentIndex = TANSTACK_MIDDLEWARE_METHOD_INDEX.get(toMethodOrderToken(methodName)) ?? -1;
41192
41377
  if (currentIndex < lastIndex) {
41193
41378
  const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
41194
41379
  context.report({
@@ -41472,13 +41657,21 @@ const webhookSignatureRisk = defineRule({
41472
41657
  //#endregion
41473
41658
  //#region src/plugin/rules/zod/utils/zod-ast.ts
41474
41659
  const ZOD_MODULE = "zod";
41660
+ const ZOD_MODULE_SOURCES = [ZOD_MODULE];
41475
41661
  const getStaticPropertyName = (member) => {
41476
41662
  const property = member.property;
41477
41663
  if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
41478
41664
  if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
41479
41665
  return null;
41480
41666
  };
41667
+ const importInfoCache = /* @__PURE__ */ new WeakMap();
41481
41668
  const getImportInfoForIdentifier = (identifier) => {
41669
+ if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
41670
+ const importInfo = computeImportInfoForIdentifier(identifier);
41671
+ importInfoCache.set(identifier, importInfo);
41672
+ return importInfo;
41673
+ };
41674
+ const computeImportInfoForIdentifier = (identifier) => {
41482
41675
  const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
41483
41676
  if (!specifier) return null;
41484
41677
  const declaration = specifier.parent;
@@ -41615,25 +41808,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
41615
41808
  tags: ["migration-hint"],
41616
41809
  severity: "warn",
41617
41810
  recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
41618
- create: (context) => ({
41619
- CallExpression(node) {
41620
- if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
41621
- if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
41622
- context.report({
41623
- node,
41624
- message: ZOD_ERROR_API_MESSAGE
41625
- });
41626
- },
41627
- MemberExpression(node) {
41628
- const parent = node.parent;
41629
- if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41630
- if (!isDeprecatedZodErrorMemberAccess(node)) return;
41631
- context.report({
41632
- node,
41633
- message: ZOD_ERROR_API_MESSAGE
41634
- });
41635
- }
41636
- })
41811
+ create: (context) => {
41812
+ let fileImportsZod = false;
41813
+ return {
41814
+ Program(node) {
41815
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
41816
+ },
41817
+ CallExpression(node) {
41818
+ if (!fileImportsZod) return;
41819
+ if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
41820
+ if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
41821
+ context.report({
41822
+ node,
41823
+ message: ZOD_ERROR_API_MESSAGE
41824
+ });
41825
+ },
41826
+ MemberExpression(node) {
41827
+ if (!fileImportsZod) return;
41828
+ const parent = node.parent;
41829
+ if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41830
+ if (!isDeprecatedZodErrorMemberAccess(node)) return;
41831
+ context.report({
41832
+ node,
41833
+ message: ZOD_ERROR_API_MESSAGE
41834
+ });
41835
+ }
41836
+ };
41837
+ }
41637
41838
  });
41638
41839
  //#endregion
41639
41840
  //#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
@@ -41825,16 +42026,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
41825
42026
  tags: ["migration-hint"],
41826
42027
  severity: "warn",
41827
42028
  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()`.",
41828
- create: (context) => ({
41829
- CallExpression(node) {
41830
- 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);
41831
- },
41832
- MemberExpression(node) {
41833
- const parent = node.parent;
41834
- if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41835
- if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
41836
- }
41837
- })
42029
+ create: (context) => {
42030
+ let fileImportsZod = false;
42031
+ return {
42032
+ Program(node) {
42033
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
42034
+ },
42035
+ CallExpression(node) {
42036
+ if (!fileImportsZod) return;
42037
+ 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);
42038
+ },
42039
+ MemberExpression(node) {
42040
+ if (!fileImportsZod) return;
42041
+ const parent = node.parent;
42042
+ if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
42043
+ if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
42044
+ }
42045
+ };
42046
+ }
41838
42047
  });
41839
42048
  //#endregion
41840
42049
  //#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
@@ -41869,13 +42078,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
41869
42078
  tags: ["migration-hint"],
41870
42079
  severity: "warn",
41871
42080
  recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
41872
- create: (context) => ({ CallExpression(node) {
41873
- if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
41874
- context.report({
41875
- node,
41876
- message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
41877
- });
41878
- } })
42081
+ create: (context) => {
42082
+ let fileImportsZod = false;
42083
+ return {
42084
+ Program(node) {
42085
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
42086
+ },
42087
+ CallExpression(node) {
42088
+ if (!fileImportsZod) return;
42089
+ if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
42090
+ context.report({
42091
+ node,
42092
+ message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
42093
+ });
42094
+ }
42095
+ };
42096
+ }
41879
42097
  });
41880
42098
  //#endregion
41881
42099
  //#region src/plugin/rule-registry.ts