oxlint-plugin-react-doctor 0.7.2-dev.0eb5293 → 0.7.2-dev.11e9c87

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 +286 -171
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -595,6 +595,19 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
595
595
  "parseInt",
596
596
  "parseFloat"
597
597
  ]);
598
+ const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
599
+ "Date",
600
+ "Map",
601
+ "Set",
602
+ "WeakMap",
603
+ "WeakSet",
604
+ "WeakRef",
605
+ "RegExp",
606
+ "Error",
607
+ "URL",
608
+ "URLSearchParams",
609
+ "AbortController"
610
+ ]);
598
611
  const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
599
612
  "Boolean",
600
613
  "String",
@@ -2235,6 +2248,38 @@ const anchorHasContent = defineRule({
2235
2248
  }
2236
2249
  });
2237
2250
  //#endregion
2251
+ //#region src/plugin/utils/get-jsx-prop-static-string-values.ts
2252
+ const MAX_CONST_RESOLUTION_HOPS = 4;
2253
+ const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
2254
+ const expression = stripParenExpression(rawExpression);
2255
+ if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
2256
+ if (isNodeOfType(expression, "TemplateLiteral")) {
2257
+ const staticValue = getStaticTemplateLiteralValue(expression);
2258
+ return staticValue === null ? null : [staticValue];
2259
+ }
2260
+ if (isNodeOfType(expression, "ConditionalExpression")) {
2261
+ const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
2262
+ if (consequentValues === null) return null;
2263
+ const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
2264
+ if (alternateValues === null) return null;
2265
+ return [...consequentValues, ...alternateValues];
2266
+ }
2267
+ if (isNodeOfType(expression, "Identifier")) {
2268
+ if (remainingHops === 0) return null;
2269
+ const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
2270
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
2271
+ return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
2272
+ }
2273
+ return null;
2274
+ };
2275
+ const getJsxPropStaticStringValues = (attribute, scopes) => {
2276
+ const value = attribute.value;
2277
+ if (!value) return null;
2278
+ if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
2279
+ if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
2280
+ return null;
2281
+ };
2282
+ //#endregion
2238
2283
  //#region src/plugin/rules/a11y/anchor-is-valid.ts
2239
2284
  const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
2240
2285
  const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
@@ -2264,28 +2309,11 @@ const isInvalidHref = (value, validHrefs) => {
2264
2309
  if (validHrefs.has(value)) return false;
2265
2310
  return value === "" || value === "#" || value === "javascript:void(0)";
2266
2311
  };
2267
- const getStaticHrefValue = (value) => {
2268
- if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
2269
- if (isNodeOfType(value, "JSXExpressionContainer")) {
2270
- const expression = value.expression;
2271
- if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
2272
- if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
2273
- }
2274
- return null;
2275
- };
2276
- const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
2277
- if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
2312
+ const isNullishOrFragmentHref = (value) => {
2278
2313
  if (isNodeOfType(value, "JSXExpressionContainer")) {
2279
2314
  const expression = value.expression;
2280
2315
  if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
2281
- if (isNodeOfType(expression, "Literal")) {
2282
- if (expression.value === null) return true;
2283
- if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
2284
- }
2285
- if (isNodeOfType(expression, "TemplateLiteral")) {
2286
- const staticValue = getStaticTemplateLiteralValue(expression);
2287
- return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
2288
- }
2316
+ if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
2289
2317
  }
2290
2318
  if (isNodeOfType(value, "JSXFragment")) return true;
2291
2319
  return false;
@@ -2318,9 +2346,10 @@ const anchorIsValid = defineRule({
2318
2346
  });
2319
2347
  return;
2320
2348
  }
