oxlint-plugin-react-doctor 0.6.2-dev.d8628d7 → 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 +270 -323
  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
  };
@@ -5718,20 +5708,12 @@ const getTemplateInterpolations = (valueTail) => {
5718
5708
  return interpolations === null ? "" : interpolations.join(" ");
5719
5709
  };
5720
5710
  const isInertParseTarget = (target, fileContent) => {
5721
- const fileHasCreateElement = fileContent.includes("createElement");
5722
- const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
5723
- if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
5724
5711
  const escapedTarget = escapeRegExp(target);
5725
5712
  const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
5726
5713
  if (new RegExp(`\\b${escapedRoot}\\s*=\\s*[^\\n;]*(?:getElementById|querySelector|getElementsBy|\\.current\\b|document\\.(?:body|head|documentElement))`).test(fileContent)) return false;
5727
- if (fileHasCreateElement) {
5728
- if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
5729
- if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
5730
- }
5731
- if (fileHasIsolatedDocument) {
5732
- if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
5733
- }
5734
- 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;
5735
5717
  if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
5736
5718
  const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
5737
5719
  const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
@@ -5824,9 +5806,10 @@ const dangerousHtmlSink = defineRule({
5824
5806
  const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
5825
5807
  if (valueIdentifier !== void 0) {
5826
5808
  const escapedIdentifier = escapeRegExp(valueIdentifier);
5827
- if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5828
- if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
5829
- 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;
5830
5813
  }
5831
5814
  }
5832
5815
  const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
@@ -6217,10 +6200,7 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
6217
6200
  const actualMinor = Number(match[2]);
6218
6201
  return actualMajor > major || actualMajor === major && actualMinor >= minor;
6219
6202
  };
6220
- const containsJsxCache = /* @__PURE__ */ new WeakMap();
6221
6203
  const containsJsx$1 = (root) => {
6222
- const cached = containsJsxCache.get(root);
6223
- if (cached !== void 0) return cached;
6224
6204
  let found = false;
6225
6205
  const visit = (node) => {
6226
6206
  if (found) return;
@@ -6243,7 +6223,6 @@ const containsJsx$1 = (root) => {
6243
6223
  }
6244
6224
  };
6245
6225
  visit(root);
6246
- containsJsxCache.set(root, found);
6247
6226
  return found;
6248
6227
  };
6249
6228
  const getStaticMemberName = (node) => {
@@ -6335,6 +6314,27 @@ const hasDisplayNameMember = (classNode) => {
6335
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;
6336
6315
  return false;
6337
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
+ };
6338
6338
  const memberExpressionPath = (node) => {
6339
6339
  if (isNodeOfType(node, "Identifier")) return [node.name];
6340
6340
  if (!isNodeOfType(node, "MemberExpression")) return [];
@@ -6342,16 +6342,15 @@ const memberExpressionPath = (node) => {
6342
6342
  const propertyName = getStaticMemberName(node);
6343
6343
  return propertyName ? [...objectPath, propertyName] : objectPath;
6344
6344
  };
6345
- const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
6346
- const getDisplayNameAssignmentIndex = (programRoot) => {
6347
- const cached = displayNameAssignmentIndexCache.get(programRoot);
6348
- if (cached) return cached;
6349
- const identifierTargets = /* @__PURE__ */ new Set();
6350
- const memberPathSegments = /* @__PURE__ */ new Set();
6345
+ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
6346
+ let found = false;
6351
6347
  const visit = (node) => {
6348
+ if (found) return;
6352
6349
  if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
6353
- if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
6354
- 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
+ }
6355
6354
  }
6356
6355
  const record = node;
6357
6356
  for (const key of Object.keys(record)) {
@@ -6360,18 +6359,12 @@ const getDisplayNameAssignmentIndex = (programRoot) => {
6360
6359
  if (Array.isArray(child)) {
6361
6360
  for (const item of child) if (isAstNode(item)) visit(item);
6362
6361
  } else if (isAstNode(child)) visit(child);
6362
+ if (found) return;
6363
6363
  }
6364
6364
  };
6365
6365
  visit(programRoot);
6366
- const index = {
6367
- identifierTargets,
6368
- memberPathSegments
6369
- };
6370
- displayNameAssignmentIndexCache.set(programRoot, index);
6371
- return index;
6366
+ return found;
6372
6367
  };
6373
- const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
6374
- const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
6375
6368
  const displayName = defineRule({
6376
6369
  id: "display-name",
6377
6370
  title: "Component missing display name",
@@ -10257,6 +10250,8 @@ const jsCachePropertyAccess = defineRule({
10257
10250
  walkAst(loopBody, (child) => {
10258
10251
  if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
10259
10252
  if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
10253
+ });
10254
+ walkAst(loopBody, (child) => {
10260
10255
  if (!isNodeOfType(child, "MemberExpression")) return;
10261
10256
  if (child.computed) return;
10262
10257
  if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
@@ -10807,11 +10802,7 @@ const isSingleFieldEqualityPredicate = (node) => {
10807
10802
  if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
10808
10803
  return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
10809
10804
  };
10810
- const loopBoundNamesCache = /* @__PURE__ */ new WeakMap();
10811
- const getLoopBoundNames = (loop) => {
10812
- const cached = loopBoundNamesCache.get(loop);
10813
- if (cached) return cached;
10814
- const names = /* @__PURE__ */ new Set();
10805
+ const collectLoopBoundNames = (loop, names) => {
10815
10806
  if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
10816
10807
  if (isNodeOfType(child, "Identifier")) names.add(child.name);
10817
10808
  });
@@ -10828,8 +10819,6 @@ const getLoopBoundNames = (loop) => {
10828
10819
  if (targetRoot) names.add(targetRoot);
10829
10820
  }
10830
10821
  });
10831
- loopBoundNamesCache.set(loop, names);
10832
- return names;
10833
10822
  };
10834
10823
  const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
10835
10824
  let cursor = receiver;
@@ -10855,7 +10844,7 @@ const isLoopVariantReceiver = (node) => {
10855
10844
  const loopBoundNames = /* @__PURE__ */ new Set();
10856
10845
  let ancestor = node.parent;
10857
10846
  while (ancestor) {
10858
- 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);
10859
10848
  ancestor = ancestor.parent;
10860
10849
  }
10861
10850
  if (loopBoundNames.has(receiverRoot)) return true;
@@ -15526,30 +15515,16 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
15526
15515
  const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
15527
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;
15528
15517
  const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
15529
- const collectSourceTextExportNames = (sourceText) => {
15518
+ const doesSourceTextExportName = (sourceText, exportedName) => {
15530
15519
  const strippedSource = stripJsComments(sourceText);
15531
- const exportedNames = /* @__PURE__ */ new Set();
15532
- if (DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) exportedNames.add("default");
15533
- for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1]) exportedNames.add(match[1]);
15534
- for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) {
15535
- const specifiersText = match[1] ?? "";
15536
- for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
15537
- }
15538
- 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;
15539
15524
  };
15540
- const exportNamesCache = /* @__PURE__ */ new Map();
15541
15525
  const doesModuleExportName = (filePath, exportedName) => {
15542
15526
  try {
15543
- const fileStat = fs.statSync(filePath);
15544
- const cached = exportNamesCache.get(filePath);
15545
- if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
15546
- const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
15547
- exportNamesCache.set(filePath, {
15548
- mtimeMs: fileStat.mtimeMs,
15549
- size: fileStat.size,
15550
- exportedNames
15551
- });
15552
- return exportedNames.has(exportedName);
15527
+ return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
15553
15528
  } catch {
15554
15529
  return false;
15555
15530
  }
@@ -17432,15 +17407,8 @@ const getProgramAnalysis = (anyNode) => {
17432
17407
  programToAnalysis.set(programNode, analysis);
17433
17408
  return analysis;
17434
17409
  };
17435
- const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
17436
17410
  const getScopeForNode = (node, manager) => {
17437
17411
  if (!node.range) return null;
17438
- let scopeByNode = scopeByNodeCache.get(manager);
17439
- if (!scopeByNode) {
17440
- scopeByNode = /* @__PURE__ */ new WeakMap();
17441
- scopeByNodeCache.set(manager, scopeByNode);
17442
- }
17443
- if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
17444
17412
  let bestScope = null;
17445
17413
  let bestSize = Infinity;
17446
17414
  for (const scope of manager.scopes) {
@@ -17453,7 +17421,6 @@ const getScopeForNode = (node, manager) => {
17453
17421
  bestScope = scope;
17454
17422
  }
17455
17423
  }
17456
- scopeByNode.set(node, bestScope);
17457
17424
  return bestScope;
17458
17425
  };
17459
17426
  //#endregion
@@ -17488,20 +17455,11 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
17488
17455
  } else if (isAstNode(child)) descend(child, visit, visited);
17489
17456
  }
17490
17457
  };
17491
- const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
17492
17458
  const getUpstreamRefs = (analysis, ref) => {
17493
- let upstreamByRef = upstreamRefsCache.get(analysis);
17494
- if (!upstreamByRef) {
17495
- upstreamByRef = /* @__PURE__ */ new WeakMap();
17496
- upstreamRefsCache.set(analysis, upstreamByRef);
17497
- }
17498
- const cached = upstreamByRef.get(ref);
17499
- if (cached) return cached;
17500
17459
  const refs = [];
17501
17460
  ascend(analysis, ref, (upRef) => {
17502
17461
  refs.push(upRef);
17503
17462
  });
17504
- upstreamByRef.set(ref, refs);
17505
17463
  return refs;
17506
17464
  };
17507
17465
  const findDownstreamNodes = (topNode, type) => {
@@ -17511,24 +17469,11 @@ const findDownstreamNodes = (topNode, type) => {
17511
17469
  });
17512
17470
  return nodes;
17513
17471
  };
