oxlint-plugin-react-doctor 0.6.2-dev.da3b19c → 0.6.2-dev.fa61c20

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 +315 -401
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -378,18 +378,13 @@ const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
378
378
  const isConfigOrCiPath = (relativePath) => /(?:^|\/)(?:package\.json|Dockerfile|docker-compose\.ya?ml|\.github\/workflows\/[^/]+\.ya?ml|vercel\.json|next\.config\.[cm]?[jt]s|netlify\.toml)$/i.test(relativePath);
379
379
  //#endregion
380
380
  //#region src/plugin/rules/security-scan/utils/is-production-file-path.ts
381
- const classificationCacheByPattern = /* @__PURE__ */ new Map();
382
381
  const isProductionFilePath = (relativePath, sourceFilePattern) => {
383
- let classificationByPath = classificationCacheByPattern.get(sourceFilePattern);
384
- if (!classificationByPath) {
385
- classificationByPath = /* @__PURE__ */ new Map();
386
- classificationCacheByPattern.set(sourceFilePattern, classificationByPath);
387
- }
388
- const cached = classificationByPath.get(relativePath);
389
- if (cached !== void 0) return cached;
390
- const isProduction = sourceFilePattern.test(relativePath) && !TEST_CONTEXT_PATTERN.test(relativePath) && !BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(relativePath) && !GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath);
391
- classificationByPath.set(relativePath, isProduction);
392
- return isProduction;
382
+ if (!sourceFilePattern.test(relativePath)) return false;
383
+ if (TEST_CONTEXT_PATTERN.test(relativePath)) return false;
384
+ if (BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath)) return false;
385
+ if (DOCUMENTATION_CONTEXT_PATTERN.test(relativePath)) return false;
386
+ if (GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath)) return false;
387
+ return true;
393
388
  };
394
389
  //#endregion
395
390
  //#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
@@ -788,6 +783,24 @@ const collectChildComponentNames = (element, into) => {
788
783
  into.add(name);
789
784
  });
790
785
  };
786
+ const findSameFileComponentBody = (programRoot, componentName) => {
787
+ let foundBody = null;
788
+ walkAst(programRoot, (node) => {
789
+ if (foundBody) return false;
790
+ if (isNodeOfType(node, "FunctionDeclaration") && node.id && node.id.name === componentName) {
791
+ foundBody = node.body;
792
+ return false;
793
+ }
794
+ if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.id.name === componentName) {
795
+ const initializer = node.init;
796
+ if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) {
797
+ foundBody = initializer.body;
798
+ return false;
799
+ }
800
+ }
801
+ });
802
+ return foundBody;
803
+ };
791
804
  const countEffectHookCalls = (body) => {
792
805
  if (!body) return 0;
793
806
  let count = 0;
@@ -797,36 +810,6 @@ const countEffectHookCalls = (body) => {
797
810
  });
798
811
  return count;
799
812
  };
