oxlint-plugin-react-doctor 0.7.4-dev.b47d053 → 0.7.4-dev.b686594

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 (3) hide show
  1. package/dist/index.d.ts +1 -93
  2. package/dist/index.js +323 -1140
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1915,10 +1915,7 @@ const isGeneratedImageRenderContext = (context, node) => {
1915
1915
  if (!node) return false;
1916
1916
  const programRoot = findProgramRoot(node);
1917
1917
  if (!programRoot) return false;
1918
- const generatedImageJsxNodes = collectGeneratedImageJsxNodes(programRoot);
1919
- if (generatedImageJsxNodes.has(node)) return true;
1920
- if (isNodeOfType(node, "JSXElement")) return generatedImageJsxNodes.has(node.openingElement);
1921
- return false;
1918
+ return collectGeneratedImageJsxNodes(programRoot).has(node);
1922
1919
  };
1923
1920
  //#endregion
1924
1921
  //#region src/plugin/utils/object-has-accessible-child.ts
@@ -7675,21 +7672,6 @@ const readStaticBoolean = (node) => {
7675
7672
  return unwrappedNode.value;
7676
7673
  };
7677
7674
  //#endregion
7678
- //#region src/plugin/utils/resolve-const-identifier-alias.ts
7679
- const resolveConstIdentifierAlias = (identifier, scopes) => {
7680
- if (!isNodeOfType(identifier, "Identifier")) return null;
7681
- const visitedSymbolIds = /* @__PURE__ */ new Set();
7682
- let symbol = scopes.symbolFor(identifier);
7683
- while (symbol?.kind === "const") {
7684
- if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
7685
- visitedSymbolIds.add(symbol.id);
7686
- const initializer = stripParenExpression(symbol.initializer);
7687
- if (!isNodeOfType(initializer, "Identifier")) return symbol;
7688
- symbol = scopes.symbolFor(initializer);
7689
- }
7690
- return symbol;
7691
- };
7692
- //#endregion
7693
7675
  //#region src/plugin/utils/is-react-api-call.ts
7694
7676
  const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
7695
7677
  const isImportedFromReact = (symbol) => {
@@ -7705,7 +7687,8 @@ const isNamedReactApiImport = (identifier, apiNames, scopes) => {
7705
7687
  return Boolean(importedName && includesApiName(apiNames, importedName));
7706
7688
  };
7707
7689
  const isReactNamespaceImport = (identifier, scopes) => {
7708
- const symbol = resolveConstIdentifierAlias(identifier, scopes);
7690
+ if (!isNodeOfType(identifier, "Identifier")) return false;
7691
+ const symbol = scopes.symbolFor(identifier);
7709
7692
  if (!symbol || !isImportedFromReact(symbol)) return false;
7710
7693
  return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
7711
7694
  };
@@ -7716,11 +7699,9 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
7716
7699
  if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
7717
7700
  return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
7718
7701
  }
7719
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
7720
- const receiver = stripParenExpression(callee.object);
7721
- if (!isNodeOfType(receiver, "Identifier")) return false;
7722
- if (isReactNamespaceImport(receiver, scopes)) return true;
7723
- return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
7702
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
7703
+ if (isReactNamespaceImport(callee.object, scopes)) return true;
7704
+ return Boolean(options.allowGlobalReactNamespace && callee.object.name === "React" && scopes.isGlobalReference(callee.object));
7724
7705
  };
7725
7706
  //#endregion
7726
7707
  //#region src/plugin/rules/state-and-effects/utils/build-listener-cleanup-mismatch-message.ts
@@ -9875,14 +9856,35 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
9875
9856
  };
9876
9857
  const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
9877
9858
  //#endregion
9859
+ //#region src/plugin/utils/is-imported-from-non-react-module.ts
9860
+ const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
9861
+ const importSource = getImportSourceForName(contextNode, localIdentifierName);
9862
+ if (importSource === null) return false;
9863
+ return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
9864
+ };
9865
+ //#endregion
9866
+ //#region src/plugin/utils/is-non-react-effect-event-callee.ts
9867
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
9868
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
9869
+ return Boolean(symbol && symbol.kind !== "import");
9870
+ };
9871
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
9872
+ if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
9873
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
9874
+ return false;
9875
+ };
9876
+ //#endregion
9878
9877
  //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
9878
+ const getUseEffectEventCalleeName = (callee) => {
9879
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
9880
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9881
+ return null;
9882
+ };
9879
9883
  const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
9880
9884
  const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
9881
9885
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
9882
- return isReactApiCall(initializer, "useEffectEvent", scopes, {
9883
- allowGlobalReactNamespace: true,
9884
- allowUnboundBareCalls: true
9885
- });
9886
+ if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
9887
+ return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
9886
9888
  };
9887
9889
  //#endregion
9888
9890
  //#region src/plugin/rules/react-builtins/exhaustive-deps.ts
@@ -15929,190 +15931,6 @@ const memoStatusForJsxOpeningName = (registry, openingName) => {
15929
15931
  return registry.get(openingName.name) ?? "unknown";
15930
15932
  };
15931
15933
  //#endregion
15932
- //#region src/plugin/utils/unwrap-react-hoc-function.ts
15933
- /**
15934
- * Resolves a `VariableDeclarator.init` (or any expression) to the inline
15935
- * function expression it binds, seeing through chains of `memo` /
15936
- * `forwardRef` / `React.memo` / `React.forwardRef` wrappers:
15937
- *
15938
- * `() => {}` → the arrow
15939
- * `memo(function Foo() {})` → the named function expression
15940
- * `React.memo(forwardRef(() => {}))` → the inner arrow
15941
- * `memo(SomeIdentifier)` → the identifier's same-file function when scopes are provided
15942
- *
15943
- * Component-shaped rules that previously gated on a direct function init
15944
- * (via `isComponentAssignment`) use this so memo-wrapped components are
15945
- * not silently skipped.
15946
- */
15947
- const resolveReactHocFunction = (node, scopes, visitedSymbolIds) => {
15948
- if (!node) return null;
15949
- const current = stripParenExpression(node);
15950
- if (isInlineFunctionExpression(current)) return current;
15951
- if (isNodeOfType(current, "Identifier") && scopes) {
15952
- const symbol = scopes.symbolFor(current);
15953
- if (!symbol || visitedSymbolIds.has(symbol.id) || !symbol.initializer || symbol.kind !== "const" && symbol.kind !== "function") return null;
15954
- if (symbol.kind === "const" && (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier)) return null;
15955
- visitedSymbolIds.add(symbol.id);
15956
- return resolveReactHocFunction(symbol.initializer, scopes, visitedSymbolIds);
15957
- }
15958
- if (isNodeOfType(current, "CallExpression")) {
15959
- const calleeName = flattenCalleeName(current.callee);
15960
- if (!calleeName || !REACT_HOC_NAMES.has(calleeName)) return null;
15961
- const firstArgument = current.arguments[0];
15962
- if (!firstArgument || isNodeOfType(firstArgument, "SpreadElement")) return null;
15963
- return resolveReactHocFunction(firstArgument, scopes, visitedSymbolIds);
15964
- }
15965
- return null;
15966
- };
15967
- const unwrapReactHocFunction = (node, scopes) => resolveReactHocFunction(node, scopes, /* @__PURE__ */ new Set());
15968
- //#endregion
15969
- //#region src/plugin/utils/build-same-file-jsx-slot-prop-registry.ts
15970
- const REACT_SLOT_TYPE_NAMES = new Set(["ReactNode", "ReactElement"]);
15971
- const unwrapTopLevelDeclaration = (statement) => {
15972
- if (isNodeOfType(statement, "ExportNamedDeclaration")) return statement.declaration;
15973
- if (isNodeOfType(statement, "ExportDefaultDeclaration")) return statement.declaration;
15974
- return statement;
15975
- };
15976
- const buildReactSlotTypeEnvironment = (program) => {
15977
- const reactSlotTypeBindings = /* @__PURE__ */ new Set();
15978
- const reactNamespaceBindings = /* @__PURE__ */ new Set();
15979
- const reactJsxNamespaceBindings = /* @__PURE__ */ new Set();
15980
- const nonReactJsxNamespaceBindings = /* @__PURE__ */ new Set();
15981
- const sameFileTypeDeclarations = /* @__PURE__ */ new Map();
15982
- let hasLocalJsxNamespace = false;
15983
- if (!isNodeOfType(program, "Program")) return {
15984
- reactSlotTypeBindings,
15985
- reactNamespaceBindings,
15986
- reactJsxNamespaceBindings,
15987
- nonReactJsxNamespaceBindings,
15988
- sameFileTypeDeclarations,
15989
- hasLocalJsxNamespace
15990
- };
15991
- for (const statement of program.body) {
15992
- if (isNodeOfType(statement, "ImportDeclaration")) {
15993
- const isReactImport = statement.source.value === "react";
15994
- for (const specifier of statement.specifiers) {
15995
- if (isReactImport && (isNodeOfType(specifier, "ImportDefaultSpecifier") || isNodeOfType(specifier, "ImportNamespaceSpecifier"))) {
15996
- reactNamespaceBindings.add(specifier.local.name);
15997
- continue;
15998
- }
15999
- if (!isNodeOfType(specifier, "ImportSpecifier")) continue;
16000
- const importedName = getImportedName(specifier);
16001
- if (!importedName) continue;
16002
- if (isReactImport && REACT_SLOT_TYPE_NAMES.has(importedName)) reactSlotTypeBindings.add(specifier.local.name);
16003
- if (importedName === "JSX") if (isReactImport) reactJsxNamespaceBindings.add(specifier.local.name);
16004
- else nonReactJsxNamespaceBindings.add(specifier.local.name);
16005
- }
16006
- continue;
16007
- }
16008
- const declaration = unwrapTopLevelDeclaration(statement);
16009
- if (!declaration) continue;
16010
- if ((isNodeOfType(declaration, "TSInterfaceDeclaration") || isNodeOfType(declaration, "TSTypeAliasDeclaration")) && isNodeOfType(declaration.id, "Identifier")) sameFileTypeDeclarations.set(declaration.id.name, declaration);
16011
- if (isNodeOfType(declaration, "TSModuleDeclaration") && isNodeOfType(declaration.id, "Identifier") && declaration.id.name === "JSX") hasLocalJsxNamespace = true;
16012
- }
16013
- return {
16014
- reactSlotTypeBindings,
16015
- reactNamespaceBindings,
16016
- reactJsxNamespaceBindings,
16017
- nonReactJsxNamespaceBindings,
16018
- sameFileTypeDeclarations,
16019
- hasLocalJsxNamespace
16020
- };
16021
- };
16022
- const isJsxElementTypeName = (typeName, environment) => {
16023
- if (!isNodeOfType(typeName, "TSQualifiedName")) return false;
16024
- if (!isNodeOfType(typeName.right, "Identifier") || typeName.right.name !== "Element") return false;
16025
- if (isNodeOfType(typeName.left, "Identifier")) {
16026
- const namespaceName = typeName.left.name;
16027
- if (environment.reactNamespaceBindings.has(namespaceName)) return true;
16028
- if (environment.reactJsxNamespaceBindings.has(namespaceName)) return true;
16029
- if (environment.nonReactJsxNamespaceBindings.has(namespaceName)) return false;
16030
- return namespaceName === "JSX" && !environment.hasLocalJsxNamespace;
16031
- }
16032
- return isNodeOfType(typeName.left, "TSQualifiedName") && isNodeOfType(typeName.left.left, "Identifier") && environment.reactNamespaceBindings.has(typeName.left.left.name) && isNodeOfType(typeName.left.right, "Identifier") && typeName.left.right.name === "JSX";
16033
- };
16034
- const isReactSlotType = (typeNode, environment, activeDeclarations) => {
16035
- if (isNodeOfType(typeNode, "TSUnionType")) return typeNode.types.some((unionMember) => isReactSlotType(unionMember, environment, activeDeclarations));
16036
- if (!isNodeOfType(typeNode, "TSTypeReference")) return false;
16037
- if (isJsxElementTypeName(typeNode.typeName, environment)) return true;
16038
- if (isNodeOfType(typeNode.typeName, "TSQualifiedName") && isNodeOfType(typeNode.typeName.left, "Identifier") && environment.reactNamespaceBindings.has(typeNode.typeName.left.name) && isNodeOfType(typeNode.typeName.right, "Identifier") && REACT_SLOT_TYPE_NAMES.has(typeNode.typeName.right.name)) return true;
16039
- if (!isNodeOfType(typeNode.typeName, "Identifier")) return false;
16040
- const typeName = typeNode.typeName.name;
16041
- const sameFileDeclaration = environment.sameFileTypeDeclarations.get(typeName);
16042
- if (sameFileDeclaration) {
16043
- if (!isNodeOfType(sameFileDeclaration, "TSTypeAliasDeclaration") || activeDeclarations.has(sameFileDeclaration)) return false;
16044
- activeDeclarations.add(sameFileDeclaration);
16045
- const isSlot = isReactSlotType(sameFileDeclaration.typeAnnotation, environment, activeDeclarations);
16046
- activeDeclarations.delete(sameFileDeclaration);
16047
- return isSlot;
16048
- }
16049
- return environment.reactSlotTypeBindings.has(typeName);
16050
- };
16051
- const getPropertyName = (property) => {
16052
- if (!isNodeOfType(property, "TSPropertySignature") || property.computed) return null;
16053
- if (isNodeOfType(property.key, "Identifier")) return property.key.name;
16054
- if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") return property.key.value;
16055
- return null;
16056
- };
16057
- const collectSlotPropertyNames = (propsType, environment, slotPropertyNames, activeDeclarations) => {
16058
- let members = null;
16059
- if (isNodeOfType(propsType, "TSTypeLiteral")) members = propsType.members;
16060
- if (isNodeOfType(propsType, "TSInterfaceDeclaration")) members = propsType.body.body;
16061
- if (members) {
16062
- for (const member of members) {
16063
- const propertyName = getPropertyName(member);
16064
- if (!propertyName || !isNodeOfType(member, "TSPropertySignature")) continue;
16065
- const annotation = member.typeAnnotation;
16066
- if (annotation && isNodeOfType(annotation, "TSTypeAnnotation") && isReactSlotType(annotation.typeAnnotation, environment, /* @__PURE__ */ new Set())) slotPropertyNames.add(propertyName);
16067
- }
16068
- return;
16069
- }
16070
- if (isNodeOfType(propsType, "TSIntersectionType")) {
16071
- for (const intersectionMember of propsType.types) collectSlotPropertyNames(intersectionMember, environment, slotPropertyNames, activeDeclarations);
16072
- return;
16073
- }
16074
- if (isNodeOfType(propsType, "TSTypeAliasDeclaration")) {
16075
- collectSlotPropertyNames(propsType.typeAnnotation, environment, slotPropertyNames, activeDeclarations);
16076
- return;
16077
- }
16078
- if (!isNodeOfType(propsType, "TSTypeReference") || !isNodeOfType(propsType.typeName, "Identifier")) return;
16079
- const declaration = environment.sameFileTypeDeclarations.get(propsType.typeName.name);
16080
- if (!declaration || activeDeclarations.has(declaration)) return;
16081
- activeDeclarations.add(declaration);
16082
- collectSlotPropertyNames(declaration, environment, slotPropertyNames, activeDeclarations);
16083
- activeDeclarations.delete(declaration);
16084
- };
16085
- const getMemoizedComponentPropsType = (initializer, scopes) => {
16086
- const componentFunction = unwrapReactHocFunction(initializer, scopes);
16087
- if (componentFunction) {
16088
- const firstParameter = componentFunction.params[0];
16089
- const annotation = firstParameter && "typeAnnotation" in firstParameter ? firstParameter.typeAnnotation : null;
16090
- if (annotation && isNodeOfType(annotation, "TSTypeAnnotation")) return annotation.typeAnnotation;
16091
- }
16092
- if (!isNodeOfType(initializer, "CallExpression")) return null;
16093
- return initializer.typeArguments?.params[0] ?? null;
16094
- };
16095
- const buildSameFileJsxSlotPropRegistry = (program, memoRegistry, scopes) => {
16096
- const registry = /* @__PURE__ */ new Map();
16097
- if (!isNodeOfType(program, "Program")) return registry;
16098
- const environment = buildReactSlotTypeEnvironment(program);
16099
- for (const statement of program.body) {
16100
- const declaration = unwrapTopLevelDeclaration(statement);
16101
- if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) continue;
16102
- for (const declarator of declaration.declarations) {
16103
- if (!isNodeOfType(declarator.id, "Identifier") || memoRegistry.get(declarator.id.name) !== "memoised" || !declarator.init) continue;
16104
- const componentSymbol = scopes.symbolFor(declarator.id);
16105
- if (!componentSymbol) continue;
16106
- const propsType = getMemoizedComponentPropsType(declarator.init, scopes);
16107
- if (!propsType) continue;
16108
- const slotPropertyNames = /* @__PURE__ */ new Set();
16109
- collectSlotPropertyNames(propsType, environment, slotPropertyNames, /* @__PURE__ */ new Set());
16110
- if (slotPropertyNames.size > 0) registry.set(componentSymbol.id, slotPropertyNames);
16111
- }
16112
- }
16113
- return registry;
16114
- };
16115
- //#endregion
16116
15934
  //#region src/plugin/utils/is-on-intrinsic-html-element.ts