17514
- const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
17515
17472
  const getRef = (analysis, identifier) => {
17516
- let refByIdentifier = refByIdentifierCache.get(analysis);
17517
- if (!refByIdentifier) {
17518
- refByIdentifier = /* @__PURE__ */ new WeakMap();
17519
- refByIdentifierCache.set(analysis, refByIdentifier);
17520
- }
17521
- if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
17522
- let resolvedReference = null;
17523
17473
  const scope = getScopeForNode(identifier, analysis.scopeManager);
17524
- if (scope) {
17525
- for (const reference of scope.references) if (reference.identifier === identifier) {
17526
- resolvedReference = reference;
17527
- break;
17528
- }
17529
- }
17530
- refByIdentifier.set(identifier, resolvedReference);
17531
- return resolvedReference;
17474
+ if (!scope) return null;
17475
+ for (const reference of scope.references) if (reference.identifier === identifier) return reference;
17476
+ return null;
17532
17477
  };
17533
17478
  const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
17534
17479
  const getDownstreamRefs = (analysis, node) => {
@@ -17603,6 +17548,22 @@ const isEventualCallTo = (analysis, ref, predicate) => {
17603
17548
  };
17604
17549
  //#endregion
17605
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
+ };
17606
17567
  const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
17607
17568
  const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
