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.
- package/dist/index.d.ts +1 -93
- package/dist/index.js +323 -1140
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7721
|
-
|
|
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
|
-
|
|
9883
|
-
|
|
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
|
|
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
|
|
28773
|
-
|
|
28774
|
-
|
|
28775
|
-
|
|
28776
|
-
|
|
28777
|
-
|
|
28778
|
-
|
|
28779
|
-
|
|
28780
|
-
|
|
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
|
|
28783
|
-
const
|
|
28784
|
-
|
|
28785
|
-
|
|
28786
|
-
|
|
28787
|
-
|
|
28788
|
-
|
|
28789
|
-
|
|
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
|
-
|
|
28793
|
-
|
|
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
|
|
28810
|
-
const
|
|
28811
|
-
const
|
|
28812
|
-
|
|
28813
|
-
|
|
28814
|
-
|
|
28815
|
-
|
|
28816
|
-
|
|
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
|
|
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
|
|
28833
|
-
const
|
|
28834
|
-
if (!isFunctionLike$1(effectCallback)) return
|
|
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
|
|
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
|
|
28588
|
+
return flagNames;
|
|
28849
28589
|
};
|
|
28850
|
-
const
|
|
28851
|
-
|
|
28852
|
-
|
|
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 (
|
|
28890
|
-
|
|
28891
|
-
|
|
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
|
-
|
|
28935
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
31029
|
-
if (
|
|
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
|
|
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 (
|
|
31069
|
-
if (
|
|
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
|
-
|
|
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
|
|
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"))
|
|
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 (
|
|
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 (!
|
|
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:
|
|
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: "
|
|
34236
|
-
create: (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
|
-
|
|
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 (
|
|
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
|
|
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 (
|
|
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],
|