16117
15935
  const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
16118
15936
  const openingElement = attribute.parent;
@@ -16386,11 +16204,9 @@ const jsxNoJsxAsProp = defineRule({
16386
16204
  create: (context) => {
16387
16205
  const isTestlikeFile = isTestlikeFilename(context.filename);
16388
16206
  let memoRegistry = null;
16389
- let jsxSlotPropRegistry = null;
16390
16207
  return {
16391
16208
  Program(node) {
16392
16209
  memoRegistry = buildSameFileMemoRegistry(node);
16393
- jsxSlotPropRegistry = buildSameFileJsxSlotPropRegistry(node, memoRegistry, context.scopes);
16394
16210
  },
16395
16211
  JSXAttribute(node) {
16396
16212
  if (isTestlikeFile) return;
@@ -16398,8 +16214,6 @@ const jsxNoJsxAsProp = defineRule({
16398
16214
  const parentJsxOpening = node.parent;
16399
16215
  const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
16400
16216
  if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
16401
- const openingSymbol = openingName && isNodeOfType(openingName, "JSXIdentifier") ? context.scopes.symbolFor(openingName) : null;
16402
- if (openingSymbol && isNodeOfType(node.name, "JSXIdentifier") && jsxSlotPropRegistry?.get(openingSymbol.id)?.has(node.name.name)) return;
16403
16217
  if (isNodeOfType(node.name, "JSXIdentifier") && isSlotPropName(node.name.name)) return;
16404
16218
  if (!isInsideFunctionScope(node)) return;
16405
16219
  const value = node.value;
@@ -27600,24 +27414,6 @@ const noEffectEventHandler = defineRule({
27600
27414
  }
27601
27415
  });
27602
27416
  //#endregion
27603
- //#region src/plugin/utils/is-imported-from-non-react-module.ts
27604
- const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
27605
- const importSource = getImportSourceForName(contextNode, localIdentifierName);
27606
- if (importSource === null) return false;
27607
- return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
27608
- };
27609
- //#endregion
27610
- //#region src/plugin/utils/is-non-react-effect-event-callee.ts
27611
- const resolvesToLocalNonImportBinding = (identifier, scopes) => {
27612
- const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
27613
- return Boolean(symbol && symbol.kind !== "import");
27614
- };
27615
- const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
27616
- if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
27617
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
27618
- return false;
27619
- };
27620
- //#endregion
27621
27417
  //#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
27622
27418
  const createComponentBindingStackTracker = (callbacks) => {
27623
27419
  const componentBindingStack = [];
@@ -28699,61 +28495,13 @@ const noEventTriggerState = defineRule({
28699
28495
  }
28700
28496
  });
28701
28497
  //#endregion
28702
- //#region src/plugin/utils/resolve-expression-key.ts
28703
- const resolveExpressionKey = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
28704
- if (!expression) return null;
28705
- const unwrappedExpression = stripParenExpression(expression);
28706
- if (isNodeOfType(unwrappedExpression, "Identifier")) {
28707
- const symbol = context.scopes.symbolFor(unwrappedExpression);
28708
- if (!symbol) return context.scopes.isGlobalReference(unwrappedExpression) ? `global:${unwrappedExpression.name}` : null;
28709
- if (visitedSymbolIds.has(symbol.id)) return `symbol:${symbol.id}`;
28710
- visitedSymbolIds.add(symbol.id);
28711
- const bindingProperty = symbol.bindingIdentifier.parent;
28712
- const bindingPattern = bindingProperty?.parent;
28713
- const variableDeclarator = bindingPattern?.parent;
28714
- const bindingPropertyName = isNodeOfType(bindingProperty, "Property") ? getStaticPropertyKeyName(bindingProperty) : null;
28715
- if (bindingPropertyName && isNodeOfType(bindingPattern, "ObjectPattern") && isNodeOfType(variableDeclarator, "VariableDeclarator") && variableDeclarator.id === bindingPattern) {
28716
- const objectKey = resolveExpressionKey(variableDeclarator.init, context, visitedSymbolIds);
28717
- return objectKey ? `${objectKey}.${bindingPropertyName}` : `symbol:${symbol.id}`;
28718
- }
28719
- const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
28720
- if (symbol.kind === "const" && initializer && (isNodeOfType(initializer, "Identifier") || isNodeOfType(initializer, "MemberExpression"))) return resolveExpressionKey(initializer, context, visitedSymbolIds) ?? `symbol:${symbol.id}`;
28721
- return `symbol:${symbol.id}`;
28722
- }
28723
- if (isNodeOfType(unwrappedExpression, "MemberExpression") && !unwrappedExpression.computed) {
28724
- if (!isNodeOfType(unwrappedExpression.property, "Identifier")) return null;
28725
- const objectKey = resolveExpressionKey(unwrappedExpression.object, context, visitedSymbolIds);
28726
- return objectKey ? `${objectKey}.${unwrappedExpression.property.name}` : null;
28727
- }
28728
- if (isNodeOfType(unwrappedExpression, "ThisExpression")) return "this";
28729
- if (isNodeOfType(unwrappedExpression, "Literal") && (typeof unwrappedExpression.value === "string" || typeof unwrappedExpression.value === "number")) return `literal:${String(unwrappedExpression.value)}`;
28730
- if (isFunctionLike$1(unwrappedExpression)) {
28731
- const rangeStart = getRangeStart(unwrappedExpression);
28732
- return rangeStart === null ? null : `function:${rangeStart}`;
28733
- }
28734
- return null;
28735
- };
28736
- //#endregion
28737
- //#region src/plugin/utils/statement-always-exits.ts
28738
- const statementAlwaysExits = (statement) => {
28739
- if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
28740
- if (isNodeOfType(statement, "IfStatement")) return Boolean(statement.alternate && statementAlwaysExits(statement.consequent) && statementAlwaysExits(statement.alternate));
28741
- if (!isNodeOfType(statement, "BlockStatement")) return false;
28742
- return statement.body.some((childStatement) => statementAlwaysExits(childStatement));
28743
- };
28744
- //#endregion
28745
28498
  //#region src/plugin/rules/state-and-effects/no-fetch-in-effect.ts
28746
28499
  const IMPORT_INITIALIZER_TYPES = new Set([
28747
28500
  "ImportSpecifier",
28748
28501
  "ImportDefaultSpecifier",
28749
28502
  "ImportNamespaceSpecifier"
28750
28503
  ]);
28751
- const CANCELLATION_FLAG_NAME_PATTERN = /cancel|ignore/i;
28752
- const PROMISE_CONTINUATION_METHOD_NAMES = new Set([
28753
- "then",
28754
- "catch",
28755
- "finally"
28756
- ]);
28504
+ const CANCELLATION_FLAG_NAME_PATTERN = /cancel/i;
28757
28505
  const isShadowedByLocalBinding = (identifier) => {
28758
28506
  if (!isNodeOfType(identifier, "Identifier")) return false;
28759
28507
  const binding = findVariableInitializer(identifier, identifier.name);
@@ -28769,54 +28517,49 @@ const isRealFetchCall = (node) => {
28769
28517
  };
28770
28518
  const isXmlHttpRequestConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "XMLHttpRequest";
28771
28519
  const isNetworkRequest = (node) => isRealFetchCall(node) || isXmlHttpRequestConstruction(node);
28772
- const resolveLocalFunction = (expression, context) => {
28773
- if (!expression) return null;
28774
- const unwrappedExpression = stripParenExpression(expression);
28775
- if (isFunctionLike$1(unwrappedExpression)) return unwrappedExpression;
28776
- if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
28777
- const initializer = context.scopes.symbolFor(unwrappedExpression)?.initializer;
28778
- if (!initializer) return null;
28779
- const unwrappedInitializer = stripParenExpression(initializer);
28780
- return isFunctionLike$1(unwrappedInitializer) ? unwrappedInitializer : null;
28520
+ const containsNetworkRequest = (root) => {
28521
+ let found = false;
28522
+ walkAst(root, (child) => {
28523
+ if (found) return false;
28524
+ if (isNetworkRequest(child)) {
28525
+ found = true;
28526
+ return false;
28527
+ }
28528
+ });
28529
+ return found;
28781
28530
  };
28782
- const collectEffectAnalysisFunctions = (effectCallback, context) => {
28783
- const analysisFunctions = /* @__PURE__ */ new Set();
28784
- const pendingFunctions = [];
28785
- const enqueueFunction = (functionNode) => {
28786
- for (const invokedFunction of collectEffectInvokedFunctions(functionNode)) {
28787
- if (analysisFunctions.has(invokedFunction)) continue;
28788
- analysisFunctions.add(invokedFunction);
28789
- pendingFunctions.push(invokedFunction);
28531
+ const collectComponentScopeFunctionBindings = (componentScope, effectCallback) => {
28532
+ const bindings = /* @__PURE__ */ new Map();
28533
+ walkAst(componentScope, (child) => {
28534
+ if (child === effectCallback) return false;
28535
+ if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
28536
+ const initializer = child.init ? stripParenExpression(child.init) : null;
28537
+ if (isFunctionLike$1(initializer)) bindings.set(child.id.name, initializer);
28538
+ return;
28790
28539
  }
28791
- };
28792
- enqueueFunction(effectCallback);
28793
- while (pendingFunctions.length > 0) {
28794
- const currentFunction = pendingFunctions.pop();
28795
- if (!currentFunction) break;
28796
- walkAst(currentFunction, (child) => {
28797
- if (child !== currentFunction && isFunctionLike$1(child)) return false;
28798
- if (!isNodeOfType(child, "CallExpression")) return;
28799
- const calledFunction = resolveLocalFunction(child.callee, context);
28800
- if (calledFunction) enqueueFunction(calledFunction);
28801
- for (const callArgument of child.arguments ?? []) {
28802
- const callbackFunction = resolveLocalFunction(callArgument, context);
28803
- if (callbackFunction) enqueueFunction(callbackFunction);
28804
- }
28805
- });
28806
- }
28807
- return analysisFunctions;
28540
+ if (isNodeOfType(child, "FunctionDeclaration") && isNodeOfType(child.id, "Identifier")) bindings.set(child.id.name, child);
28541
+ });
28542
+ return bindings;
28808
28543
  };
28809
- const collectNodesFromAnalysisFunctions = (analysisFunctions, predicate) => {
28810
- const nodes = [];
28811
- const seenNodes = /* @__PURE__ */ new Set();
28812
- for (const analysisFunction of analysisFunctions) walkAst(analysisFunction, (child) => {
28813
- if (child !== analysisFunction && isFunctionLike$1(child)) return false;
28814
- if (!seenNodes.has(child) && predicate(child)) {
28815
- seenNodes.add(child);
28816
- nodes.push(child);
28544
+ const containsNetworkRequestInEffect = (effectCallback) => {
28545
+ const invokedFunctions = collectEffectInvokedFunctions(effectCallback);
28546
+ for (const invokedFunction of invokedFunctions) if (containsNetworkRequest(invokedFunction)) return true;
28547
+ const enclosingComponent = findEnclosingFunction(effectCallback);
28548
+ if (!enclosingComponent) return false;
28549
+ const scopeFunctions = collectComponentScopeFunctionBindings(enclosingComponent, effectCallback);
28550
+ let found = false;
28551
+ walkAst(effectCallback, (child) => {
28552
+ if (found) return false;
28553
+ if (child !== effectCallback && isFunctionLike$1(child) && invokedFunctions.has(child)) return false;
28554
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier")) {
28555
+ const scopeFunction = scopeFunctions.get(child.callee.name);
28556
+ if (scopeFunction && containsNetworkRequest(scopeFunction)) {
28557
+ found = true;
28558
+ return false;
28559
+ }
28817
28560
  }
28818
28561
  });
28819
- return nodes;
28562
+ return found;
28820
28563
  };
28821
28564
  const findEffectCleanupFunction = (callback) => {
28822
28565
  if (!isFunctionLike$1(callback)) return null;
@@ -28829,177 +28572,36 @@ const findEffectCleanupFunction = (callback) => {
28829
28572
  });
28830
28573
  return cleanup;
28831
28574
  };
28832
- const collectEffectCancellationFlagKeys = (effectCallback, context) => {
28833
- const flagKeys = /* @__PURE__ */ new Set();
28834
- if (!isFunctionLike$1(effectCallback)) return flagKeys;
28575
+ const collectEffectCancellationFlagNames = (effectCallback) => {
28576
+ const flagNames = /* @__PURE__ */ new Set();
28577
+ if (!isFunctionLike$1(effectCallback)) return flagNames;
28835
28578
  const body = effectCallback.body;
28836
- if (!isNodeOfType(body, "BlockStatement")) return flagKeys;
28579
+ if (!isNodeOfType(body, "BlockStatement")) return flagNames;
28837
28580
  for (const statement of body.body ?? []) {
28838
28581
  if (!isNodeOfType(statement, "VariableDeclaration") || statement.kind !== "let") continue;
28839
28582
  for (const declarator of statement.declarations ?? []) {
28840
28583
  if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier")) continue;
28841
28584
  const initializer = declarator.init;
28842
- if (isNodeOfType(initializer, "Literal") && initializer.value === false && CANCELLATION_FLAG_NAME_PATTERN.test(declarator.id.name)) {
28843
- const flagKey = resolveExpressionKey(declarator.id, context);
28844
- if (flagKey) flagKeys.add(flagKey);
28845
- }
28585
+ if (isNodeOfType(initializer, "Literal") && initializer.value === false && CANCELLATION_FLAG_NAME_PATTERN.test(declarator.id.name)) flagNames.add(declarator.id.name);
28846
28586
  }
28847
28587
  }
28848
- return flagKeys;
28588
+ return flagNames;
28849
28589
  };
28850
- const resolveConstValue = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
28851
- if (!expression) return null;
28852
- const unwrappedExpression = stripParenExpression(expression);
28853
- if (!isNodeOfType(unwrappedExpression, "Identifier")) return unwrappedExpression;
28854
- const symbol = context.scopes.symbolFor(unwrappedExpression);
28855
- if (!symbol || symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return unwrappedExpression;
28856
- visitedSymbolIds.add(symbol.id);
28857
- return resolveConstValue(symbol.initializer, context, visitedSymbolIds);
28858
- };
28859
- const resolveRequestAbortControllerKey = (request, context) => {
28860
- if (isNodeOfType(request, "NewExpression")) {
28861
- const declarator = request.parent;
28862
- return isNodeOfType(declarator, "VariableDeclarator") && declarator.init === request ? resolveExpressionKey(declarator.id, context) : null;
28863
- }
28864
- if (!isNodeOfType(request, "CallExpression")) return null;
28865
- for (const requestArgument of request.arguments ?? []) {
28866
- const options = resolveConstValue(requestArgument, context);
28867
- if (!isNodeOfType(options, "ObjectExpression")) continue;
28868
- for (const property of options.properties ?? []) {
28869
- if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property, { allowComputedString: true }) !== "signal") continue;
28870
- const signalKey = resolveExpressionKey(property.value, context);
28871
- return signalKey?.endsWith(".signal") ? signalKey.slice(0, -7) : null;
28872
- }
28873
- }
28874
- return null;
28875
- };
28876
- const collectCleanupAbortedControllerKeys = (cleanup, context) => {
28877
- const controllerKeys = /* @__PURE__ */ new Set();
28878
- walkAst(cleanup, (child) => {
28879
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && !child.callee.computed && isNodeOfType(child.callee.property, "Identifier") && child.callee.property.name === "abort") {
28880
- const controllerKey = resolveExpressionKey(child.callee.object, context);
28881
- if (controllerKey) controllerKeys.add(controllerKey);
28882
- }
28883
- });
28884
- return controllerKeys;
28885
- };
28886
- const collectCleanupAssignedCancellationFlagKeys = (cleanup, cancellationFlagKeys, context) => {
28887
- const assignedFlagKeys = /* @__PURE__ */ new Set();
28590
+ const isCancellationCleanup = (cleanup, effectCallback) => {
28591
+ const cancellationFlagNames = collectEffectCancellationFlagNames(effectCallback);
28592
+ let found = false;
28888
28593
  walkAst(cleanup, (child) => {
28889
- if (isNodeOfType(child, "AssignmentExpression") && isNodeOfType(child.right, "Literal") && child.right.value === true) {
28890
- const assignedKey = resolveExpressionKey(child.left, context);
28891
- if (assignedKey && cancellationFlagKeys.has(assignedKey)) assignedFlagKeys.add(assignedKey);
28892
- }
28893
- });
28894
- return assignedFlagKeys;
28895
- };
28896
- const readCancellationCondition = (expression, cancellationFlagKey, context) => {
28897
- const unwrappedExpression = stripParenExpression(expression);
28898
- if (resolveExpressionKey(unwrappedExpression, context) === cancellationFlagKey) return true;
28899
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
28900
- const argumentResult = readCancellationCondition(unwrappedExpression.argument, cancellationFlagKey, context);
28901
- return argumentResult === null ? null : !argumentResult;
28902
- }
28903
- if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
28904
- const leftResult = readCancellationCondition(unwrappedExpression.left, cancellationFlagKey, context);
28905
- const rightResult = readCancellationCondition(unwrappedExpression.right, cancellationFlagKey, context);
28906
- const leftBoolean = isNodeOfType(unwrappedExpression.left, "Literal") && typeof unwrappedExpression.left.value === "boolean" ? unwrappedExpression.left.value : null;
28907
- const rightBoolean = isNodeOfType(unwrappedExpression.right, "Literal") && typeof unwrappedExpression.right.value === "boolean" ? unwrappedExpression.right.value : null;
28908
- const comparedCancellationValue = leftResult !== null && rightBoolean !== null ? rightBoolean : rightResult !== null && leftBoolean !== null ? leftBoolean : null;
28909
- if (comparedCancellationValue === null) return null;
28910
- if (unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==") return comparedCancellationValue;
28911
- if (unwrappedExpression.operator === "!==" || unwrappedExpression.operator === "!=") return !comparedCancellationValue;
28912
- return null;
28913
- };
28914
- const isCompletionSinkInsideCancellationGuard = (completionSink, cancellationFlagKey, context) => {
28915
- let currentNode = completionSink;
28916
- let parentNode = currentNode.parent;
28917
- while (parentNode) {
28918
- if (isFunctionLike$1(parentNode)) return false;
28919
- if (isNodeOfType(parentNode, "IfStatement")) {
28920
- const cancellationResult = readCancellationCondition(parentNode.test, cancellationFlagKey, context);
28921
- if (currentNode === parentNode.consequent && cancellationResult === false) return true;
28922
- if (currentNode === parentNode.alternate && cancellationResult === true) return true;
28923
- }
28924
- if (isNodeOfType(parentNode, "ConditionalExpression")) {
28925
- const cancellationResult = readCancellationCondition(parentNode.test, cancellationFlagKey, context);
28926
- if (currentNode === parentNode.consequent && cancellationResult === false) return true;
28927
- if (currentNode === parentNode.alternate && cancellationResult === true) return true;
28928
- }
28929
- if (isNodeOfType(parentNode, "LogicalExpression") && currentNode === parentNode.right) {
28930
- const cancellationResult = readCancellationCondition(parentNode.left, cancellationFlagKey, context);
28931
- if (parentNode.operator === "&&" && cancellationResult === false) return true;
28932
- if (parentNode.operator === "||" && cancellationResult === true) return true;
28594
+ if (found) return false;
28595
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && child.callee.property.name === "abort") {
28596
+ found = true;
28597
+ return false;
28933
28598
  }
28934
- currentNode = parentNode;
28935
- parentNode = currentNode.parent;
28936
- }
28937
- return false;
28938
- };
28939
- const isCompletionSinkAfterCancellationEarlyExit = (completionSink, cancellationFlagKey, context) => {
28940
- let currentNode = completionSink;
28941
- let parentNode = currentNode.parent;
28942
- while (parentNode) {
28943
- if (isFunctionLike$1(parentNode)) return false;
28944
- if (isNodeOfType(parentNode, "BlockStatement")) {
28945
- const statementIndex = parentNode.body.findIndex((statement) => statement === currentNode);
28946
- if (statementIndex >= 0) for (const statement of parentNode.body.slice(0, statementIndex)) {
28947
- if (!isNodeOfType(statement, "IfStatement")) continue;
28948
- const cancellationResult = readCancellationCondition(statement.test, cancellationFlagKey, context);
28949
- if (cancellationResult === true && statementAlwaysExits(statement.consequent) || cancellationResult === false && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
28950
- }
28599
+ if (isNodeOfType(child, "AssignmentExpression") && isNodeOfType(child.left, "Identifier") && cancellationFlagNames.has(child.left.name) && isNodeOfType(child.right, "Literal") && child.right.value === true) {
28600
+ found = true;
28601
+ return false;
28951
28602
  }
28952
- currentNode = parentNode;
28953
- parentNode = currentNode.parent;
28954
- }
28955
- return false;
28956
- };
28957
- const isCompletionSinkGuardedByCancellationFlag = (completionSink, cancellationFlagKey, context) => isCompletionSinkInsideCancellationGuard(completionSink, cancellationFlagKey, context) || isCompletionSinkAfterCancellationEarlyExit(completionSink, cancellationFlagKey, context);
28958
- const isDescendantOf = (node, ancestor) => {
28959
- let currentNode = node;
28960
- while (currentNode) {
28961
- if (currentNode === ancestor) return true;
28962
- currentNode = currentNode.parent ?? null;
28963
- }
28964
- return false;
28965
- };
28966
- const isPromiseContinuationForRequest = (functionNode, request) => {
28967
- const callNode = functionNode.parent;
28968
- if (!isNodeOfType(callNode, "CallExpression") || !callNode.arguments?.some((callArgument) => callArgument === functionNode) || !isNodeOfType(callNode.callee, "MemberExpression") || callNode.callee.computed || !isNodeOfType(callNode.callee.property, "Identifier") || !PROMISE_CONTINUATION_METHOD_NAMES.has(callNode.callee.property.name)) return false;
28969
- return isDescendantOf(request, callNode.callee.object);
28970
- };
28971
- const isAwaitedInFunction = (request, functionNode) => {
28972
- let currentNode = request.parent;
28973
- while (currentNode && currentNode !== functionNode) {
28974
- if (isNodeOfType(currentNode, "AwaitExpression")) return true;
28975
- currentNode = currentNode.parent ?? null;
28976
- }
28977
- return false;
28978
- };
28979
- const isCompletionSinkForRequest = (completionSink, request) => {
28980
- const requestFunction = findEnclosingFunction(request);
28981
- const completionSinkFunction = findEnclosingFunction(completionSink);
28982
- if (!requestFunction || !completionSinkFunction) return false;
28983
- if (requestFunction === completionSinkFunction) {
28984
- if (!isAwaitedInFunction(request, requestFunction)) return false;
28985
- const requestStart = getRangeStart(request);
28986
- const completionSinkStart = getRangeStart(completionSink);
28987
- return requestStart === null || completionSinkStart === null || completionSinkStart > requestStart;
28988
- }
28989
- return isPromiseContinuationForRequest(completionSinkFunction, request);
28990
- };
28991
- const requestHasGuardedCompletionSinks = (request, completionSinks, assignedCancellationFlagKeys, context) => {
28992
- const followingCompletionSinks = completionSinks.filter((completionSink) => isCompletionSinkForRequest(completionSink, request));
28993
- return followingCompletionSinks.length > 0 && followingCompletionSinks.every((completionSink) => [...assignedCancellationFlagKeys].some((cancellationFlagKey) => isCompletionSinkGuardedByCancellationFlag(completionSink, cancellationFlagKey, context)));
28994
- };
28995
- const allRequestsHaveCorrelatedCancellation = (requests, completionSinks, cleanup, effectCallback, context) => {
28996
- const abortedControllerKeys = collectCleanupAbortedControllerKeys(cleanup, context);
28997
- const assignedCancellationFlagKeys = collectCleanupAssignedCancellationFlagKeys(cleanup, collectEffectCancellationFlagKeys(effectCallback, context), context);
28998
- return requests.every((request) => {
28999
- const requestControllerKey = resolveRequestAbortControllerKey(request, context);
29000
- if (requestControllerKey && abortedControllerKeys.has(requestControllerKey)) return true;
29001
- return requestHasGuardedCompletionSinks(request, completionSinks, assignedCancellationFlagKeys, context);
29002
28603
  });
28604
+ return found;
29003
28605
  };
29004
28606
  const noFetchInEffect = defineRule({
29005
28607
  id: "no-fetch-in-effect",
@@ -29010,13 +28612,9 @@ const noFetchInEffect = defineRule({
29010
28612
  if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
29011
28613
  const callback = getEffectCallback(node);
29012
28614
  if (!callback) return;
29013
- const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
29014
- const requests = collectNodesFromAnalysisFunctions(analysisFunctions, isNetworkRequest);
29015
- if (requests.length === 0) return;
28615
+ if (!containsNetworkRequestInEffect(callback)) return;
29016
28616
  const cleanup = findEffectCleanupFunction(callback);
29017
- if (cleanup) {
29018
- if (allRequestsHaveCorrelatedCancellation(requests, collectNodesFromAnalysisFunctions(analysisFunctions, isSetterCall), cleanup, callback, context)) return;
29019
- }
28617
+ if (cleanup && isCancellationCleanup(cleanup, callback)) return;
29020
28618
  context.report({
29021
28619
  node,
29022
28620
  message: "fetch() inside useEffect can race, double-fire, or leak. Use a data-fetching layer or Server Component instead."
@@ -29316,6 +28914,34 @@ const noGenericHandlerNames = defineRule({
29316
28914
  } })
29317
28915
  });
29318
28916
  //#endregion
28917
+ //#region src/plugin/utils/unwrap-react-hoc-function.ts
28918
+ /**
28919
+ * Resolves a `VariableDeclarator.init` (or any expression) to the inline
28920
+ * function expression it binds, seeing through chains of `memo` /
28921
+ * `forwardRef` / `React.memo` / `React.forwardRef` wrappers:
28922
+ *
28923
+ * `() => {}` → the arrow
28924
+ * `memo(function Foo() {})` → the named function expression
28925
+ * `React.memo(forwardRef(() => {}))` → the inner arrow
28926
+ * `memo(SomeIdentifier)` → `null` (no inline function)
28927
+ *
28928
+ * Component-shaped rules that previously gated on a direct function init
28929
+ * (via `isComponentAssignment`) use this so memo-wrapped components are
28930
+ * not silently skipped.
28931
+ */
28932
+ const unwrapReactHocFunction = (node) => {
28933
+ if (!node) return null;
28934
+ let current = stripParenExpression(node);
28935
+ while (isNodeOfType(current, "CallExpression")) {
28936
+ const calleeName = flattenCalleeName(current.callee);
28937
+ if (!calleeName || !REACT_HOC_NAMES.has(calleeName)) return null;
28938
+ const firstArgument = current.arguments[0];
28939
+ if (!firstArgument) return null;
28940
+ current = stripParenExpression(firstArgument);
28941
+ }
28942
+ return isInlineFunctionExpression(current) ? current : null;
28943
+ };
28944
+ //#endregion
29319
28945
  //#region src/plugin/rules/architecture/no-giant-component.ts
29320
28946
  const noGiantComponent = defineRule({
29321
28947
  id: "no-giant-component",
@@ -29568,470 +29194,6 @@ const noGrayOnColoredBackground = defineRule({
29568
29194
  } })
29569
29195
  });
29570
29196
  //#endregion
29571
- //#region src/plugin/utils/find-declarator-for-binding.ts
29572
- const findDeclaratorForBinding = (bindingIdentifier) => {
29573
- let ancestor = bindingIdentifier.parent;
29574
- while (ancestor) {
29575
- if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
29576
- if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
29577
- ancestor = ancestor.parent ?? null;
29578
- }
29579
- return null;
29580
- };
29581
- //#endregion
29582
- //#region src/plugin/utils/executes-during-render.ts
29583
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
29584
- "map",
29585
- "filter",
29586
- "forEach",
29587
- "flatMap",
29588
- "reduce",
29589
- "reduceRight",
29590
- "some",
29591
- "every",
29592
- "find",
29593
- "findIndex",
29594
- "findLast",
29595
- "findLastIndex",
29596
- "sort",
29597
- "toSorted"
29598
- ]);
29599
- const REACT_RENDER_PHASE_HOOK_NAMES = new Set(["useMemo", "useState"]);
29600
- const isGlobalArrayFromMember = (node, scopes) => {
29601
- const unwrappedNode = stripParenExpression(node);
29602
- if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed || !isNodeOfType(unwrappedNode.object, "Identifier") || unwrappedNode.object.name !== "Array" || !scopes.isGlobalReference(unwrappedNode.object) || !isNodeOfType(unwrappedNode.property, "Identifier")) return false;
29603
- return unwrappedNode.property.name === "from";
29604
- };
29605
- const isConstAliasOfGlobalArrayFrom = (node, scopes) => {
29606
- const unwrappedNode = stripParenExpression(node);
29607
- if (!isNodeOfType(unwrappedNode, "Identifier")) return false;
29608
- const binding = findVariableInitializer(unwrappedNode, unwrappedNode.name);
29609
- if (!binding?.initializer) return false;
29610
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29611
- return Boolean(declarator && scopes.symbolFor(unwrappedNode)?.declarationNode === declarator && declarator.init && declarator.parent && isNodeOfType(declarator.parent, "VariableDeclaration") && declarator.parent.kind === "const" && isGlobalArrayFromMember(declarator.init, scopes));
29612
- };
29613
- const executesDuringRender = (functionNode, scopes) => {
29614
- const parent = functionNode.parent;
29615
- if (!isNodeOfType(parent, "CallExpression")) return false;
29616
- if (parent.callee === functionNode) return true;
29617
- if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === functionNode) return true;
29618
- if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
29619
- return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
29620
- };
29621
- //#endregion
29622
- //#region src/plugin/utils/find-enclosing-jsx-opening-element.ts
29623
- const findEnclosingJsxOpeningElement = (node) => {
29624
- let cursor = node.parent;
29625
- while (cursor) {
29626
- if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
29627
- if (isNodeOfType(cursor, "JSXFragment")) return null;
29628
- cursor = cursor.parent ?? null;
29629
- }
29630
- return null;
29631
- };
29632
- //#endregion
29633
- //#region src/plugin/utils/find-render-phase-component-or-hook.ts
29634
- const findRenderPhaseComponentOrHook = (node, scopes) => {
29635
- let functionNode = findEnclosingFunction(node);
29636
- while (functionNode) {
29637
- if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
29638
- if (!executesDuringRender(functionNode, scopes)) return null;
29639
- functionNode = findEnclosingFunction(functionNode);
29640
- }
29641
- return null;
29642
- };
29643
- //#endregion
29644
- //#region src/plugin/utils/has-client-render-evidence.ts
29645
- const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
29646
- if (fileHasUseClientDirective) return true;
29647
- const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
29648
- if (displayName && isReactHookName(displayName)) return true;
29649
- let callsHook = false;
29650
- walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
29651
- if (callsHook) return false;
29652
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
29653
- callsHook = true;
29654
- return false;
29655
- }
29656
- });
29657
- return callsHook;
29658
- };
29659
- //#endregion
29660
- //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
29661
- const hasSuppressHydrationWarningAttribute = (openingElement) => {
29662
- if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
29663
- for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
29664
- return false;
29665
- };
29666
- //#endregion
29667
- //#region src/plugin/utils/read-initial-state-boolean.ts
29668
- const readLogicalResult = (operator, leftResult, rightResult) => {
29669
- if (operator === "&&") {
29670
- if (leftResult === false || rightResult === false) return false;
29671
- if (leftResult === true && rightResult === true) return true;
29672
- return null;
29673
- }
29674
- if (leftResult === true || rightResult === true) return true;
29675
- if (leftResult === false && rightResult === false) return false;
29676
- return null;
29677
- };
29678
- const readIdentifierInitialStateBoolean = (identifier, scopes, visitedBindings, allowLazyInitializer) => {
29679
- if (!isNodeOfType(identifier, "Identifier")) return null;
29680
- if (identifier.name === "undefined" && scopes.isGlobalReference(identifier)) return false;
29681
- const binding = findVariableInitializer(identifier, identifier.name);
29682
- if (!binding || visitedBindings.has(binding.bindingIdentifier)) return null;
29683
- visitedBindings.add(binding.bindingIdentifier);
29684
- const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
29685
- if (!declarator?.init || scopes.symbolFor(identifier)?.declarationNode !== declarator) return null;
29686
- const initializer = stripParenExpression(declarator.init);
29687
- if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements?.[0] === binding.bindingIdentifier && isReactApiCall(initializer, "useState", scopes, { allowGlobalReactNamespace: true }) && isNodeOfType(initializer, "CallExpression")) {
29688
- const initialState = initializer.arguments?.[0];
29689
- if (!initialState) return false;
29690
- if (initialState.type === "SpreadElement") return null;
29691
- return readInitialStateBooleanInternal(initialState, scopes, visitedBindings, true);
29692
- }
29693
- const declaration = declarator.parent;
29694
- if (!isNodeOfType(declarator.id, "Identifier") || declarator.id !== binding.bindingIdentifier || !isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
29695
- return readInitialStateBooleanInternal(initializer, scopes, visitedBindings, allowLazyInitializer);
29696
- };
29697
- const readInitialStateBooleanInternal = (expression, scopes, visitedBindings, allowLazyInitializer) => {
29698
- const unwrappedExpression = stripParenExpression(expression);
29699
- if (isNodeOfType(unwrappedExpression, "Literal")) return Boolean(unwrappedExpression.value);
29700
- if (isNodeOfType(unwrappedExpression, "Identifier")) return readIdentifierInitialStateBoolean(unwrappedExpression, scopes, visitedBindings, allowLazyInitializer);
29701
- if (allowLazyInitializer && (isNodeOfType(unwrappedExpression, "ArrowFunctionExpression") || isNodeOfType(unwrappedExpression, "FunctionExpression"))) {
29702
- if (unwrappedExpression.async || isNodeOfType(unwrappedExpression, "FunctionExpression") && unwrappedExpression.generator) return null;
29703
- if (!isNodeOfType(unwrappedExpression.body, "BlockStatement")) return readInitialStateBooleanInternal(unwrappedExpression.body, scopes, visitedBindings, false);
29704
- if (unwrappedExpression.body.body.length !== 1) return null;
29705
- const returnStatement = unwrappedExpression.body.body[0];
29706
- if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return null;
29707
- return readInitialStateBooleanInternal(returnStatement.argument, scopes, visitedBindings, false);
29708
- }
29709
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
29710
- const argumentResult = readInitialStateBooleanInternal(unwrappedExpression.argument, scopes, visitedBindings, false);
29711
- return argumentResult === null ? null : !argumentResult;
29712
- }
29713
- if (isNodeOfType(unwrappedExpression, "LogicalExpression") && (unwrappedExpression.operator === "&&" || unwrappedExpression.operator === "||")) return readLogicalResult(unwrappedExpression.operator, readInitialStateBooleanInternal(unwrappedExpression.left, scopes, new Set(visitedBindings), false), readInitialStateBooleanInternal(unwrappedExpression.right, scopes, new Set(visitedBindings), false));
29714
- return null;
29715
- };
29716
- const readInitialStateBoolean = (expression, scopes) => readInitialStateBooleanInternal(expression, scopes, /* @__PURE__ */ new Set(), false);
29717
- //#endregion
29718
- //#region src/plugin/utils/is-after-client-only-early-return.ts
29719
- const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode, scopes) => {
29720
- const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
29721
- if (!isNodeOfType(body, "BlockStatement")) return false;
29722
- const ancestors = /* @__PURE__ */ new Set();
29723
- let currentNode = node;
29724
- while (currentNode) {
29725
- ancestors.add(currentNode);
29726
- currentNode = currentNode.parent ?? null;
29727
- }
29728
- for (const statement of body.body ?? []) {
29729
- if (ancestors.has(statement)) return false;
29730
- if (!isNodeOfType(statement, "IfStatement")) continue;
29731
- const initialConditionResult = readInitialStateBoolean(statement.test, scopes);
29732
- if (initialConditionResult === true && statementAlwaysExits(statement.consequent)) return true;
29733
- if (initialConditionResult === false && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
29734
- }
29735
- return false;
29736
- };
29737
- //#endregion
29738
- //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
29739
- const isGatedByFalsyInitialState = (node, scopes) => {
29740
- let cursor = node;
29741
- let parent = node.parent;
29742
- while (parent) {
29743
- if (isNodeOfType(parent, "LogicalExpression") && parent.right === cursor && (parent.operator === "&&" && readInitialStateBoolean(parent.left, scopes) === false || parent.operator === "||" && readInitialStateBoolean(parent.left, scopes) === true)) return true;
29744
- if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
29745
- if (isNodeOfType(parent, "IfStatement") && (parent.consequent === cursor && readInitialStateBoolean(parent.test, scopes) === false || parent.alternate === cursor && readInitialStateBoolean(parent.test, scopes) === true)) return true;
29746
- cursor = parent;
29747
- parent = parent.parent ?? null;
29748
- }
29749
- return false;
29750
- };
29751
- //#endregion
29752
- //#region src/plugin/utils/is-event-handler-attribute.ts
29753
- const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
29754
- //#endregion
29755
- //#region src/plugin/rules/performance/no-hydration-branch-on-browser-global.ts
29756
- const evaluateEquality = (operator, left, right) => {
29757
- if (operator === "===" || operator === "==") return left === right;
29758
- if (operator === "!==" || operator === "!=") return left !== right;
29759
- return null;
29760
- };
29761
- const readTypeofBrowserGlobal = (expression, context) => {
29762
- const unwrappedExpression = stripParenExpression(expression);
29763
- if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
29764
- const argument = stripParenExpression(unwrappedExpression.argument);
29765
- if (isNodeOfType(argument, "Identifier")) return (argument.name === "window" || argument.name === "document") && context.scopes.isGlobalReference(argument) ? argument.name : null;
29766
- if (!isNodeOfType(argument, "MemberExpression") || argument.computed || !isNodeOfType(argument.object, "Identifier") || argument.object.name !== "globalThis" || !context.scopes.isGlobalReference(argument.object) || !isNodeOfType(argument.property, "Identifier") || argument.property.name !== "window" && argument.property.name !== "document") return null;
29767
- return argument.property.name;
29768
- };
29769
- const matchBrowserPredicate = (expression, context) => {
29770
- const unwrappedExpression = stripParenExpression(expression);
29771
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
29772
- const innerMatch = matchBrowserPredicate(unwrappedExpression.argument, context);
29773
- return innerMatch ? {
29774
- browserGlobalName: innerMatch.browserGlobalName,
29775
- clientResult: !innerMatch.clientResult,
29776
- serverResult: !innerMatch.serverResult
29777
- } : null;
29778
- }
29779
- if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
29780
- const leftGlobalName = readTypeofBrowserGlobal(unwrappedExpression.left, context);
29781
- const rightGlobalName = readTypeofBrowserGlobal(unwrappedExpression.right, context);
29782
- const leftString = isNodeOfType(unwrappedExpression.left, "Literal") ? unwrappedExpression.left.value : null;
29783
- const rightString = isNodeOfType(unwrappedExpression.right, "Literal") ? unwrappedExpression.right.value : null;
29784
- const browserGlobalName = leftGlobalName && typeof rightString === "string" ? leftGlobalName : rightGlobalName && typeof leftString === "string" ? rightGlobalName : null;
29785
- const comparedType = leftGlobalName && typeof rightString === "string" ? rightString : rightGlobalName && typeof leftString === "string" ? leftString : null;
29786
- if (!browserGlobalName || !comparedType) return null;
29787
- const clientResult = evaluateEquality(unwrappedExpression.operator, "object", comparedType);
29788
- const serverResult = evaluateEquality(unwrappedExpression.operator, "undefined", comparedType);
29789
- if (clientResult === null || serverResult === null || clientResult === serverResult) return null;
29790
- return {
29791
- browserGlobalName,
29792
- clientResult,
29793
- serverResult
29794
- };
29795
- };
29796
- const readLogicalConditionResult = (operator, leftResult, rightResult) => {
29797
- if (operator === "&&") {
29798
- if (leftResult === false || rightResult === false) return false;
29799
- if (leftResult === true && rightResult === true) return true;
29800
- return null;
29801
- }
29802
- if (leftResult === true || rightResult === true) return true;
29803
- if (leftResult === false && rightResult === false) return false;
29804
- return null;
29805
- };
29806
- const readHydrationConditionResult = (expression, context, runtime) => {
29807
- const unwrappedExpression = stripParenExpression(expression);
29808
- const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
29809
- if (predicateMatch) return predicateMatch[`${runtime}Result`];
29810
- const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
29811
- if (staticResult !== null) return staticResult;
29812
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
29813
- const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
29814
- return argumentResult === null ? null : !argumentResult;
29815
- }
29816
- if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
29817
- return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
29818
- };
29819
- const matchHydrationCondition = (expression, context) => {
29820
- const unwrappedExpression = stripParenExpression(expression);
29821
- const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
29822
- if (predicateMatch) return {
29823
- predicateMatch,
29824
- predicateNode: unwrappedExpression
29825
- };
29826
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationCondition(unwrappedExpression.argument, context);
29827
- if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
29828
- const leftMatch = matchHydrationCondition(unwrappedExpression.left, context);
29829
- const rightMatch = matchHydrationCondition(unwrappedExpression.right, context);
29830
- if (leftMatch && rightMatch) {
29831
- const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client");
29832
- const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server");
29833
- return clientResult !== null && serverResult !== null && clientResult !== serverResult ? leftMatch : null;
29834
- }
29835
- const nestedMatch = leftMatch ?? rightMatch;
29836
- if (!nestedMatch) return null;
29837
- const otherResult = readInitialStateBoolean(leftMatch ? unwrappedExpression.right : unwrappedExpression.left, context.scopes);
29838
- if (unwrappedExpression.operator === "&&" && otherResult === false || unwrappedExpression.operator === "||" && otherResult === true) return null;
29839
- return nestedMatch;
29840
- };
29841
- const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
29842
- const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
29843
- if (!leftNode || !rightNode) return leftNode === rightNode;
29844
- const left = stripParenExpression(leftNode);
29845
- const right = stripParenExpression(rightNode);
29846
- if (areExpressionsStructurallyEqual(left, right)) return true;
29847
- if (left.type !== right.type) return false;
29848
- if (isNodeOfType(left, "JSXText") && isNodeOfType(right, "JSXText")) return left.value === right.value;
29849
- if (isNodeOfType(left, "JSXExpressionContainer") && isNodeOfType(right, "JSXExpressionContainer")) {
29850
- if (!isAstNode(left.expression) || !isAstNode(right.expression)) return left.expression.type === right.expression.type;
29851
- return areRenderedBranchesEquivalent(left.expression, right.expression);
29852
- }
29853
- if (isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement")) {
29854
- if (flattenJsxName$1(left.openingElement.name) !== flattenJsxName$1(right.openingElement.name)) return false;
29855
- if (!areNodeArraysEquivalent(left.openingElement.attributes, right.openingElement.attributes)) return false;
29856
- return areNodeArraysEquivalent(left.children, right.children);
29857
- }
29858
- if (isNodeOfType(left, "JSXFragment") && isNodeOfType(right, "JSXFragment")) return areNodeArraysEquivalent(left.children, right.children);
29859
- if (isNodeOfType(left, "JSXAttribute") && isNodeOfType(right, "JSXAttribute")) {
29860
- if (flattenJsxName$1(left.name) !== flattenJsxName$1(right.name)) return false;
29861
- return areRenderedBranchesEquivalent(left.value, right.value);
29862
- }
29863
- if (isNodeOfType(left, "JSXSpreadAttribute") && isNodeOfType(right, "JSXSpreadAttribute")) return areRenderedBranchesEquivalent(left.argument, right.argument);
29864
- if (isNodeOfType(left, "TemplateLiteral") && isNodeOfType(right, "TemplateLiteral")) {
29865
- if (left.quasis.length !== right.quasis.length) return false;
29866
- if (!left.quasis.every((quasi, index) => quasi.value.cooked === right.quasis[index]?.value.cooked && quasi.value.raw === right.quasis[index]?.value.raw)) return false;
29867
- return areNodeArraysEquivalent(left.expressions, right.expressions);
29868
- }
29869
- return false;
29870
- };
29871
- const isRenderedValue = (node) => {
29872
- const unwrappedNode = stripParenExpression(node);
29873
- if (isNodeOfType(unwrappedNode, "Literal")) return unwrappedNode.value !== null && unwrappedNode.value !== true && unwrappedNode.value !== false && unwrappedNode.value !== "";
29874
- if (isNodeOfType(unwrappedNode, "TemplateLiteral")) return unwrappedNode.expressions.length > 0 || unwrappedNode.quasis[0]?.value.cooked !== "";
29875
- return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
29876
- };
29877
- const findRenderedValueInAndBranch = (node) => {
29878
- const unwrappedNode = stripParenExpression(node);
29879
- if (isRenderedValue(unwrappedNode)) return unwrappedNode;
29880
- if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "&&") return null;
29881
- return findRenderedValueInAndBranch(unwrappedNode.right);
29882
- };
29883
- const findEnclosingJsxAttribute = (node) => {
29884
- let currentNode = node.parent;
29885
- while (currentNode) {
29886
- if (isNodeOfType(currentNode, "JSXAttribute")) return currentNode;
29887
- if (isNodeOfType(currentNode, "JSXElement") || isNodeOfType(currentNode, "JSXFragment") || isFunctionLike$1(currentNode)) return null;
29888
- currentNode = currentNode.parent;
29889
- }
29890
- return null;
29891
- };
29892
- const isInRenderedOutput = (node, componentOrHookNode, scopes) => {
29893
- let currentNode = node;
29894
- let parentNode = currentNode.parent;
29895
- while (parentNode) {
29896
- if (isNodeOfType(parentNode, "JSXExpressionContainer")) {
29897
- const attribute = findEnclosingJsxAttribute(parentNode);
29898
- return attribute ? !isEventHandlerAttribute(attribute) : true;
29899
- }
29900
- if (isNodeOfType(parentNode, "ReturnStatement")) {
29901
- if (findEnclosingFunction(parentNode) === componentOrHookNode) return true;
29902
- }
29903
- if (parentNode === componentOrHookNode) return isFunctionLike$1(componentOrHookNode) && !isNodeOfType(componentOrHookNode.body, "BlockStatement") && componentOrHookNode.body === currentNode;
29904
- if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, scopes)) return false;
29905
- currentNode = parentNode;
29906
- parentNode = currentNode.parent;
29907
- }
29908
- return false;
29909
- };
29910
- const getReturnedValues = (statement) => {
29911
- if (!statement) return [];
29912
- if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? [statement.argument] : [];
29913
- if (isNodeOfType(statement, "IfStatement")) return [...getReturnedValues(statement.consequent), ...getReturnedValues(statement.alternate)];
29914
- if (!isNodeOfType(statement, "BlockStatement")) return [];
29915
- const returnedValues = [];
29916
- for (const childStatement of statement.body) {
29917
- returnedValues.push(...getReturnedValues(childStatement));
29918
- if (statementAlwaysExits(childStatement)) break;
29919
- }
29920
- return returnedValues;
29921
- };
29922
- const findFollowingReturnedValues = (ifStatement) => {
29923
- const parentNode = ifStatement.parent;
29924
- if (!isNodeOfType(parentNode, "BlockStatement")) return [];
29925
- const statementIndex = parentNode.body.findIndex((statement) => statement === ifStatement);
29926
- if (statementIndex < 0) return [];
29927
- const returnedValues = [];
29928
- for (const statement of parentNode.body.slice(statementIndex + 1)) {
29929
- returnedValues.push(...getReturnedValues(statement));
29930
- if (statementAlwaysExits(statement)) break;
29931
- }
29932
- return returnedValues;
29933
- };
29934
- const areConditionExpressionsEquivalent = (leftExpression, rightExpression) => {
29935
- const left = stripParenExpression(leftExpression);
29936
- const right = stripParenExpression(rightExpression);
29937
- if (areExpressionsStructurallyEqual(left, right)) return true;
29938
- if (left.type !== right.type) return false;
29939
- if (isNodeOfType(left, "UnaryExpression") && isNodeOfType(right, "UnaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.argument, right.argument);
29940
- if (isNodeOfType(left, "LogicalExpression") && isNodeOfType(right, "LogicalExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
29941
- if (isNodeOfType(left, "BinaryExpression") && isNodeOfType(right, "BinaryExpression")) return left.operator === right.operator && areConditionExpressionsEquivalent(left.left, right.left) && areConditionExpressionsEquivalent(left.right, right.right);
29942
- return false;
29943
- };
29944
- const areReturnTreesEquivalent = (leftStatement, rightStatement) => {
29945
- if (!leftStatement || !rightStatement) return leftStatement === rightStatement;
29946
- if (isNodeOfType(leftStatement, "ReturnStatement") && isNodeOfType(rightStatement, "ReturnStatement")) return areRenderedBranchesEquivalent(leftStatement.argument, rightStatement.argument);
29947
- if (isNodeOfType(leftStatement, "IfStatement") && isNodeOfType(rightStatement, "IfStatement")) return areConditionExpressionsEquivalent(leftStatement.test, rightStatement.test) && areReturnTreesEquivalent(leftStatement.consequent, rightStatement.consequent) && areReturnTreesEquivalent(leftStatement.alternate, rightStatement.alternate);
29948
- if (!isNodeOfType(leftStatement, "BlockStatement") || !isNodeOfType(rightStatement, "BlockStatement")) return false;
29949
- const leftReturningStatements = leftStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
29950
- const rightReturningStatements = rightStatement.body.filter((statement) => getReturnedValues(statement).length > 0);
29951
- return leftReturningStatements.length === rightReturningStatements.length && leftReturningStatements.every((statement, index) => areReturnTreesEquivalent(statement, rightReturningStatements[index]));
29952
- };
29953
- const isStructuralRenderedValue = (node) => {
29954
- if (!node) return false;
29955
- const unwrappedNode = stripParenExpression(node);
29956
- return isNodeOfType(unwrappedNode, "JSXElement") || isNodeOfType(unwrappedNode, "JSXFragment");
29957
- };
29958
- const branchRootsSuppressSameElement = (leftBranch, rightBranch) => {
29959
- if (!rightBranch) return false;
29960
- const left = stripParenExpression(leftBranch);
29961
- const right = stripParenExpression(rightBranch);
29962
- return isNodeOfType(left, "JSXElement") && isNodeOfType(right, "JSXElement") && flattenJsxName$1(left.openingElement.name) === flattenJsxName$1(right.openingElement.name) && hasSuppressHydrationWarningAttribute(left.openingElement) && hasSuppressHydrationWarningAttribute(right.openingElement);
29963
- };
29964
- const noHydrationBranchOnBrowserGlobal = defineRule({
29965
- id: "no-hydration-branch-on-browser-global",
29966
- title: "Server and client render different branches",
29967
- severity: "error",
29968
- category: "Correctness",
29969
- requires: ["ssr"],
29970
- recommendation: "Render the same initial output on the server and client, then switch after mount or use useSyncExternalStore with a stable server snapshot.",
29971
- create: (context) => {
29972
- if (isTestlikeFilename(context.filename)) return {};
29973
- if (classifyReactNativeFileTarget(context) === "react-native") return {};
29974
- let fileHasUseClientDirective = false;
29975
- let fileIsEmailTemplate = false;
29976
- const reportedNodes = /* @__PURE__ */ new Set();
29977
- const reportHydrationBranch = (conditionNode, leftBranch, rightBranch, requiresRenderedContext) => {
29978
- const conditionMatch = matchHydrationCondition(conditionNode, context);
29979
- if (!conditionMatch) return;
29980
- const { predicateMatch, predicateNode } = conditionMatch;
29981
- if (reportedNodes.has(predicateNode)) return;
29982
- if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
29983
- const componentOrHookNode = findRenderPhaseComponentOrHook(predicateNode, context.scopes);
29984
- if (!componentOrHookNode) return;
29985
- if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
29986
- if (requiresRenderedContext && !isInRenderedOutput(predicateNode, componentOrHookNode, context.scopes)) return;
29987
- if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
29988
- const attribute = findEnclosingJsxAttribute(predicateNode);
29989
- if (!attribute || isEventHandlerAttribute(attribute)) return;
29990
- }
29991
- if (fileIsEmailTemplate || isGatedByFalsyInitialState(predicateNode, context.scopes)) return;
29992
- if (isAfterClientOnlyEarlyReturn(predicateNode, componentOrHookNode, context.scopes)) return;
29993
- const openingElement = findEnclosingJsxOpeningElement(predicateNode);
29994
- if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
29995
- if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
29996
- if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
29997
- reportedNodes.add(predicateNode);
29998
- context.report({
29999
- node: predicateNode,
30000
- message: `\`typeof ${predicateMatch.browserGlobalName}\` selects different rendered output on the server and during hydration. Render the same initial output, then switch after mount.`
30001
- });
30002
- };
30003
- return {
30004
- Program(node) {
30005
- fileHasUseClientDirective = hasDirective(node, "use client");
30006
- fileIsEmailTemplate = hasEmailTemplateImport(node);
30007
- },
30008
- ConditionalExpression(node) {
30009
- reportHydrationBranch(node.test, node.consequent, node.alternate, true);
30010
- },
30011
- LogicalExpression(node) {
30012
- if (node.operator !== "&&" && node.operator !== "||") return;
30013
- const renderedValue = node.operator === "&&" ? findRenderedValueInAndBranch(node.right) : isRenderedValue(node.right) ? node.right : null;
30014
- if (!renderedValue) return;
30015
- reportHydrationBranch(node, renderedValue, null, true);
30016
- },
30017
- IfStatement(node) {
30018
- if (node.alternate && areReturnTreesEquivalent(node.consequent, node.alternate)) return;
30019
- const consequentValues = getReturnedValues(node.consequent);
30020
- const alternateValues = node.alternate ? getReturnedValues(node.alternate) : findFollowingReturnedValues(node);
30021
- if (consequentValues.length === 0 || alternateValues.length === 0) return;
30022
- const componentOrHookNode = findRenderPhaseComponentOrHook(node.test, context.scopes);
30023
- if (!componentOrHookNode) return;
30024
- const enclosingFunction = findEnclosingFunction(node);
30025
- if (enclosingFunction !== componentOrHookNode && (!enclosingFunction || !isInRenderedOutput(enclosingFunction, componentOrHookNode, context.scopes))) return;
30026
- for (const consequentValue of consequentValues) for (const alternateValue of alternateValues) {
30027
- if (!isRenderedValue(consequentValue) && !isRenderedValue(alternateValue)) continue;
30028
- reportHydrationBranch(node.test, consequentValue, alternateValue, false);
30029
- }
30030
- }
30031
- };
30032
- }
30033
- });
30034
- //#endregion
30035
29197
  //#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
30036
29198
  const MESSAGE$22 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
30037
29199
  const noImgLazyWithHighFetchpriority = defineRule({
@@ -30877,6 +30039,111 @@ const noLegacyContextApi = defineRule({
30877
30039
  }
30878
30040
  });
30879
30041
  //#endregion
30042
+ //#region src/plugin/utils/executes-during-render.ts
30043
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
30044
+ "map",
30045
+ "filter",
30046
+ "forEach",
30047
+ "flatMap",
30048
+ "reduce",
30049
+ "reduceRight",
30050
+ "some",
30051
+ "every",
30052
+ "find",
30053
+ "findIndex",
30054
+ "findLast",
30055
+ "findLastIndex",
30056
+ "sort",
30057
+ "toSorted"
30058
+ ]);
30059
+ const executesDuringRender = (functionNode) => {
30060
+ const parent = functionNode.parent;
30061
+ if (!isNodeOfType(parent, "CallExpression")) return false;
30062
+ if (parent.callee === functionNode) return true;
30063
+ if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
30064
+ return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
30065
+ };
30066
+ //#endregion
30067
+ //#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
30068
+ const hasSuppressHydrationWarningAttribute = (openingElement) => {
30069
+ if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
30070
+ for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
30071
+ return false;
30072
+ };
30073
+ //#endregion
30074
+ //#region src/plugin/utils/find-declarator-for-binding.ts
30075
+ const findDeclaratorForBinding = (bindingIdentifier) => {
30076
+ let ancestor = bindingIdentifier.parent;
30077
+ while (ancestor) {
30078
+ if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
30079
+ if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
30080
+ ancestor = ancestor.parent ?? null;
30081
+ }
30082
+ return null;
30083
+ };
30084
+ //#endregion
30085
+ //#region src/plugin/utils/references-falsy-initial-state.ts
30086
+ const isFalsyLiteral = (node) => {
30087
+ if (!node) return true;
30088
+ if (isNodeOfType(node, "Literal")) return !node.value;
30089
+ return isNodeOfType(node, "Identifier") && node.name === "undefined";
30090
+ };
30091
+ const isFalsyInitialStateBinding = (identifier) => {
30092
+ if (!isNodeOfType(identifier, "Identifier")) return false;
30093
+ const binding = findVariableInitializer(identifier, identifier.name);
30094
+ if (!binding) return false;
30095
+ const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
30096
+ if (!declarator?.init) return false;
30097
+ const init = stripParenExpression(declarator.init);
30098
+ if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
30099
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
30100
+ if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
30101
+ return isFalsyLiteral(init.arguments?.[0]);
30102
+ };
30103
+ const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
30104
+ //#endregion
30105
+ //#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
30106
+ const isGatedByFalsyInitialState = (node) => {
30107
+ let cursor = node;
30108
+ let parent = node.parent;
30109
+ while (parent) {
30110
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
30111
+ if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
30112
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
30113
+ cursor = parent;
30114
+ parent = parent.parent ?? null;
30115
+ }
30116
+ return false;
30117
+ };
30118
+ //#endregion
30119
+ //#region src/plugin/utils/references-client-only-flag.ts
30120
+ const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
30121
+ const referencesClientOnlyFlag = (expression) => {
30122
+ const unwrapped = stripParenExpression(expression);
30123
+ if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
30124
+ if (isNodeOfType(unwrapped, "MemberExpression")) {
30125
+ const property = unwrapped.property;
30126
+ return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
30127
+ }
30128
+ if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
30129
+ if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
30130
+ return false;
30131
+ };
30132
+ //#endregion
30133
+ //#region src/plugin/utils/is-inside-client-only-guard.ts
30134
+ const isInsideClientOnlyGuard = (node) => {
30135
+ let cursor = node;
30136
+ let parent = node.parent;
30137
+ while (parent) {
30138
+ if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
30139
+ if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
30140
+ if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
30141
+ cursor = parent;
30142
+ parent = parent.parent ?? null;
30143
+ }
30144
+ return false;
30145
+ };
30146
+ //#endregion
30880
30147
  //#region src/plugin/rules/performance/no-locale-format-in-render.ts
30881
30148
  const LOCALE_FORMAT_METHOD_NAMES = new Set([
30882
30149
  "toLocaleString",
@@ -31006,12 +30273,75 @@ const matchDateDefaultStringification = (node) => {
31006
30273
  }
31007
30274
  return null;
31008
30275
  };
30276
+ const findRenderPhaseComponentOrHook = (node) => {
30277
+ let functionNode = findEnclosingFunction(node);
30278
+ while (functionNode) {
30279
+ if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
30280
+ if (!executesDuringRender(functionNode)) return null;
30281
+ functionNode = findEnclosingFunction(functionNode);
30282
+ }
30283
+ return null;
30284
+ };
30285
+ const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
30286
+ if (fileHasUseClientDirective) return true;
30287
+ const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
30288
+ if (displayName && isReactHookName(displayName)) return true;
30289
+ let callsHook = false;
30290
+ walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
30291
+ if (callsHook) return false;
30292
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
30293
+ callsHook = true;
30294
+ return false;
30295
+ }
30296
+ });
30297
+ return callsHook;
30298
+ };
30299
+ const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
30300
+ const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
30301
+ if (!isNodeOfType(body, "BlockStatement")) return false;
30302
+ const ancestors = /* @__PURE__ */ new Set();
30303
+ let cursor = node;
30304
+ while (cursor) {
30305
+ ancestors.add(cursor);
30306
+ cursor = cursor.parent ?? null;
30307
+ }
30308
+ for (const statement of body.body ?? []) {
30309
+ if (ancestors.has(statement)) return false;
30310
+ if (!isNodeOfType(statement, "IfStatement")) continue;
30311
+ if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
30312
+ let returnsEarly = false;
30313
+ walkAst(statement.consequent, (child) => {
30314
+ if (isFunctionLike$1(child)) return false;
30315
+ if (isNodeOfType(child, "ReturnStatement")) {
30316
+ returnsEarly = true;
30317
+ return false;
30318
+ }
30319
+ });
30320
+ if (returnsEarly) return true;
30321
+ }
30322
+ return false;
30323
+ };
30324
+ const findEnclosingJsxOpeningElement = (node) => {
30325
+ let cursor = node.parent;
30326
+ while (cursor) {
30327
+ if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
30328
+ if (isNodeOfType(cursor, "JSXFragment")) return null;
30329
+ cursor = cursor.parent ?? null;
30330
+ }
30331
+ return null;
30332
+ };
31009
30333
  const noLocaleFormatInRender = defineRule({
31010
30334
  id: "no-locale-format-in-render",
31011
30335
  title: "Locale/timezone formatting during render",
31012
30336
  severity: "warn",
31013
30337
  category: "Correctness",
31014
- requires: ["ssr"],
30338
+ disabledWhen: [
30339
+ "vite",
30340
+ "cra",
30341
+ "expo",
30342
+ "react-native",
30343
+ "unknown"
30344
+ ],
31015
30345
  recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
31016
30346
  create: (context) => {
31017
30347
  if (isTestlikeFilename(context.filename)) return {};
@@ -31021,12 +30351,13 @@ const noLocaleFormatInRender = defineRule({
31021
30351
  const reportedNodes = /* @__PURE__ */ new Set();
31022
30352
  const reportIfRenderPhase = (match) => {
31023
30353
  if (reportedNodes.has(match.node)) return;
31024
- const componentOrHookNode = findRenderPhaseComponentOrHook(match.node, context.scopes);
30354
+ const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
31025
30355
  if (!componentOrHookNode) return;
31026
30356
  if (fileIsEmailTemplate) return;
31027
30357
  if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
31028
- if (isGatedByFalsyInitialState(match.node, context.scopes)) return;
31029
- if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode, context.scopes)) return;
30358
+ if (isInsideClientOnlyGuard(match.node)) return;
30359
+ if (isGatedByFalsyInitialState(match.node)) return;
30360
+ if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
31030
30361
  if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
31031
30362
  if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
31032
30363
  reportedNodes.add(match.node);
@@ -31053,7 +30384,7 @@ const noLocaleFormatInRender = defineRule({
31053
30384
  if (!isNodeOfType(expression, "CallExpression")) return;
31054
30385
  if (!isNodeOfType(expression.callee, "Identifier")) return;
31055
30386
  const helperName = expression.callee.name;
31056
- const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
30387
+ const componentOrHookNode = findRenderPhaseComponentOrHook(node);
31057
30388
  if (!componentOrHookNode) return;
31058
30389
  const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
31059
30390
  if (!helperNode || !isFunctionLike$1(helperNode)) return;
@@ -31065,8 +30396,9 @@ const noLocaleFormatInRender = defineRule({
31065
30396
  if (!match || reportedNodes.has(match.node)) return;
31066
30397
  if (fileIsEmailTemplate) return;
31067
30398
  if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
31068
- if (isGatedByFalsyInitialState(node, context.scopes)) return;
31069
- if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
30399
+ if (isInsideClientOnlyGuard(node)) return;
30400
+ if (isGatedByFalsyInitialState(node)) return;
30401
+ if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
31070
30402
  if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
31071
30403
  reportedNodes.add(match.node);
31072
30404
  context.report({
@@ -31168,6 +30500,9 @@ const noLongTransitionDuration = defineRule({
31168
30500
  const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
31169
30501
  const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
31170
30502
  //#endregion
30503
+ //#region src/plugin/utils/is-event-handler-attribute.ts
30504
+ const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
30505
+ //#endregion
31171
30506
  //#region src/plugin/rules/architecture/no-many-boolean-props.ts
31172
30507
  const IMPERATIVE_CALLBACK_PREFIX_PATTERN = /^(?:show|hide|enable|disable)[A-Z]/;
31173
30508
  const collectCallbackUsedNames = (componentBody, propsParam, scopes) => {
@@ -31299,7 +30634,7 @@ const noMatchMediaInStateInitializer = defineRule({
31299
30634
  title: "matchMedia in state initializer",
31300
30635
  severity: "warn",
31301
30636
  category: "Correctness",
31302
- requires: ["ssr"],
30637
+ disabledWhen: ["vite", "cra"],
31303
30638
  recommendation: "Prefer CSS media queries for layout, or subscribe with `useSyncExternalStore` and provide a stable server snapshot.",
31304
30639
  create: (context) => {
31305
30640
  if (isTestlikeFilename(context.filename)) return {};
@@ -34103,11 +33438,7 @@ const noReactChildren = defineRule({
34103
33438
  //#endregion
34104
33439
  //#region src/plugin/rules/architecture/utils/create-deprecated-react-import-rule.ts
34105
33440
  const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }) => ({ create: (context) => {
34106
- const namespaceImportSpecifiers = /* @__PURE__ */ new Set();
34107
- const resolvesToNamespaceImport = (identifier) => {
34108
- const symbol = resolveConstIdentifierAlias(identifier, context.scopes);
34109
- return Boolean(symbol && namespaceImportSpecifiers.has(symbol.declarationNode));
34110
- };
33441
+ const namespaceBindings = /* @__PURE__ */ new Set();
34111
33442
  return {
34112
33443
  ImportDeclaration(node) {
34113
33444
  const sourceValue = node.source?.value;
@@ -34127,15 +33458,18 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
34127
33458
  });
34128
33459
  continue;
34129
33460
  }
34130
- if (isNodeOfType(specifier, "ImportDefaultSpecifier") || isNodeOfType(specifier, "ImportNamespaceSpecifier")) namespaceImportSpecifiers.add(specifier);
33461
+ if (isNodeOfType(specifier, "ImportDefaultSpecifier") || isNodeOfType(specifier, "ImportNamespaceSpecifier")) {
33462
+ const localName = specifier.local?.name;
33463
+ if (localName) namespaceBindings.add(localName);
33464
+ }
34131
33465
  }
34132
33466
  },
34133
33467
  MemberExpression(node) {
34134
- if (namespaceImportSpecifiers.size === 0) return;
33468
+ if (namespaceBindings.size === 0) return;
34135
33469
  if (node.computed) return;
34136
33470
  const receiver = stripParenExpression(node.object);
34137
33471
  if (!isNodeOfType(receiver, "Identifier")) return;
34138
- if (!resolvesToNamespaceImport(receiver)) return;
33472
+ if (!namespaceBindings.has(receiver.name)) return;
34139
33473
  if (!isNodeOfType(node.property, "Identifier")) return;
34140
33474
  const message = messages.get(node.property.name);
34141
33475
  if (message) context.report({
@@ -34200,9 +33534,15 @@ const noReactDomDeprecatedApis = defineRule({
34200
33534
  });
34201
33535
  //#endregion
34202
33536
  //#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
33537
+ const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
33538
+ const isVendoredShadcnUiFilename = (rawFilename) => {
33539
+ if (!rawFilename) return false;
33540
+ const filename = rawFilename.replaceAll("\\", "/");
33541
+ return (filename.startsWith("/") ? filename : `/${filename}`).includes("/components/ui/");
33542
+ };
34203
33543
  const deprecatedReactImportRule = createDeprecatedReactImportRule({
34204
33544
  source: "react",
34205
- messages: new Map([["createFactory", "React 19 removed createFactory. Use JSX or createElement instead."]])
33545
+ messages: REACT_19_DEPRECATED_MESSAGES
34206
33546
  });
34207
33547
  const buildOncePerApiContext = (context) => {
34208
33548
  const reportedMessages = /* @__PURE__ */ new Set();
@@ -34232,8 +33572,11 @@ const noReact19DeprecatedApis = defineRule({
34232
33572
  requires: ["react:19"],
34233
33573
  tags: ["test-noise", "migration-hint"],
34234
33574
  severity: "warn",
34235
- recommendation: "Replace removed React APIs with their supported React 19 alternatives. Only runs on React 19+ projects.",
34236
- create: (context) => deprecatedReactImportRule.create(buildOncePerApiContext(context))
33575
+ recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
33576
+ create: (context) => {
33577
+ if (isVendoredShadcnUiFilename(context.filename)) return {};
33578
+ return deprecatedReactImportRule.create(buildOncePerApiContext(context));
33579
+ }
34237
33580
  });
34238
33581
  //#endregion
34239
33582
  //#region src/plugin/constants/aria-element-roles.ts
@@ -36566,140 +35909,6 @@ const noUnescapedEntities = defineRule({
36566
35909
  } })
36567
35910
  });
36568
35911
  //#endregion
36569
- //#region src/plugin/rules/performance/no-unguarded-browser-global-in-render-or-hook-init.ts
36570
- const BROWSER_GLOBAL_NAMES = new Set([
36571
- "window",
36572
- "document",
36573
- "localStorage",
36574
- "sessionStorage",
36575
- "navigator",
36576
- "matchMedia"
36577
- ]);
36578
- const getTypeofBrowserGlobalName = (expression, context) => {
36579
- const unwrappedExpression = stripParenExpression(expression);
36580
- if (!isNodeOfType(unwrappedExpression, "UnaryExpression") || unwrappedExpression.operator !== "typeof") return null;
36581
- const argument = stripParenExpression(unwrappedExpression.argument);
36582
- if (isNodeOfType(argument, "Identifier")) return BROWSER_GLOBAL_NAMES.has(argument.name) && context.scopes.isGlobalReference(argument) ? argument.name : null;
36583
- if (!isNodeOfType(argument, "MemberExpression") || argument.computed || !isNodeOfType(argument.object, "Identifier") || argument.object.name !== "globalThis" || !context.scopes.isGlobalReference(argument.object) || !isNodeOfType(argument.property, "Identifier") || !BROWSER_GLOBAL_NAMES.has(argument.property.name)) return null;
36584
- return argument.property.name;
36585
- };
36586
- const browserGuardCoversGlobal = (guardName, browserGlobalName) => guardName === browserGlobalName || guardName === "window" || guardName === "document";
36587
- const mergeAvailability = (leftAvailability, rightAvailability) => {
36588
- if (leftAvailability === null) return rightAvailability;
36589
- if (rightAvailability === null) return leftAvailability;
36590
- return leftAvailability === rightAvailability ? leftAvailability : null;
36591
- };
36592
- const readAvailabilityWhenPredicate = (expression, browserGlobalName, context, predicateResult) => {
36593
- const unwrappedExpression = stripParenExpression(expression);
36594
- if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return readAvailabilityWhenPredicate(unwrappedExpression.argument, browserGlobalName, context, !predicateResult);
36595
- if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
36596
- if (unwrappedExpression.operator === "&&" && predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, true), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, true));
36597
- if (unwrappedExpression.operator === "||" && !predicateResult) return mergeAvailability(readAvailabilityWhenPredicate(unwrappedExpression.left, browserGlobalName, context, false), readAvailabilityWhenPredicate(unwrappedExpression.right, browserGlobalName, context, false));
36598
- return null;
36599
- }
36600
- if (!isNodeOfType(unwrappedExpression, "BinaryExpression")) return null;
36601
- const leftTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.left, context);
36602
- const rightTypeofName = getTypeofBrowserGlobalName(unwrappedExpression.right, context);
36603
- const leftComparedType = isNodeOfType(unwrappedExpression.left, "Literal") && typeof unwrappedExpression.left.value === "string" ? unwrappedExpression.left.value : null;
36604
- const rightComparedType = isNodeOfType(unwrappedExpression.right, "Literal") && typeof unwrappedExpression.right.value === "string" ? unwrappedExpression.right.value : null;
36605
- const guardName = leftTypeofName && rightComparedType ? leftTypeofName : rightTypeofName && leftComparedType ? rightTypeofName : null;
36606
- const comparedType = leftTypeofName && rightComparedType ? rightComparedType : rightTypeofName && leftComparedType ? leftComparedType : null;
36607
- if (!guardName || !browserGuardCoversGlobal(guardName, browserGlobalName)) return null;
36608
- if (!comparedType) return null;
36609
- const isEquality = unwrappedExpression.operator === "===" || unwrappedExpression.operator === "==";
36610
- const isInequality = unwrappedExpression.operator === "!==" || unwrappedExpression.operator === "!=";
36611
- if (!isEquality && !isInequality) return null;
36612
- const browserType = guardName === "matchMedia" ? "function" : "object";
36613
- const browserResult = isEquality ? browserType === comparedType : browserType !== comparedType;
36614
- if (browserResult === (isEquality ? comparedType === "undefined" : comparedType !== "undefined")) return null;
36615
- return predicateResult === browserResult;
36616
- };
36617
- const isInsideAvailabilityGuard = (node, browserGlobalName, context) => {
36618
- let currentNode = node;
36619
- let parentNode = currentNode.parent;
36620
- while (parentNode) {
36621
- if (isFunctionLike$1(parentNode) && !executesDuringRender(parentNode, context.scopes)) break;
36622
- if (isNodeOfType(parentNode, "LogicalExpression") && (parentNode.operator === "&&" || parentNode.operator === "||") && parentNode.right === currentNode && readAvailabilityWhenPredicate(parentNode.left, browserGlobalName, context, parentNode.operator === "&&") === true) return true;
36623
- if (isNodeOfType(parentNode, "ConditionalExpression")) {
36624
- if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
36625
- }
36626
- if (isNodeOfType(parentNode, "IfStatement")) {
36627
- if (parentNode.consequent === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, true) === true || parentNode.alternate === currentNode && readAvailabilityWhenPredicate(parentNode.test, browserGlobalName, context, false) === true) return true;
36628
- }
36629
- currentNode = parentNode;
36630
- parentNode = currentNode.parent;
36631
- }
36632
- return false;
36633
- };
36634
- const isAfterAvailabilityEarlyExit = (node, componentOrHookNode, browserGlobalName, context) => {
36635
- const enclosingFunction = findEnclosingFunction(node);
36636
- if (!enclosingFunction || enclosingFunction !== componentOrHookNode && !executesDuringRender(enclosingFunction, context.scopes) || !isFunctionLike$1(enclosingFunction) || !isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
36637
- let currentNode = node;
36638
- while (currentNode !== enclosingFunction) {
36639
- const parentNode = currentNode.parent;
36640
- if (!parentNode) return false;
36641
- if (isNodeOfType(parentNode, "BlockStatement")) for (const statement of parentNode.body) {
36642
- if (statement === currentNode) break;
36643
- if (!isNodeOfType(statement, "IfStatement")) continue;
36644
- if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, false) === true && statementAlwaysExits(statement.consequent)) return true;
36645
- if (readAvailabilityWhenPredicate(statement.test, browserGlobalName, context, true) === true && statement.alternate && statementAlwaysExits(statement.alternate)) return true;
36646
- }
36647
- currentNode = parentNode;
36648
- }
36649
- return false;
36650
- };
36651
- const isTypeofProbe = (node) => {
36652
- const expressionRoot = findTransparentExpressionRoot(node);
36653
- const parentNode = expressionRoot.parent;
36654
- return isNodeOfType(parentNode, "UnaryExpression") && parentNode.operator === "typeof" && parentNode.argument === expressionRoot;
36655
- };
36656
- const noUnguardedBrowserGlobalInRenderOrHookInit = defineRule({
36657
- id: "no-unguarded-browser-global-in-render-or-hook-init",
36658
- title: "Browser global read during server render",
36659
- severity: "error",
36660
- category: "Correctness",
36661
- requires: ["ssr"],
36662
- recommendation: "Move browser-only reads into an effect or event, guard them behind a client-only render path, or use useSyncExternalStore with a stable server snapshot.",
36663
- create: (context) => {
36664
- if (isTestlikeFilename(context.filename)) return {};
36665
- if (classifyReactNativeFileTarget(context) === "react-native") return {};
36666
- let fileIsEmailTemplate = false;
36667
- const reportedNodes = /* @__PURE__ */ new Set();
36668
- const reportBrowserRead = (node, browserGlobalName) => {
36669
- if (reportedNodes.has(node) || isTypeofProbe(node)) return;
36670
- const componentOrHookNode = findRenderPhaseComponentOrHook(node, context.scopes);
36671
- if (!componentOrHookNode) return;
36672
- if (fileIsEmailTemplate) return;
36673
- if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(node) ?? node)) return;
36674
- if (isGatedByFalsyInitialState(node, context.scopes)) return;
36675
- if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode, context.scopes)) return;
36676
- if (isInsideAvailabilityGuard(node, browserGlobalName, context)) return;
36677
- if (isAfterAvailabilityEarlyExit(node, componentOrHookNode, browserGlobalName, context)) return;
36678
- reportedNodes.add(node);
36679
- context.report({
36680
- node,
36681
- message: `\`${browserGlobalName}\` is read while React is rendering on the server, where browser globals are unavailable. Move the read into an effect or event, or provide a stable server snapshot.`
36682
- });
36683
- };
36684
- return {
36685
- Program(node) {
36686
- fileIsEmailTemplate = hasEmailTemplateImport(node);
36687
- },
36688
- Identifier(node) {
36689
- if (!BROWSER_GLOBAL_NAMES.has(node.name)) return;
36690
- if (!context.scopes.isGlobalReference(node)) return;
36691
- reportBrowserRead(node, node.name);
36692
- },
36693
- MemberExpression(node) {
36694
- if (node.computed) return;
36695
- const objectNode = stripParenExpression(node.object);
36696
- if (!isNodeOfType(objectNode, "Identifier") || objectNode.name !== "globalThis" || !context.scopes.isGlobalReference(objectNode) || !isNodeOfType(node.property, "Identifier") || !BROWSER_GLOBAL_NAMES.has(node.property.name)) return;
36697
- reportBrowserRead(node, node.property.name);
36698
- }
36699
- };
36700
- }
36701
- });
36702
- //#endregion
36703
35912
  //#region src/plugin/constants/dom-aria-properties.ts
36704
35913
  const ARIA_PROPERTY_NAMES = new Set([
36705
35914
  "activedescendant",
@@ -42010,7 +41219,7 @@ const renderingHydrationMismatchTime = defineRule({
42010
41219
  title: "Time or random value in JSX",
42011
41220
  severity: "warn",
42012
41221
  category: "Correctness",
42013
- requires: ["ssr"],
41222
+ disabledWhen: ["vite", "cra"],
42014
41223
  recommendation: "Move time or random values into useEffect+useState so they only run in the browser, or add suppressHydrationWarning to the parent if it's intentional",
42015
41224
  create: (context) => {
42016
41225
  const isTestlikeFile = isTestlikeFilename(context.filename);
@@ -42024,7 +41233,8 @@ const renderingHydrationMismatchTime = defineRule({
42024
41233
  const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
42025
41234
  if (matched) {
42026
41235
  if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
42027
- if (isGatedByFalsyInitialState(node, context.scopes)) return;
41236
+ if (isInsideClientOnlyGuard(node)) return;
41237
+ if (isGatedByFalsyInitialState(node)) return;
42028
41238
  if (isInsideMotionTransitionAttribute(node)) return;
42029
41239
  context.report({
42030
41240
  node,
@@ -42033,10 +41243,11 @@ const renderingHydrationMismatchTime = defineRule({
42033
41243
  return;
42034
41244
  }
42035
41245
  walkAst(node.expression, (child) => {
42036
- if (isFunctionLike$1(child) && !executesDuringRender(child, context.scopes)) return false;
41246
+ if (isFunctionLike$1(child) && !executesDuringRender(child)) return false;
42037
41247
  for (const pattern of NONDETERMINISTIC_RENDER_PATTERNS) if (pattern.matches(child)) {
42038
41248
  if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
42039
- if (isGatedByFalsyInitialState(child, context.scopes)) return;
41249
+ if (isInsideClientOnlyGuard(child)) return;
41250
+ if (isGatedByFalsyInitialState(child)) return;
42040
41251
  if (isInsideMotionTransitionAttribute(child)) return;
42041
41252
  if (pattern.display === "new Date()" && isYearOnlyDateRead(child)) return;
42042
41253
  context.report({
@@ -55373,18 +54584,6 @@ const reactDoctorRules = [
55373
54584
  category: "Accessibility"
55374
54585
  }
55375
54586
  },
55376
- {
55377
- key: "react-doctor/no-hydration-branch-on-browser-global",
55378
- id: "no-hydration-branch-on-browser-global",
55379
- source: "react-doctor",
55380
- originallyExternal: false,
55381
- rule: {
55382
- ...noHydrationBranchOnBrowserGlobal,
55383
- framework: "global",
55384
- category: "Bugs",
55385
- requires: [...new Set(["react", ...noHydrationBranchOnBrowserGlobal.requires ?? []])]
55386
- }
55387
- },
55388
54587
  {
55389
54588
  key: "react-doctor/no-img-lazy-with-high-fetchpriority",
55390
54589
  id: "no-img-lazy-with-high-fetchpriority",
@@ -56148,18 +55347,6 @@ const reactDoctorRules = [
56148
55347
  requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
56149
55348
  }
56150
55349
  },
56151
- {
56152
- key: "react-doctor/no-unguarded-browser-global-in-render-or-hook-init",
56153
- id: "no-unguarded-browser-global-in-render-or-hook-init",
56154
- source: "react-doctor",
56155
- originallyExternal: false,
56156
- rule: {
56157
- ...noUnguardedBrowserGlobalInRenderOrHookInit,
56158
- framework: "global",
56159
- category: "Bugs",
56160
- requires: [...new Set(["react", ...noUnguardedBrowserGlobalInRenderOrHookInit.requires ?? []])]
56161
- }
56162
- },
56163
55350
  {
56164
55351
  key: "react-doctor/no-unknown-property",
56165
55352
  id: "no-unknown-property",
@@ -58492,12 +57679,10 @@ const CROSS_FILE_RULE_IDS = new Set([
58492
57679
  "nextjs-no-use-search-params-without-suspense",
58493
57680
  "no-dynamic-import-path",
58494
57681
  "no-full-lodash-import",
58495
- "no-hydration-branch-on-browser-global",
58496
57682
  "no-indeterminate-attribute",
58497
57683
  "no-locale-format-in-render",
58498
57684
  "no-match-media-in-state-initializer",
58499
57685
  "no-mutating-reducer-state",
58500
- "no-unguarded-browser-global-in-render-or-hook-init",
58501
57686
  "prefer-dynamic-import",
58502
57687
  "rendering-hydration-mismatch-time",
58503
57688
  "rn-no-legacy-shadow-styles",
@@ -58631,12 +57816,10 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
58631
57816
  ["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
58632
57817
  ["no-dynamic-import-path", collectNearestManifestDependencies],
58633
57818
  ["no-full-lodash-import", collectNearestManifestDependencies],
58634
- ["no-hydration-branch-on-browser-global", collectNearestManifestDependencies],
58635
57819
  ["no-indeterminate-attribute", collectNearestManifestDependencies],
58636
57820
  ["no-locale-format-in-render", collectNearestManifestDependencies],
58637
57821
  ["no-match-media-in-state-initializer", collectNearestManifestDependencies],
58638
57822
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
58639
- ["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
58640
57823
  ["prefer-dynamic-import", collectNearestManifestDependencies],
58641
57824
  ["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
58642
57825
  ["rn-no-legacy-shadow-styles", collectLegacyArchDependencies],