17608
17569
  const isReactFunctionalComponent = (node) => {
@@ -17633,7 +17594,7 @@ const isReactFunctionalHOC = (analysis, node) => {
17633
17594
  const isWrappedSeparately = () => {
17634
17595
  if (!isNodeOfType(node.id, "Identifier")) return false;
17635
17596
  const bindingName = node.id.name;
17636
- const containingScope = getScopeForNode(node, analysis.scopeManager);
17597
+ const containingScope = getOuterScopeContaining(analysis, node);
17637
17598
  if (!containingScope) return false;
17638
17599
  const variable = containingScope.variables.find((v) => v.name === bindingName);
17639
17600
  if (!variable) return false;
@@ -19912,44 +19873,38 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
19912
19873
  };
19913
19874
  const parseColorToRgb = (value) => {
19914
19875
  const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
19915
- if (trimmed.startsWith("#")) {
19916
- const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
19917
- if (hex8Match) return {
19918
- red: parseInt(hex8Match[1], 16),
19919
- green: parseInt(hex8Match[2], 16),
19920
- blue: parseInt(hex8Match[3], 16)
19921
- };
19922
- const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
19923
- if (hex6Match) return {
19924
- red: parseInt(hex6Match[1], 16),
19925
- green: parseInt(hex6Match[2], 16),
19926
- blue: parseInt(hex6Match[3], 16)
19927
- };
19928
- const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
19929
- if (hex4Match) return {
19930
- red: parseInt(hex4Match[1] + hex4Match[1], 16),
19931
- green: parseInt(hex4Match[2] + hex4Match[2], 16),
19932
- blue: parseInt(hex4Match[3] + hex4Match[3], 16)
19933
- };
19934
- const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
19935
- if (hex3Match) return {
19936
- red: parseInt(hex3Match[1] + hex3Match[1], 16),
19937
- green: parseInt(hex3Match[2] + hex3Match[2], 16),
19938
- blue: parseInt(hex3Match[3] + hex3Match[3], 16)
19939
- };
19940
- }
19941
- if (trimmed.includes("rgb")) {
19942
- const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
19943
- if (rgbMatch) return {
19944
- red: parseInt(rgbMatch[1], 10),
19945
- green: parseInt(rgbMatch[2], 10),
19946
- blue: parseInt(rgbMatch[3], 10)
19947
- };
19948
- }
19949
- if (trimmed.includes("hsl")) {
19950
- const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
19951
- if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
19952
- }
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]));
19953
19908
  return null;