2321
- if (checkValueIsEmptyOrInvalid(hrefAttribute.value, settings.validHrefs)) {
2349
+ const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
2350
+ if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
2322
2351
  const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
2323
- if (!hasOnClick && getStaticHrefValue(hrefAttribute.value) === "#") return;
2352
+ if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
2324
2353
  context.report({
2325
2354
  node: node.name,
2326
2355
  message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
@@ -3376,6 +3405,25 @@ const ariaRole = defineRule({
3376
3405
  if (!roleAttribute) return;
3377
3406
  const elementType = getElementType(node, context.settings);
3378
3407
  if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
3408
+ const reportFirstInvalidCandidate = (candidates) => {
3409
+ for (const candidate of candidates) {
3410
+ if (candidate.trim().length === 0) {
3411
+ context.report({
3412
+ node: roleAttribute,
3413
+ message: buildBaseMessage("")
3414
+ });
3415
+ return;
3416
+ }
3417
+ const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
3418
+ for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
3419
+ context.report({
3420
+ node: roleAttribute,
3421
+ message: buildBaseMessage(` \`${token}\` is not one.`)
3422
+ });
3423
+ return;
3424
+ }
3425
+ }
3426
+ };
3379
3427
  const value = roleAttribute.value;
3380
3428
  if (!value) {
3381
3429
  context.report({
@@ -3392,22 +3440,7 @@ const ariaRole = defineRule({
3392
3440
  });
3393
3441
  return;
3394
3442
  }
3395
- const stringValue = value.value;
3396
- if (stringValue.trim().length === 0) {
3397
- context.report({
3398
- node: roleAttribute,
3399
- message: buildBaseMessage("")
3400
- });
3401
- return;
3402
- }
3403
- const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
3404
- for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
3405
- context.report({
3406
- node: roleAttribute,
3407
- message: buildBaseMessage(` \`${token}\` is not one.`)
3408
- });
3409
- return;
3410
- }
3443
+ reportFirstInvalidCandidate([value.value]);
3411
3444
  return;
3412
3445
  }
3413
3446
  if (isNodeOfType(value, "JSXExpressionContainer")) {
@@ -3428,6 +3461,11 @@ const ariaRole = defineRule({
3428
3461
  });
3429
3462
  return;
3430
3463
  }
3464
+ const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
3465
+ if (resolvedCandidates !== null) {
3466
+ reportFirstInvalidCandidate(resolvedCandidates);
3467
+ return;
3468
+ }
3431
3469
  return;
3432
3470
  }
3433
3471
  context.report({
@@ -9616,7 +9654,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9616
9654
  });
9617
9655
  //#endregion
9618
9656
  //#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
9619
- const EMPTY_VISITORS$5 = {};
9657
+ const EMPTY_VISITORS$6 = {};
9620
9658
  const NODE_OR_BUILD_FILE = /(\.config\.[cm]?[jt]sx?$)|((^|\/)(scripts|tools|tooling|cli|bin)\/)|(\+(api|html)\.[cm]?[jt]sx?$)|(\.server\.[cm]?[jt]sx?$)|(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)__tests__\/)|(\.e2e\.[cm]?[jt]sx?$)/;
9621
9659
  const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
9622
9660
  const isProcessEnv = (node) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "process" && isNodeOfType(node.property, "Identifier") && node.property.name === "env";
@@ -9628,7 +9666,7 @@ const expoNoNonInlinedEnv = defineRule({
9628
9666
  recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
9629
9667
  create: (context) => {
9630
9668
  const filename = normalizeFilename(context.filename ?? "");
9631
- if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$5;
9669
+ if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
9632
9670
  return {
9633
9671
  MemberExpression(node) {
9634
9672
  if (!node.computed) return;
@@ -11046,8 +11084,8 @@ const interactiveSupportsFocus = defineRule({
11046
11084
  if (node.attributes.length === 0) return;
11047
11085
  if (hasJsxSpreadAttribute$1(node.attributes)) return;
11048
11086
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
11049
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
11050
- if (!role) return;
11087
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
11088
+ if (roleCandidates === null || roleCandidates.length === 0) return;
11051
11089
  let hasInteractiveHandler = false;
11052
11090
  for (const attribute of node.attributes) {
11053
11091
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -11061,11 +11099,16 @@ const interactiveSupportsFocus = defineRule({
11061
11099
  const elementType = getElementType(node, context.settings);
11062
11100
  if (!HTML_TAGS.has(elementType)) return;
11063
11101
  if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
11064
- if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11065
- if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
11066
11102
  const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
11067
- if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11068
- const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
11103
+ const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
11104
+ for (const role of roleCandidates) {
11105
+ if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
11106
+ if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
11107
+ if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
11108
+ }
11109
+ const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
11110
+ const roleDisplay = roleCandidates.join("' / '");
11111
+ const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
11069
11112
  context.report({
11070
11113
  node,
11071
11114
  message
@@ -22477,6 +22520,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
22477
22520
  const section = manifest[sectionName];
22478
22521
  return typeof section === "object" && section !== null && Object.keys(section).length > 0;
22479
22522
  });
22523
+ const declaresDependency = (manifest, dependencyName) => {
22524
+ for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
22525
+ return false;
22526
+ };
22480
22527
  const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
22481
22528
  const classifyPackagePlatform = (filename) => {
22482
22529
  const manifest = readNearestPackageManifest(filename);
@@ -22493,9 +22540,7 @@ const classifyPackagePlatform = (filename) => {
22493
22540
  return result;
22494
22541
  };
22495
22542
  //#endregion
22496
- //#region src/plugin/utils/is-react-native-file.ts
22497
- const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22498
- const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22543
+ //#region src/plugin/utils/is-package-nested-below-project-root.ts
22499
22544
  const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
22500
22545
  const resolveRealDirectory = (directory) => {
22501
22546
  const cached = cachedRealDirectoryByDirectory.get(directory);
@@ -22516,6 +22561,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
22516
22561
  const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
22517
22562
  return realPackageDirectory.startsWith(rootPrefix);
22518
22563
  };
22564
+ //#endregion
22565
+ //#region src/plugin/utils/is-react-native-file.ts
22566
+ const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
22567
+ const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
22519
22568
  const classifyReactNativeFileTarget = (context) => {
22520
22569
  const rawFilename = context.filename;
22521
22570
  if (!rawFilename) return "unknown";
@@ -22932,7 +22981,6 @@ const countStatementSequenceSetStateCalls = (statements, context) => {
22932
22981
  let fallThroughCount = 0;
22933
22982
  let maxTerminatingPathCount = 0;
22934
22983
  for (const statement of statements) {
22935
- if (isFunctionLike$1(statement)) continue;
22936
22984
  const guardBranch = isGuardWithTerminatingBranch(statement);
22937
22985
  if (guardBranch) {
22938
22986
  maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
@@ -22968,14 +23016,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
22968
23016
  if (!isNodeOfType(parent, "ExpressionStatement")) return false;
22969
23017
  return parent.parent === effectCallback.body;
22970
23018
  };
23019
+ const countFunctionBodySetStateCalls = (functionNode, context) => {
23020
+ if (isAsyncFunctionLike(functionNode)) return 0;
23021
+ const body = functionNode.body;
23022
+ if (!body) return 0;
23023
+ return countMaxPathSetStateCalls(body, context);
23024
+ };
22971
23025
  const countMaxPathSetStateCalls = (node, context) => {
22972
23026
  if (!node || typeof node !== "object") return 0;
22973
- if (isAsyncFunctionLike(node)) return 0;
23027
+ if (isFunctionLike$1(node)) return 0;
22974
23028
  if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
22975
23029
  if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
22976
23030
  const consequent = node.consequent;
22977
23031
  const alternate = node.alternate;
22978
- return countMaxPathSetStateCalls(consequent, context) + (alternate ? countMaxPathSetStateCalls(alternate, context) : 0);
23032
+ const testCount = countMaxPathSetStateCalls(node.test, context);
23033
+ const thenCount = countMaxPathSetStateCalls(consequent, context);
23034
+ const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
23035
+ return testCount + Math.max(thenCount, elseCount);
22979
23036
  }
22980
23037
  if (isNodeOfType(node, "SwitchStatement")) {
22981
23038
  let maxRunSetters = 0;
@@ -23005,28 +23062,31 @@ const countMaxPathSetStateCalls = (node, context) => {
23005
23062
  }
23006
23063
  if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
23007
23064
  let nestedSettersInArgs = 0;
23008
- for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
23065
+ for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
23009
23066
  return 1 + nestedSettersInArgs;
23010
23067
  }
23011
23068
  if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
23012
23069
  const helperFunction = context.helpersByName.get(node.callee.name);
23013
23070
  if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
23014
23071
  context.activeHelpers.add(helperFunction);
23015
- let helperCount = countMaxPathSetStateCalls(helperFunction, context);
23072
+ let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
23016
23073
  context.activeHelpers.delete(helperFunction);
23017
23074
  for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
23018
23075
  return helperCount;
23019
23076
  }
23020
23077
  }
23021
- const shouldWalkChild = (child) => !isFunctionLike$1(child) || runsOnEffectDispatch(child);
23078
+ const countChild = (child) => {
23079
+ if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
23080
+ return countMaxPathSetStateCalls(child, context);
23081
+ };
23022
23082
  let total = 0;
23023
23083
  const nodeRecord = node;
23024
23084
  for (const key of Object.keys(nodeRecord)) {
23025
23085
  if (key === "parent") continue;
23026
23086
  const child = nodeRecord[key];
23027
23087
  if (Array.isArray(child)) {
23028
- for (const item of child) if (item && typeof item === "object" && "type" in item && shouldWalkChild(item)) total += countMaxPathSetStateCalls(item, context);
23029
- } else if (child && typeof child === "object" && "type" in child && shouldWalkChild(child)) total += countMaxPathSetStateCalls(child, context);
23088
+ for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
23089
+ } else if (child && typeof child === "object" && "type" in child) total += countChild(child);
23030
23090
  }
23031
23091
  return total;
23032
23092
  };
@@ -23075,7 +23135,7 @@ const noCascadingSetState = defineRule({
23075
23135
  const callback = getEffectCallback(node);
23076
23136
  if (!callback) return;
23077
23137
  if (isDevOnlyGuardedEffect(callback)) return;
23078
- const setStateCallCount = countMaxPathSetStateCalls(callback, {
23138
+ const setStateCallCount = countFunctionBodySetStateCalls(callback, {
23079
23139
  helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
23080
23140
  activeHelpers: /* @__PURE__ */ new Set(),
23081
23141
  effectCallback: callback
@@ -26293,7 +26353,11 @@ const noEffectEventInDeps = defineRule({
26293
26353
  const initializer = declaratorNode.init;
26294
26354
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26295
26355
  if (!isHookCall$2(initializer, "useEffectEvent")) return;
26296
- if (isNodeOfType(initializer.callee, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26356
+ if (isNodeOfType(initializer.callee, "Identifier")) {
26357
+ if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26358
+ const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
26359
+ if (calleeSymbol && calleeSymbol.kind !== "import") return;
26360
+ }
26297
26361
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26298
26362
  } });
26299
26363
  return {
@@ -30015,6 +30079,40 @@ const noMutableInDeps = defineRule({
30015
30079
  }
30016
30080
  });
30017
30081
  //#endregion
30082
+ //#region src/plugin/utils/is-result-discarded-call.ts
30083
+ const isResultDiscardedCall = (callExpression) => {
30084
+ let node = callExpression;
30085
+ let parent = node.parent;
30086
+ while (parent) {
30087
+ if (isNodeOfType(parent, "ExpressionStatement")) return true;
30088
+ if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
30089
+ if (isNodeOfType(parent, "ChainExpression")) {
30090
+ node = parent;
30091
+ parent = node.parent;
30092
+ continue;
30093
+ }
30094
+ if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
30095
+ node = parent;
30096
+ parent = node.parent;
30097
+ continue;
30098
+ }
30099
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
30100
+ node = parent;
30101
+ parent = node.parent;
30102
+ continue;
30103
+ }
30104
+ if (isNodeOfType(parent, "SequenceExpression")) {
30105
+ const expressions = parent.expressions ?? [];
30106
+ if (expressions[expressions.length - 1] !== node) return true;
30107
+ node = parent;
30108
+ parent = node.parent;
30109
+ continue;
30110
+ }
30111
+ return false;
30112
+ }
30113
+ return false;
30114
+ };
30115
+ //#endregion
30018
30116
  //#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
30019
30117
  const LODASH_MUTATOR_NAMES = new Set([
30020
30118
  "set",
@@ -30112,7 +30210,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
30112
30210
  "reverse",
30113
30211
  "sort"
30114
30212
  ]);
30115
- const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
30116
30213
  const OBJECT_MUTATION_METHODS = new Set([
30117
30214
  "assign",
30118
30215
  "defineProperties",
@@ -30186,7 +30283,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
30186
30283
  const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
30187
30284
  if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
30188
30285
  if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30189
- if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
30190
30286
  }
30191
30287
  }
30192
30288
  if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
@@ -30225,6 +30321,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
30225
30321
  if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
30226
30322
  const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
30227
30323
  if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
30324
+ if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
30228
30325
  if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
30229
30326
  });
30230
30327
  return mutations;
@@ -31936,40 +32033,6 @@ const noPreventDefault = defineRule({
31936
32033
  }
31937
32034
  });
31938
32035
  //#endregion
31939
- //#region src/plugin/utils/is-result-discarded-call.ts
31940
- const isResultDiscardedCall = (callExpression) => {
31941
- let node = callExpression;
31942
- let parent = node.parent;
31943
- while (parent) {
31944
- if (isNodeOfType(parent, "ExpressionStatement")) return true;
31945
- if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
31946
- if (isNodeOfType(parent, "ChainExpression")) {
31947
- node = parent;
31948
- parent = node.parent;
31949
- continue;
31950
- }
31951
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
31952
- node = parent;
31953
- parent = node.parent;
31954
- continue;
31955
- }
31956
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
31957
- node = parent;
31958
- parent = node.parent;
31959
- continue;
31960
- }
31961
- if (isNodeOfType(parent, "SequenceExpression")) {
31962
- const expressions = parent.expressions ?? [];
31963
- if (expressions[expressions.length - 1] !== node) return true;
31964
- node = parent;
31965
- parent = node.parent;
31966
- continue;
31967
- }
31968
- return false;
31969
- }
31970
- return false;
31971
- };
31972
- //#endregion
31973
32036
  //#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
31974
32037
  const isRefLatchGuardedEffect = (callbackBody) => {
31975
32038
  const refNamesReadInGuards = /* @__PURE__ */ new Set();
@@ -36575,8 +36638,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
36575
36638
  const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
36576
36639
  const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
36577
36640
  const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
36578
- const LOCAL_COMPONENT_MESSAGE = "This component is not exported, so Fast Refresh skips it and local edits can full-reload.";
36579
- const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
36641
+ const NAMESPACE_OBJECT_MESSAGE = "This export bundles components inside an object, so Fast Refresh can't track them and falls back to a full reload.";
36580
36642
  const DEFAULT_REACT_HOCS = [
36581
36643
  "memo",
36582
36644
  "forwardRef",
@@ -36647,6 +36709,20 @@ const isReactComponentInitializer = (expression, state) => {
36647
36709
  if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
36648
36710
  return false;
36649
36711
  };
36712
+ const objectExpressionBundlesComponents = (objectExpression, state) => {
36713
+ for (const property of objectExpression.properties ?? []) {
36714
+ if (!isNodeOfType(property, "Property")) continue;
36715
+ const value = skipTsExpression(property.value);
36716
+ if (isNodeOfType(value, "Identifier")) {
36717
+ if (state.localComponentNames.has(value.name)) return true;
36718
+ continue;
36719
+ }
36720
+ if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
36721
+ if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
36722
+ if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
36723
+ }
36724
+ return false;
36725
+ };
36650
36726
  const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36651
36727
  if (initializer) {
36652
36728
  const expression = skipTsExpression(initializer);
@@ -36677,6 +36753,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
36677
36753
  reportNode
36678
36754
  };
36679
36755
  }
36756
+ if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
36757
+ kind: "namespace-object",
36758
+ reportNode
36759
+ };
36680
36760
  if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
36681
36761
  kind: "non-component",
36682
36762
  reportNode
@@ -36693,7 +36773,7 @@ const collectRelevantNodes = (programRoot) => {
36693
36773
  walkAst(programRoot, (child) => {
36694
36774
  const childType = child.type;
36695
36775
  if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
36696
- else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
36776
+ else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
36697
36777
  });
36698
36778
  return {
36699
36779
  exportNodes,
@@ -36738,18 +36818,31 @@ const onlyExportComponents = defineRule({
36738
36818
  category: "Architecture",
36739
36819
  create: (context) => {
36740
36820
  const settings = resolveSettings$6(context.settings);
36741
- const state = {
36742
- customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36743
- allowExportNames: new Set(settings.allowExportNames),
36744
- allowConstantExport: settings.allowConstantExport
36745
- };
36746
36821
  return { Program(node) {
36747
36822
  if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
36748
36823
  const { exportNodes, componentCandidates } = collectRelevantNodes(node);
36824
+ const localComponentNames = /* @__PURE__ */ new Set();
36825
+ const state = {
36826
+ customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
36827
+ allowExportNames: new Set(settings.allowExportNames),
36828
+ allowConstantExport: settings.allowConstantExport,
36829
+ localComponentNames
36830
+ };
36831
+ for (const child of componentCandidates) {
36832
+ if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36833
+ if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
36834
+ }
36835
+ if (isNodeOfType(child, "ClassDeclaration") && child.id) {
36836
+ if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
36837
+ }
36838
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36839
+ const initializer = child.init;
36840
+ if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
36841
+ }
36842
+ }
36749
36843
  const exports = [];
36750
36844
  let hasReactExport = false;
36751
36845
  let hasAnyExports = false;
36752
- const localComponents = [];
36753
36846
  const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
36754
36847
  for (const child of exportNodes) {
36755
36848
  if (isNodeOfType(child, "ExportAllDeclaration")) {
@@ -36820,7 +36913,14 @@ const onlyExportComponents = defineRule({
36820
36913
  });
36821
36914
  continue;
36822
36915
  }
36823
- if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "Literal")) {
36916
+ if (isNodeOfType(stripped, "ObjectExpression")) {
36917
+ context.report({
36918
+ node: stripped,
36919
+ message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
36920
+ });
36921
+ continue;
36922
+ }
36923
+ if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
36824
36924
  context.report({
36825
36925
  node: stripped,
36826
36926
  message: ANONYMOUS_MESSAGE
@@ -36873,32 +36973,23 @@ const onlyExportComponents = defineRule({
36873
36973
  let entry;
36874
36974
  if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
36875
36975
  else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
36876
- else entry = {
36877
- kind: "non-component",
36878
- reportNode
36879
- };
36976
+ else {
36977
+ entry = {
36978
+ kind: "non-component",
36979
+ reportNode
36980
+ };
36981
+ if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
36982
+ }
36880
36983
  if (isReExportFromSource && entry.kind !== "react-component") continue;
36881
36984
  exports.push(entry);
36882
36985
  }
36883
36986
  }
36884
36987
  }
36885
36988
  for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
36886
- const isInsideExport = (node) => {
36887
- let walker = node.parent;
36888
- while (walker) {
36889
- if (isNodeOfType(walker, "ExportNamedDeclaration") || isNodeOfType(walker, "ExportDefaultDeclaration") || isNodeOfType(walker, "ExportAllDeclaration")) return true;
36890
- walker = walker.parent ?? null;
36891
- }
36892
- return false;
36893
- };
36894
- for (const child of componentCandidates) {
36895
- if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
36896
- if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36897
- }
36898
- if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
36899
- if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
36900
- }
36901
- }
36989
+ for (const entry of exports) if (entry.kind === "namespace-object") context.report({
36990
+ node: entry.reportNode,
36991
+ message: NAMESPACE_OBJECT_MESSAGE
36992
+ });
36902
36993
  if (hasAnyExports && hasReactExport) for (const entry of exports) {
36903
36994
  if (entry.kind === "non-component") context.report({
36904
36995
  node: entry.reportNode,
@@ -36909,14 +37000,6 @@ const onlyExportComponents = defineRule({
36909
37000
  message: REACT_CONTEXT_MESSAGE
36910
37001
  });
36911
37002
  }
36912
- else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
36913
- node: local,
36914
- message: LOCAL_COMPONENT_MESSAGE
36915
- });
36916
- else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
36917
- node: local,
36918
- message: NO_EXPORT_MESSAGE
36919
- });
36920
37003
  } };
36921
37004
  }
36922
37005
  });
@@ -37714,8 +37797,8 @@ const preferHtmlDialog = defineRule({
37714
37797
  if (!HTML_TAGS.has(tagName)) return;
37715
37798
  const roleAttribute = findJsxAttribute(node.attributes, "role");
37716
37799
  if (roleAttribute) {
37717
- const roleValue = getJsxPropStringValue(roleAttribute);
37718
- if (roleValue !== null && ROLE_DIALOG_VALUES.has(roleValue)) {
37800
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
37801
+ if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
37719
37802
  if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
37720
37803
  const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
37721
37804
  const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
@@ -40704,6 +40787,7 @@ const rerenderLazyRefInit = defineRule({
40704
40787
  const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
40705
40788
  const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
40706
40789
  if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
40790
+ if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
40707
40791
  if (isPlainCall && isReactHookName(calleeName)) return;
40708
40792
  const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
40709
40793
  context.report({
@@ -40736,18 +40820,6 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
40736
40820
  "getUTCMilliseconds",
40737
40821
  "valueOf"
40738
40822
  ]);
40739
- const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
40740
- "Date",
40741
- "Map",
40742
- "Set",
40743
- "WeakMap",
40744
- "WeakSet",
40745
- "RegExp",
40746
- "Error",
40747
- "URL",
40748
- "URLSearchParams",
40749
- "AbortController"
40750
- ]);
40751
40823
  const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
40752
40824
  const findEagerInitializerCall = (expression, depth = 0) => {
40753
40825
  if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
@@ -41491,7 +41563,7 @@ const rnBottomSheetPreferNative = defineRule({
41491
41563
  });
41492
41564
  //#endregion
41493
41565
  //#region src/plugin/rules/react-native/rn-detox-missing-await.ts
41494
- const EMPTY_VISITORS$4 = {};
41566
+ const EMPTY_VISITORS$5 = {};
41495
41567
  const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
41496
41568
  const DETOX_ELEMENT_ACTIONS = new Set([
41497
41569
  "tap",
@@ -41551,7 +41623,7 @@ const rnDetoxMissingAwait = defineRule({
41551
41623
  recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
41552
41624
  create: (context) => {
41553
41625
  const filename = normalizeFilename(context.filename ?? "");
41554
- if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$4;
41626
+ if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
41555
41627
  return { ExpressionStatement(node) {
41556
41628
  const expression = node.expression;
41557
41629
  if (!isNodeOfType(expression, "CallExpression")) return;
@@ -42526,7 +42598,7 @@ const isLegacyArchReactNativeFile = (filename) => {
42526
42598
  };
42527
42599
  //#endregion
42528
42600
  //#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
42529
- const EMPTY_VISITORS$3 = {};
42601
+ const EMPTY_VISITORS$4 = {};
42530
42602
  const reportLegacyShadowProperties = (objectExpression, context) => {
42531
42603
  const legacyShadowPropertyNames = [];
42532
42604
  for (const property of objectExpression.properties ?? []) {
@@ -42549,7 +42621,7 @@ const rnNoLegacyShadowStyles = defineRule({
42549
42621
  severity: "warn",
42550
42622
  recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
42551
42623
  create: (context) => {
42552
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$3;
42624
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
42553
42625
  return {
42554
42626
  JSXAttribute(node) {
42555
42627
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -43470,7 +43542,7 @@ const isExpoManagedFileActive = (context) => {
43470
43542
  };
43471
43543
  //#endregion
43472
43544
  //#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
43473
- const EMPTY_VISITORS$2 = {};
43545
+ const EMPTY_VISITORS$3 = {};
43474
43546
  const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
43475
43547
  const MEMBER_PATH_FAN_OUT_LIMIT = 32;
43476
43548
  const isRequireOfBundledAsset = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "require" && node.arguments?.length === 1 && isNodeOfType(node.arguments[0], "Literal") && typeof node.arguments[0].value === "string" && BUNDLED_ASSET_SOURCE_PATTERN.test(node.arguments[0].value);
@@ -43504,7 +43576,7 @@ const rnPreferExpoImage = defineRule({
43504
43576
  severity: "warn",
43505
43577
  recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
43506
43578
  create: (context) => {
43507
- if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$2;
43579
+ if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
43508
43580
  const flaggedImports = [];
43509
43581
  const assetBindingNames = /* @__PURE__ */ new Set();
43510
43582
  const moduleObjectLiterals = /* @__PURE__ */ new Map();
@@ -43991,7 +44063,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
43991
44063
  });
43992
44064
  //#endregion
43993
44065
  //#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
43994
- const EMPTY_VISITORS$1 = {};
44066
+ const EMPTY_VISITORS$2 = {};
43995
44067
  const IOS_SHADOW_KEYS = new Set([
43996
44068
  "shadowColor",
43997
44069
  "shadowOffset",
@@ -44077,7 +44149,7 @@ const rnStylePreferBoxShadow = defineRule({
44077
44149
  severity: "warn",
44078
44150
  recommendation: "These shadow keys only work on one platform. On RN v7+, use the CSS `boxShadow` string instead, like `boxShadow: \"0 2px 8px rgba(0,0,0,0.1)\"`, which works on both.",
44079
44151
  create: (context) => {
44080
- if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$1;
44152
+ if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
44081
44153
  return {
44082
44154
  JSXAttribute(node) {
44083
44155
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
@@ -44174,9 +44246,9 @@ const roleHasRequiredAriaProps = defineRule({
44174
44246
  if (!HTML_TAGS.has(elementType)) return;
44175
44247
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
44176
44248
  if (!roleAttribute) return;
44177
- const roleValue = getJsxPropStringValue(roleAttribute);
44178
- if (roleValue === null) return;
44179
- const roles = roleValue.split(/\s+/).filter((token) => token.length > 0);
44249
+ const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
44250
+ if (roleCandidates === null) return;
44251
+ const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
44180
44252
  for (const role of roles) {
44181
44253
  const required = ROLE_REQUIRED_PROPS.get(role);
44182
44254
  if (!required) continue;
@@ -47293,7 +47365,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
47293
47365
  };
47294
47366
  //#endregion
47295
47367
  //#region src/plugin/rules/a11y/role-supports-aria-props.ts
47296
- const buildMessageDefault = (role, propName) => `Screen reader users get no help from \`${propName}\` because role \`${role}\` ignores it, so remove it or change the role.`;
47368
+ const buildMessageDefault = (roles, propName) => {
47369
+ return `Screen reader users get no help from \`${propName}\` because role ${roles.map((role) => `\`${role}\``).join(" / ")} ignores it, so remove it or change the role.`;
47370
+ };
47297
47371
  const buildMessageImplicit = (role, propName, elementType) => `Screen reader users get no help from \`${propName}\` because \`${elementType}\` has role \`${role}\`, which ignores it, so remove \`${propName}\` or change the element.`;
47298
47372
  const roleSupportsAriaProps = defineRule({
47299
47373
  id: "role-supports-aria-props",
@@ -47321,17 +47395,21 @@ const roleSupportsAriaProps = defineRule({
47321
47395
  if (!ariaAttributes) return;
47322
47396
  const elementType = getElementType(node, context.settings);
47323
47397
  const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
47324
- const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
47325
- if (!role) return;
47326
- if (!VALID_ARIA_ROLES.has(role)) return;
47398
+ const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
47399
+ if (roleCandidates === null || roleCandidates.length === 0) return;
47400
+ const supportedSets = [];
47401
+ for (const role of roleCandidates) {
47402
+ if (!VALID_ARIA_ROLES.has(role)) return;
47403
+ const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47404
+ if (!supported) return;
47405
+ supportedSets.push(supported);
47406
+ }
47327
47407
  const isImplicit = !roleAttribute;
47328
- const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
47329
- if (!supported) return;
47330
47408
  for (const { attribute, propName } of ariaAttributes) {
47331
- if (supported.has(propName)) continue;
47409
+ if (supportedSets.some((supported) => supported.has(propName))) continue;
47332
47410
  context.report({
47333
47411
  node: attribute,
47334
- message: isImplicit ? buildMessageImplicit(role, propName, elementType) : buildMessageDefault(role, propName)
47412
+ message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
47335
47413
  });
47336
47414
  }
47337
47415
  } })
@@ -47683,11 +47761,15 @@ const isUseEffectEventSymbol = (symbol) => {
47683
47761
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47684
47762
  return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
47685
47763
  };
47686
- const isNonReactEffectEventCallee = (callee, contextNode) => isNodeOfType(callee, "Identifier") && isImportedFromNonReactModule(contextNode, callee.name);
47687
- const isNonReactEffectEventSymbol = (symbol, contextNode) => {
47764
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
47765
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
47766
+ return Boolean(symbol && symbol.kind !== "import");
47767
+ };
47768
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
47769
+ const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
47688
47770
  const initializer = symbol.initializer;
47689
47771
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
47690
- return isNonReactEffectEventCallee(initializer.callee, contextNode);
47772
+ return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
47691
47773
  };
47692
47774
  const findEnclosingComponentOrHookFunction = (node) => {
47693
47775
  let current = node.parent;
@@ -47766,7 +47848,7 @@ const rulesOfHooks = defineRule({
47766
47848
  const hookContext = isHookCall$1(node, context.scopes, settings);
47767
47849
  if (!hookContext) return;
47768
47850
  const { hookName } = hookContext;
47769
- if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
47851
+ if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
47770
47852
  context.report({
47771
47853
  node: node.callee,
47772
47854
  message: buildEffectEventPassedDownMessage()
@@ -47879,7 +47961,7 @@ const rulesOfHooks = defineRule({
47879
47961
  Identifier(node) {
47880
47962
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
47881
47963
  if (!symbol || !isUseEffectEventSymbol(symbol)) return;
47882
- if (isNonReactEffectEventSymbol(symbol, node)) return;
47964
+ if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
47883
47965
  if (!isSameComponentOrHookScope(symbol, node)) return;
47884
47966
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
47885
47967
  context.report({
@@ -55451,6 +55533,34 @@ const reactDoctorRules = [
55451
55533
  ];
55452
55534
  const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
55453
55535
  //#endregion
55536
+ //#region src/plugin/utils/is-next-file.ts
55537
+ const isNextFileActive = (context) => {
55538
+ const rawFilename = context.filename;
55539
+ if (!rawFilename) return true;
55540
+ const filename = normalizeFilename(rawFilename);
55541
+ const manifest = readNearestPackageManifest(filename);
55542
+ if (!manifest) return true;
55543
+ if (declaresDependency(manifest, "next")) return true;
55544
+ if (!declaresAnyDependency(manifest)) return true;
55545
+ const packageDirectory = findNearestPackageDirectory(filename);
55546
+ const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
55547
+ if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
55548
+ return true;
55549
+ };
55550
+ //#endregion
55551
+ //#region src/plugin/utils/wrap-nextjs-rule.ts
55552
+ const EMPTY_VISITORS$1 = {};
55553
+ const wrapNextjsRule = (rule) => {
55554
+ const innerCreate = rule.create.bind(rule);
55555
+ return {
55556
+ ...rule,
55557
+ create: (context) => {
55558
+ if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
55559
+ return innerCreate(context);
55560
+ }
55561
+ };
55562
+ };
55563
+ //#endregion
55454
55564
  //#region src/plugin/utils/wrap-react-native-rule.ts
55455
55565
  const EMPTY_VISITORS = {};
55456
55566
  const wrapReactNativeRule = (rule) => {
@@ -55920,9 +56030,14 @@ const wrapWithSemanticContext = (rule) => ({
55920
56030
  });
55921
56031
  //#endregion
55922
56032
  //#region src/plugin/react-doctor-plugin.ts
56033
+ const applyFrameworkGate = (rule) => {
56034
+ if (rule.framework === "react-native") return wrapReactNativeRule(rule);
56035
+ if (rule.framework === "nextjs") return wrapNextjsRule(rule);
56036
+ return rule;
56037
+ };
55923
56038
  const applyFrameworkRuleWrappers = (registry) => {
55924
56039
  const wrapped = {};
55925
- for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(rule.framework === "react-native" ? wrapReactNativeRule(rule) : rule);
56040
+ for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
55926
56041
  return wrapped;
55927
56042
  };
55928
56043
  const plugin = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.2-dev.0eb5293",
3
+ "version": "0.7.2-dev.11e9c87",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",