oxlint-plugin-react-doctor 0.6.2-dev.397816a → 0.6.2-dev.ac71a3b

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 +610 -465
  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
@@ -760,24 +783,6 @@ const collectChildComponentNames = (element, into) => {
760
783
  into.add(name);
761
784
  });
762
785
  };
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
786
  const countEffectHookCalls = (body) => {
782
787
  if (!body) return 0;
783
788
  let count = 0;
@@ -787,6 +792,36 @@ const countEffectHookCalls = (body) => {
787
792
  });
788
793
  return count;
789
794
  };
795
+ const componentEffectIndexCache = /* @__PURE__ */ new WeakMap();
796
+ const getComponentEffectIndex = (programRoot) => {
797
+ const cached = componentEffectIndexCache.get(programRoot);
798
+ if (cached) return cached;
799
+ const bodyByName = /* @__PURE__ */ new Map();
800
+ walkAst(programRoot, (node) => {
801
+ if (isNodeOfType(node, "FunctionDeclaration") && node.id && !bodyByName.has(node.id.name)) {
802
+ bodyByName.set(node.id.name, node.body);
803
+ return;
804
+ }
805
+ if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && !bodyByName.has(node.id.name)) {
806
+ const initializer = node.init;
807
+ if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) bodyByName.set(node.id.name, initializer.body);
808
+ }
809
+ });
810
+ const index = {
811
+ bodyByName,
812
+ effectCountByName: /* @__PURE__ */ new Map()
813
+ };
814
+ componentEffectIndexCache.set(programRoot, index);
815
+ return index;
816
+ };
817
+ const getSameFileComponentEffectCount = (programRoot, componentName) => {
818
+ const index = getComponentEffectIndex(programRoot);
819
+ const cachedCount = index.effectCountByName.get(componentName);
820
+ if (cachedCount !== void 0) return cachedCount;
821
+ const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
822
+ index.effectCountByName.set(componentName, count);
823
+ return count;
824
+ };
790
825
  const activityWrapsEffectHeavySubtree = defineRule({
791
826
  id: "activity-wraps-effect-heavy-subtree",
792
827
  title: "Activity wraps an effect-heavy subtree",
@@ -843,9 +878,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
843
878
  let totalEffects = 0;
844
879
  const effectfulChildren = [];
845
880
  for (const componentName of childComponentNames) {
846
- const body = findSameFileComponentBody(programRoot, componentName);
847
- if (!body) continue;
848
- const effectCount = countEffectHookCalls(body);
881
+ const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
849
882
  if (effectCount === 0) continue;
850
883
  totalEffects += effectCount;
851
884
  effectfulChildren.push(`<${componentName}>`);
@@ -1394,11 +1427,16 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
1394
1427
  };
1395
1428
  //#endregion
1396
1429
  //#region src/plugin/utils/get-element-type.ts
1430
+ const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
1431
+ const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
1397
1432
  const readJsxA11ySettings = (settings) => {
1398
- if (!settings) return {};
1433
+ if (!settings) return EMPTY_JSX_A11Y_SETTINGS;
1434
+ const cachedSettings = jsxA11ySettingsCache.get(settings);
1435
+ if (cachedSettings) return cachedSettings;
1399
1436
  const block = settings["jsx-a11y"];
1400
- if (!block || typeof block !== "object") return {};
1401
- return block;
1437
+ const a11ySettings = block && typeof block === "object" ? block : EMPTY_JSX_A11Y_SETTINGS;
1438
+ jsxA11ySettingsCache.set(settings, a11ySettings);
1439
+ return a11ySettings;
1402
1440
  };
1403
1441
  const flattenJsxName$2 = (name) => {
1404
1442
  if (isNodeOfType(name, "JSXIdentifier")) return name.name;
@@ -1406,8 +1444,7 @@ const flattenJsxName$2 = (name) => {
1406
1444
  if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
1407
1445
  return "";
1408
1446
  };
1409
- const getElementType = (openingElement, settings) => {
1410
- const a11ySettings = readJsxA11ySettings(settings);
1447
+ const computeElementType = (openingElement, a11ySettings) => {
1411
1448
  const baseName = flattenJsxName$2(openingElement.name);
1412
1449
  if (a11ySettings.polymorphicPropName) {
1413
1450
  const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
@@ -1419,6 +1456,23 @@ const getElementType = (openingElement, settings) => {
1419
1456
  if (a11ySettings.components && baseName in a11ySettings.components) return a11ySettings.components[baseName];
1420
1457
  return baseName;
1421
1458
  };
1459
+ const elementTypeCache = /* @__PURE__ */ new WeakMap();
1460
+ const getElementType = (openingElement, settings) => {
1461
+ const cachedEntry = elementTypeCache.get(openingElement);
1462
+ if (cachedEntry && cachedEntry.settings === settings) return cachedEntry.elementType;
1463
+ const elementType = computeElementType(openingElement, readJsxA11ySettings(settings));
1464
+ elementTypeCache.set(openingElement, {
1465
+ settings,
1466
+ elementType
1467
+ });
1468
+ return elementType;
1469
+ };
1470
+ //#endregion
1471
+ //#region src/plugin/utils/has-jsx-a11y-settings.ts
1472
+ const hasJsxA11ySettings = (settings) => {
1473
+ const block = settings?.["jsx-a11y"];
1474
+ return typeof block === "object" && block !== null;
1475
+ };
1422
1476
  //#endregion
1423
1477
  //#region src/plugin/utils/find-import-source-for-name.ts
1424
1478
  const collectFromProgram = (programRoot) => {
@@ -1972,7 +2026,12 @@ const altText = defineRule({
1972
2026
  const objectAliases = new Set(settings.object ?? []);
1973
2027
  const areaAliases = new Set(settings.area ?? []);
1974
2028
  const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
2029
+ const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
1975
2030
  return { JSXOpeningElement(node) {
2031
+ if (!fileHasJsxA11ySettings && isNodeOfType(node.name, "JSXIdentifier")) {
2032
+ const rawName = node.name.name;
2033
+ if (rawName !== "img" && rawName !== "object" && rawName !== "area" && rawName.toLowerCase() !== "input" && !imgAliases.has(rawName) && !objectAliases.has(rawName) && !areaAliases.has(rawName) && !inputImageAliases.has(rawName)) return;
2034
+ }
1976
2035
  if (isGeneratedImageRenderContext(context, node)) return;
1977
2036
  const tag = getElementType(node, context.settings);
1978
2037
  if (checkImg && (tag === "img" || imgAliases.has(tag))) {
@@ -2155,7 +2214,9 @@ const anchorIsValid = defineRule({
2155
2214
  category: "Accessibility",
2156
2215
  create: (context) => {
2157
2216
  const settings = resolveSettings$50(context.settings);
2217
+ const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
2158
2218
  return { JSXOpeningElement(node) {
2219
+ if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
2159
2220
  if (getElementType(node, context.settings) !== "a") return;
2160
2221
  let hrefAttribute;
2161
2222
  for (const attributeName of settings.hrefAttributeNames) {
@@ -3458,7 +3519,7 @@ const SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS = [
3458
3519
  ];
3459
3520
  const SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS = [/(?:^|[_\-\s])(?:example|sample|dummy)(?:$|[_\-\s])/i];
3460
3521
  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;
3522
+ const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth(?!or(?!i[sz])))/i;
3462
3523
  const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
3463
3524
  const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
3464
3525
  const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
@@ -3581,8 +3642,9 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
3581
3642
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3582
3643
  //#endregion
3583
3644
  //#region src/plugin/rules/security-scan/utils/find-suspicious-public-env-secret-name.ts
3645
+ const PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN = new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi");
3584
3646
  const findSuspiciousPublicEnvSecretNamePattern = (content) => {
3585
- for (const match of content.matchAll(new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi"))) {
3647
+ for (const match of content.matchAll(PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN)) {
3586
3648
  const value = match[0] ?? "";
3587
3649
  if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
3588
3650
  }
@@ -4049,62 +4111,56 @@ const collectPatternIdentifiers = (pattern, target) => {
4049
4111
  for (const element of pattern.elements ?? []) if (element) collectPatternIdentifiers(element, target);
4050
4112
  } else if (isNodeOfType(pattern, "AssignmentPattern") && pattern.left) collectPatternIdentifiers(pattern.left, target);
4051
4113
  };
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
4114
  const ARRAY_MUTATION_METHOD_NAMES = new Set([
4070
4115
  "push",
4071
4116
  "unshift",
4072
4117
  "splice"
4073
4118
  ]);
4074
- const collectMutatedArrayNames = (block) => {
4075
- const mutated = /* @__PURE__ */ new Set();
4119
+ const addDerivedBindings = (block, names) => {
4120
+ const declaratorBindings = [];
4076
4121
  walkAst(block, (child) => {
4077
4122
  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);
4123
+ if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
4124
+ if (!isNodeOfType(child.id, "Identifier")) return;
4125
+ const referencedNames = /* @__PURE__ */ new Set();
4126
+ collectReferenceIdentifierNames(child.init, referencedNames);
4127
+ declaratorBindings.push({
4128
+ declaredName: child.id.name,
4129
+ referencedNames
4130
+ });
4081
4131
  });
4082
- return mutated;
4083
- };
4084
- const addDerivedBindings = (block, names) => {
4085
4132
  let didGrow = true;
4086
4133
  while (didGrow) {
4087
4134
  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);
4135
+ for (const { declaredName, referencedNames } of declaratorBindings) {
4136
+ if (names.has(declaredName)) continue;
4137
+ for (const referenced of referencedNames) if (names.has(referenced)) {
4138
+ names.add(declaredName);
4096
4139
  didGrow = true;
4097
4140
  break;
4098
4141
  }
4099
- });
4142
+ }
4100
4143
  }
4101
4144
  };
4102
4145
  const hasLoopCarriedDependency = (block) => {
4103
- const carried = collectAssignedIdentifiers(block);
4104
- for (const name of collectMutatedArrayNames(block)) carried.add(name);
4146
+ const carried = /* @__PURE__ */ new Set();
4147
+ const awaitedReferences = /* @__PURE__ */ new Set();
4148
+ walkAst(block, (child) => {
4149
+ if (child !== block && isFunctionLike$1(child)) return false;
4150
+ if (isNodeOfType(child, "AssignmentExpression") && child.left) {
4151
+ collectPatternIdentifiers(child.left, carried);
4152
+ return;
4153
+ }
4154
+ if (isNodeOfType(child, "AwaitExpression") && child.argument) {
4155
+ collectReferenceIdentifierNames(child.argument, awaitedReferences);
4156
+ return;
4157
+ }
4158
+ if (!isNodeOfType(child, "CallExpression")) return;
4159
+ const callee = child.callee;
4160
+ 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);
4161
+ });
4105
4162
  if (carried.size === 0) return false;
4106
4163
  addDerivedBindings(block, carried);
4107
- const awaitedReferences = collectAwaitedArgIdentifiers(block);
4108
4164
  for (const name of carried) if (awaitedReferences.has(name)) return true;
4109
4165
  return false;
4110
4166
  };
@@ -4839,9 +4895,8 @@ const isCreateElementCall = (node) => {
4839
4895
  const callee = node.callee;
4840
4896
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createElement";
4841
4897
  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";
4898
+ if (!(callee.computed ? isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement" : isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement")) return false;
4899
+ return !memberChainContainsDocument(callee.object);
4845
4900
  }
4846
4901
  return false;
4847
4902
  };
@@ -5658,12 +5713,20 @@ const getTemplateInterpolations = (valueTail) => {
5658
5713
  return interpolations === null ? "" : interpolations.join(" ");
5659
5714
  };
5660
5715
  const isInertParseTarget = (target, fileContent) => {
5716
+ const fileHasCreateElement = fileContent.includes("createElement");
5717
+ const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
5718
+ if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
5661
5719
  const escapedTarget = escapeRegExp(target);
5662
5720
  const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
5663
5721
  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;
5722
+ if (fileHasCreateElement) {
5723
+ if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
5724
+ if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
5725
+ }
5726
+ if (fileHasIsolatedDocument) {
5727
+ if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
5728
+ }
5729
+ if (!fileHasCreateElement) return false;
5667
5730
  if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
5668
5731
  const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
5669
5732
  const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
@@ -5756,10 +5819,9 @@ const dangerousHtmlSink = defineRule({
5756
5819
  const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
5757
5820
  if (valueIdentifier !== void 0) {
5758
5821
  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;
5822
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5823
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5824
+ if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
5763
5825
  }
5764
5826
  }
5765
5827
  const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
@@ -5908,6 +5970,7 @@ const noRedundantPaddingAxes = defineRule({
5908
5970
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5909
5971
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5910
5972
  if (!classNameLiteral) return;
5973
+ if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
5911
5974
  if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
5912
5975
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
5913
5976
  if (matchedPairs.length === 0) return;
@@ -5932,6 +5995,7 @@ const noRedundantSizeAxes = defineRule({
5932
5995
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5933
5996
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5934
5997
  if (!classNameLiteral) return;
5998
+ if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
5935
5999
  if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
5936
6000
  const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
5937
6001
  if (matchedPairs.length === 0) return;
@@ -5956,6 +6020,7 @@ const noSpaceOnFlexChildren = defineRule({
5956
6020
  if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
5957
6021
  const classNameLiteral = getClassNameLiteral(jsxAttribute);
5958
6022
  if (!classNameLiteral) return;
6023
+ if (!classNameLiteral.includes("space-")) return;
5959
6024
  const tokens = tokenizeClassName(classNameLiteral);
5960
6025
  let hasFlexOrGridLayout = false;
5961
6026
  for (const token of tokens) {
@@ -6147,7 +6212,10 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
6147
6212
  const actualMinor = Number(match[2]);
6148
6213
  return actualMajor > major || actualMajor === major && actualMinor >= minor;
6149
6214
  };
6215
+ const containsJsxCache = /* @__PURE__ */ new WeakMap();
6150
6216
  const containsJsx$1 = (root) => {
6217
+ const cached = containsJsxCache.get(root);
6218
+ if (cached !== void 0) return cached;
6151
6219
  let found = false;
6152
6220
  const visit = (node) => {
6153
6221
  if (found) return;
@@ -6170,6 +6238,7 @@ const containsJsx$1 = (root) => {
6170
6238
  }
6171
6239
  };
6172
6240
  visit(root);
6241
+ containsJsxCache.set(root, found);
6173
6242
  return found;
6174
6243
  };
6175
6244
  const getStaticMemberName = (node) => {
@@ -6261,27 +6330,6 @@ const hasDisplayNameMember = (classNode) => {
6261
6330
  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
6331
  return false;
6263
6332
  };
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
6333
  const memberExpressionPath = (node) => {
6286
6334
  if (isNodeOfType(node, "Identifier")) return [node.name];
6287
6335
  if (!isNodeOfType(node, "MemberExpression")) return [];
@@ -6289,15 +6337,16 @@ const memberExpressionPath = (node) => {
6289
6337
  const propertyName = getStaticMemberName(node);
6290
6338
  return propertyName ? [...objectPath, propertyName] : objectPath;
6291
6339
  };
6292
- const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6293
- let found = false;
6340
+ const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
6341
+ const getDisplayNameAssignmentIndex = (programRoot) => {
6342
+ const cached = displayNameAssignmentIndexCache.get(programRoot);
6343
+ if (cached) return cached;
6344
+ const identifierTargets = /* @__PURE__ */ new Set();
6345
+ const memberPathSegments = /* @__PURE__ */ new Set();
6294
6346
  const visit = (node) => {
6295
- if (found) return;
6296
6347
  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
- }
6348
+ if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
6349
+ for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
6301
6350
  }
6302
6351
  const record = node;
6303
6352
  for (const key of Object.keys(record)) {
@@ -6306,12 +6355,18 @@ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6306
6355
  if (Array.isArray(child)) {
6307
6356
  for (const item of child) if (isAstNode(item)) visit(item);
6308
6357
  } else if (isAstNode(child)) visit(child);
6309
- if (found) return;
6310
6358
  }
6311
6359
  };
6312
6360
  visit(programRoot);
6313
- return found;
6361
+ const index = {
6362
+ identifierTargets,
6363
+ memberPathSegments
6364
+ };
6365
+ displayNameAssignmentIndexCache.set(programRoot, index);
6366
+ return index;
6314
6367
  };
6368
+ const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
6369
+ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
6315
6370
  const displayName = defineRule({
6316
6371
  id: "display-name",
6317
6372
  title: "Component missing display name",
@@ -7183,27 +7238,8 @@ const isDescendantScope = (inner, outer) => {
7183
7238
  return false;
7184
7239
  };
7185
7240
  //#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
7241
  //#region src/plugin/semantic/closure-captures.ts
7206
- const closureCaptures = (functionNode, scopes) => {
7242
+ const computeClosureCaptures = (functionNode, scopes) => {
7207
7243
  const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
7208
7244
  const out = [];
7209
7245
  const seen = /* @__PURE__ */ new Set();
@@ -7238,7 +7274,20 @@ const closureCaptures = (functionNode, scopes) => {
7238
7274
  }
7239
7275
  };
7240
7276
  visit(functionNode);
7241
- return out.filter((reference) => isAstDescendant(reference.identifier, functionNode));
7277
+ return out;
7278
+ };
7279
+ const capturesByAnalysis = /* @__PURE__ */ new WeakMap();
7280
+ const closureCaptures = (functionNode, scopes) => {
7281
+ let capturesByFunction = capturesByAnalysis.get(scopes);
7282
+ if (!capturesByFunction) {
7283
+ capturesByFunction = /* @__PURE__ */ new WeakMap();
7284
+ capturesByAnalysis.set(scopes, capturesByFunction);
7285
+ }
7286
+ const memoizedCaptures = capturesByFunction.get(functionNode);
7287
+ if (memoizedCaptures) return memoizedCaptures;
7288
+ const computedCaptures = computeClosureCaptures(functionNode, scopes);
7289
+ capturesByFunction.set(functionNode, computedCaptures);
7290
+ return computedCaptures;
7242
7291
  };
7243
7292
  //#endregion
7244
7293
  //#region src/plugin/utils/is-react-hook-name.ts
@@ -7367,6 +7416,25 @@ const resolveExhaustiveDepsSettings = (settings) => {
7367
7416
  };
7368
7417
  };
7369
7418
  //#endregion
7419
+ //#region src/plugin/utils/is-ast-descendant.ts
7420
+ /**
7421
+ * True when `inner` is `outer` itself or any descendant in the AST
7422
+ * `parent` chain. Walks `inner.parent` upward and stops at either a
7423
+ * match (`true`) or the chain's root (`false`).
7424
+ *
7425
+ * Was duplicated byte-identical in:
7426
+ * - exhaustive-deps-symbol-stability.ts
7427
+ * - semantic/closure-captures.ts
7428
+ */
7429
+ const isAstDescendant = (inner, outer) => {
7430
+ let current = inner;
7431
+ while (current) {
7432
+ if (current === outer) return true;
7433
+ current = current.parent ?? null;
7434
+ }
7435
+ return false;
7436
+ };
7437
+ //#endregion
7370
7438
  //#region src/plugin/rules/react-builtins/exhaustive-deps-symbol-stability.ts
7371
7439
  /**
7372
7440
  * Symbol-stability helpers consumed by the `exhaustive-deps` rule.
@@ -8266,9 +8334,14 @@ const firebaseQueryFilterAsAuth = defineRule({
8266
8334
  * `*Handler`), so the cheap escape-then-replace shape is enough
8267
8335
  * and avoids pulling picomatch into the per-file rule path.
8268
8336
  */
8337
+ const compiledGlobs = /* @__PURE__ */ new Map();
8269
8338
  const compileGlob = (pattern) => {
8339
+ const cached = compiledGlobs.get(pattern);
8340
+ if (cached) return cached;
8270
8341
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
8271
- return new RegExp(`^${escaped}$`);
8342
+ const compiled = new RegExp(`^${escaped}$`);
8343
+ compiledGlobs.set(pattern, compiled);
8344
+ return compiled;
8272
8345
  };
8273
8346
  //#endregion
8274
8347
  //#region src/plugin/rules/react-builtins/forbid-component-props.ts
@@ -9210,10 +9283,10 @@ const imgRedundantAlt = defineRule({
9210
9283
  if (isGeneratedImageRenderContext(context)) return {};
9211
9284
  const settings = resolveSettings$37(context.settings);
9212
9285
  return { JSXOpeningElement(node) {
9213
- if (isGeneratedImageRenderContext(context, node)) return;
9214
9286
  const tag = getElementType(node, context.settings);
9215
9287
  if (!settings.components.includes(tag)) return;
9216
9288
  if (isHiddenFromScreenReader(node, context.settings)) return;
9289
+ if (isGeneratedImageRenderContext(context, node)) return;
9217
9290
  const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
9218
9291
  if (!altAttribute) return;
9219
9292
  if (altValueRedundant(altAttribute, settings.words)) context.report({
@@ -9268,6 +9341,7 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
9268
9341
  const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
9269
9342
  const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
9270
9343
  const SECURITY_CONTEXT_WINDOW_CHARS = 250;
9344
+ const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
9271
9345
  const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
9272
9346
  for (const callMatch of content.matchAll(pattern)) {
9273
9347
  const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
@@ -9297,6 +9371,7 @@ const insecureCryptoRisk = defineRule({
9297
9371
  if (!isProductionSourcePath(file.relativePath)) return [];
9298
9372
  if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
9299
9373
  if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
9374
+ if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
9300
9375
  const content = getScannableContent(file);
9301
9376
  let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
9302
9377
  if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
@@ -9529,15 +9604,16 @@ const interactiveSupportsFocus = defineRule({
9529
9604
  const settings = resolveSettings$36(context.settings);
9530
9605
  const tabbableSet = new Set(settings.tabbable);
9531
9606
  return { JSXOpeningElement(node) {
9607
+ if (node.attributes.length === 0) return;
9532
9608
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
9533
9609
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
9534
9610
  const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
9611
+ if (!role) return;
9612
+ if (!ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)))) return;
9535
9613
  const elementType = getElementType(node, context.settings);
9536
9614
  if (!HTML_TAGS.has(elementType)) return;
9537
- const hasInteractiveHandler = ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)));
9615
+ if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9538
9616
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
9539
- if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
9540
- if (!role) return;
9541
9617
  if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
9542
9618
  const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
9543
9619
  context.report({
@@ -10176,8 +10252,6 @@ const jsCachePropertyAccess = defineRule({
10176
10252
  walkAst(loopBody, (child) => {
10177
10253
  if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
10178
10254
  if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
10179
- });
10180
- walkAst(loopBody, (child) => {
10181
10255
  if (!isNodeOfType(child, "MemberExpression")) return;
10182
10256
  if (child.computed) return;
10183
10257
  if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
@@ -10637,7 +10711,6 @@ const jsHoistIntl = defineRule({
10637
10711
  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
10712
  create: (context) => ({ NewExpression(node) {
10639
10713
  if (!isIntlNewExpression(node)) return;
10640
- if (isInsideCacheMemo(node)) return;
10641
10714
  let cursor = node.parent ?? null;
10642
10715
  let inFunctionBody = false;
10643
10716
  while (cursor) {
@@ -10654,6 +10727,7 @@ const jsHoistIntl = defineRule({
10654
10727
  cursor = cursor.parent ?? null;
10655
10728
  }
10656
10729
  if (!inFunctionBody) return;
10730
+ if (isInsideCacheMemo(node)) return;
10657
10731
  const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
10658
10732
  context.report({
10659
10733
  node,
@@ -10728,7 +10802,11 @@ const isSingleFieldEqualityPredicate = (node) => {
10728
10802
  if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
10729
10803
  return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
10730
10804
  };
10731
- const collectLoopBoundNames = (loop, names) => {
10805
+ const loopBoundNamesCache = /* @__PURE__ */ new WeakMap();
10806
+ const getLoopBoundNames = (loop) => {
10807
+ const cached = loopBoundNamesCache.get(loop);
10808
+ if (cached) return cached;
10809
+ const names = /* @__PURE__ */ new Set();
10732
10810
  if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
10733
10811
  if (isNodeOfType(child, "Identifier")) names.add(child.name);
10734
10812
  });
@@ -10745,6 +10823,8 @@ const collectLoopBoundNames = (loop, names) => {
10745
10823
  if (targetRoot) names.add(targetRoot);
10746
10824
  }
10747
10825
  });
10826
+ loopBoundNamesCache.set(loop, names);
10827
+ return names;
10748
10828
  };
10749
10829
  const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
10750
10830
  let cursor = receiver;
@@ -10770,7 +10850,7 @@ const isLoopVariantReceiver = (node) => {
10770
10850
  const loopBoundNames = /* @__PURE__ */ new Set();
10771
10851
  let ancestor = node.parent;
10772
10852
  while (ancestor) {
10773
- if (LOOP_TYPES.includes(ancestor.type)) collectLoopBoundNames(ancestor, loopBoundNames);
10853
+ if (LOOP_TYPES.includes(ancestor.type)) for (const name of getLoopBoundNames(ancestor)) loopBoundNames.add(name);
10774
10854
  ancestor = ancestor.parent;
10775
10855
  }
10776
10856
  if (loopBoundNames.has(receiverRoot)) return true;
@@ -12529,6 +12609,7 @@ const KNOWN_SLOT_PROP_NAMES = new Set([
12529
12609
  "bottomContent",
12530
12610
  "leftContent",
12531
12611
  "rightContent",
12612
+ "config",
12532
12613
  "value",
12533
12614
  "currentValue",
12534
12615
  "form",
@@ -12667,6 +12748,10 @@ const SLOT_PROP_SUFFIXES = [
12667
12748
  "Panel",
12668
12749
  "Overlay",
12669
12750
  "Shape",
12751
+ "Avatar",
12752
+ "Text",
12753
+ "State",
12754
+ "Zone",
12670
12755
  "Override",
12671
12756
  "Overrides",
12672
12757
  "Items",
@@ -12683,6 +12768,8 @@ const SLOT_PROP_SUFFIXES = [
12683
12768
  ];
12684
12769
  const isSlotPropName = (propName) => {
12685
12770
  if (KNOWN_SLOT_PROP_NAMES.has(propName)) return true;
12771
+ const decapitalized = propName.charAt(0).toLowerCase() + propName.slice(1);
12772
+ if (decapitalized !== propName && KNOWN_SLOT_PROP_NAMES.has(decapitalized)) return true;
12686
12773
  for (const suffix of SLOT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
12687
12774
  return false;
12688
12775
  };
@@ -15588,8 +15675,12 @@ const containsFetchCall = (node, options) => {
15588
15675
  const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
15589
15676
  let didFindFetchCall = false;
15590
15677
  walkAst(node, (child) => {
15678
+ if (didFindFetchCall) return false;
15591
15679
  if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
15592
- if (isFetchCall$1(child)) didFindFetchCall = true;
15680
+ if (isFetchCall$1(child)) {
15681
+ didFindFetchCall = true;
15682
+ return false;
15683
+ }
15593
15684
  });
15594
15685
  return didFindFetchCall;
15595
15686
  };
@@ -15603,6 +15694,8 @@ const nextjsNoClientFetchForServerData = defineRule({
15603
15694
  severity: "warn",
15604
15695
  recommendation: "Remove 'use client' and fetch directly in the Server Component. No API round-trip, and secrets stay on the server.",
15605
15696
  create: (context) => {
15697
+ const filename = normalizeFilename(context.filename ?? "");
15698
+ if (!(PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages"))) return {};
15606
15699
  let fileHasUseClient = false;
15607
15700
  return {
15608
15701
  Program(programNode) {
@@ -15612,8 +15705,7 @@ const nextjsNoClientFetchForServerData = defineRule({
15612
15705
  if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
15613
15706
  const callback = getEffectCallback(node);
15614
15707
  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({
15708
+ context.report({
15617
15709
  node,
15618
15710
  message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
15619
15711
  });
@@ -16278,16 +16370,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
16278
16370
  recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
16279
16371
  create: (context) => {
16280
16372
  let resolveBinding = () => null;
16373
+ let isRouteHandlerFile = false;
16374
+ let mutatingSegment = null;
16281
16375
  return {
16282
16376
  Program(node) {
16283
16377
  resolveBinding = buildProgramBindingLookup(node);
16378
+ const filename = normalizeFilename(context.filename ?? "");
16379
+ isRouteHandlerFile = ROUTE_HANDLER_FILE_PATTERN.test(filename) && !CRON_ROUTE_PATTERN.test(filename);
16380
+ mutatingSegment = isRouteHandlerFile ? extractMutatingRouteSegment(filename) : null;
16284
16381
  },
16285
16382
  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;
16383
+ if (!isRouteHandlerFile) return;
16289
16384
  if (!isExportedGetHandler(node)) return;
16290
- const mutatingSegment = extractMutatingRouteSegment(filename);
16291
16385
  const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
16292
16386
  for (const handlerBody of handlerBodies) {
16293
16387
  const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
@@ -18856,7 +18950,17 @@ const containsRenderOutput = (node, rootNode, scopes) => {
18856
18950
  }
18857
18951
  return false;
18858
18952
  };
18859
- const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
18953
+ const renderOutputCache = /* @__PURE__ */ new WeakMap();
18954
+ const functionContainsReactRenderOutput = (functionNode, scopes) => {
18955
+ const cachedEntry = renderOutputCache.get(functionNode);
18956
+ if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
18957
+ const hasRenderOutput = containsRenderOutput(functionNode, functionNode, scopes);
18958
+ renderOutputCache.set(functionNode, {
18959
+ scopes,
18960
+ hasRenderOutput
18961
+ });
18962
+ return hasRenderOutput;
18963
+ };
18860
18964
  //#endregion
18861
18965
  //#region src/plugin/utils/is-component-declaration.ts
18862
18966
  const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
@@ -19361,8 +19465,8 @@ const CONTEXT_MODULES = [
19361
19465
  ];
19362
19466
  const isCreateContextCallee = (callee) => {
19363
19467
  if (isNodeOfType(callee, "Identifier")) {
19364
- for (const moduleName of CONTEXT_MODULES) if (getImportedNameFromModule(callee, callee.name, moduleName) === "createContext") return true;
19365
- return false;
19468
+ const binding = getImportBindingForName(callee, callee.name);
19469
+ return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
19366
19470
  }
19367
19471
  if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
19368
19472
  const namespaceIdentifier = callee.object;
@@ -19372,8 +19476,8 @@ const isCreateContextCallee = (callee) => {
19372
19476
  if (propertyIdentifier.name !== "createContext") return false;
19373
19477
  const namespaceName = namespaceIdentifier.name;
19374
19478
  if (isCanonicalReactNamespaceName(namespaceName)) return true;
19375
- for (const moduleName of CONTEXT_MODULES) if (isImportedFromModule(namespaceIdentifier, namespaceName, moduleName)) return true;
19376
- return false;
19479
+ const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
19480
+ return importSource !== null && CONTEXT_MODULES.includes(importSource);
19377
19481
  }
19378
19482
  return false;
19379
19483
  };
@@ -19775,38 +19879,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
19775
19879
  };
19776
19880
  const parseColorToRgb = (value) => {
19777
19881
  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]));
19882
+ if (trimmed.startsWith("#")) {
19883
+ const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
19884
+ if (hex8Match) return {
19885
+ red: parseInt(hex8Match[1], 16),
19886
+ green: parseInt(hex8Match[2], 16),
19887
+ blue: parseInt(hex8Match[3], 16)
19888
+ };
19889
+ const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
19890
+ if (hex6Match) return {
19891
+ red: parseInt(hex6Match[1], 16),
19892
+ green: parseInt(hex6Match[2], 16),
19893
+ blue: parseInt(hex6Match[3], 16)
19894
+ };
19895
+ const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
19896
+ if (hex4Match) return {
19897
+ red: parseInt(hex4Match[1] + hex4Match[1], 16),
19898
+ green: parseInt(hex4Match[2] + hex4Match[2], 16),
19899
+ blue: parseInt(hex4Match[3] + hex4Match[3], 16)
19900
+ };
19901
+ const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
19902
+ if (hex3Match) return {
19903
+ red: parseInt(hex3Match[1] + hex3Match[1], 16),
19904
+ green: parseInt(hex3Match[2] + hex3Match[2], 16),
19905
+ blue: parseInt(hex3Match[3] + hex3Match[3], 16)
19906
+ };
19907
+ }
19908
+ if (trimmed.includes("rgb")) {
19909
+ const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
19910
+ if (rgbMatch) return {
19911
+ red: parseInt(rgbMatch[1], 10),
19912
+ green: parseInt(rgbMatch[2], 10),
19913
+ blue: parseInt(rgbMatch[3], 10)
19914
+ };
19915
+ }
19916
+ if (trimmed.includes("hsl")) {
19917
+ const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
19918
+ if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
19919
+ }
19810
19920
  return null;
19811
19921
  };
19812
19922
  //#endregion
@@ -19834,9 +19944,18 @@ const extractColorFromShadowLayer = (layer) => {
19834
19944
  if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
19835
19945
  return null;
19836
19946
  };
19947
+ const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
19948
+ const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
19949
+ const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
19950
+ const SHADOW_BLUR_TOKEN_INDEX = 2;
19837
19951
  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;
19952
+ const withoutColors = layer.replace(RGB_FUNCTION_PATTERN, "").replace(HEX_COLOR_PATTERN, "");
19953
+ let tokenIndex = 0;
19954
+ for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
19955
+ if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
19956
+ tokenIndex += 1;
19957
+ }
19958
+ return 0;
19840
19959
  };
19841
19960
  const hasColoredGlowShadow = (shadowValue) => {
19842
19961
  for (const layer of splitShadowLayers(shadowValue)) {
@@ -19956,7 +20075,10 @@ const isIntrinsicJsxAttribute = (node) => {
19956
20075
  };
19957
20076
  //#endregion
19958
20077
  //#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
20078
+ const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
19959
20079
  const collectComponentPropNames = (componentFunction) => {
20080
+ const cached = componentPropNamesCache.get(componentFunction);
20081
+ if (cached) return cached;
19960
20082
  const propNames = /* @__PURE__ */ new Set();
19961
20083
  if (!isFunctionLike$1(componentFunction)) return propNames;
19962
20084
  const propsObjectParamNames = /* @__PURE__ */ new Set();
@@ -19970,27 +20092,23 @@ const collectComponentPropNames = (componentFunction) => {
19970
20092
  if (child !== componentBody && isFunctionLike$1(child)) return false;
19971
20093
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
19972
20094
  });
20095
+ componentPropNamesCache.set(componentFunction, propNames);
19973
20096
  return propNames;
19974
20097
  };
19975
- const declaresBindingNamed = (functionNode, bindingName) => {
20098
+ const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
20099
+ const getOwnScopeBoundNames = (functionNode) => {
20100
+ const cached = ownScopeBoundNamesCache.get(functionNode);
20101
+ if (cached) return cached;
19976
20102
  const boundNames = /* @__PURE__ */ new Set();
19977
20103
  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
20104
  walkAst(functionNode, (child) => {
19981
- if (declaresName) return false;
19982
20105
  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
- }
20106
+ if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
19991
20107
  });
19992
- return declaresName;
20108
+ ownScopeBoundNamesCache.set(functionNode, boundNames);
20109
+ return boundNames;
19993
20110
  };
20111
+ const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
19994
20112
  const isPropertyNamePosition = (identifier) => {
19995
20113
  const parent = identifier.parent;
19996
20114
  if (!parent) return false;
@@ -20413,36 +20531,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
20413
20531
  }
20414
20532
  return hasNestedFunction;
20415
20533
  };
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;
20534
+ const isInRenderScope = (node, componentFunction) => {
20535
+ let cursor = node.parent ?? null;
20536
+ while (cursor && cursor !== componentFunction) {
20537
+ if (isFunctionLike$1(cursor)) return false;
20538
+ cursor = cursor.parent ?? null;
20539
+ }
20540
+ return true;
20430
20541
  };
20431
- const isAdjustedDuringRender = (useStateCall, isPropName) => {
20542
+ const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
20432
20543
  const setterName = getStateSetterName(useStateCall);
20433
20544
  if (!setterName) return false;
20434
20545
  const componentFunction = findEnclosingFunction(useStateCall);
20435
20546
  if (!componentFunction) return false;
20436
- let isAdjusted = false;
20547
+ let isExempt = false;
20437
20548
  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;
20549
+ if (isExempt) return false;
20550
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && (isHandlerShapedReseed(child, componentFunction) || isInRenderScope(child, componentFunction))) {
20551
+ isExempt = true;
20442
20552
  return false;
20443
20553
  }
20444
20554
  });
20445
- return isAdjusted;
20555
+ return isExempt;
20446
20556
  };
20447
20557
  const noDerivedUseState = defineRule({
20448
20558
  id: "no-derived-useState",
@@ -20459,8 +20569,7 @@ const noDerivedUseState = defineRule({
20459
20569
  const initializer = node.arguments[0];
20460
20570
  if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
20461
20571
  if (isInitialOnlyPropName(initializer.name)) return;
20462
- if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20463
- if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20572
+ if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20464
20573
  context.report({
20465
20574
  node,
20466
20575
  message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
@@ -20471,8 +20580,7 @@ const noDerivedUseState = defineRule({
20471
20580
  const rootIdentifierName = getRootIdentifierName(initializer);
20472
20581
  if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
20473
20582
  if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
20474
- if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20475
- if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20583
+ if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20476
20584
  context.report({
20477
20585
  node,
20478
20586
  message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
@@ -22171,20 +22279,20 @@ const getTriggerGuardRootName = (testNode) => {
22171
22279
  return null;
22172
22280
  };
22173
22281
  const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
22282
+ const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
22283
+ const settersWithAnyCall = /* @__PURE__ */ new Set();
22284
+ const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
22285
+ walkAst(componentBody, (child) => {
22286
+ if (!isNodeOfType(child, "CallExpression")) return;
22287
+ if (!isNodeOfType(child.callee, "Identifier")) return;
22288
+ const setterName = child.callee.name;
22289
+ if (!setterNames.has(setterName)) return;
22290
+ settersWithAnyCall.add(setterName);
22291
+ if (settersWithNonHandlerCall.has(setterName)) return;
22292
+ if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
22293
+ });
22174
22294
  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
- }
22295
+ for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
22188
22296
  return handlerOnlyWriteStateNames;
22189
22297
  };
22190
22298
  const noEventTriggerState = defineRule({
@@ -22524,6 +22632,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
22524
22632
  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
22633
  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
22634
  const splitVariantScope = (token) => {
22635
+ if (!token.includes(":")) return {
22636
+ scope: "",
22637
+ utility: token.startsWith("!") ? token.slice(1) : token
22638
+ };
22527
22639
  const segments = token.split(":");
22528
22640
  const rawUtility = segments[segments.length - 1];
22529
22641
  return {
@@ -22547,6 +22659,7 @@ const noGrayOnColoredBackground = defineRule({
22547
22659
  const bgColorScopes = /* @__PURE__ */ new Set();
22548
22660
  for (const token of classStr.split(/\s+/)) {
22549
22661
  if (!token) continue;
22662
+ if (!token.includes("text-") && !token.includes("bg-")) continue;
22550
22663
  const { scope, utility } = splitVariantScope(token);
22551
22664
  if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
22552
22665
  if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
@@ -23276,6 +23389,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
23276
23389
  return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
23277
23390
  });
23278
23391
  const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
23392
+ const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
23393
+ const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
23279
23394
  const noLongTransitionDuration = defineRule({
23280
23395
  id: "no-long-transition-duration",
23281
23396
  title: "Transition duration too long",
@@ -23299,11 +23414,10 @@ const noLongTransitionDuration = defineRule({
23299
23414
  if (key === "transitionDuration" || key === "animationDuration") {
23300
23415
  let longestDurationPropertyMs = 0;
23301
23416
  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);
23417
+ const durationMatch = segment.trim().match(DURATION_SEGMENT_PATTERN);
23418
+ if (!durationMatch) continue;
23419
+ const segmentDurationMs = durationMatch[2] === "ms" ? parseFloat(durationMatch[1]) : parseFloat(durationMatch[1]) * 1e3;
23420
+ longestDurationPropertyMs = Math.max(longestDurationPropertyMs, segmentDurationMs);
23307
23421
  }
23308
23422
  if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
23309
23423
  }
@@ -23311,7 +23425,7 @@ const noLongTransitionDuration = defineRule({
23311
23425
  let longestDurationMs = 0;
23312
23426
  for (const segment of value.split(",")) {
23313
23427
  if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
23314
- const firstTimeMatch = segment.match(/(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/);
23428
+ const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
23315
23429
  if (!firstTimeMatch) continue;
23316
23430
  const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
23317
23431
  longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
@@ -26069,14 +26183,14 @@ const noRenderInRender = defineRule({
26069
26183
  const expression = node.expression;
26070
26184
  if (!isNodeOfType(expression, "CallExpression")) return;
26071
26185
  let calleeName = null;
26186
+ if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
26187
+ else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
26188
+ if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
26072
26189
  if (isNodeOfType(expression.callee, "Identifier")) {
26073
26190
  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")) {
26191
+ } else if (isNodeOfType(expression.callee, "MemberExpression")) {
26076
26192
  if (rootsInProps(expression.callee.object, context.scopes)) return;
26077
- calleeName = expression.callee.property.name;
26078
26193
  }
26079
- if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
26080
26194
  context.report({
26081
26195
  node: expression,
26082
26196
  message: `Your users lose state because "${calleeName}()" builds UI from an inline call that React remounts, so pull it into its own component instead.`
@@ -28791,32 +28905,26 @@ const isFirstArgumentOfHocCall = (node) => {
28791
28905
  if (!isHocCallee$1(parent)) return false;
28792
28906
  return parent.arguments[0] === node;
28793
28907
  };
28908
+ const MAP_LIKE_METHOD_NAMES = new Set([
28909
+ "map",
28910
+ "forEach",
28911
+ "filter",
28912
+ "flatMap",
28913
+ "reduce",
28914
+ "reduceRight"
28915
+ ]);
28794
28916
  const isReturnOfMapCallback = (node) => {
28795
28917
  const parent = node.parent;
28796
28918
  if (!parent) return false;
28797
28919
  if (isNodeOfType(parent, "CallExpression")) {
28798
28920
  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);
28921
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28807
28922
  }
28808
28923
  if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
28809
28924
  const callbackParent = parent.parent;
28810
28925
  if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
28811
28926
  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);
28927
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
28820
28928
  }
28821
28929
  }
28822
28930
  return false;
@@ -28852,10 +28960,10 @@ const isRenderFlowingReadReference = (identifier) => {
28852
28960
  valueNode = parent;
28853
28961
  parent = parent.parent;
28854
28962
  continue;
28855
- case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isUppercaseName(parent.id.name);
28963
+ case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isReactComponentName(parent.id.name);
28856
28964
  case "AssignmentExpression": {
28857
28965
  const assignmentTarget = parent.left;
28858
- return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isUppercaseName(assignmentTarget.name);
28966
+ return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
28859
28967
  }
28860
28968
  case "Property":
28861
28969
  if (parent.value !== valueNode) return false;
@@ -28948,14 +29056,14 @@ const noUnstableNestedComponents = defineRule({
28948
29056
  };
28949
29057
  return {
28950
29058
  JSXOpeningElement(node) {
28951
- if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) {
29059
+ if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
28952
29060
  recordInstantiation(node.name, node.name.name);
28953
29061
  return;
28954
29062
  }
28955
29063
  if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
28956
29064
  },
28957
29065
  Identifier(node) {
28958
- if (!isUppercaseName(node.name)) return;
29066
+ if (!isReactComponentName(node.name)) return;
28959
29067
  if (!isRenderFlowingReadReference(node)) return;
28960
29068
  recordInstantiation(node, node.name);
28961
29069
  },
@@ -29728,10 +29836,10 @@ const onlyExportComponents = defineRule({
29728
29836
  };
29729
29837
  for (const child of componentCandidates) {
29730
29838
  if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
29731
- if (isReactComponentName(child.id.name) && !isInsideExport(child)) localComponents.push(child.id);
29839
+ if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
29732
29840
  }
29733
29841
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
29734
- if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child)) localComponents.push(child.id);
29842
+ if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
29735
29843
  }
29736
29844
  }
29737
29845
  if (hasAnyExports && hasReactExport) for (const entry of exports) {
@@ -30765,13 +30873,13 @@ const preferStableEmptyFallback = defineRule({
30765
30873
  memoRegistry = buildSameFileMemoRegistry(node);
30766
30874
  },
30767
30875
  JSXAttribute(node) {
30768
- if (!isInsideFunctionScope(node)) return;
30769
- if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30770
30876
  if (!node.value) return;
30771
30877
  if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
30772
30878
  const innerExpression = node.value.expression;
30773
30879
  if (!innerExpression) return;
30774
30880
  if (innerExpression.type === "JSXEmptyExpression") return;
30881
+ if (!isInsideFunctionScope(node)) return;
30882
+ if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
30775
30883
  const parentJsxOpening = node.parent;
30776
30884
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
30777
30885
  if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
@@ -32143,41 +32251,11 @@ const callsIdentifier = (root, identifierName) => {
32143
32251
  });
32144
32252
  return found;
32145
32253
  };
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
32254
  const PROMISE_CHAIN_METHOD_NAMES = new Set([
32159
32255
  "then",
32160
32256
  "catch",
32161
32257
  "finally"
32162
32258
  ]);
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
32259
  const ASYNC_DATA_CALLEE_NAMES = new Set([
32182
32260
  "useApolloClient",
32183
32261
  "useMutation",
@@ -32190,20 +32268,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
32190
32268
  "fetch",
32191
32269
  "axios"
32192
32270
  ]);
32193
- const referencesAsyncDataApi = (body) => {
32194
- if (!body) return false;
32271
+ const hasAsyncLoadingWork = (fnBody, setterName) => {
32195
32272
  let found = false;
32196
- walkAst(body, (child) => {
32197
- if (found) return;
32273
+ walkAst(fnBody, (child) => {
32274
+ if (found) return false;
32198
32275
  if (isNodeOfType(child, "CallExpression")) {
32199
32276
  const callee = child.callee;
32200
32277
  if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
32201
32278
  found = true;
32202
- return;
32279
+ return false;
32280
+ }
32281
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
32282
+ if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
32283
+ found = true;
32284
+ return false;
32285
+ }
32286
+ if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
32287
+ if (!isFunctionLike$1(argument)) continue;
32288
+ if (callsIdentifier(argument.body, setterName)) {
32289
+ found = true;
32290
+ return false;
32291
+ }
32292
+ }
32203
32293
  }
32204
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
32294
+ return;
32295
+ }
32296
+ if (setterName !== null && isFunctionLike$1(child)) {
32297
+ const functionBody = child.body;
32298
+ if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
32205
32299
  found = true;
32206
- return;
32300
+ return false;
32207
32301
  }
32208
32302
  }
32209
32303
  });
@@ -32228,7 +32322,7 @@ const renderingUsetransitionLoading = defineRule({
32228
32322
  const secondBinding = node.id.elements[1];
32229
32323
  const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
32230
32324
  const fnBody = enclosingFunctionBody(node);
32231
- if (fnBody && (setterName && setterIsCalledInAsyncContext(fnBody, setterName) || setterName && setterIsCalledInPromiseChain(fnBody, setterName) || referencesAsyncDataApi(fnBody))) return;
32325
+ if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
32232
32326
  context.report({
32233
32327
  node: node.init,
32234
32328
  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 +33116,17 @@ const rerenderStateOnlyInHandlers = defineRule({
33022
33116
  const effectTriggerNames = /* @__PURE__ */ new Set();
33023
33117
  for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
33024
33118
  for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
33119
+ const setterNames = new Set(bindings.map((binding) => binding.setterName));
33120
+ const calledSetterNames = /* @__PURE__ */ new Set();
33121
+ walkAst(componentBody, (child) => {
33122
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
33123
+ });
33025
33124
  for (const binding of bindings) {
33026
33125
  if (renderReachableNames.has(binding.valueName)) continue;
33027
33126
  if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
33028
33127
  const setterSuffix = binding.setterName.slice(3);
33029
33128
  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;
33129
+ if (!calledSetterNames.has(binding.setterName)) continue;
33036
33130
  if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
33037
33131
  context.report({
33038
33132
  node: binding.declarator,
@@ -33364,6 +33458,7 @@ const RECYCLABLE_LIST_PACKAGES = {
33364
33458
  FlashList: ["@shopify/flash-list"],
33365
33459
  LegendList: ["@legendapp/list"]
33366
33460
  };
33461
+ const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
33367
33462
  const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
33368
33463
  const RENDER_ITEM_PROP_NAMES = new Set([
33369
33464
  "renderItem",
@@ -33606,33 +33701,42 @@ const rnListMissingEstimatedItemSize = defineRule({
33606
33701
  requires: ["react-native"],
33607
33702
  severity: "warn",
33608
33703
  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;
33704
+ create: (context) => {
33705
+ let fileImportsRecycler = false;
33706
+ return {
33707
+ Program(node) {
33708
+ fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
33709
+ },
33710
+ JSXOpeningElement(node) {
33711
+ if (!fileImportsRecycler) return;
33712
+ const localElementName = resolveJsxElementName(node);
33713
+ if (!localElementName) return;
33714
+ const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
33715
+ if (canonicalRecyclerName === null) return;
33716
+ if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
33717
+ let hasSizingHint = false;
33718
+ let dataIsEmptyLiteral = false;
33719
+ let hasDataProp = false;
33720
+ for (const attribute of node.attributes ?? []) {
33721
+ if (!isNodeOfType(attribute, "JSXAttribute")) continue;
33722
+ if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
33723
+ const attributeName = attribute.name.name;
33724
+ if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
33725
+ if (attributeName === "data") {
33726
+ hasDataProp = true;
33727
+ if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
33728
+ }
33729
+ }
33730
+ if (hasSizingHint) return;
33731
+ if (dataIsEmptyLiteral) return;
33732
+ if (!hasDataProp) return;
33733
+ context.report({
33734
+ node,
33735
+ message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
33736
+ });
33626
33737
  }
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
- } })
33738
+ };
33739
+ }
33636
33740
  });
33637
33741
  //#endregion
33638
33742
  //#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
@@ -33643,25 +33747,34 @@ const rnListRecyclableWithoutTypes = defineRule({
33643
33747
  requires: ["react-native"],
33644
33748
  severity: "warn",
33645
33749
  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
- } })
33750
+ create: (context) => {
33751
+ let fileImportsRecycler = false;
33752
+ return {
33753
+ Program(node) {
33754
+ fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
33755
+ },
33756
+ JSXOpeningElement(node) {
33757
+ if (!fileImportsRecycler) return;
33758
+ const elementName = resolveJsxElementName(node);
33759
+ if (!elementName) return;
33760
+ if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
33761
+ let hasRecycleItemsEnabled = false;
33762
+ let hasGetItemType = false;
33763
+ for (const attr of node.attributes ?? []) {
33764
+ if (!isNodeOfType(attr, "JSXAttribute")) continue;
33765
+ if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
33766
+ if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
33767
+ else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
33768
+ else hasRecycleItemsEnabled = true;
33769
+ if (attr.name.name === "getItemType") hasGetItemType = true;
33770
+ }
33771
+ if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
33772
+ node,
33773
+ message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
33774
+ });
33775
+ }
33776
+ };
33777
+ }
33665
33778
  });
33666
33779
  //#endregion
33667
33780
  //#region src/plugin/rules/react-native/rn-no-deep-imports.ts
@@ -34691,6 +34804,7 @@ const rnNoRawText = defineRule({
34691
34804
  return {
34692
34805
  Program(programNode) {
34693
34806
  isDomComponentFile = hasDirective(programNode, "use dom");
34807
+ if (!containsJsxElement(programNode)) return;
34694
34808
  const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
34695
34809
  autoDetectedTextWrappers = childrenForwarding.textWrappers;
34696
34810
  autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
@@ -38645,14 +38759,7 @@ const roleSupportsAriaProps = defineRule({
38645
38759
  recommendation: "Only use `aria-*` attributes that the element's role supports.",
38646
38760
  category: "Accessibility",
38647
38761
  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;
38762
+ let ariaAttributes = null;
38656
38763
  for (const attribute of node.attributes) {
38657
38764
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
38658
38765
  const attributeNode = attribute;
@@ -38662,6 +38769,21 @@ const roleSupportsAriaProps = defineRule({
38662
38769
  const propName = propRawName.toLowerCase();
38663
38770
  if (!propName.startsWith("aria-")) continue;
38664
38771
  if (!ARIA_PROPERTIES.has(propName)) continue;
38772
+ (ariaAttributes ??= []).push({
38773
+ attribute,
38774
+ propName
38775
+ });
38776
+ }
38777
+ if (!ariaAttributes) return;
38778
+ const elementType = getElementType(node, context.settings);
38779
+ const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
38780
+ const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
38781
+ if (!role) return;
38782
+ if (!VALID_ARIA_ROLES.has(role)) return;
38783
+ const isImplicit = !roleAttribute;
38784
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
38785
+ if (!supported) return;
38786
+ for (const { attribute, propName } of ariaAttributes) {
38665
38787
  if (supported.has(propName)) continue;
38666
38788
  context.report({
38667
38789
  node: attribute,
@@ -39499,6 +39621,7 @@ const serverCacheWithObjectLiteral = defineRule({
39499
39621
  cachedFunctionNames.add(node.id.name);
39500
39622
  },
39501
39623
  CallExpression(node) {
39624
+ if (cachedFunctionNames.size === 0) return;
39502
39625
  if (!isNodeOfType(node.callee, "Identifier")) return;
39503
39626
  if (!cachedFunctionNames.has(node.callee.name)) return;
39504
39627
  const firstArg = node.arguments?.[0];
@@ -40425,7 +40548,16 @@ const supabaseRlsPolicyRisk = defineRule({
40425
40548
  //#endregion
40426
40549
  //#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
40427
40550
  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");
40551
+ 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;
40552
+ const collectLastEnableRlsIndexByTable = (content) => {
40553
+ const lastEnableIndexByTable = /* @__PURE__ */ new Map();
40554
+ for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
40555
+ const tableName = match[1];
40556
+ if (tableName === void 0) continue;
40557
+ lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
40558
+ }
40559
+ return lastEnableIndexByTable;
40560
+ };
40429
40561
  const supabaseTableMissingRls = defineRule({
40430
40562
  id: "supabase-table-missing-rls",
40431
40563
  title: "Supabase table created without Row Level Security",
@@ -40436,11 +40568,13 @@ const supabaseTableMissingRls = defineRule({
40436
40568
  const content = sanitizeSqlForScan(file.content);
40437
40569
  if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
40438
40570
  const findings = [];
40571
+ const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
40439
40572
  CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
40440
40573
  for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
40441
40574
  const tableName = match[1];
40442
40575
  if (tableName === void 0) continue;
40443
- if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
40576
+ const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
40577
+ if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
40444
40578
  const location = getLocationAtIndex(content, match.index);
40445
40579
  findings.push({
40446
40580
  message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
@@ -40716,6 +40850,7 @@ const tanstackStartMissingHeadContent = defineRule({
40716
40850
  severity: "warn",
40717
40851
  recommendation: "Add `<HeadContent />` inside `<head>` in your __root route. Without it, route `head()` meta tags are dropped.",
40718
40852
  create: (context) => {
40853
+ if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(context.filename ?? "")) return {};
40719
40854
  let hasHeadContentElement = false;
40720
40855
  let hasDocumentHeadElement = false;
40721
40856
  let hasCustomHeadChildElement = false;
@@ -40752,8 +40887,6 @@ const tanstackStartMissingHeadContent = defineRule({
40752
40887
  };
40753
40888
  return {
40754
40889
  Program(node) {
40755
- const filename = context.filename ?? "";
40756
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40757
40890
  const statements = node.body ?? [];
40758
40891
  for (const statement of statements) collectImportBindings(statement);
40759
40892
  for (const statement of statements) {
@@ -40762,18 +40895,12 @@ const tanstackStartMissingHeadContent = defineRule({
40762
40895
  }
40763
40896
  },
40764
40897
  ImportDeclaration(node) {
40765
- const filename = context.filename ?? "";
40766
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40767
40898
  collectImportBindings(node);
40768
40899
  },
40769
40900
  VariableDeclarator(node) {
40770
- const filename = context.filename ?? "";
40771
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40772
40901
  collectVariableAlias(node);
40773
40902
  },
40774
40903
  JSXOpeningElement(node) {
40775
- const filename = normalizeFilename(context.filename ?? "");
40776
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40777
40904
  if (isNodeOfType(node.name, "JSXIdentifier")) {
40778
40905
  if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
40779
40906
  if (headContentComponentNames.has(node.name.name)) hasHeadContentElement = true;
@@ -40787,8 +40914,6 @@ const tanstackStartMissingHeadContent = defineRule({
40787
40914
  if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
40788
40915
  },
40789
40916
  "Program:exit"(programNode) {
40790
- const filename = normalizeFilename(context.filename ?? "");
40791
- if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
40792
40917
  if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
40793
40918
  node: programNode,
40794
40919
  message: "Without <HeadContent /> in the __root route, your route head() meta tags never render."
@@ -40812,27 +40937,29 @@ const tanstackStartNoAnchorElement = defineRule({
40812
40937
  requires: ["tanstack-start"],
40813
40938
  severity: "warn",
40814
40939
  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
- } })
40940
+ create: (context) => {
40941
+ if (!isInProjectDirectory(context, "routes")) return {};
40942
+ return { JSXOpeningElement(node) {
40943
+ if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
40944
+ const attributes = node.attributes ?? [];
40945
+ const hrefAttribute = findJsxAttribute(attributes, "href");
40946
+ if (!hrefAttribute?.value) return;
40947
+ let hrefValue = null;
40948
+ if (isNodeOfType(hrefAttribute.value, "Literal")) hrefValue = hrefAttribute.value.value;
40949
+ else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
40950
+ if (typeof hrefValue !== "string" || !hrefValue.startsWith("/")) return;
40951
+ if (hrefValue.startsWith("//")) return;
40952
+ const pathname = hrefValue.split(/[?#]/)[0] ?? hrefValue;
40953
+ if (pathname.startsWith("/api/")) return;
40954
+ if (/\.[a-z0-9]{1,8}$/i.test(pathname)) return;
40955
+ if (findJsxAttribute(attributes, "download")) return;
40956
+ if (getAttributeStringValue(findJsxAttribute(attributes, "target")) === "_blank") return;
40957
+ context.report({
40958
+ node,
40959
+ message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
40960
+ });
40961
+ } };
40962
+ }
40836
40963
  });
40837
40964
  //#endregion
40838
40965
  //#region src/plugin/rules/tanstack-start/tanstack-start-no-direct-fetch-in-loader.ts
@@ -40896,7 +41023,7 @@ const tanstackStartNoNavigateInRender = defineRule({
40896
41023
  severity: "warn",
40897
41024
  recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
40898
41025
  create: (context) => {
40899
- const isRouteFile = isInProjectDirectory(context, "routes");
41026
+ if (!isInProjectDirectory(context, "routes")) return {};
40900
41027
  let deferredCallbackDepth = 0;
40901
41028
  let eventHandlerDepth = 0;
40902
41029
  const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
@@ -40960,7 +41087,6 @@ const tanstackStartNoNavigateInRender = defineRule({
40960
41087
  };
40961
41088
  return {
40962
41089
  CallExpression(node) {
40963
- if (!isRouteFile) return;
40964
41090
  if (isDeferredHookCall(node)) deferredCallbackDepth++;
40965
41091
  if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
40966
41092
  if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
@@ -40972,39 +41098,30 @@ const tanstackStartNoNavigateInRender = defineRule({
40972
41098
  }
40973
41099
  },
40974
41100
  "CallExpression:exit"(node) {
40975
- if (!isRouteFile) return;
40976
41101
  if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
40977
41102
  },
40978
41103
  JSXAttribute(node) {
40979
- if (!isRouteFile) return;
40980
41104
  if (isEventHandlerAttribute(node)) eventHandlerDepth++;
40981
41105
  },
40982
41106
  "JSXAttribute:exit"(node) {
40983
- if (!isRouteFile) return;
40984
41107
  if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40985
41108
  },
40986
41109
  Property(node) {
40987
- if (!isRouteFile) return;
40988
41110
  if (isEventHandlerProperty(node)) eventHandlerDepth++;
40989
41111
  },
40990
41112
  "Property:exit"(node) {
40991
- if (!isRouteFile) return;
40992
41113
  if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
40993
41114
  },
40994
41115
  VariableDeclarator(node) {
40995
- if (!isRouteFile) return;
40996
41116
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
40997
41117
  },
40998
41118
  "VariableDeclarator:exit"(node) {
40999
- if (!isRouteFile) return;
41000
41119
  if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
41001
41120
  },
41002
41121
  FunctionDeclaration(node) {
41003
- if (!isRouteFile) return;
41004
41122
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
41005
41123
  },
41006
41124
  "FunctionDeclaration:exit"(node) {
41007
- if (!isRouteFile) return;
41008
41125
  if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
41009
41126
  }
41010
41127
  };
@@ -41088,23 +41205,25 @@ const tanstackStartNoUseEffectFetch = defineRule({
41088
41205
  requires: ["tanstack-start"],
41089
41206
  severity: "warn",
41090
41207
  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
- } })
41208
+ create: (context) => {
41209
+ if (!isInProjectDirectory(context, "routes")) return {};
41210
+ return { CallExpression(node) {
41211
+ if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
41212
+ const callback = node.arguments?.[0];
41213
+ if (!callback) return;
41214
+ let hasFetchCall = false;
41215
+ const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
41216
+ walkAst(callback, (child) => {
41217
+ if (hasFetchCall) return false;
41218
+ if (child !== callback && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
41219
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === "fetch") hasFetchCall = true;
41220
+ });
41221
+ if (hasFetchCall) context.report({
41222
+ node,
41223
+ message: "fetch() inside useEffect makes your users wait through a loading spinner after render."
41224
+ });
41225
+ } };
41226
+ }
41108
41227
  });
41109
41228
  //#endregion
41110
41229
  //#region src/plugin/rules/tanstack-start/tanstack-start-redirect-in-try-catch.ts
@@ -41472,6 +41591,7 @@ const webhookSignatureRisk = defineRule({
41472
41591
  //#endregion
41473
41592
  //#region src/plugin/rules/zod/utils/zod-ast.ts
41474
41593
  const ZOD_MODULE = "zod";
41594
+ const ZOD_MODULE_SOURCES = [ZOD_MODULE];
41475
41595
  const getStaticPropertyName = (member) => {
41476
41596
  const property = member.property;
41477
41597
  if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
@@ -41615,25 +41735,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
41615
41735
  tags: ["migration-hint"],
41616
41736
  severity: "warn",
41617
41737
  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
- })
41738
+ create: (context) => {
41739
+ let fileImportsZod = false;
41740
+ return {
41741
+ Program(node) {
41742
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
41743
+ },
41744
+ CallExpression(node) {
41745
+ if (!fileImportsZod) return;
41746
+ if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
41747
+ if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
41748
+ context.report({
41749
+ node,
41750
+ message: ZOD_ERROR_API_MESSAGE
41751
+ });
41752
+ },
41753
+ MemberExpression(node) {
41754
+ if (!fileImportsZod) return;
41755
+ const parent = node.parent;
41756
+ if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41757
+ if (!isDeprecatedZodErrorMemberAccess(node)) return;
41758
+ context.report({
41759
+ node,
41760
+ message: ZOD_ERROR_API_MESSAGE
41761
+ });
41762
+ }
41763
+ };
41764
+ }
41637
41765
  });
41638
41766
  //#endregion
41639
41767
  //#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
@@ -41825,16 +41953,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
41825
41953
  tags: ["migration-hint"],
41826
41954
  severity: "warn",
41827
41955
  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
- })
41956
+ create: (context) => {
41957
+ let fileImportsZod = false;
41958
+ return {
41959
+ Program(node) {
41960
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
41961
+ },
41962
+ CallExpression(node) {
41963
+ if (!fileImportsZod) return;
41964
+ 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);
41965
+ },
41966
+ MemberExpression(node) {
41967
+ if (!fileImportsZod) return;
41968
+ const parent = node.parent;
41969
+ if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
41970
+ if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
41971
+ }
41972
+ };
41973
+ }
41838
41974
  });
41839
41975
  //#endregion
41840
41976
  //#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
@@ -41869,13 +42005,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
41869
42005
  tags: ["migration-hint"],
41870
42006
  severity: "warn",
41871
42007
  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
- } })
42008
+ create: (context) => {
42009
+ let fileImportsZod = false;
42010
+ return {
42011
+ Program(node) {
42012
+ fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
42013
+ },
42014
+ CallExpression(node) {
42015
+ if (!fileImportsZod) return;
42016
+ if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
42017
+ context.report({
42018
+ node,
42019
+ message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
42020
+ });
42021
+ }
42022
+ };
42023
+ }
41879
42024
  });
41880
42025
  //#endregion
41881
42026
  //#region src/plugin/rule-registry.ts