19954
19909
  };
19955
19910
  //#endregion
@@ -19977,18 +19932,9 @@ const extractColorFromShadowLayer = (layer) => {
19977
19932
  if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
19978
19933
  return null;
19979
19934
  };
19980
- const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
19981
- const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
19982
- const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
19983
- const SHADOW_BLUR_TOKEN_INDEX = 2;
19984
19935
  const parseShadowLayerBlur = (layer) => {
19985
- const withoutColors = layer.replace(RGB_FUNCTION_PATTERN, "").replace(HEX_COLOR_PATTERN, "");
19986
- let tokenIndex = 0;
19987
- for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
19988
- if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
19989
- tokenIndex += 1;
19990
- }
19991
- 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;
19992
19938
  };
19993
19939
  const hasColoredGlowShadow = (shadowValue) => {
19994
19940
  for (const layer of splitShadowLayers(shadowValue)) {
@@ -20108,10 +20054,7 @@ const isIntrinsicJsxAttribute = (node) => {
20108
20054
  };
20109
20055
  //#endregion
20110
20056
  //#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
20111
- const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
20112
20057
  const collectComponentPropNames = (componentFunction) => {
20113
- const cached = componentPropNamesCache.get(componentFunction);
20114
- if (cached) return cached;
20115
20058
  const propNames = /* @__PURE__ */ new Set();
20116
20059
  if (!isFunctionLike$1(componentFunction)) return propNames;
20117
20060
  const propsObjectParamNames = /* @__PURE__ */ new Set();
@@ -20125,23 +20068,27 @@ const collectComponentPropNames = (componentFunction) => {
20125
20068
  if (child !== componentBody && isFunctionLike$1(child)) return false;
20126
20069
  if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
20127
20070
  });
20128
- componentPropNamesCache.set(componentFunction, propNames);
20129
20071
  return propNames;
20130
20072
  };
20131
- const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
20132
- const getOwnScopeBoundNames = (functionNode) => {
20133
- const cached = ownScopeBoundNamesCache.get(functionNode);
20134
- if (cached) return cached;
20073
+ const declaresBindingNamed = (functionNode, bindingName) => {
20135
20074
  const boundNames = /* @__PURE__ */ new Set();
20136
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;
20137
20078
  walkAst(functionNode, (child) => {
20079
+ if (declaresName) return false;
20138
20080
  if (child !== functionNode && isFunctionLike$1(child)) return false;
20139
- 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
+ }
20140
20089
  });
20141
- ownScopeBoundNamesCache.set(functionNode, boundNames);
20142
- return boundNames;
20090
+ return declaresName;
20143
20091
  };
20144
- const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
20145
20092
  const isPropertyNamePosition = (identifier) => {
20146
20093
  const parent = identifier.parent;
20147
20094
  if (!parent) return false;
@@ -20564,28 +20511,36 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
20564
20511
  }
20565
20512
  return hasNestedFunction;
20566
20513
  };