800
- const componentEffectIndexCache = /* @__PURE__ */ new WeakMap();
801
- const getComponentEffectIndex = (programRoot) => {
802
- const cached = componentEffectIndexCache.get(programRoot);
803
- if (cached) return cached;
804
- const bodyByName = /* @__PURE__ */ new Map();
805
- walkAst(programRoot, (node) => {
806
- if (isNodeOfType(node, "FunctionDeclaration") && node.id && !bodyByName.has(node.id.name)) {
807
- bodyByName.set(node.id.name, node.body);
808
- return;
809
- }
810
- if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && !bodyByName.has(node.id.name)) {
811
- const initializer = node.init;
812
- if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) bodyByName.set(node.id.name, initializer.body);
813
- }
814
- });
815
- const index = {
816
- bodyByName,
817
- effectCountByName: /* @__PURE__ */ new Map()
818
- };
819
- componentEffectIndexCache.set(programRoot, index);
820
- return index;
821
- };
822
- const getSameFileComponentEffectCount = (programRoot, componentName) => {
823
- const index = getComponentEffectIndex(programRoot);
824
- const cachedCount = index.effectCountByName.get(componentName);
825
- if (cachedCount !== void 0) return cachedCount;
826
- const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
827
- index.effectCountByName.set(componentName, count);
828
- return count;
829
- };
830
813
  const activityWrapsEffectHeavySubtree = defineRule({
831
814
  id: "activity-wraps-effect-heavy-subtree",
832
815
  title: "Activity wraps an effect-heavy subtree",
@@ -883,7 +866,9 @@ const activityWrapsEffectHeavySubtree = defineRule({
883
866
  let totalEffects = 0;
884
867
  const effectfulChildren = [];
885
868
  for (const componentName of childComponentNames) {
886
- const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
869
+ const body = findSameFileComponentBody(programRoot, componentName);
870
+ if (!body) continue;
871
+ const effectCount = countEffectHookCalls(body);
887
872
  if (effectCount === 0) continue;
888
873
  totalEffects += effectCount;
889
874
  effectfulChildren.push(`<${componentName}>`);
@@ -1682,7 +1667,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
1682
1667
  };
1683
1668
  //#endregion
1684
1669
  //#region src/plugin/utils/normalize-filename.ts
1685
- const normalizeFilename = (filename) => filename.includes("\\") ? filename.replaceAll("\\", "/") : filename;
1670
+ const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
1686
1671
  //#endregion
1687
1672
  //#region src/plugin/utils/is-generated-image-render-context.ts
1688
1673
  const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
@@ -3647,9 +3632,8 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
3647
3632
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3648
3633
  //#endregion
3649
3634
  //#region src/plugin/rules/security-scan/utils/find-suspicious-public-env-secret-name.ts
3650
- const PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN = new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi");
3651
3635
  const findSuspiciousPublicEnvSecretNamePattern = (content) => {
3652
- for (const match of content.matchAll(PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN)) {
3636
+ for (const match of content.matchAll(new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi"))) {
3653
3637
  const value = match[0] ?? "";
3654
3638
  if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
3655
3639
  }
@@ -4116,56 +4100,62 @@ const collectPatternIdentifiers = (pattern, target) => {
4116
4100
  for (const element of pattern.elements ?? []) if (element) collectPatternIdentifiers(element, target);
4117
4101
  } else if (isNodeOfType(pattern, "AssignmentPattern") && pattern.left) collectPatternIdentifiers(pattern.left, target);
4118
4102
  };
4103
+ const collectAssignedIdentifiers = (block) => {
4104
+ const assigned = /* @__PURE__ */ new Set();
4105
+ walkAst(block, (child) => {
4106
+ if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
4107
+ if (isNodeOfType(child, "AssignmentExpression") && child.left) collectPatternIdentifiers(child.left, assigned);
4108
+ });
4109
+ return assigned;
4110
+ };
4111
+ const collectAwaitedArgIdentifiers = (block) => {
4112
+ const referenced = /* @__PURE__ */ new Set();
4113
+ walkAst(block, (child) => {
4114
+ if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
4115
+ if (!isNodeOfType(child, "AwaitExpression") || !child.argument) return;
4116
+ collectReferenceIdentifierNames(child.argument, referenced);
4117
+ });
4118
+ return referenced;
4119
+ };
4119
4120
  const ARRAY_MUTATION_METHOD_NAMES = new Set([
4120
4121
  "push",
4121
4122
  "unshift",
4122
4123
  "splice"
4123
4124
  ]);
4124
- const addDerivedBindings = (block, names) => {
4125
- const declaratorBindings = [];
4125
+ const collectMutatedArrayNames = (block) => {
4126
+ const mutated = /* @__PURE__ */ new Set();
4126
4127
  walkAst(block, (child) => {
4127
4128
  if (child !== block && isFunctionLike$1(child)) return false;
4128
- if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
4129
- if (!isNodeOfType(child.id, "Identifier")) return;
4130
- const referencedNames = /* @__PURE__ */ new Set();
4131
- collectReferenceIdentifierNames(child.init, referencedNames);
4132
- declaratorBindings.push({
4133
- declaredName: child.id.name,
4134
- referencedNames
4135
- });
4129
+ if (!isNodeOfType(child, "CallExpression")) return;
4130
+ const callee = child.callee;
4131
+ 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);
4136
4132
  });
4133
+ return mutated;
4134
+ };
4135
+ const addDerivedBindings = (block, names) => {
4137
4136
  let didGrow = true;
4138
4137
  while (didGrow) {
4139
4138
  didGrow = false;
4140
- for (const { declaredName, referencedNames } of declaratorBindings) {
4141
- if (names.has(declaredName)) continue;
4142
- for (const referenced of referencedNames) if (names.has(referenced)) {
4143
- names.add(declaredName);
4139
+ walkAst(block, (child) => {
4140
+ if (child !== block && isFunctionLike$1(child)) return false;
4141
+ if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
4142
+ if (!isNodeOfType(child.id, "Identifier") || names.has(child.id.name)) return;
4143
+ const initReferences = /* @__PURE__ */ new Set();
4144
+ collectReferenceIdentifierNames(child.init, initReferences);
4145
+ for (const referenced of initReferences) if (names.has(referenced)) {
4146
+ names.add(child.id.name);
4144
4147
  didGrow = true;
4145
4148
  break;
4146
4149
  }
4147
- }
4150
+ });
4148
4151
  }
4149
4152
  };
4150
4153
  const hasLoopCarriedDependency = (block) => {
4151
- const carried = /* @__PURE__ */ new Set();
4152
- const awaitedReferences = /* @__PURE__ */ new Set();
4153
- walkAst(block, (child) => {
4154
- if (child !== block && isFunctionLike$1(child)) return false;
4155
- if (isNodeOfType(child, "AssignmentExpression") && child.left) {
4156
- collectPatternIdentifiers(child.left, carried);
4157
- return;
4158
- }
4159
- if (isNodeOfType(child, "AwaitExpression") && child.argument) {
4160
- collectReferenceIdentifierNames(child.argument, awaitedReferences);
4161
- return;
4162
- }
4163
- if (!isNodeOfType(child, "CallExpression")) return;
4164
- const callee = child.callee;
4165
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_MUTATION_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier")) carried.add(callee.object.name);
4166
- });
4154
+ const carried = collectAssignedIdentifiers(block);
4155
+ for (const name of collectMutatedArrayNames(block)) carried.add(name);
4167
4156
  if (carried.size === 0) return false;
4168
4157
  addDerivedBindings(block, carried);
4158
+ const awaitedReferences = collectAwaitedArgIdentifiers(block);
4169
4159
  for (const name of carried) if (awaitedReferences.has(name)) return true;
4170
4160
  return false;
4171
4161
  };
@@ -4843,8 +4833,7 @@ const FORM_CONTROL_TAGS = new Set([
4843
4833
  ]);
4844
4834
  const resolveSettings$48 = (settings) => {
4845
4835
  const reactDoctor = settings?.["react-doctor"];
4846
- const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {};
4847
- return { inputComponents: new Set(ruleSettings.inputComponents ?? []) };
4836
+ return { inputComponents: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {}).inputComponents ?? [] };
4848
4837
  };
4849
4838
  const autocompleteValid = defineRule({
4850
4839
  id: "autocomplete-valid",
@@ -4857,7 +4846,7 @@ const autocompleteValid = defineRule({
4857
4846
  const settings = resolveSettings$48(context.settings);
4858
4847
  return { JSXOpeningElement: (node) => {
4859
4848
  const tag = getElementType(node, context.settings);
4860
- if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.has(tag)) return;
4849
+ if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.includes(tag)) return;
4861
4850
  const attribute = hasJsxPropIgnoreCase(node.attributes, "autoComplete");
4862
4851
  if (!attribute) return;
4863
4852
  const value = getJsxPropStringValue(attribute);
@@ -5719,20 +5708,12 @@ const getTemplateInterpolations = (valueTail) => {
5719
5708
  return interpolations === null ? "" : interpolations.join(" ");
5720
5709
  };
5721
5710
  const isInertParseTarget = (target, fileContent) => {
5722
- const fileHasCreateElement = fileContent.includes("createElement");
5723
- const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
5724
- if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
5725
5711
  const escapedTarget = escapeRegExp(target);
5726
5712
  const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
5727
5713
  if (new RegExp(`\\b${escapedRoot}\\s*=\\s*[^\\n;]*(?:getElementById|querySelector|getElementsBy|\\.current\\b|document\\.(?:body|head|documentElement))`).test(fileContent)) return false;
5728
- if (fileHasCreateElement) {
5729
- if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
5730
- if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
5731
- }
5732
- if (fileHasIsolatedDocument) {
5733
- if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
5734
- }
5735
- if (!fileHasCreateElement) return false;
5714
+ if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
5715
+ if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
5716
+ if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
5736
5717
  if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
5737
5718
  const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
5738
5719
  const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
@@ -5825,9 +5806,10 @@ const dangerousHtmlSink = defineRule({
5825
5806
  const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
5826
5807
  if (valueIdentifier !== void 0) {
5827
5808
  const escapedIdentifier = escapeRegExp(valueIdentifier);
5828
- if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5829
- if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5830
- if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
5809
+ const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
5810
+ const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
5811
+ const fromDomContent = new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`);
5812
+ if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
5831
5813
  }
5832
5814
  }
5833
5815
  const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
@@ -6218,10 +6200,7 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
6218
6200
  const actualMinor = Number(match[2]);
6219
6201
  return actualMajor > major || actualMajor === major && actualMinor >= minor;
6220
6202
  };
6221
- const containsJsxCache = /* @__PURE__ */ new WeakMap();
6222
6203
  const containsJsx$1 = (root) => {
6223
- const cached = containsJsxCache.get(root);
6224
- if (cached !== void 0) return cached;
6225
6204
  let found = false;
6226
6205
  const visit = (node) => {
6227
6206
  if (found) return;
@@ -6244,7 +6223,6 @@ const containsJsx$1 = (root) => {
6244
6223
  }
6245
6224
  };
6246
6225
  visit(root);
6247
- containsJsxCache.set(root, found);
6248
6226
  return found;
6249
6227
  };
6250
6228
  const getStaticMemberName = (node) => {
@@ -6336,6 +6314,27 @@ const hasDisplayNameMember = (classNode) => {
6336
6314
  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;
6337
6315
  return false;
6338
6316
  };
6317
+ const hasDisplayNameAssignment = (className, programRoot) => {
6318
+ let found = false;
6319
+ const visit = (node) => {
6320
+ if (found) return;
6321
+ if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && isNodeOfType(node.left.object, "Identifier") && node.left.object.name === className && getStaticMemberName(node.left) === "displayName") {
6322
+ found = true;
6323
+ return;
6324
+ }
6325
+ const record = node;
6326
+ for (const key of Object.keys(record)) {
6327
+ if (key === "parent") continue;
6328
+ const child = record[key];
6329
+ if (Array.isArray(child)) {
6330
+ for (const item of child) if (isAstNode(item)) visit(item);
6331
+ } else if (isAstNode(child)) visit(child);
6332
+ if (found) return;
6333
+ }
6334
+ };
6335
+ visit(programRoot);
6336
+ return found;
6337
+ };
6339
6338
  const memberExpressionPath = (node) => {
6340
6339
  if (isNodeOfType(node, "Identifier")) return [node.name];
6341
6340
  if (!isNodeOfType(node, "MemberExpression")) return [];
@@ -6343,16 +6342,15 @@ const memberExpressionPath = (node) => {
6343
6342
  const propertyName = getStaticMemberName(node);
6344
6343
  return propertyName ? [...objectPath, propertyName] : objectPath;
6345
6344
  };
6346
- const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
6347
- const getDisplayNameAssignmentIndex = (programRoot) => {
6348
- const cached = displayNameAssignmentIndexCache.get(programRoot);
6349
- if (cached) return cached;
6350
- const identifierTargets = /* @__PURE__ */ new Set();
6351
- const memberPathSegments = /* @__PURE__ */ new Set();
6345
+ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6346
+ let found = false;
6352
6347
  const visit = (node) => {
6348
+ if (found) return;
6353
6349
  if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
6354
- if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
6355
- for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
6350
+ if (memberExpressionPath(node.left.object).includes(propertyName)) {
6351
+ found = true;
6352
+ return;
6353
+ }
6356
6354
  }
6357
6355
  const record = node;
6358
6356
  for (const key of Object.keys(record)) {
@@ -6361,18 +6359,12 @@ const getDisplayNameAssignmentIndex = (programRoot) => {
6361
6359
  if (Array.isArray(child)) {
6362
6360
  for (const item of child) if (isAstNode(item)) visit(item);
6363
6361
  } else if (isAstNode(child)) visit(child);
6362
+ if (found) return;
6364
6363
  }
6365
6364
  };
6366
6365
  visit(programRoot);
6367
- const index = {
6368
- identifierTargets,
6369
- memberPathSegments
6370
- };
6371
- displayNameAssignmentIndexCache.set(programRoot, index);
6372
- return index;
6366
+ return found;
6373
6367
  };
6374
- const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
6375
- const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
6376
6368
  const displayName = defineRule({
6377
6369
  id: "display-name",
6378
6370
  title: "Component missing display name",
@@ -8637,7 +8629,7 @@ const DEFAULT_HEADING_TAGS = [
8637
8629
  const resolveSettings$40 = (settings) => {
8638
8630
  const reactDoctor = settings?.["react-doctor"];
8639
8631
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
8640
- return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
8632
+ return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
8641
8633
  };
8642
8634
  const headingHasContent = defineRule({
8643
8635
  id: "heading-has-content",
@@ -8650,7 +8642,7 @@ const headingHasContent = defineRule({
8650
8642
  const settings = resolveSettings$40(context.settings);
8651
8643
  return { JSXOpeningElement(node) {
8652
8644
  const elementType = getElementType(node, context.settings);
8653
- if (!settings.headingTags.has(elementType)) return;
8645
+ if (!settings.headingTags.includes(elementType)) return;
8654
8646
  const parent = node.parent;
8655
8647
  if (parent && isNodeOfType(parent, "JSXElement")) {
8656
8648
  if (objectHasAccessibleChild(parent, context.settings)) return;
@@ -9556,7 +9548,6 @@ const KEYBOARD_EVENT_HANDLERS = [
9556
9548
  "onKeyUp"
9557
9549
  ];
9558
9550
  const ALL_EVENT_HANDLERS = [...MOUSE_EVENT_HANDLERS, ...KEYBOARD_EVENT_HANDLERS];
9559
- const ALL_EVENT_HANDLERS_LOWER = new Set(ALL_EVENT_HANDLERS.map((handlerName) => handlerName.toLowerCase()));
9560
9551
  //#endregion
9561
9552
  //#region src/plugin/utils/is-disabled-element.ts
9562
9553
  const isDisabledElement = (openingElement) => {
@@ -9616,16 +9607,7 @@ const interactiveSupportsFocus = defineRule({
9616
9607
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
9617
9608
  const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
9618
9609
  if (!role) return;
9619
- let hasInteractiveHandler = false;
9620
- for (const attribute of node.attributes) {
9621
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
9622
- const attributeName = getJsxAttributeName(attribute.name);
9623
- if (attributeName && ALL_EVENT_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
9624
- hasInteractiveHandler = true;
9625
- break;
9626
- }
9627
- }
9628
- if (!hasInteractiveHandler) return;
9610
+ if (!ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)))) return;
9629
9611
  const elementType = getElementType(node, context.settings);
9630
9612
  if (!HTML_TAGS.has(elementType)) return;
9631
9613
  if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
@@ -10268,6 +10250,8 @@ const jsCachePropertyAccess = defineRule({
10268
10250
  walkAst(loopBody, (child) => {
10269
10251
  if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
10270
10252
  if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
10253
+ });
10254
+ walkAst(loopBody, (child) => {
10271
10255
  if (!isNodeOfType(child, "MemberExpression")) return;
10272
10256
  if (child.computed) return;
10273
10257
  if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
@@ -10818,11 +10802,7 @@ const isSingleFieldEqualityPredicate = (node) => {
10818
10802
  if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
10819
10803
  return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
10820
10804
  };
10821
- const loopBoundNamesCache = /* @__PURE__ */ new WeakMap();
10822
- const getLoopBoundNames = (loop) => {
10823
- const cached = loopBoundNamesCache.get(loop);
10824
- if (cached) return cached;
10825
- const names = /* @__PURE__ */ new Set();
10805
+ const collectLoopBoundNames = (loop, names) => {
10826
10806
  if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
10827
10807
  if (isNodeOfType(child, "Identifier")) names.add(child.name);
10828
10808
  });
@@ -10839,8 +10819,6 @@ const getLoopBoundNames = (loop) => {
10839
10819
  if (targetRoot) names.add(targetRoot);
10840
10820
  }
10841
10821
  });
10842
- loopBoundNamesCache.set(loop, names);
10843
- return names;
10844
10822
  };
10845
10823
  const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
10846
10824
  let cursor = receiver;
@@ -10866,7 +10844,7 @@ const isLoopVariantReceiver = (node) => {
10866
10844
  const loopBoundNames = /* @__PURE__ */ new Set();
10867
10845
  let ancestor = node.parent;
10868
10846
  while (ancestor) {
10869
- if (LOOP_TYPES.includes(ancestor.type)) for (const name of getLoopBoundNames(ancestor)) loopBoundNames.add(name);
10847
+ if (LOOP_TYPES.includes(ancestor.type)) collectLoopBoundNames(ancestor, loopBoundNames);
10870
10848
  ancestor = ancestor.parent;
10871
10849
  }
10872
10850
  if (loopBoundNames.has(receiverRoot)) return true;
@@ -14474,14 +14452,14 @@ const keyLifecycleRisk = defineRule({
14474
14452
  //#region src/plugin/rules/a11y/label-has-associated-control.ts
14475
14453
  const MESSAGE_NO_LABEL = "Blind users can't identify this field because screen readers find no label text, so add visible text, `aria-label`, or `aria-labelledby`.";
14476
14454
  const MESSAGE_NO_CONTROL = "Screen reader users can't tell which input this label names because it's tied to none, so add `htmlFor` or wrap the input inside it.";
14477
- const DEFAULT_CONTROL_COMPONENTS = new Set([
14455
+ const DEFAULT_CONTROL_COMPONENTS = [
14478
14456
  "input",
14479
14457
  "meter",
14480
14458
  "output",
14481
14459
  "progress",
14482
14460
  "select",
14483
14461
  "textarea"
14484
- ]);
14462
+ ];
14485
14463
  const DEFAULT_LABEL_ATTRIBUTES = [
14486
14464
  "alt",
14487
14465
  "aria-label",
@@ -14493,8 +14471,8 @@ const resolveSettings$24 = (settings) => {
14493
14471
  const jsxA11y = settings?.["jsx-a11y"];
14494
14472
  const forAttributes = (typeof jsxA11y === "object" && jsxA11y !== null ? jsxA11y : {}).attributes?.for ?? ["htmlFor"];
14495
14473
  return {
14496
- labelComponents: new Set(["label", ...ruleSettings.labelComponents ?? []]),
14497
- labelAttributes: new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []]),
14474
+ labelComponents: ["label", ...ruleSettings.labelComponents ?? []].sort(),
14475
+ labelAttributes: [...new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []])].sort(),
14498
14476
  controlComponents: ruleSettings.controlComponents ?? [],
14499
14477
  assert: ruleSettings.assert ?? "either",
14500
14478
  depth: Math.min(ruleSettings.depth ?? 5, 25),
@@ -14502,7 +14480,7 @@ const resolveSettings$24 = (settings) => {
14502
14480
  };
14503
14481
  };
14504
14482
  const isControlComponent = (tagName, controlComponents) => {
14505
- if (DEFAULT_CONTROL_COMPONENTS.has(tagName)) return true;
14483
+ if (DEFAULT_CONTROL_COMPONENTS.includes(tagName)) return true;
14506
14484
  return controlComponents.some((pattern) => compileGlob(pattern).test(tagName));
14507
14485
  };
14508
14486
  const searchForNestedControl = (child, currentDepth, searchContext) => {
@@ -14528,7 +14506,7 @@ const searchForAccessibleLabel = (child, currentDepth, searchContext) => {
14528
14506
  const attributeName = attribute.name;
14529
14507
  if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
14530
14508
  const propName = getJsxAttributeName(attributeName);
14531
- if (!propName || !searchContext.labelAttributes.has(propName)) continue;
14509
+ if (!propName || !searchContext.labelAttributes.includes(propName)) continue;
14532
14510
  const attributeValue = attribute.value;
14533
14511
  if (!attributeValue) continue;
14534
14512
  if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
@@ -14550,7 +14528,7 @@ const hasAccessibleLabel = (element, searchContext) => {
14550
14528
  const attributeName = attribute.name;
14551
14529
  if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
14552
14530
  const propName = getJsxAttributeName(attributeName);
14553
- if (propName && searchContext.labelAttributes.has(propName)) return true;
14531
+ if (propName && searchContext.labelAttributes.includes(propName)) return true;
14554
14532
  }
14555
14533
  for (const child of element.children) if (searchForAccessibleLabel(child, 1, searchContext)) return true;
14556
14534
  return false;
@@ -14573,7 +14551,7 @@ const labelHasAssociatedControl = defineRule({
14573
14551
  if (isTestlikeFile) return;
14574
14552
  const opening = node.openingElement;
14575
14553
  const tagName = getElementType(opening, context.settings);
14576
- if (!settings.labelComponents.has(tagName)) return;
14554
+ if (!settings.labelComponents.includes(tagName)) return;
14577
14555
  const hasHtmlFor = settings.forAttributes.some((attributeName) => Boolean(hasJsxPropIgnoreCase(opening.attributes, attributeName)));
14578
14556
  const searchContext = {
14579
14557
  depth: settings.depth,
@@ -15162,9 +15140,9 @@ const resolveSettings$23 = (settings) => {
15162
15140
  const reactDoctor = settings?.["react-doctor"];
15163
15141
  const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.mediaHasCaption ?? {} : {};
15164
15142
  return {
15165
- audio: new Set([...DEFAULT_AUDIO, ...ruleSettings.audio ?? []]),
15166
- video: new Set([...DEFAULT_VIDEO, ...ruleSettings.video ?? []]),
15167
- track: new Set([...DEFAULT_TRACK, ...ruleSettings.track ?? []])
15143
+ audio: [...DEFAULT_AUDIO, ...ruleSettings.audio ?? []],
15144
+ video: [...DEFAULT_VIDEO, ...ruleSettings.video ?? []],
15145
+ track: [...DEFAULT_TRACK, ...ruleSettings.track ?? []]
15168
15146
  };
15169
15147
  };
15170
15148
  const evaluateMuted = (attribute) => {
@@ -15193,7 +15171,7 @@ const childMayRenderTrack = (child, trackTags, settings) => {
15193
15171
  let rendersCaptionTrack = false;
15194
15172
  walkAst(expression, (inner) => {
15195
15173
  if (rendersCaptionTrack) return false;
15196
- if (isNodeOfType(inner, "JSXElement") && trackTags.has(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
15174
+ if (isNodeOfType(inner, "JSXElement") && trackTags.includes(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
15197
15175
  rendersCaptionTrack = true;
15198
15176
  return false;
15199
15177
  }
@@ -15211,7 +15189,7 @@ const mediaHasCaption = defineRule({
15211
15189
  const settings = resolveSettings$23(context.settings);
15212
15190
  return { JSXOpeningElement(node) {
15213
15191
  const tag = getElementType(node, context.settings);
15214
- if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
15192
+ if (!(settings.audio.includes(tag) || settings.video.includes(tag))) return;
15215
15193
  if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
15216
15194
  const parent = node.parent;
15217
15195
  if (!parent || !isNodeOfType(parent, "JSXElement")) {
@@ -15226,7 +15204,7 @@ const mediaHasCaption = defineRule({
15226
15204
  if (!isNodeOfType(child, "JSXElement")) return false;
15227
15205
  const opening = child.openingElement;
15228
15206
  const childTag = getElementType(opening, context.settings);
15229
- if (!settings.track.has(childTag)) return false;
15207
+ if (!settings.track.includes(childTag)) return false;
15230
15208
  const kindAttribute = hasJsxPropIgnoreCase(opening.attributes, "kind");
15231
15209
  if (!kindAttribute) return false;
15232
15210
  let kindValue = kindAttribute.value;
@@ -15537,30 +15515,16 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
15537
15515
  const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
15538
15516
  const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
15539
15517
  const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
15540
- const collectSourceTextExportNames = (sourceText) => {
15518
+ const doesSourceTextExportName = (sourceText, exportedName) => {
15541
15519
  const strippedSource = stripJsComments(sourceText);
15542
- const exportedNames = /* @__PURE__ */ new Set();
15543
- if (DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) exportedNames.add("default");
15544
- for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1]) exportedNames.add(match[1]);
15545
- for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) {
15546
- const specifiersText = match[1] ?? "";
15547
- for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
15548
- }
15549
- return exportedNames;
15520
+ if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
15521
+ for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
15522
+ for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
15523
+ return false;
15550
15524
  };
15551
- const exportNamesCache = /* @__PURE__ */ new Map();
15552
15525
  const doesModuleExportName = (filePath, exportedName) => {
15553
15526
  try {
15554
- const fileStat = fs.statSync(filePath);
15555
- const cached = exportNamesCache.get(filePath);
15556
- if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
15557
- const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
15558
- exportNamesCache.set(filePath, {
15559
- mtimeMs: fileStat.mtimeMs,
15560
- size: fileStat.size,
15561
- exportedNames
15562
- });
15563
- return exportedNames.has(exportedName);
15527
+ return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
15564
15528
  } catch {
15565
15529
  return false;
15566
15530
  }
@@ -17443,15 +17407,8 @@ const getProgramAnalysis = (anyNode) => {
17443
17407
  programToAnalysis.set(programNode, analysis);
17444
17408
  return analysis;
17445
17409
  };
17446
- const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
17447
17410
  const getScopeForNode = (node, manager) => {
17448
17411
  if (!node.range) return null;
17449
- let scopeByNode = scopeByNodeCache.get(manager);
17450
- if (!scopeByNode) {
17451
- scopeByNode = /* @__PURE__ */ new WeakMap();
17452
- scopeByNodeCache.set(manager, scopeByNode);
17453
- }
17454
- if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
17455
17412
  let bestScope = null;
17456
17413
  let bestSize = Infinity;
17457
17414
  for (const scope of manager.scopes) {
@@ -17464,7 +17421,6 @@ const getScopeForNode = (node, manager) => {
17464
17421
  bestScope = scope;
17465
17422
  }
17466
17423
  }
17467
- scopeByNode.set(node, bestScope);
17468
17424
  return bestScope;
17469
17425
  };
17470
17426
  //#endregion
@@ -17499,20 +17455,11 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
17499
17455
  } else if (isAstNode(child)) descend(child, visit, visited);
17500
17456
  }
17501
17457
  };
17502
- const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
17503
17458
  const getUpstreamRefs = (analysis, ref) => {
17504
- let upstreamByRef = upstreamRefsCache.get(analysis);
17505
- if (!upstreamByRef) {
17506
- upstreamByRef = /* @__PURE__ */ new WeakMap();
17507
- upstreamRefsCache.set(analysis, upstreamByRef);
17508
- }
17509
- const cached = upstreamByRef.get(ref);
17510
- if (cached) return cached;
17511
17459
  const refs = [];
17512
17460
  ascend(analysis, ref, (upRef) => {
17513
17461
  refs.push(upRef);
17514
17462
  });
17515
- upstreamByRef.set(ref, refs);
17516
17463
  return refs;
17517
17464
  };
17518
17465
  const findDownstreamNodes = (topNode, type) => {
@@ -17522,24 +17469,11 @@ const findDownstreamNodes = (topNode, type) => {
17522
17469
  });
17523
17470
  return nodes;
17524
17471
  };
17525
- const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
17526
17472
  const getRef = (analysis, identifier) => {
17527
- let refByIdentifier = refByIdentifierCache.get(analysis);
17528
- if (!refByIdentifier) {
17529
- refByIdentifier = /* @__PURE__ */ new WeakMap();
17530
- refByIdentifierCache.set(analysis, refByIdentifier);
17531
- }
17532
- if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
17533
- let resolvedReference = null;
17534
17473
  const scope = getScopeForNode(identifier, analysis.scopeManager);
17535
- if (scope) {
17536
- for (const reference of scope.references) if (reference.identifier === identifier) {
17537
- resolvedReference = reference;
17538
- break;
17539
- }
17540
- }
17541
- refByIdentifier.set(identifier, resolvedReference);
17542
- return resolvedReference;
17474
+ if (!scope) return null;
17475
+ for (const reference of scope.references) if (reference.identifier === identifier) return reference;
17476
+ return null;
17543
17477
  };
17544
17478
  const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
17545
17479
  const getDownstreamRefs = (analysis, node) => {
@@ -17614,6 +17548,22 @@ const isEventualCallTo = (analysis, ref, predicate) => {
17614
17548
  };
17615
17549
  //#endregion
17616
17550
  //#region src/plugin/rules/state-and-effects/utils/effect/react.ts
17551
+ const getOuterScopeContaining = (analysis, node) => {
17552
+ if (!node.range) return null;
17553
+ let best = null;
17554
+ let bestSize = Infinity;
17555
+ for (const scope of analysis.scopeManager.scopes) {
17556
+ const block = scope.block;
17557
+ if (!block?.range) continue;
17558
+ if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
17559
+ const size = block.range[1] - block.range[0];
17560
+ if (size <= bestSize) {
17561
+ bestSize = size;
17562
+ best = scope;
17563
+ }
17564
+ }
17565
+ return best;
17566
+ };
17617
17567
  const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
17618
17568
  const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
17619
17569
  const isReactFunctionalComponent = (node) => {
@@ -17644,7 +17594,7 @@ const isReactFunctionalHOC = (analysis, node) => {
17644
17594
  const isWrappedSeparately = () => {
17645
17595
  if (!isNodeOfType(node.id, "Identifier")) return false;
17646
17596
  const bindingName = node.id.name;
17647
- const containingScope = getScopeForNode(node, analysis.scopeManager);
17597
+ const containingScope = getOuterScopeContaining(analysis, node);
17648
17598
  if (!containingScope) return false;
17649
17599
  const variable = containingScope.variables.find((v) => v.name === bindingName);
17650
17600
  if (!variable) return false;
@@ -19923,44 +19873,38 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
19923
19873
  };
19924
19874
  const parseColorToRgb = (value) => {
19925
19875
  const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
19926
- if (trimmed.startsWith("#")) {
19927
- const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
19928
- if (hex8Match) return {
19929
- red: parseInt(hex8Match[1], 16),
19930
- green: parseInt(hex8Match[2], 16),
19931
- blue: parseInt(hex8Match[3], 16)
19932
- };
19933
- const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
19934
- if (hex6Match) return {
19935
- red: parseInt(hex6Match[1], 16),
19936
- green: parseInt(hex6Match[2], 16),
19937
- blue: parseInt(hex6Match[3], 16)
19938
- };
19939
- const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
19940
- if (hex4Match) return {
19941
- red: parseInt(hex4Match[1] + hex4Match[1], 16),
19942
- green: parseInt(hex4Match[2] + hex4Match[2], 16),
19943
- blue: parseInt(hex4Match[3] + hex4Match[3], 16)
19944
- };
19945
- const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
19946
- if (hex3Match) return {
19947
- red: parseInt(hex3Match[1] + hex3Match[1], 16),
19948
- green: parseInt(hex3Match[2] + hex3Match[2], 16),
19949
- blue: parseInt(hex3Match[3] + hex3Match[3], 16)
19950
- };
19951
- }
19952
- if (trimmed.includes("rgb")) {
19953
- const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
19954
- if (rgbMatch) return {
19955
- red: parseInt(rgbMatch[1], 10),
19956
- green: parseInt(rgbMatch[2], 10),
19957
- blue: parseInt(rgbMatch[3], 10)
19958
- };
19959
- }
19960
- if (trimmed.includes("hsl")) {
19961
- const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
19962
- if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
19963
- }
19876
+ const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
19877
+ if (hex8Match) return {
19878
+ red: parseInt(hex8Match[1], 16),
19879
+ green: parseInt(hex8Match[2], 16),
19880
+ blue: parseInt(hex8Match[3], 16)
19881
+ };
19882
+ const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
19883
+ if (hex6Match) return {
19884
+ red: parseInt(hex6Match[1], 16),
19885
+ green: parseInt(hex6Match[2], 16),
19886
+ blue: parseInt(hex6Match[3], 16)
19887
+ };
19888
+ const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
19889
+ if (hex4Match) return {
19890
+ red: parseInt(hex4Match[1] + hex4Match[1], 16),
19891
+ green: parseInt(hex4Match[2] + hex4Match[2], 16),
19892
+ blue: parseInt(hex4Match[3] + hex4Match[3], 16)
19893
+ };
19894
+ const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
19895
+ if (hex3Match) return {
19896
+ red: parseInt(hex3Match[1] + hex3Match[1], 16),
19897
+ green: parseInt(hex3Match[2] + hex3Match[2], 16),
19898
+ blue: parseInt(hex3Match[3] + hex3Match[3], 16)
19899
+ };
19900
+ const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
19901
+ if (rgbMatch) return {
19902
+ red: parseInt(rgbMatch[1], 10),
19903
+ green: parseInt(rgbMatch[2], 10),
19904
+ blue: parseInt(rgbMatch[3], 10)
19905
+ };
19906
+ const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
19907
+ if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
19964
19908
  return null;
19965
19909
  };
19966
19910
  //#endregion
@@ -19988,18 +19932,9 @@ const extractColorFromShadowLayer = (layer) => {
19988
19932
  if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
19989
19933
  return null;
19990
19934
  };
19991
- const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
19992
- const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
19993
- const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
19994
- const SHADOW_BLUR_TOKEN_INDEX = 2;
19995
19935
  const parseShadowLayerBlur = (layer) => {
19996
- const withoutColors = layer.replace(RGB_FUNCTION_PATTERN, "").replace(HEX_COLOR_PATTERN, "");
19997
- let tokenIndex = 0;
19998
- for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
19999
- if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
20000
- tokenIndex += 1;
20001
- }
20002
- return 0;
19936
+ const numericTokens = [...layer.replace(/rgba?\([^)]*\)/g, "").replace(/#[0-9a-f]{3,8}\b/gi, "").matchAll(/(\d+(?:\.\d+)?)(px)?/g)].map((match) => parseFloat(match[1]));
19937
+ return numericTokens.length >= 3 ? numericTokens[2] : 0;
20003
19938
  };
20004
19939
  const hasColoredGlowShadow = (shadowValue) => {
20005
19940
  for (const layer of splitShadowLayers(shadowValue)) {
@@ -20119,10 +20054,7 @@ const isIntrinsicJsxAttribute = (node) => {
20119
20054
  };
20120
20055
  //#endregion
20121
20056
  //#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
20122
- const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
20123
20057
  const collectComponentPropNames = (componentFunction) => {
20124
- const cached = componentPropNamesCache.get(componentFunction);
20125
- if (cached) return cached;
20126
20058
  const propNames = /* @__PURE__ */ new Set();
20127
20059
  if (!isFunctionLike$1(componentFunction)) return propNames;
20128
20060
  const propsObjectParamNames = /* @__PURE__ */ new Set();
@@ -20136,23 +20068,27 @@ const collectComponentPropNames = (componentFunction) => {
20136
20068
  if (child !== componentBody && isFunctionLike$1(child)) return false;
20137
20069
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
20138
20070
  });
20139
- componentPropNamesCache.set(componentFunction, propNames);
20140
20071
  return propNames;
20141
20072
  };
20142
- const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
20143
- const getOwnScopeBoundNames = (functionNode) => {
20144
- const cached = ownScopeBoundNamesCache.get(functionNode);
20145
- if (cached) return cached;
20073
+ const declaresBindingNamed = (functionNode, bindingName) => {
20146
20074
  const boundNames = /* @__PURE__ */ new Set();
20147
20075
  if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
20076
+ if (boundNames.has(bindingName)) return true;
20077
+ let declaresName = false;
20148
20078
  walkAst(functionNode, (child) => {
20079
+ if (declaresName) return false;
20149
20080
  if (child !== functionNode && isFunctionLike$1(child)) return false;
20150
- if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
20081
+ if (isNodeOfType(child, "VariableDeclarator")) {
20082
+ const declaratorNames = /* @__PURE__ */ new Set();
20083
+ collectPatternNames(child.id, declaratorNames);
20084
+ if (declaratorNames.has(bindingName)) {
20085
+ declaresName = true;
20086
+ return false;
20087
+ }
20088
+ }
20151
20089
  });
20152
- ownScopeBoundNamesCache.set(functionNode, boundNames);
20153
- return boundNames;
20090
+ return declaresName;
20154
20091
  };
20155
- const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
20156
20092
  const isPropertyNamePosition = (identifier) => {
20157
20093
  const parent = identifier.parent;
20158
20094
  if (!parent) return false;
@@ -20575,28 +20511,36 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
20575
20511
  }
20576
20512
  return hasNestedFunction;
20577
20513
  };
20578
- const isInRenderScope = (node, componentFunction) => {
20579
- let cursor = node.parent ?? null;
20580
- while (cursor && cursor !== componentFunction) {
20581
- if (isFunctionLike$1(cursor)) return false;
20582
- cursor = cursor.parent ?? null;
20583
- }
20584
- return true;
20514
+ const isReseededDraftBuffer = (useStateCall, isPropName) => {
20515
+ const setterName = getStateSetterName(useStateCall);
20516
+ if (!setterName) return false;
20517
+ const componentFunction = findEnclosingFunction(useStateCall);
20518
+ if (!componentFunction) return false;
20519
+ let isReseeded = false;
20520
+ walkAst(componentFunction, (child) => {
20521
+ if (isReseeded) return false;
20522
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
20523
+ isReseeded = true;
20524
+ return false;
20525
+ }
20526
+ });
20527
+ return isReseeded;
20585
20528
  };
20586
- const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
20529
+ const isAdjustedDuringRender = (useStateCall, isPropName) => {
20587
20530
  const setterName = getStateSetterName(useStateCall);
20588
20531
  if (!setterName) return false;
20589
20532
  const componentFunction = findEnclosingFunction(useStateCall);
20590
20533
  if (!componentFunction) return false;
20591
- let isExempt = false;
20534
+ let isAdjusted = false;
20592
20535
  walkAst(componentFunction, (child) => {
20593
- if (isExempt) return false;
20594
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && (isHandlerShapedReseed(child, componentFunction) || isInRenderScope(child, componentFunction))) {
20595
- isExempt = true;
20536
+ if (isAdjusted) return false;
20537
+ if (child !== componentFunction && isFunctionLike$1(child)) return false;
20538
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName)) {
20539
+ isAdjusted = true;
20596
20540
  return false;
20597
20541
  }
20598
20542
  });
20599
- return isExempt;
20543
+ return isAdjusted;
20600
20544
  };
20601
20545
  const noDerivedUseState = defineRule({
20602
20546
  id: "no-derived-useState",
@@ -20613,7 +20557,8 @@ const noDerivedUseState = defineRule({
20613
20557
  const initializer = node.arguments[0];
20614
20558
  if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
20615
20559
  if (isInitialOnlyPropName(initializer.name)) return;
20616
- if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20560
+ if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20561
+ if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20617
20562
  context.report({
20618
20563
  node,
20619
20564
  message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
@@ -20624,7 +20569,8 @@ const noDerivedUseState = defineRule({
20624
20569
  const rootIdentifierName = getRootIdentifierName(initializer);
20625
20570
  if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
20626
20571
  if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
20627
- if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20572
+ if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20573
+ if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20628
20574
  context.report({
20629
20575
  node,
20630
20576
  message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
@@ -22323,20 +22269,20 @@ const getTriggerGuardRootName = (testNode) => {
22323
22269
  return null;
22324
22270
  };
22325
22271
  const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
22326
- const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
22327
- const settersWithAnyCall = /* @__PURE__ */ new Set();
22328
- const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
22329
- walkAst(componentBody, (child) => {
22330
- if (!isNodeOfType(child, "CallExpression")) return;
22331
- if (!isNodeOfType(child.callee, "Identifier")) return;
22332
- const setterName = child.callee.name;
22333
- if (!setterNames.has(setterName)) return;
22334
- settersWithAnyCall.add(setterName);
22335
- if (settersWithNonHandlerCall.has(setterName)) return;
22336
- if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
22337
- });
22338
22272
  const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
22339
- for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
22273
+ for (const binding of useStateBindings) {
22274
+ let didFindAnySetterCall = false;
22275
+ let areAllSetterCallsInHandlers = true;
22276
+ walkAst(componentBody, (child) => {
22277
+ if (!areAllSetterCallsInHandlers) return false;
22278
+ if (!isNodeOfType(child, "CallExpression")) return;
22279
+ if (!isNodeOfType(child.callee, "Identifier")) return;
22280
+ if (child.callee.name !== binding.setterName) return;
22281
+ didFindAnySetterCall = true;
22282
+ if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
22283
+ });
22284
+ if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
22285
+ }
22340
22286
  return handlerOnlyWriteStateNames;
22341
22287
  };
22342
22288
  const noEventTriggerState = defineRule({
@@ -22676,10 +22622,6 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
22676
22622
  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)-)/;
22677
22623
  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)-)/;
22678
22624
  const splitVariantScope = (token) => {
22679
- if (!token.includes(":")) return {
22680
- scope: "",
22681
- utility: token.startsWith("!") ? token.slice(1) : token
22682
- };
22683
22625
  const segments = token.split(":");
22684
22626
  const rawUtility = segments[segments.length - 1];
22685
22627
  return {
@@ -22703,7 +22645,6 @@ const noGrayOnColoredBackground = defineRule({
22703
22645
  const bgColorScopes = /* @__PURE__ */ new Set();
22704
22646
  for (const token of classStr.split(/\s+/)) {
22705
22647
  if (!token) continue;
22706
- if (!token.includes("text-") && !token.includes("bg-")) continue;
22707
22648
  const { scope, utility } = splitVariantScope(token);
22708
22649
  if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
22709
22650
  if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
@@ -23433,8 +23374,6 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
23433
23374
  return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
23434
23375
  });
23435
23376
  const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
23436
- const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
23437
- const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
23438
23377
  const noLongTransitionDuration = defineRule({
23439
23378
  id: "no-long-transition-duration",
23440
23379
  title: "Transition duration too long",
@@ -23458,10 +23397,11 @@ const noLongTransitionDuration = defineRule({
23458
23397
  if (key === "transitionDuration" || key === "animationDuration") {
23459
23398
  let longestDurationPropertyMs = 0;
23460
23399
  for (const segment of value.split(",")) {
23461
- const durationMatch = segment.trim().match(DURATION_SEGMENT_PATTERN);
23462
- if (!durationMatch) continue;
23463
- const segmentDurationMs = durationMatch[2] === "ms" ? parseFloat(durationMatch[1]) : parseFloat(durationMatch[1]) * 1e3;
23464
- longestDurationPropertyMs = Math.max(longestDurationPropertyMs, segmentDurationMs);
23400
+ const trimmedSegment = segment.trim();
23401
+ const msMatch = trimmedSegment.match(/^([\d.]+)ms$/);
23402
+ const secondsMatch = trimmedSegment.match(/^([\d.]+)s$/);
23403
+ if (msMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(msMatch[1]));
23404
+ else if (secondsMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(secondsMatch[1]) * 1e3);
23465
23405
  }
23466
23406
  if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
23467
23407
  }
@@ -23469,7 +23409,7 @@ const noLongTransitionDuration = defineRule({
23469
23409
  let longestDurationMs = 0;
23470
23410
  for (const segment of value.split(",")) {
23471
23411
  if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
23472
- const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
23412
+ const firstTimeMatch = segment.match(/(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/);
23473
23413
  if (!firstTimeMatch) continue;
23474
23414
  const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
23475
23415
  longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
@@ -24641,14 +24581,14 @@ const collectRoleBranches = (expression, out) => {
24641
24581
  out.hasOpaqueBranch = true;
24642
24582
  };
24643
24583
  const buildMessage$12 = (tag) => `Keyboard & screen reader users can't trigger this \`<${tag}>\` because it isn't interactive, so use a button or link or add an interactive role.`;
24644
- const INTERACTIVE_HANDLERS_LOWER = new Set([
24584
+ const INTERACTIVE_HANDLERS = [
24645
24585
  "onClick",
24646
24586
  "onMouseDown",
24647
24587
  "onMouseUp",
24648
24588
  "onKeyDown",
24649
24589
  "onKeyPress",
24650
24590
  "onKeyUp"
24651
- ].map((handlerName) => handlerName.toLowerCase()));
24591
+ ];
24652
24592
  const noNoninteractiveElementInteractions = defineRule({
24653
24593
  id: "no-noninteractive-element-interactions",
24654
24594
  title: "Handler on non-interactive element",
@@ -24663,16 +24603,7 @@ const noNoninteractiveElementInteractions = defineRule({
24663
24603
  const tag = getElementType(node, context.settings);
24664
24604
  if (!NON_INTERACTIVE_ELEMENTS.has(tag)) return;
24665
24605
  if (tag === "label") return;
24666
- let hasHandler = false;
24667
- for (const attribute of node.attributes) {
24668
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
24669
- const attributeName = getJsxAttributeName(attribute.name);
24670
- if (attributeName && INTERACTIVE_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
24671
- hasHandler = true;
24672
- break;
24673
- }
24674
- }
24675
- if (!hasHandler) return;
24606
+ if (!INTERACTIVE_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
24676
24607
  if (isHiddenFromScreenReader(node, context.settings)) return;
24677
24608
  const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
24678
24609
  if (roleAttr) {
@@ -25985,22 +25916,16 @@ const ELEMENT_ROLE_PAIRS = [
25985
25916
  ["tr", "row"],
25986
25917
  ["ul", "list"]
25987
25918
  ];
25988
- const EMPTY_ROLE_LIST = [];
25989
- const buildLookup = (pairs, keyIndex) => {
25990
- const lookup = /* @__PURE__ */ new Map();
25991
- for (const pair of pairs) {
25992
- const key = pair[keyIndex];
25993
- const value = pair[keyIndex === 0 ? 1 : 0];
25994
- const values = lookup.get(key);
25995
- if (!values) lookup.set(key, [value]);
25996
- else if (!values.includes(value)) values.push(value);
25997
- }
25998
- return lookup;
25919
+ const getElementImplicitRoles = (tag) => {
25920
+ const out = [];
25921
+ for (const [element, role] of ELEMENT_ROLE_PAIRS) if (element === tag && !out.includes(role)) out.push(role);
25922
+ return out;
25923
+ };
25924
+ const getTagsForRole = (role) => {
25925
+ const out = [];
25926
+ for (const [element, r] of ELEMENT_ROLE_PAIRS) if (r === role && !out.includes(element)) out.push(element);
25927
+ return out;
25999
25928
  };
26000
- const IMPLICIT_ROLES_BY_TAG = buildLookup(ELEMENT_ROLE_PAIRS, 0);
26001
- const TAGS_BY_ROLE = buildLookup(ELEMENT_ROLE_PAIRS, 1);
26002
- const getElementImplicitRoles = (tag) => IMPLICIT_ROLES_BY_TAG.get(tag) ?? EMPTY_ROLE_LIST;
26003
- const getTagsForRole = (role) => TAGS_BY_ROLE.get(role) ?? EMPTY_ROLE_LIST;
26004
25929
  //#endregion
26005
25930
  //#region src/plugin/utils/get-implicit-role.ts
26006
25931
  const getImplicitRole = (node, elementType) => {
@@ -26500,7 +26425,6 @@ const TANSTACK_ROUTE_PROPERTY_ORDER = [
26500
26425
  "headers",
26501
26426
  "remountDeps"
26502
26427
  ];
26503
- const TANSTACK_ROUTE_PROPERTY_INDEX = new Map(TANSTACK_ROUTE_PROPERTY_ORDER.map((propertyName, orderIndex) => [propertyName, orderIndex]));
26504
26428
  const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
26505
26429
  "createFileRoute",
26506
26430
  "createRoute",
@@ -26516,7 +26440,6 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
26516
26440
  "server",
26517
26441
  "handler"
26518
26442
  ];
26519
- const TANSTACK_MIDDLEWARE_METHOD_INDEX = new Map(TANSTACK_MIDDLEWARE_METHOD_ORDER.map((methodName, orderIndex) => [methodName, orderIndex]));
26520
26443
  const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
26521
26444
  const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
26522
26445
  const TANSTACK_QUERY_HOOKS = new Set([
@@ -27221,21 +27144,14 @@ const noStaticElementInteractions = defineRule({
27221
27144
  category: "Accessibility",
27222
27145
  create: (context) => {
27223
27146
  const settings = resolveSettings$12(context.settings);
27224
- const handlersLower = new Set(settings.handlers.map((handlerName) => handlerName.toLowerCase()));
27225
27147
  const isTestlikeFile = isTestlikeFilename(context.filename);
27226
27148
  return { JSXOpeningElement(node) {
27227
27149
  if (isTestlikeFile) return;
27228
27150
  let hasNonBlockerHandler = false;
27229
27151
  let hasAnyHandler = false;
27230
- let seenHandlerNames = null;
27231
- for (const attribute of node.attributes) {
27232
- if (!isNodeOfType(attribute, "JSXAttribute")) continue;
27233
- const attributeName = getJsxAttributeName(attribute.name);
27234
- if (!attributeName) continue;
27235
- const handlerNameLower = attributeName.toLowerCase();
27236
- if (!handlersLower.has(handlerNameLower)) continue;
27237
- if (seenHandlerNames?.has(handlerNameLower)) continue;
27238
- (seenHandlerNames ??= /* @__PURE__ */ new Set()).add(handlerNameLower);
27152
+ for (const handler of settings.handlers) {
27153
+ const attribute = hasJsxPropIgnoreCase(node.attributes, handler);
27154
+ if (!attribute) continue;
27239
27155
  if (isNullValue(attribute)) continue;
27240
27156
  hasAnyHandler = true;
27241
27157
  if (!isPureEventBlockerHandler(attribute)) {
@@ -28416,13 +28332,13 @@ const DOM_ATTRIBUTES_TO_CAMEL = new Map([
28416
28332
  ["xml:lang", "xmlLang"],
28417
28333
  ["xml:space", "xmlSpace"]
28418
28334
  ]);
28419
- const DOM_PROPERTIES_IGNORE_CASE_BY_LOWER = new Map([
28335
+ const DOM_PROPERTIES_IGNORE_CASE = [
28420
28336
  "charset",
28421
28337
  "allowFullScreen",
28422
28338
  "webkitAllowFullScreen",
28423
28339
  "mozAllowFullScreen",
28424
28340
  "webkitDirectory"
28425
- ].map((name) => [name.toLowerCase(), name]));
28341
+ ];
28426
28342
  //#endregion
28427
28343
  //#region src/plugin/constants/dom-property-tags.ts
28428
28344
  const DOM_PROPERTY_TO_ALLOWED_TAGS = new Map([
@@ -28626,7 +28542,10 @@ const matchesHtmlTagConventions = (tagName) => {
28626
28542
  if (!(firstCharacter >= 97 && firstCharacter <= 122)) return false;
28627
28543
  return !tagName.includes("-");
28628
28544
  };
28629
- const normalizeAttributeCase = (name) => DOM_PROPERTIES_IGNORE_CASE_BY_LOWER.get(name.toLowerCase()) ?? name;
28545
+ const normalizeAttributeCase = (name) => {
28546
+ for (const ignoreCaseName of DOM_PROPERTIES_IGNORE_CASE) if (ignoreCaseName.toLowerCase() === name.toLowerCase()) return ignoreCaseName;
28547
+ return name;
28548
+ };
28630
28549
  const hasUppercaseChar = (input) => /[A-Z]/.test(input);
28631
28550
  const INVALID_PROP_ON_TAG = (propName, allowedTags) => `React ignores \`${propName}\` here because it only works on these tags: ${allowedTags}.`;
28632
28551
  const DATA_LOWERCASE_REQUIRED = () => `React drops this \`data-*\` prop because of its capital letters.`;
@@ -32316,11 +32235,41 @@ const callsIdentifier = (root, identifierName) => {
32316
32235
  });
32317
32236
  return found;
32318
32237
  };
32238
+ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
32239
+ if (!componentBody) return false;
32240
+ let found = false;
32241
+ walkAst(componentBody, (child) => {
32242
+ if (found) return;
32243
+ if (!isFunctionLike$1(child)) return;
32244
+ const functionBody = child.body;
32245
+ if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
32246
+ if (callsIdentifier(functionBody, setterName)) found = true;
32247
+ });
32248
+ return found;
32249
+ };
32319
32250
  const PROMISE_CHAIN_METHOD_NAMES = new Set([
32320
32251
  "then",
32321
32252
  "catch",
32322
32253
  "finally"
32323
32254
  ]);
32255
+ const setterIsCalledInPromiseChain = (componentBody, setterName) => {
32256
+ if (!componentBody) return false;
32257
+ let found = false;
32258
+ walkAst(componentBody, (child) => {
32259
+ if (found) return;
32260
+ if (!isNodeOfType(child, "CallExpression")) return;
32261
+ const callee = child.callee;
32262
+ if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
32263
+ for (const argument of child.arguments ?? []) {
32264
+ if (!isFunctionLike$1(argument)) continue;
32265
+ if (callsIdentifier(argument.body, setterName)) {
32266
+ found = true;
32267
+ return;
32268
+ }
32269
+ }
32270
+ });
32271
+ return found;
32272
+ };
32324
32273
  const ASYNC_DATA_CALLEE_NAMES = new Set([
32325
32274
  "useApolloClient",
32326
32275
  "useMutation",
@@ -32333,36 +32282,20 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
32333
32282
  "fetch",
32334
32283
  "axios"
32335
32284
  ]);
32336
- const hasAsyncLoadingWork = (fnBody, setterName) => {
32285
+ const referencesAsyncDataApi = (body) => {
32286
+ if (!body) return false;
32337
32287
  let found = false;
32338
- walkAst(fnBody, (child) => {
32339
- if (found) return false;
32288
+ walkAst(body, (child) => {
32289
+ if (found) return;
32340
32290
  if (isNodeOfType(child, "CallExpression")) {
32341
32291
  const callee = child.callee;
32342
32292
  if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
32343
32293
  found = true;
32344
- return false;
32345
- }
32346
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
32347
- if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
32348
- found = true;
32349
- return false;
32350
- }
32351
- if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
32352
- if (!isFunctionLike$1(argument)) continue;
32353
- if (callsIdentifier(argument.body, setterName)) {
32354
- found = true;
32355
- return false;
32356
- }
32357
- }
32294
+ return;
32358
32295
  }
32359
- return;
32360
- }
32361
- if (setterName !== null && isFunctionLike$1(child)) {
32362
- const functionBody = child.body;
32363
- if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
32296
+ if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
32364
32297
  found = true;
32365
- return false;
32298
+ return;
32366
32299
  }
32367
32300
  }
32368
32301
  });
@@ -32387,7 +32320,7 @@ const renderingUsetransitionLoading = defineRule({
32387
32320
  const secondBinding = node.id.elements[1];
32388
32321
  const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
32389
32322
  const fnBody = enclosingFunctionBody(node);
32390
- if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
32323
+ if (fnBody && (setterName && setterIsCalledInAsyncContext(fnBody, setterName) || setterName && setterIsCalledInPromiseChain(fnBody, setterName) || referencesAsyncDataApi(fnBody))) return;
32391
32324
  context.report({
32392
32325
  node: node.init,
32393
32326
  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`
@@ -33181,17 +33114,17 @@ const rerenderStateOnlyInHandlers = defineRule({
33181
33114
  const effectTriggerNames = /* @__PURE__ */ new Set();
33182
33115
  for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
33183
33116
  for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
33184
- const setterNames = new Set(bindings.map((binding) => binding.setterName));
33185
- const calledSetterNames = /* @__PURE__ */ new Set();
33186
- walkAst(componentBody, (child) => {
33187
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
33188
- });
33189
33117
  for (const binding of bindings) {
33190
33118
  if (renderReachableNames.has(binding.valueName)) continue;
33191
33119
  if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
33192
33120
  const setterSuffix = binding.setterName.slice(3);
33193
33121
  if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
33194
- if (!calledSetterNames.has(binding.setterName)) continue;
33122
+ let setterCalled = false;
33123
+ walkAst(componentBody, (child) => {
33124
+ if (setterCalled) return;
33125
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
33126
+ });
33127
+ if (!setterCalled) continue;
33195
33128
  if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
33196
33129
  context.report({
33197
33130
  node: binding.declarator,
@@ -34823,10 +34756,9 @@ const resolveTextBoundaryName = (openingElement) => {
34823
34756
  if (isNodeOfType(openingElement.name, "JSXNamespacedName")) return openingElement.name.namespace.name;
34824
34757
  return resolveJsxElementName(openingElement);
34825
34758
  };
34826
- const TEXT_COMPONENT_KEYWORDS = [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS];
34827
34759
  const isTextHandlingComponent = (elementName) => {
34828
34760
  if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
34829
- return TEXT_COMPONENT_KEYWORDS.some((keyword) => elementName.includes(keyword));
34761
+ return [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS].some((keyword) => elementName.includes(keyword));
34830
34762
  };
34831
34763
  const isTransparentTextWrapper = (elementName) => elementName !== null && REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS.has(elementName);
34832
34764
  const isInsideTextHandlingComponent = (node) => {
@@ -40614,16 +40546,7 @@ const supabaseRlsPolicyRisk = defineRule({
40614
40546
  //#endregion
40615
40547
  //#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
40616
40548
  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;
40617
- const ENABLE_RLS_PATTERN = /alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?\s+(?:force\s+)?enable\s+row\s+level\s+security/gi;
40618
- const collectLastEnableRlsIndexByTable = (content) => {
40619
- const lastEnableIndexByTable = /* @__PURE__ */ new Map();
40620
- for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
40621
- const tableName = match[1];
40622
- if (tableName === void 0) continue;
40623
- lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
40624
- }
40625
- return lastEnableIndexByTable;
40626
- };
40549
+ 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");
40627
40550
  const supabaseTableMissingRls = defineRule({
40628
40551
  id: "supabase-table-missing-rls",
40629
40552
  title: "Supabase table created without Row Level Security",
@@ -40634,13 +40557,11 @@ const supabaseTableMissingRls = defineRule({
40634
40557
  const content = sanitizeSqlForScan(file.content);
40635
40558
  if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
40636
40559
  const findings = [];
40637
- const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
40638
40560
  CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
40639
40561
  for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
40640
40562
  const tableName = match[1];
40641
40563
  if (tableName === void 0) continue;
40642
- const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
40643
- if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
40564
+ if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
40644
40565
  const location = getLocationAtIndex(content, match.index);
40645
40566
  findings.push({
40646
40567
  message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
@@ -41330,10 +41251,10 @@ const tanstackStartRoutePropertyOrder = defineRule({
41330
41251
  const propertyName = getPropertyKeyName(property);
41331
41252
  if (propertyName !== null) orderedPropertyNames.push(propertyName);
41332
41253
  }
41333
- const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_INDEX.has(propertyName));
41254
+ const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_ORDER.includes(propertyName));
41334
41255
  let lastIndex = -1;
41335
41256
  for (const propertyName of sensitiveProperties) {
41336
- const currentIndex = TANSTACK_ROUTE_PROPERTY_INDEX.get(propertyName) ?? -1;
41257
+ const currentIndex = TANSTACK_ROUTE_PROPERTY_ORDER.indexOf(propertyName);
41337
41258
  if (currentIndex < lastIndex) {
41338
41259
  const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
41339
41260
  context.report({
@@ -41370,10 +41291,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
41370
41291
  } else return;
41371
41292
  const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
41372
41293
  if (methodNames[methodNames.length - 1] !== ownMethodName) return;
41373
- const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_INDEX.has(toMethodOrderToken(name)));
41294
+ const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_ORDER.includes(toMethodOrderToken(name)));
41374
41295
  let lastIndex = -1;
41375
41296
  for (const methodName of orderSensitiveMethods) {
41376
- const currentIndex = TANSTACK_MIDDLEWARE_METHOD_INDEX.get(toMethodOrderToken(methodName)) ?? -1;
41297
+ const currentIndex = TANSTACK_MIDDLEWARE_METHOD_ORDER.indexOf(toMethodOrderToken(methodName));
41377
41298
  if (currentIndex < lastIndex) {
41378
41299
  const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
41379
41300
  context.report({
@@ -41664,14 +41585,7 @@ const getStaticPropertyName = (member) => {
41664
41585
  if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
41665
41586
  return null;
41666
41587
  };
41667
- const importInfoCache = /* @__PURE__ */ new WeakMap();
41668
41588
  const getImportInfoForIdentifier = (identifier) => {
41669
- if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
41670
- const importInfo = computeImportInfoForIdentifier(identifier);
41671
- importInfoCache.set(identifier, importInfo);
41672
- return importInfo;
41673
- };
41674
- const computeImportInfoForIdentifier = (identifier) => {
41675
41589
  const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
41676
41590
  if (!specifier) return null;
41677
41591
  const declaration = specifier.parent;