20567
- const isInRenderScope = (node, componentFunction) => {
20568
- let cursor = node.parent ?? null;
20569
- while (cursor && cursor !== componentFunction) {
20570
- if (isFunctionLike$1(cursor)) return false;
20571
- cursor = cursor.parent ?? null;
20572
- }
20573
- 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;
20574
20528
  };
20575
- const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
20529
+ const isAdjustedDuringRender = (useStateCall, isPropName) => {
20576
20530
  const setterName = getStateSetterName(useStateCall);
20577
20531
  if (!setterName) return false;
20578
20532
  const componentFunction = findEnclosingFunction(useStateCall);
20579
20533
  if (!componentFunction) return false;
20580
- let isExempt = false;
20534
+ let isAdjusted = false;
20581
20535
  walkAst(componentFunction, (child) => {
20582
- if (isExempt) return false;
20583
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && (isHandlerShapedReseed(child, componentFunction) || isInRenderScope(child, componentFunction))) {
20584
- 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;
20585
20540
  return false;
20586
20541
  }
20587
20542
  });
20588
- return isExempt;
20543
+ return isAdjusted;
20589
20544
  };
20590
20545
  const noDerivedUseState = defineRule({
20591
20546
  id: "no-derived-useState",
@@ -20602,7 +20557,8 @@ const noDerivedUseState = defineRule({
20602
20557
  const initializer = node.arguments[0];
20603
20558
  if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
20604
20559
  if (isInitialOnlyPropName(initializer.name)) return;
20605
- if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20560
+ if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20561
+ if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20606
20562
  context.report({
20607
20563
  node,
20608
20564
  message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
@@ -20613,7 +20569,8 @@ const noDerivedUseState = defineRule({
20613
20569
  const rootIdentifierName = getRootIdentifierName(initializer);
20614
20570
  if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
20615
20571
  if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
20616
- if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
20572
+ if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
20573
+ if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
20617
20574
  context.report({
20618
20575
  node,
20619
20576
  message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
@@ -22312,20 +22269,20 @@ const getTriggerGuardRootName = (testNode) => {
22312
22269
  return null;
22313
22270
  };
22314
22271
  const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
22315
- const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
22316
- const settersWithAnyCall = /* @__PURE__ */ new Set();
22317
- const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
22318
- walkAst(componentBody, (child) => {
22319
- if (!isNodeOfType(child, "CallExpression")) return;
22320
- if (!isNodeOfType(child.callee, "Identifier")) return;
22321
- const setterName = child.callee.name;
22322
- if (!setterNames.has(setterName)) return;
22323
- settersWithAnyCall.add(setterName);
22324
- if (settersWithNonHandlerCall.has(setterName)) return;
22325
- if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
22326
- });
22327
22272
  const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
22328
- 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
+ }
22329
22286
  return handlerOnlyWriteStateNames;
22330
22287
  };
22331
22288
  const noEventTriggerState = defineRule({
@@ -22665,10 +22622,6 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
22665
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)-)/;
22666
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)-)/;
22667
22624
  const splitVariantScope = (token) => {
22668
- if (!token.includes(":")) return {
22669
- scope: "",
22670
- utility: token.startsWith("!") ? token.slice(1) : token
22671
- };
22672
22625
  const segments = token.split(":");
22673
22626
  const rawUtility = segments[segments.length - 1];
22674
22627
  return {
@@ -22692,7 +22645,6 @@ const noGrayOnColoredBackground = defineRule({
22692
22645
  const bgColorScopes = /* @__PURE__ */ new Set();
22693
22646
  for (const token of classStr.split(/\s+/)) {
22694
22647
  if (!token) continue;
22695
- if (!token.includes("text-") && !token.includes("bg-")) continue;
22696
22648
  const { scope, utility } = splitVariantScope(token);
22697
22649
  if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
22698
22650
  if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
@@ -23422,8 +23374,6 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
23422
23374
  return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
23423
23375
  });
23424
23376
  const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
23425
- const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
23426
- const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
23427
23377
  const noLongTransitionDuration = defineRule({
23428
23378
  id: "no-long-transition-duration",
23429
23379
  title: "Transition duration too long",
@@ -23447,10 +23397,11 @@ const noLongTransitionDuration = defineRule({
23447
23397
  if (key === "transitionDuration" || key === "animationDuration") {
23448
23398
  let longestDurationPropertyMs = 0;
23449
23399
  for (const segment of value.split(",")) {
23450
- const durationMatch = segment.trim().match(DURATION_SEGMENT_PATTERN);
23451
- if (!durationMatch) continue;
23452
- const segmentDurationMs = durationMatch[2] === "ms" ? parseFloat(durationMatch[1]) : parseFloat(durationMatch[1]) * 1e3;
23453
- 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);
23454
23405
  }
23455
23406
  if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
23456
23407
  }
@@ -23458,7 +23409,7 @@ const noLongTransitionDuration = defineRule({
23458
23409
  let longestDurationMs = 0;
23459
23410
  for (const segment of value.split(",")) {
23460
23411
  if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
23461
- const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
23412
+ const firstTimeMatch = segment.match(/(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/);
23462
23413
  if (!firstTimeMatch) continue;
23463
23414
  const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
23464
23415
  longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
@@ -32284,11 +32235,41 @@ const callsIdentifier = (root, identifierName) => {
32284
32235
  });
32285
32236
  return found;
32286
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
+ };
32287
32250
  const PROMISE_CHAIN_METHOD_NAMES = new Set([
32288
32251
  "then",
32289
32252
  "catch",
32290
32253
  "finally"
32291
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
+ };
32292
32273
  const ASYNC_DATA_CALLEE_NAMES = new Set([
32293
32274
  "useApolloClient",
32294
32275
  "useMutation",
@@ -32301,36 +32282,20 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
32301
32282
  "fetch",
32302
32283
  "axios"
32303
32284
  ]);
32304
- const hasAsyncLoadingWork = (fnBody, setterName) => {
32285
+ const referencesAsyncDataApi = (body) => {
32286
+ if (!body) return false;
32305
32287
  let found = false;
32306
- walkAst(fnBody, (child) => {
32307
- if (found) return false;
32288
+ walkAst(body, (child) => {
32289
+ if (found) return;
32308
32290
  if (isNodeOfType(child, "CallExpression")) {
32309
32291
  const callee = child.callee;
32310
32292
  if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
32311
32293
  found = true;
32312
- return false;
32313
- }
32314
- if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
32315
- if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
32316
- found = true;
32317
- return false;
32318
- }
32319
- if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
32320
- if (!isFunctionLike$1(argument)) continue;
32321
- if (callsIdentifier(argument.body, setterName)) {
32322
- found = true;
32323
- return false;
32324
- }
32325
- }
32294
+ return;
32326
32295
  }
32327
- return;
32328
- }
32329
- if (setterName !== null && isFunctionLike$1(child)) {
32330
- const functionBody = child.body;
32331
- 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)) {
32332
32297
  found = true;
32333
- return false;
32298
+ return;
32334
32299
  }
32335
32300
  }
32336
32301
  });
@@ -32355,7 +32320,7 @@ const renderingUsetransitionLoading = defineRule({
32355
32320
  const secondBinding = node.id.elements[1];
32356
32321
  const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
32357
32322
  const fnBody = enclosingFunctionBody(node);
32358
- if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
32323
+ if (fnBody && (setterName && setterIsCalledInAsyncContext(fnBody, setterName) || setterName && setterIsCalledInPromiseChain(fnBody, setterName) || referencesAsyncDataApi(fnBody))) return;
32359
32324
  context.report({
32360
32325
  node: node.init,
32361
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`
@@ -33149,17 +33114,17 @@ const rerenderStateOnlyInHandlers = defineRule({
33149
33114
  const effectTriggerNames = /* @__PURE__ */ new Set();
33150
33115
  for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
33151
33116
  for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
33152
- const setterNames = new Set(bindings.map((binding) => binding.setterName));
33153
- const calledSetterNames = /* @__PURE__ */ new Set();
33154
- walkAst(componentBody, (child) => {
33155
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
33156
- });
33157
33117
  for (const binding of bindings) {
33158
33118
  if (renderReachableNames.has(binding.valueName)) continue;
33159
33119
  if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
33160
33120
  const setterSuffix = binding.setterName.slice(3);
33161
33121
  if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
33162
- 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;
33163
33128
  if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
33164
33129
  context.report({
33165
33130
  node: binding.declarator,
@@ -40581,16 +40546,7 @@ const supabaseRlsPolicyRisk = defineRule({
40581
40546
  //#endregion
40582
40547
  //#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
40583
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;
40584
- 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;
40585
- const collectLastEnableRlsIndexByTable = (content) => {
40586
- const lastEnableIndexByTable = /* @__PURE__ */ new Map();
40587
- for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
40588
- const tableName = match[1];
40589
- if (tableName === void 0) continue;
40590
- lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
40591
- }
40592
- return lastEnableIndexByTable;
40593
- };
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");
40594
40550
  const supabaseTableMissingRls = defineRule({
40595
40551
  id: "supabase-table-missing-rls",
40596
40552
  title: "Supabase table created without Row Level Security",
@@ -40601,13 +40557,11 @@ const supabaseTableMissingRls = defineRule({
40601
40557
  const content = sanitizeSqlForScan(file.content);
40602
40558
  if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
40603
40559
  const findings = [];
40604
- const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
40605
40560
  CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
40606
40561
  for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
40607
40562
  const tableName = match[1];
40608
40563
  if (tableName === void 0) continue;
40609
- const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
40610
- if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
40564
+ if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
40611
40565
  const location = getLocationAtIndex(content, match.index);
40612
40566
  findings.push({
40613
40567
  message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
@@ -41631,14 +41585,7 @@ const getStaticPropertyName = (member) => {
41631
41585
  if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
41632
41586
  return null;
41633
41587
  };
41634
- const importInfoCache = /* @__PURE__ */ new WeakMap();
41635
41588
  const getImportInfoForIdentifier = (identifier) => {
41636
- if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
41637
- const importInfo = computeImportInfoForIdentifier(identifier);
41638
- importInfoCache.set(identifier, importInfo);
41639
- return importInfo;
41640
- };
41641
- const computeImportInfoForIdentifier = (identifier) => {
41642
41589
  const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
41643
41590
  if (!specifier) return null;
41644
41591
  const declaration = specifier.parent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.6.2-dev.d8628d7",
3
+ "version": "0.6.2-dev.fa61c20",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",