oxlint-plugin-react-doctor 0.7.9-dev.71892af → 0.7.9-dev.842a55f
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 +46 -0
- package/dist/index.js +2217 -249
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1237,12 +1237,13 @@ const stripParenExpression = (node) => {
|
|
|
1237
1237
|
};
|
|
1238
1238
|
//#endregion
|
|
1239
1239
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
1240
|
-
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
1240
|
+
const resolveConstIdentifierAlias = (identifier, scopes, allowPatternBinding = false) => {
|
|
1241
1241
|
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
1242
1242
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
1243
1243
|
let symbol = scopes.symbolFor(identifier);
|
|
1244
1244
|
while (symbol?.kind === "const") {
|
|
1245
|
-
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")
|
|
1245
|
+
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return null;
|
|
1246
|
+
if (symbol.declarationNode.id !== symbol.bindingIdentifier) return allowPatternBinding ? symbol : null;
|
|
1246
1247
|
visitedSymbolIds.add(symbol.id);
|
|
1247
1248
|
const initializer = stripParenExpression(symbol.initializer);
|
|
1248
1249
|
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
@@ -6736,6 +6737,28 @@ const asyncParallel = defineRule({
|
|
|
6736
6737
|
}
|
|
6737
6738
|
});
|
|
6738
6739
|
//#endregion
|
|
6740
|
+
//#region src/plugin/utils/collect-function-return-statements.ts
|
|
6741
|
+
const collectFunctionReturnStatements = (functionNode) => {
|
|
6742
|
+
if (!isFunctionLike$1(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return [];
|
|
6743
|
+
const returnStatements = [];
|
|
6744
|
+
walkAst(functionNode.body, (node) => {
|
|
6745
|
+
if (node !== functionNode.body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
6746
|
+
if (isNodeOfType(node, "ReturnStatement")) returnStatements.push(node);
|
|
6747
|
+
});
|
|
6748
|
+
return returnStatements;
|
|
6749
|
+
};
|
|
6750
|
+
//#endregion
|
|
6751
|
+
//#region src/plugin/utils/is-nullish-expression.ts
|
|
6752
|
+
const isNullishExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined" || isNodeOfType(expression, "UnaryExpression") && expression.operator === "void";
|
|
6753
|
+
//#endregion
|
|
6754
|
+
//#region src/plugin/utils/strip-this-parameter.ts
|
|
6755
|
+
const stripThisParameter = (parameters) => {
|
|
6756
|
+
const firstParameter = parameters[0];
|
|
6757
|
+
if (!firstParameter) return parameters;
|
|
6758
|
+
if (isNodeOfType(firstParameter, "Identifier") && firstParameter.name === "this") return parameters.slice(1);
|
|
6759
|
+
return parameters;
|
|
6760
|
+
};
|
|
6761
|
+
//#endregion
|
|
6739
6762
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
6740
6763
|
const MESSAGE$60 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
|
|
6741
6764
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
@@ -6747,12 +6770,8 @@ const STORAGE_GLOBALS = new Set([
|
|
|
6747
6770
|
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
6748
6771
|
const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
|
|
6749
6772
|
const STRONG_AUTH_KEY_PATTERN = /jwt|secret|password|passwd|credential|private[-_]?key|api[-_]?key|bearer|access[-_]?token|refresh[-_]?token|auth[-_]?token|id[-_]?token|session/i;
|
|
6750
|
-
const PRODUCT_API_KEY_RECORDS_PATTERN = /(?:^|[._:-])(?:created|saved|integration|mailing)[-_]?api[-_]?keys$/i;
|
|
6751
|
-
const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
|
|
6752
6773
|
const isAuthCredentialKey = (key) => {
|
|
6753
|
-
if (PRODUCT_API_KEY_RECORDS_PATTERN.test(key)) return false;
|
|
6754
6774
|
if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
|
|
6755
|
-
if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
|
|
6756
6775
|
if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
|
|
6757
6776
|
return true;
|
|
6758
6777
|
};
|
|
@@ -6761,11 +6780,63 @@ const isDirectWebStorageObject = (node) => {
|
|
|
6761
6780
|
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
|
|
6762
6781
|
return false;
|
|
6763
6782
|
};
|
|
6764
|
-
const
|
|
6765
|
-
if (
|
|
6766
|
-
|
|
6767
|
-
const binding = findVariableInitializer(
|
|
6768
|
-
|
|
6783
|
+
const immutableInitializer = (identifier, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
6784
|
+
if (visitedIdentifiers.has(identifier)) return null;
|
|
6785
|
+
visitedIdentifiers.add(identifier);
|
|
6786
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
6787
|
+
if (!binding?.initializer) return null;
|
|
6788
|
+
if (isNodeOfType(binding.initializer, "FunctionDeclaration")) return binding.initializer;
|
|
6789
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
6790
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
6791
|
+
const declaration = declarator.parent;
|
|
6792
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
6793
|
+
if (declaration.kind !== "const") return null;
|
|
6794
|
+
const initializer = stripParenExpression(binding.initializer);
|
|
6795
|
+
if (!isNodeOfType(initializer, "Identifier")) return initializer;
|
|
6796
|
+
return immutableInitializer(initializer, visitedIdentifiers) ?? initializer;
|
|
6797
|
+
};
|
|
6798
|
+
const isWebStorageFactoryResult = (node, visitedNodes) => {
|
|
6799
|
+
const expression = stripParenExpression(node);
|
|
6800
|
+
if (isWebStorageObject(expression, new Set(visitedNodes))) return true;
|
|
6801
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
6802
|
+
const consequent = stripParenExpression(expression.consequent);
|
|
6803
|
+
const alternate = stripParenExpression(expression.alternate);
|
|
6804
|
+
return (isNullishExpression(consequent) || isWebStorageFactoryResult(consequent, visitedNodes)) && (isNullishExpression(alternate) || isWebStorageFactoryResult(alternate, visitedNodes)) && (!isNullishExpression(consequent) || !isNullishExpression(alternate));
|
|
6805
|
+
}
|
|
6806
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
6807
|
+
if (expression.operator === "&&") return isWebStorageFactoryResult(expression.right, visitedNodes);
|
|
6808
|
+
const left = stripParenExpression(expression.left);
|
|
6809
|
+
const right = stripParenExpression(expression.right);
|
|
6810
|
+
const isLeftStorage = isWebStorageFactoryResult(left, visitedNodes);
|
|
6811
|
+
const isRightStorage = isWebStorageFactoryResult(right, visitedNodes);
|
|
6812
|
+
return (isLeftStorage || isNullishExpression(left)) && (isRightStorage || isNullishExpression(right)) && (isLeftStorage || isRightStorage);
|
|
6813
|
+
}
|
|
6814
|
+
return false;
|
|
6815
|
+
};
|
|
6816
|
+
const isWebStorageObject = (node, visitedNodes = /* @__PURE__ */ new Set()) => {
|
|
6817
|
+
const expression = stripParenExpression(node);
|
|
6818
|
+
if (visitedNodes.has(expression)) return false;
|
|
6819
|
+
visitedNodes.add(expression);
|
|
6820
|
+
if (isDirectWebStorageObject(expression)) return true;
|
|
6821
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
6822
|
+
const initializer = immutableInitializer(expression);
|
|
6823
|
+
return initializer ? isWebStorageObject(initializer, new Set(visitedNodes)) : false;
|
|
6824
|
+
}
|
|
6825
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
6826
|
+
const callee = stripParenExpression(expression.callee);
|
|
6827
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
6828
|
+
const factory = immutableInitializer(callee);
|
|
6829
|
+
if (!isFunctionLike$1(factory)) return false;
|
|
6830
|
+
if (isNodeOfType(factory, "ArrowFunctionExpression") && !isNodeOfType(factory.body, "BlockStatement")) return isWebStorageFactoryResult(factory.body, visitedNodes);
|
|
6831
|
+
let didReturnWebStorage = false;
|
|
6832
|
+
for (const returnStatement of collectFunctionReturnStatements(factory)) {
|
|
6833
|
+
if (!returnStatement.argument) continue;
|
|
6834
|
+
const strippedReturn = stripParenExpression(returnStatement.argument);
|
|
6835
|
+
if (isNullishExpression(strippedReturn)) continue;
|
|
6836
|
+
if (!isWebStorageFactoryResult(strippedReturn, visitedNodes)) return false;
|
|
6837
|
+
didReturnWebStorage = true;
|
|
6838
|
+
}
|
|
6839
|
+
return didReturnWebStorage;
|
|
6769
6840
|
};
|
|
6770
6841
|
const resolveStaticKeyString = (node) => {
|
|
6771
6842
|
if (isNodeOfType(node, "Literal") && typeof node.value === "string") return node.value;
|
|
@@ -6785,6 +6856,56 @@ const staticMemberName = (member) => {
|
|
|
6785
6856
|
if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
|
|
6786
6857
|
return null;
|
|
6787
6858
|
};
|
|
6859
|
+
const parameterIndex = (expression, parameterSymbolIds, scopes, canUnwrapSerialization, visitedNodes = /* @__PURE__ */ new Set()) => {
|
|
6860
|
+
const strippedExpression = stripParenExpression(expression);
|
|
6861
|
+
if (visitedNodes.has(strippedExpression)) return null;
|
|
6862
|
+
visitedNodes.add(strippedExpression);
|
|
6863
|
+
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
6864
|
+
const directSymbolId = scopes.symbolFor(strippedExpression)?.id;
|
|
6865
|
+
const directIndex = directSymbolId === void 0 ? -1 : parameterSymbolIds.indexOf(directSymbolId);
|
|
6866
|
+
if (directIndex !== -1) return directIndex;
|
|
6867
|
+
const initializer = immutableInitializer(strippedExpression);
|
|
6868
|
+
return initializer ? parameterIndex(initializer, parameterSymbolIds, scopes, canUnwrapSerialization, visitedNodes) : null;
|
|
6869
|
+
}
|
|
6870
|
+
if (canUnwrapSerialization && isNodeOfType(strippedExpression, "CallExpression") && isNodeOfType(strippedExpression.callee, "MemberExpression") && !strippedExpression.callee.computed && isNodeOfType(strippedExpression.callee.object, "Identifier") && strippedExpression.callee.object.name === "JSON" && isNodeOfType(strippedExpression.callee.property, "Identifier") && strippedExpression.callee.property.name === "stringify") {
|
|
6871
|
+
const serializedArgument = strippedExpression.arguments[0];
|
|
6872
|
+
return serializedArgument ? parameterIndex(serializedArgument, parameterSymbolIds, scopes, true, visitedNodes) : null;
|
|
6873
|
+
}
|
|
6874
|
+
return null;
|
|
6875
|
+
};
|
|
6876
|
+
const storageHelperSinkCache = /* @__PURE__ */ new WeakMap();
|
|
6877
|
+
const findStorageHelperSinks = (functionNode, scopes) => {
|
|
6878
|
+
const cachedSinks = storageHelperSinkCache.get(functionNode);
|
|
6879
|
+
if (cachedSinks) return cachedSinks;
|
|
6880
|
+
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) {
|
|
6881
|
+
storageHelperSinkCache.set(functionNode, []);
|
|
6882
|
+
return [];
|
|
6883
|
+
}
|
|
6884
|
+
const parameterSymbolIds = stripThisParameter(functionNode.params).map((parameter) => {
|
|
6885
|
+
const strippedParameter = stripParenExpression(parameter);
|
|
6886
|
+
const identifier = isNodeOfType(strippedParameter, "Identifier") ? strippedParameter : isNodeOfType(strippedParameter, "AssignmentPattern") && isNodeOfType(strippedParameter.left, "Identifier") ? strippedParameter.left : null;
|
|
6887
|
+
return identifier ? scopes.symbolFor(identifier)?.id ?? null : null;
|
|
6888
|
+
});
|
|
6889
|
+
const helperSinks = [];
|
|
6890
|
+
walkAst(functionNode.body, (child) => {
|
|
6891
|
+
if (child !== functionNode.body && isFunctionLike$1(child)) return false;
|
|
6892
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
6893
|
+
const callee = stripParenExpression(child.callee);
|
|
6894
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem" || !isWebStorageObject(callee.object)) return;
|
|
6895
|
+
const keyExpression = child.arguments[0];
|
|
6896
|
+
const valueExpression = child.arguments[1];
|
|
6897
|
+
if (!keyExpression || !valueExpression) return;
|
|
6898
|
+
const keyParameterIndex = parameterIndex(keyExpression, parameterSymbolIds, scopes, false);
|
|
6899
|
+
const valueParameterIndex = parameterIndex(valueExpression, parameterSymbolIds, scopes, true);
|
|
6900
|
+
if (keyParameterIndex === null || valueParameterIndex === null) return;
|
|
6901
|
+
helperSinks.push({
|
|
6902
|
+
keyParameterIndex,
|
|
6903
|
+
valueParameterIndex
|
|
6904
|
+
});
|
|
6905
|
+
});
|
|
6906
|
+
storageHelperSinkCache.set(functionNode, helperSinks);
|
|
6907
|
+
return helperSinks;
|
|
6908
|
+
};
|
|
6788
6909
|
const authTokenInWebStorage = defineRule({
|
|
6789
6910
|
id: "auth-token-in-web-storage",
|
|
6790
6911
|
title: "Auth token in web storage",
|
|
@@ -6792,14 +6913,23 @@ const authTokenInWebStorage = defineRule({
|
|
|
6792
6913
|
recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
|
|
6793
6914
|
create: skipNonProductionFiles((context) => ({
|
|
6794
6915
|
CallExpression(node) {
|
|
6795
|
-
const callee = node.callee;
|
|
6796
|
-
|
|
6797
|
-
if (!isNodeOfType(callee.property, "Identifier")
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
if (
|
|
6801
|
-
|
|
6802
|
-
|
|
6916
|
+
const callee = stripParenExpression(node.callee);
|
|
6917
|
+
const keyArguments = [];
|
|
6918
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "setItem" && isWebStorageObject(stripParenExpression(callee.object))) {
|
|
6919
|
+
const keyArgument = node.arguments[0];
|
|
6920
|
+
if (keyArgument) keyArguments.push(keyArgument);
|
|
6921
|
+
} else if (isNodeOfType(callee, "Identifier")) {
|
|
6922
|
+
const helperFunction = immutableInitializer(callee);
|
|
6923
|
+
const helperSinks = helperFunction ? findStorageHelperSinks(helperFunction, context.scopes) : [];
|
|
6924
|
+
for (const helperSink of helperSinks) {
|
|
6925
|
+
const keyArgument = node.arguments[helperSink.keyParameterIndex];
|
|
6926
|
+
if (keyArgument && node.arguments[helperSink.valueParameterIndex]) keyArguments.push(keyArgument);
|
|
6927
|
+
}
|
|
6928
|
+
}
|
|
6929
|
+
if (!keyArguments.some((keyArgument) => {
|
|
6930
|
+
const keyString = resolveStaticKeyString(keyArgument);
|
|
6931
|
+
return keyString !== null && isAuthCredentialKey(keyString);
|
|
6932
|
+
})) return;
|
|
6803
6933
|
context.report({
|
|
6804
6934
|
node,
|
|
6805
6935
|
message: MESSAGE$60
|
|
@@ -7047,9 +7177,6 @@ const isCreateElementCall = (node) => {
|
|
|
7047
7177
|
return false;
|
|
7048
7178
|
};
|
|
7049
7179
|
//#endregion
|
|
7050
|
-
//#region src/plugin/utils/is-nullish-expression.ts
|
|
7051
|
-
const isNullishExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined" || isNodeOfType(expression, "UnaryExpression") && expression.operator === "void";
|
|
7052
|
-
//#endregion
|
|
7053
7180
|
//#region src/plugin/rules/react-builtins/button-has-type.ts
|
|
7054
7181
|
const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
|
|
7055
7182
|
const INVALID_MESSAGE = "This button has an invalid `type`, so the browser may treat it like a submit button.";
|
|
@@ -7511,6 +7638,7 @@ const areExpressionsStructurallyEqual = (a, b) => {
|
|
|
7511
7638
|
if (a.type !== b.type) return false;
|
|
7512
7639
|
if (isNodeOfType(a, "ThisExpression")) return true;
|
|
7513
7640
|
if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
|
|
7641
|
+
if (isNodeOfType(a, "PrivateIdentifier") && isNodeOfType(b, "PrivateIdentifier")) return a.name === b.name;
|
|
7514
7642
|
if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
|
|
7515
7643
|
if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
|
|
7516
7644
|
if (a.computed !== b.computed) return false;
|
|
@@ -8566,17 +8694,6 @@ const createMethodMutationAnalysis = (context) => {
|
|
|
8566
8694
|
//#region src/plugin/utils/is-member-property.ts
|
|
8567
8695
|
const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === propertyName);
|
|
8568
8696
|
//#endregion
|
|
8569
|
-
//#region src/plugin/utils/collect-function-return-statements.ts
|
|
8570
|
-
const collectFunctionReturnStatements = (functionNode) => {
|
|
8571
|
-
if (!isFunctionLike$1(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return [];
|
|
8572
|
-
const returnStatements = [];
|
|
8573
|
-
walkAst(functionNode.body, (node) => {
|
|
8574
|
-
if (node !== functionNode.body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
8575
|
-
if (isNodeOfType(node, "ReturnStatement")) returnStatements.push(node);
|
|
8576
|
-
});
|
|
8577
|
-
return returnStatements;
|
|
8578
|
-
};
|
|
8579
|
-
//#endregion
|
|
8580
8697
|
//#region src/plugin/utils/statement-always-exits.ts
|
|
8581
8698
|
const statementAlwaysExits = (statement) => {
|
|
8582
8699
|
if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
|
|
@@ -8843,7 +8960,7 @@ const isReactNamespaceImport = (identifier, scopes) => {
|
|
|
8843
8960
|
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
8844
8961
|
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
8845
8962
|
};
|
|
8846
|
-
const isReactNamespaceReceiver = (receiver, scopes, options) => {
|
|
8963
|
+
const isReactNamespaceReceiver$1 = (receiver, scopes, options) => {
|
|
8847
8964
|
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
8848
8965
|
if (isReactNamespaceImport(receiver, scopes)) return true;
|
|
8849
8966
|
return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
|
|
@@ -8856,7 +8973,7 @@ const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) =>
|
|
|
8856
8973
|
for (const property of pattern.properties) {
|
|
8857
8974
|
if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
|
|
8858
8975
|
const propertyName = getStaticPropertyKeyName(property);
|
|
8859
|
-
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver(stripParenExpression(symbol.initializer), scopes, options));
|
|
8976
|
+
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver$1(stripParenExpression(symbol.initializer), scopes, options));
|
|
8860
8977
|
}
|
|
8861
8978
|
return false;
|
|
8862
8979
|
};
|
|
@@ -8880,7 +8997,7 @@ const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds
|
|
|
8880
8997
|
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
8881
8998
|
}
|
|
8882
8999
|
if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
|
|
8883
|
-
return isReactNamespaceReceiver(stripParenExpression(callee.object), scopes, options);
|
|
9000
|
+
return isReactNamespaceReceiver$1(stripParenExpression(callee.object), scopes, options);
|
|
8884
9001
|
};
|
|
8885
9002
|
//#endregion
|
|
8886
9003
|
//#region src/plugin/utils/is-proven-browser-api-receiver.ts
|
|
@@ -13536,7 +13653,7 @@ const getPromiseChainCallForCallback = (candidate) => {
|
|
|
13536
13653
|
if (!callbackContainer.arguments?.some((argument) => stripParenExpression(argument) === candidate)) return null;
|
|
13537
13654
|
return isPromiseChainCall(stripParenExpression(callbackContainer.callee)) ? callbackContainer : null;
|
|
13538
13655
|
};
|
|
13539
|
-
const
|
|
13656
|
+
const collectInvokedFunctions = (effectCallback, includePromiseCallbacks) => {
|
|
13540
13657
|
const invokedFunctions = new Set([effectCallback]);
|
|
13541
13658
|
const localFunctionBindings = /* @__PURE__ */ new Map();
|
|
13542
13659
|
const calledBindingNames = /* @__PURE__ */ new Set();
|
|
@@ -13570,12 +13687,14 @@ const collectEffectInvokedFunctions = (effectCallback) => {
|
|
|
13570
13687
|
calledBindingNames.add(callee.name);
|
|
13571
13688
|
return;
|
|
13572
13689
|
}
|
|
13573
|
-
if (isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
13690
|
+
if (includePromiseCallbacks && isPromiseChainCall(callee)) for (const callArgument of child.arguments ?? []) enqueue(callArgument);
|
|
13574
13691
|
});
|
|
13575
13692
|
for (const calledName of calledBindingNames) enqueue(localFunctionBindings.get(calledName));
|
|
13576
13693
|
}
|
|
13577
13694
|
return invokedFunctions;
|
|
13578
13695
|
};
|
|
13696
|
+
const collectEffectInvokedFunctions = (effectCallback) => collectInvokedFunctions(effectCallback, true);
|
|
13697
|
+
const collectSynchronouslyEffectInvokedFunctions = (effectCallback) => collectInvokedFunctions(effectCallback, false);
|
|
13579
13698
|
//#endregion
|
|
13580
13699
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
13581
13700
|
const isReactHookName = (name) => {
|
|
@@ -13734,15 +13853,19 @@ const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
|
13734
13853
|
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
13735
13854
|
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
13736
13855
|
};
|
|
13737
|
-
const
|
|
13856
|
+
const resolveReactRefCurrentOriginSymbol = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
13738
13857
|
const expression = stripParenExpression(node);
|
|
13739
|
-
|
|
13740
|
-
if (
|
|
13741
|
-
|
|
13742
|
-
|
|
13858
|
+
const refSymbol = resolveReactRefSymbol(expression, scopes);
|
|
13859
|
+
if (refSymbol) return refSymbol;
|
|
13860
|
+
if (!isNodeOfType(expression, "Identifier")) return null;
|
|
13861
|
+
const symbol = scopes.symbolFor(expression);
|
|
13862
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return null;
|
|
13863
|
+
const initializer = getDirectUnreassignedInitializer(symbol);
|
|
13864
|
+
if (!initializer) return null;
|
|
13743
13865
|
visitedSymbolIds.add(symbol.id);
|
|
13744
|
-
return
|
|
13866
|
+
return resolveReactRefCurrentOriginSymbol(initializer, scopes, visitedSymbolIds);
|
|
13745
13867
|
};
|
|
13868
|
+
const hasReactRefCurrentOrigin = (node, scopes) => resolveReactRefCurrentOriginSymbol(node, scopes) !== null;
|
|
13746
13869
|
//#endregion
|
|
13747
13870
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
13748
13871
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
@@ -14224,6 +14347,39 @@ const findContainingCollectionKey = (resourceNode, context) => {
|
|
|
14224
14347
|
}
|
|
14225
14348
|
return null;
|
|
14226
14349
|
};
|
|
14350
|
+
const findPushedResourceCollectionKey = (usage, context) => {
|
|
14351
|
+
if (!isNodeOfType(usage.node, "CallExpression")) return null;
|
|
14352
|
+
const registrationCallee = stripParenExpression(usage.node.callee);
|
|
14353
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression") || registrationCallee.computed) return null;
|
|
14354
|
+
const resourceIdentifier = stripParenExpression(registrationCallee.object);
|
|
14355
|
+
if (!isPrivatePlainConstIdentifier(resourceIdentifier, context)) return null;
|
|
14356
|
+
const resourceSymbol = context.scopes.symbolFor(resourceIdentifier);
|
|
14357
|
+
if (!resourceSymbol) return null;
|
|
14358
|
+
const pushCalls = resourceSymbol.references.flatMap((reference) => {
|
|
14359
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
14360
|
+
const callNode = referenceRoot.parent;
|
|
14361
|
+
if (!isNodeOfType(callNode, "CallExpression") || !callNode.arguments?.some((argument) => argument === referenceRoot)) return [];
|
|
14362
|
+
const pushCallee = stripParenExpression(callNode.callee);
|
|
14363
|
+
return isNodeOfType(pushCallee, "MemberExpression") && !pushCallee.computed && isNodeOfType(pushCallee.object, "Identifier") && isNodeOfType(pushCallee.property, "Identifier") && pushCallee.property.name === "push" ? [callNode] : [];
|
|
14364
|
+
});
|
|
14365
|
+
if (pushCalls.length !== 1) return null;
|
|
14366
|
+
const pushCall = pushCalls[0];
|
|
14367
|
+
if (findEnclosingFunction$1(pushCall) !== findEnclosingFunction$1(usage.node) || !doMatchingNodesCoverEveryPathAfterUsage(usage.node, [pushCall], context)) return null;
|
|
14368
|
+
const pushCallee = stripParenExpression(pushCall.callee);
|
|
14369
|
+
if (!isNodeOfType(pushCallee, "MemberExpression") || !isNodeOfType(pushCallee.object, "Identifier") || !isPrivatePlainConstIdentifier(pushCallee.object, context)) return null;
|
|
14370
|
+
const collectionSymbol = context.scopes.symbolFor(pushCallee.object);
|
|
14371
|
+
const collectionInitializer = collectionSymbol?.initializer ? stripParenExpression(collectionSymbol.initializer) : null;
|
|
14372
|
+
if (!collectionSymbol || !isNodeOfType(collectionInitializer, "ArrayExpression") || (collectionInitializer.elements?.length ?? 0) !== 0 || findEnclosingFunction$1(collectionSymbol.declarationNode) !== findEnclosingFunction$1(usage.node)) return null;
|
|
14373
|
+
return collectionSymbol.references.every((reference) => {
|
|
14374
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
14375
|
+
const forOfStatement = referenceRoot.parent;
|
|
14376
|
+
if (isNodeOfType(forOfStatement, "ForOfStatement") && forOfStatement.right === referenceRoot && forOfStatement.await !== true) return true;
|
|
14377
|
+
const memberNode = referenceRoot.parent;
|
|
14378
|
+
const callNode = memberNode?.parent;
|
|
14379
|
+
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== referenceRoot || memberNode.computed || !isNodeOfType(memberNode.property, "Identifier") || !isNodeOfType(callNode, "CallExpression") || callNode.callee !== memberNode) return false;
|
|
14380
|
+
return memberNode.property.name === "forEach" || memberNode.property.name === "push";
|
|
14381
|
+
}) ? resolveExpressionKey(pushCallee.object, context) : null;
|
|
14382
|
+
};
|
|
14227
14383
|
const isWithinAssignmentTarget = (identifier) => {
|
|
14228
14384
|
let currentNode = identifier;
|
|
14229
14385
|
let parentNode = currentNode.parent;
|
|
@@ -14282,6 +14438,14 @@ const isSynchronousIteratorCallback = (functionNode) => {
|
|
|
14282
14438
|
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
|
|
14283
14439
|
return SYNCHRONOUS_ITERATOR_METHOD_NAMES$2.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
|
|
14284
14440
|
};
|
|
14441
|
+
const findEnclosingForEachCall = (node) => {
|
|
14442
|
+
const callbackNode = findEnclosingFunction$1(node);
|
|
14443
|
+
if (!callbackNode) return null;
|
|
14444
|
+
const callNode = callbackNode.parent;
|
|
14445
|
+
if (!isNodeOfType(callNode, "CallExpression") || callNode.arguments?.[0] !== callbackNode) return null;
|
|
14446
|
+
const callee = stripParenExpression(callNode.callee);
|
|
14447
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" ? callNode : null;
|
|
14448
|
+
};
|
|
14285
14449
|
const findDirectCallForReference = (identifier) => {
|
|
14286
14450
|
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
14287
14451
|
const callNode = expressionRoot.parent;
|
|
@@ -14296,7 +14460,7 @@ const findSingleDirectInvocation = (functionNode, caller, context) => {
|
|
|
14296
14460
|
const callNode = findDirectCallForReference(reference.identifier);
|
|
14297
14461
|
return callNode ? [callNode] : [];
|
|
14298
14462
|
});
|
|
14299
|
-
if (invocationCalls.length !== 1) return null;
|
|
14463
|
+
if (invocationCalls.length !== 1 || symbol.references.length !== 1) return null;
|
|
14300
14464
|
const invocationCall = invocationCalls[0];
|
|
14301
14465
|
return findEnclosingFunction$1(invocationCall) === caller && isNodeReachableWithinFunction(invocationCall, context) ? invocationCall : null;
|
|
14302
14466
|
};
|
|
@@ -14330,7 +14494,15 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
|
|
|
14330
14494
|
if (cleanupChild !== cleanupFunction.body && isFunctionLike$1(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
|
|
14331
14495
|
const cleanupCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
|
|
14332
14496
|
if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
|
|
14333
|
-
const
|
|
14497
|
+
const cleanupForEachCall = findEnclosingForEachCall(cleanupChild);
|
|
14498
|
+
const cleanupCallee = isNodeOfType(cleanupCall, "CallExpression") ? stripParenExpression(cleanupCall.callee) : null;
|
|
14499
|
+
const cleanupReceiverForOfStatement = isNodeOfType(cleanupCallee, "MemberExpression") ? findForOfStatementForIteratorExpression(cleanupCallee.object, context) : null;
|
|
14500
|
+
const cleanupReceiverCollectionKey = cleanupReceiverForOfStatement ? resolveExpressionKey(cleanupReceiverForOfStatement.right, context) : isNodeOfType(cleanupCallee, "MemberExpression") ? resolveIteratorCollectionKey(cleanupCallee.object, context) : null;
|
|
14501
|
+
if (cleanupReceiverCollectionKey !== null && findEnclosingFunction$1(cleanupChild) !== cleanupFunction) {
|
|
14502
|
+
if (cleanupForEachCall && findPushedResourceCollectionKey(usage, context) === cleanupReceiverCollectionKey) matchingLoopOrHelperAnchors.push(cleanupForEachCall);
|
|
14503
|
+
return;
|
|
14504
|
+
}
|
|
14505
|
+
const cleanupForOfStatement = findForOfStatementForIteratorExpression(isNodeOfType(cleanupCall, "CallExpression") ? cleanupCall.arguments?.[0] : null, context) ?? cleanupReceiverForOfStatement;
|
|
14334
14506
|
if (!cleanupForOfStatement) {
|
|
14335
14507
|
didCleanupFunctionMatch = true;
|
|
14336
14508
|
return false;
|
|
@@ -14374,28 +14546,28 @@ const callbackReturnsCleanupForUsage = (callback, usage, context) => {
|
|
|
14374
14546
|
});
|
|
14375
14547
|
return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
|
|
14376
14548
|
};
|
|
14377
|
-
const
|
|
14378
|
-
if (
|
|
14379
|
-
const
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
const unwrappedOperand = stripParenExpression(operand);
|
|
14385
|
-
return isNodeOfType(unwrappedOperand, "Literal") && unwrappedOperand.value === null || isNodeOfType(unwrappedOperand, "Identifier") && unwrappedOperand.name === "undefined" && context.scopes.isGlobalReference(unwrappedOperand);
|
|
14386
|
-
};
|
|
14387
|
-
return resolveExpressionKey(unwrappedTest.left, context) === usage.handleKey && isNullishOperand(unwrappedTest.right) || resolveExpressionKey(unwrappedTest.right, context) === usage.handleKey && isNullishOperand(unwrappedTest.left);
|
|
14549
|
+
const doesTestRequireLiveExpressionKey = (test, expressionKey, context) => {
|
|
14550
|
+
if (resolveExpressionKey(test, context) === expressionKey) return true;
|
|
14551
|
+
const unwrappedTest = stripParenExpression(test);
|
|
14552
|
+
if (!isNodeOfType(unwrappedTest, "BinaryExpression") || unwrappedTest.operator !== "!=" && unwrappedTest.operator !== "!==") return false;
|
|
14553
|
+
const isNullishOperand = (operand) => {
|
|
14554
|
+
const unwrappedOperand = stripParenExpression(operand);
|
|
14555
|
+
return isNodeOfType(unwrappedOperand, "Literal") && unwrappedOperand.value === null || isNodeOfType(unwrappedOperand, "Identifier") && unwrappedOperand.name === "undefined" && context.scopes.isGlobalReference(unwrappedOperand);
|
|
14388
14556
|
};
|
|
14557
|
+
return resolveExpressionKey(unwrappedTest.left, context) === expressionKey && isNullishOperand(unwrappedTest.right) || resolveExpressionKey(unwrappedTest.right, context) === expressionKey && isNullishOperand(unwrappedTest.left);
|
|
14558
|
+
};
|
|
14559
|
+
const findLiveExpressionGuardForRelease = (releaseCall, owner, expressionKey, context) => {
|
|
14389
14560
|
let ancestor = releaseCall.parent;
|
|
14390
14561
|
while (ancestor && ancestor !== owner) {
|
|
14391
14562
|
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
14392
|
-
if (ancestor.alternate !== null || !
|
|
14563
|
+
if (ancestor.alternate !== null || !doesTestRequireLiveExpressionKey(ancestor.test, expressionKey, context) || !doMatchingNodesCoverEveryPathAfterUsage(ancestor.consequent, [releaseCall], context)) return null;
|
|
14393
14564
|
return ancestor;
|
|
14394
14565
|
}
|
|
14395
14566
|
ancestor = ancestor.parent;
|
|
14396
14567
|
}
|
|
14397
14568
|
return null;
|
|
14398
14569
|
};
|
|
14570
|
+
const findDirectHandleGuardForRelease = (releaseCall, owner, usage, context) => usage.handleKey === null ? null : findLiveExpressionGuardForRelease(releaseCall, owner, usage.handleKey, context);
|
|
14399
14571
|
const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
|
|
14400
14572
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
14401
14573
|
const functionCfg = context.cfg.cfgFor(callback);
|
|
@@ -14583,7 +14755,108 @@ const hasPotentialInterruptionAfterGuard = (callback, guardState, usageNode, con
|
|
|
14583
14755
|
});
|
|
14584
14756
|
return hasPotentialInterruption;
|
|
14585
14757
|
};
|
|
14758
|
+
const getNumericReactRefCurrentKey = (expression, context) => {
|
|
14759
|
+
const refSymbol = resolveReactRefSymbol(stripParenExpression(expression), context.scopes);
|
|
14760
|
+
const initializer = refSymbol?.initializer ? stripParenExpression(refSymbol.initializer) : null;
|
|
14761
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
14762
|
+
const initialValue = initializer.arguments?.[0] ? stripParenExpression(initializer.arguments[0]) : null;
|
|
14763
|
+
if (!isNodeOfType(initialValue, "Literal") || typeof initialValue.value !== "number") return null;
|
|
14764
|
+
return resolveExpressionKey(expression, context);
|
|
14765
|
+
};
|
|
14766
|
+
const getBlockingGenerationKey = (expression, context) => {
|
|
14767
|
+
const test = stripParenExpression(expression);
|
|
14768
|
+
if (isNodeOfType(test, "LogicalExpression") && test.operator === "||") return getBlockingGenerationKey(test.left, context) ?? getBlockingGenerationKey(test.right, context);
|
|
14769
|
+
if (!isNodeOfType(test, "BinaryExpression") || test.operator !== "!==" && test.operator !== "!=") return null;
|
|
14770
|
+
const leftKey = getNumericReactRefCurrentKey(test.left, context);
|
|
14771
|
+
const rightKey = getNumericReactRefCurrentKey(test.right, context);
|
|
14772
|
+
const snapshotExpression = leftKey ? stripParenExpression(test.right) : stripParenExpression(test.left);
|
|
14773
|
+
const key = leftKey ?? rightKey;
|
|
14774
|
+
return key && isNodeOfType(snapshotExpression, "Identifier") ? key : null;
|
|
14775
|
+
};
|
|
14776
|
+
const findGenerationGuardKeyForDeferredUsage = (usageFunction, usageNode, context) => {
|
|
14777
|
+
if (!isFunctionLike$1(usageFunction)) return null;
|
|
14778
|
+
let generationKey = null;
|
|
14779
|
+
walkAst(usageFunction.body, (child) => {
|
|
14780
|
+
if (generationKey) return false;
|
|
14781
|
+
if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
|
|
14782
|
+
if (!isNodeOfType(child, "IfStatement") || child.alternate) return;
|
|
14783
|
+
const key = getBlockingGenerationKey(child.test, context);
|
|
14784
|
+
if (!key || canNodeReachLaterNodeWithinFunction(child.consequent, usageNode, usageFunction, context) || !doMatchingNodesCoverEveryPathBeforeUsage(usageNode, [child], usageFunction, context)) return;
|
|
14785
|
+
generationKey = key;
|
|
14786
|
+
});
|
|
14787
|
+
return generationKey;
|
|
14788
|
+
};
|
|
14789
|
+
const isGenerationAdvance = (node, generationKey, context) => {
|
|
14790
|
+
if (isNodeOfType(node, "UpdateExpression") && resolveExpressionKey(node.argument, context) === generationKey) return true;
|
|
14791
|
+
if (!isNodeOfType(node, "AssignmentExpression") || resolveExpressionKey(node.left, context) !== generationKey || node.operator !== "+=" && node.operator !== "-=") return false;
|
|
14792
|
+
const amount = stripParenExpression(node.right);
|
|
14793
|
+
return isNodeOfType(amount, "Literal") && typeof amount.value === "number" && amount.value !== 0;
|
|
14794
|
+
};
|
|
14795
|
+
const functionAdvancesGeneration = (owner, generationKey, context) => {
|
|
14796
|
+
if (!isFunctionLike$1(owner)) return false;
|
|
14797
|
+
let didAdvanceGeneration = false;
|
|
14798
|
+
walkAst(owner.body, (child) => {
|
|
14799
|
+
if (didAdvanceGeneration) return false;
|
|
14800
|
+
if (child !== owner.body && isFunctionLike$1(child)) return false;
|
|
14801
|
+
if (isGenerationAdvance(child, generationKey, context)) {
|
|
14802
|
+
didAdvanceGeneration = true;
|
|
14803
|
+
return false;
|
|
14804
|
+
}
|
|
14805
|
+
});
|
|
14806
|
+
return didAdvanceGeneration;
|
|
14807
|
+
};
|
|
14808
|
+
const cleanupReturnsReleaseUsage = (cleanupReturns, usage, context) => cleanupReturns.length > 0 && cleanupReturns.every((cleanupReturn) => {
|
|
14809
|
+
if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return false;
|
|
14810
|
+
const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
|
|
14811
|
+
return Boolean(cleanupFunction && isFunctionLike$1(cleanupFunction) && doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context));
|
|
14812
|
+
});
|
|
14813
|
+
const getOwnedFunctionReference = (reference, usageFunction, usageNode, callback, cleanupReturns, context) => {
|
|
14814
|
+
const directCall = findDirectCallForReference(reference);
|
|
14815
|
+
if (directCall) {
|
|
14816
|
+
const referenceOwner = findEnclosingFunction$1(directCall);
|
|
14817
|
+
if (referenceOwner && referenceOwner !== usageFunction && collectSynchronouslyEffectInvokedFunctions(callback).has(referenceOwner)) return { generationKey: null };
|
|
14818
|
+
const generationKey = referenceOwner ? findGenerationGuardKeyForDeferredUsage(referenceOwner, directCall, context) : null;
|
|
14819
|
+
return generationKey ? { generationKey } : null;
|
|
14820
|
+
}
|
|
14821
|
+
const referenceRoot = findTransparentExpressionRoot(reference);
|
|
14822
|
+
const schedulerCall = referenceRoot.parent;
|
|
14823
|
+
if (!isNodeOfType(schedulerCall, "CallExpression") || !schedulerCall.arguments.some((argument) => argument === referenceRoot) || !isNodeOfType(schedulerCall.callee, "Identifier") || schedulerCall.callee.name !== "setTimeout" || !context.scopes.isGlobalReference(schedulerCall.callee)) return null;
|
|
14824
|
+
const schedulerUsage = {
|
|
14825
|
+
kind: "timer",
|
|
14826
|
+
node: schedulerCall,
|
|
14827
|
+
resourceName: schedulerCall.callee.name,
|
|
14828
|
+
handleKey: findAssignedResourceKey(schedulerCall, context),
|
|
14829
|
+
receiverKey: null,
|
|
14830
|
+
registrationVerbName: schedulerCall.callee.name,
|
|
14831
|
+
eventKey: null,
|
|
14832
|
+
handlerKey: null
|
|
14833
|
+
};
|
|
14834
|
+
const generationKey = findGenerationGuardKeyForDeferredUsage(usageFunction, usageNode, context);
|
|
14835
|
+
return schedulerUsage.handleKey !== null && cleanupReturnsReleaseUsage(cleanupReturns, schedulerUsage, context) && generationKey ? { generationKey } : null;
|
|
14836
|
+
};
|
|
14837
|
+
const hasGuardedRefOwnedNestedCleanup = (callback, usage, cleanupReturns, context) => {
|
|
14838
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14839
|
+
const usageExpression = findTransparentExpressionRoot(usage.node);
|
|
14840
|
+
const usageAssignment = usageExpression.parent;
|
|
14841
|
+
if (usage.kind !== "subscribe" && usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usageAssignment, "AssignmentExpression") || usageAssignment.operator !== "=" || usageAssignment.right !== usageExpression || !resolveReactRefSymbol(stripParenExpression(usageAssignment.left), context.scopes) || !collectSynchronouslyEffectInvokedFunctions(callback).has(usageFunction) || !cleanupReturnsReleaseUsage(cleanupReturns, usage, context) || !doMatchingNodesCoverEveryPathFromFunctionEntry(callback, cleanupReturns, context)) return false;
|
|
14842
|
+
const cleanupFunctions = cleanupReturns.flatMap((cleanupReturn) => {
|
|
14843
|
+
if (!isNodeOfType(cleanupReturn, "ReturnStatement") || !cleanupReturn.argument) return [];
|
|
14844
|
+
const cleanupFunction = resolveStableValue(cleanupReturn.argument, context);
|
|
14845
|
+
return cleanupFunction && isFunctionLike$1(cleanupFunction) ? [cleanupFunction] : [];
|
|
14846
|
+
});
|
|
14847
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(usageFunction);
|
|
14848
|
+
const functionSymbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
|
|
14849
|
+
if (!functionSymbol || functionSymbol.references.length === 0) return false;
|
|
14850
|
+
const ownedReferences = functionSymbol.references.map((reference) => getOwnedFunctionReference(reference.identifier, usageFunction, usage.node, callback, cleanupReturns, context));
|
|
14851
|
+
if (ownedReferences.some((reference) => reference === null)) return false;
|
|
14852
|
+
const generationKeys = new Set(ownedReferences.flatMap((reference) => reference?.generationKey ? [reference.generationKey] : []));
|
|
14853
|
+
if (generationKeys.size !== 1) return false;
|
|
14854
|
+
const generationKey = generationKeys.values().next().value;
|
|
14855
|
+
if (typeof generationKey !== "string") return false;
|
|
14856
|
+
return [...collectSynchronouslyEffectInvokedFunctions(callback), ...cleanupFunctions].some((owner) => functionAdvancesGeneration(owner, generationKey, context));
|
|
14857
|
+
};
|
|
14586
14858
|
const hasGuardedDeferredCleanup = (callback, usage, cleanupReturns, context) => {
|
|
14859
|
+
if (hasGuardedRefOwnedNestedCleanup(callback, usage, cleanupReturns, context)) return true;
|
|
14587
14860
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14588
14861
|
const promiseChainCall = usageFunction ? getPromiseChainCallForCallback(usageFunction) : null;
|
|
14589
14862
|
if (usage.kind !== "timer" || usage.handleKey === null || !usageFunction || !isFunctionLike$1(usageFunction) || usageFunction === callback || usageFunction.async || usageFunction.generator || !isNodeOfType(usage.node, "CallExpression") || !isNodeOfType(usage.node.callee, "Identifier") || !context.scopes.isGlobalReference(usage.node.callee) || !promiseChainCall || !collectEffectInvokedFunctions(callback).has(usageFunction) || !doMatchingNodesCoverEveryPathAfterUsage(promiseChainCall, cleanupReturns, context)) return false;
|
|
@@ -14751,6 +15024,85 @@ const getReleaseVerbName = (node) => {
|
|
|
14751
15024
|
}
|
|
14752
15025
|
return null;
|
|
14753
15026
|
};
|
|
15027
|
+
const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) => {
|
|
15028
|
+
const releaseFunction = findEnclosingFunction$1(releaseReceiver);
|
|
15029
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
15030
|
+
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !resolveReactRefCurrentOriginSymbol(releaseReceiver, context.scopes)) return false;
|
|
15031
|
+
const controllerKey = getListenerAbortControllerKey(usage, context);
|
|
15032
|
+
const refCurrentKey = resolveExpressionKey(releaseReceiver, context);
|
|
15033
|
+
if (controllerKey === null || refCurrentKey === null) return false;
|
|
15034
|
+
const usageFunctionBody = usageFunction.body;
|
|
15035
|
+
const previousAbortCalls = [];
|
|
15036
|
+
const ownershipAssignments = [];
|
|
15037
|
+
walkAst(usageFunctionBody, (child) => {
|
|
15038
|
+
if (child !== usageFunctionBody && isFunctionLike$1(child)) return false;
|
|
15039
|
+
if (isNodeOfType(child, "AssignmentExpression") && resolveExpressionKey(child.left, context) === refCurrentKey && resolveExpressionKey(child.right, context) === controllerKey) {
|
|
15040
|
+
ownershipAssignments.push(child);
|
|
15041
|
+
return;
|
|
15042
|
+
}
|
|
15043
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
15044
|
+
const childCallee = isNodeOfType(child.callee, "ChainExpression") ? child.callee.expression : stripParenExpression(child.callee);
|
|
15045
|
+
if (isNodeOfType(childCallee, "MemberExpression") && !childCallee.computed && isNodeOfType(childCallee.property, "Identifier") && childCallee.property.name === "abort" && resolveExpressionKey(childCallee.object, context) === refCurrentKey) previousAbortCalls.push(child);
|
|
15046
|
+
});
|
|
15047
|
+
const safeOwnershipAssignments = ownershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, previousAbortCalls, usageFunction, context));
|
|
15048
|
+
return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
|
|
15049
|
+
};
|
|
15050
|
+
const isJsxRefAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "ref";
|
|
15051
|
+
const isFunctionForwardedToReactRef = (functionNode, context) => {
|
|
15052
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
15053
|
+
if (!bindingIdentifier) return false;
|
|
15054
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
15055
|
+
if (!symbol) return false;
|
|
15056
|
+
return symbol.references.some((reference) => {
|
|
15057
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
15058
|
+
const expressionContainer = referenceRoot.parent;
|
|
15059
|
+
return Boolean(isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === referenceRoot && isJsxRefAttribute(expressionContainer.parent));
|
|
15060
|
+
});
|
|
15061
|
+
};
|
|
15062
|
+
const isFunctionReturnedFromReactHook = (functionNode, context, requireRefPropertyName) => {
|
|
15063
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
15064
|
+
if (!bindingIdentifier) return false;
|
|
15065
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
15066
|
+
if (!symbol) return false;
|
|
15067
|
+
return symbol.references.some((reference) => {
|
|
15068
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
15069
|
+
const property = referenceRoot.parent;
|
|
15070
|
+
const propertyName = isNodeOfType(property, "Property") ? getStaticPropertyKeyName(property) : null;
|
|
15071
|
+
if (!isNodeOfType(property, "Property") || property.value !== referenceRoot || !isNodeOfType(property.parent, "ObjectExpression") || requireRefPropertyName && propertyName !== "ref" && !propertyName?.endsWith("Ref")) return false;
|
|
15072
|
+
const returnedObject = findTransparentExpressionRoot(property.parent);
|
|
15073
|
+
const returnStatement = returnedObject.parent;
|
|
15074
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement") || returnStatement.argument !== returnedObject) return false;
|
|
15075
|
+
const ownerFunction = findEnclosingFunction$1(returnStatement);
|
|
15076
|
+
return Boolean(ownerFunction && isReactHookName(getFunctionBindingIdentifier$1(ownerFunction)?.name ?? ""));
|
|
15077
|
+
});
|
|
15078
|
+
};
|
|
15079
|
+
const isFunctionUsedAsReactRef = (functionNode, context) => isFunctionForwardedToReactRef(functionNode, context) || isFunctionReturnedFromReactHook(functionNode, context, true);
|
|
15080
|
+
const isReactRefListenerReplacementRelease = (releaseCall, usage, context) => {
|
|
15081
|
+
if (!isNodeOfType(usage.node, "CallExpression")) return false;
|
|
15082
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
15083
|
+
if (!usageFunction || !isFunctionLike$1(usageFunction) || usageFunction !== findEnclosingFunction$1(releaseCall) || !isFunctionUsedAsReactRef(usageFunction, context)) return false;
|
|
15084
|
+
const registrationCallee = stripParenExpression(usage.node.callee);
|
|
15085
|
+
const releaseCallee = stripParenExpression(releaseCall.callee);
|
|
15086
|
+
const releaseRefSymbol = isNodeOfType(releaseCallee, "MemberExpression") ? resolveReactRefCurrentOriginSymbol(releaseCallee.object, context.scopes) : null;
|
|
15087
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression") || registrationCallee.computed || !isNodeOfType(registrationCallee.property, "Identifier") || registrationCallee.property.name !== "addEventListener" || !isNodeOfType(releaseCallee, "MemberExpression") || releaseCallee.computed || !isNodeOfType(releaseCallee.property, "Identifier") || releaseCallee.property.name !== "removeEventListener" || !releaseRefSymbol) return false;
|
|
15088
|
+
const registrationReceiverKey = resolveExpressionKey(stripParenExpression(registrationCallee.object), context);
|
|
15089
|
+
const nodeParameterKey = resolveExpressionKey(usageFunction.params?.[0], context);
|
|
15090
|
+
const releaseReceiverKey = resolveExpressionKey(releaseCallee.object, context);
|
|
15091
|
+
if (registrationReceiverKey === null || registrationReceiverKey !== nodeParameterKey || releaseReceiverKey === null || usage.eventKey === null || usage.eventKey !== resolveExpressionKey(releaseCall.arguments?.[0], context) || usage.handlerKey === null || usage.handlerKey !== resolveExpressionKey(releaseCall.arguments?.[1], context)) return false;
|
|
15092
|
+
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15093
|
+
const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15094
|
+
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15095
|
+
const releaseStart = getRangeStart(releaseCall);
|
|
15096
|
+
const matchingOwnershipAssignments = [];
|
|
15097
|
+
const usageFunctionBody = usageFunction.body;
|
|
15098
|
+
walkAst(usageFunctionBody, (child) => {
|
|
15099
|
+
if (child !== usageFunctionBody && isFunctionLike$1(child)) return false;
|
|
15100
|
+
if (isNodeOfType(child, "AssignmentExpression") && child.operator === "=" && resolveReactRefSymbol(stripParenExpression(child.left), context.scopes)?.id === releaseRefSymbol.id && resolveExpressionKey(child.right, context) === registrationReceiverKey && releaseStart !== null && (getRangeStart(child) ?? -1) > releaseStart) matchingOwnershipAssignments.push(child);
|
|
15101
|
+
});
|
|
15102
|
+
const releaseAnchor = findLiveExpressionGuardForRelease(releaseCall, usageFunction, releaseReceiverKey, context) ?? releaseCall;
|
|
15103
|
+
const safeOwnershipAssignments = matchingOwnershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, [releaseAnchor], usageFunction, context));
|
|
15104
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(usageFunction, [releaseAnchor], context) && doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
|
|
15105
|
+
};
|
|
14754
15106
|
const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
14755
15107
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
14756
15108
|
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
@@ -14767,13 +15119,21 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14767
15119
|
if (!releaseVerbName) return false;
|
|
14768
15120
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
14769
15121
|
const releaseReceiverKey = resolveExpressionKey(callee.object, context);
|
|
15122
|
+
const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
|
|
15123
|
+
const pairedReleaseVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
15124
|
+
const pushedResourceCollectionKey = findPushedResourceCollectionKey(usage, context);
|
|
15125
|
+
const releaseReceiverForOfStatement = findForOfStatementForIteratorExpression(callee.object, context);
|
|
15126
|
+
const releaseReceiverCollectionKey = releaseReceiverForOfStatement ? resolveExpressionKey(releaseReceiverForOfStatement.right, context) : resolveIteratorCollectionKey(callee.object, context);
|
|
15127
|
+
if (pairedReleaseVerbNames && matchesPairedReleaseVerb(releaseVerbName, pairedReleaseVerbNames) && pushedResourceCollectionKey !== null && pushedResourceCollectionKey === releaseReceiverCollectionKey && (releaseVerbName !== "unobserve" || usage.eventKey !== null && releaseEventKey === usage.eventKey)) return true;
|
|
15128
|
+
if (isReactRefListenerReplacementRelease(callNode, usage, context)) return true;
|
|
14770
15129
|
if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
14771
15130
|
if (usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName))) return true;
|
|
14772
15131
|
if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
|
|
15132
|
+
if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
|
|
14773
15133
|
if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
|
|
15134
|
+
if (usage.registrationVerbName === "subscribe" && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub") && usage.handleKey !== null && resolveExpressionKey(callNode.arguments?.[0], context) === usage.handleKey) return true;
|
|
14774
15135
|
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
14775
15136
|
if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
|
|
14776
|
-
const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
|
|
14777
15137
|
const usageEventArgument = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[0] : null;
|
|
14778
15138
|
const releaseEventArgument = callNode.arguments?.[0];
|
|
14779
15139
|
if (isAssignmentFormForOfIteratorReference(usageEventArgument, context) || isAssignmentFormForOfIteratorReference(releaseEventArgument, context)) return false;
|
|
@@ -14803,9 +15163,12 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14803
15163
|
return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
|
|
14804
15164
|
}
|
|
14805
15165
|
if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
|
|
14806
|
-
const
|
|
15166
|
+
const usesUnaryListenerSignature = usage.registrationVerbName === "addListener" && isNodeOfType(usage.node, "CallExpression") && usage.node.arguments?.length === 1 && callNode.arguments?.length === 1;
|
|
15167
|
+
const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
|
|
14807
15168
|
if (!releaseHandler) return releaseVerbName === "off";
|
|
14808
|
-
|
|
15169
|
+
const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
|
|
15170
|
+
const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignature ? 0 : 1] : null;
|
|
15171
|
+
return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
|
|
14809
15172
|
}
|
|
14810
15173
|
if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
|
|
14811
15174
|
return true;
|
|
@@ -14818,8 +15181,7 @@ const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
|
14818
15181
|
currentNode = parentNode;
|
|
14819
15182
|
parentNode = currentNode.parent;
|
|
14820
15183
|
}
|
|
14821
|
-
|
|
14822
|
-
const effectCallback = findEnclosingFunction$1(parentNode);
|
|
15184
|
+
const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
|
|
14823
15185
|
const effectCall = effectCallback?.parent;
|
|
14824
15186
|
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
|
|
14825
15187
|
};
|
|
@@ -14831,13 +15193,165 @@ const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
|
14831
15193
|
if (!symbol) return false;
|
|
14832
15194
|
return symbol.references.some((reference) => findEnclosingFunction$1(reference.identifier) !== functionNode);
|
|
14833
15195
|
};
|
|
15196
|
+
const findRetainedDisposerStorages = (disposerFunction, usage, context) => {
|
|
15197
|
+
if (!isFunctionLike$1(disposerFunction) || disposerFunction.async || disposerFunction.generator) return [];
|
|
15198
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
15199
|
+
if (!usageFunction || !isFunctionLike$1(usageFunction)) return [];
|
|
15200
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
15201
|
+
const collectAssignment = (expression) => {
|
|
15202
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
15203
|
+
const assignment = expressionRoot.parent;
|
|
15204
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.right !== expressionRoot) return;
|
|
15205
|
+
const refSymbol = resolveReactRefSymbol(stripParenExpression(assignment.left), context.scopes);
|
|
15206
|
+
const refCurrentKey = resolveExpressionKey(assignment.left, context);
|
|
15207
|
+
const retainedFunction = findEnclosingFunction$1(assignment);
|
|
15208
|
+
const assignmentStart = getRangeStart(assignment);
|
|
15209
|
+
if (!refSymbol || !refCurrentKey || !retainedFunction || retainedFunction !== usageFunction || assignmentStart === null) return;
|
|
15210
|
+
assignments.set(assignmentStart, {
|
|
15211
|
+
assignmentNode: assignment,
|
|
15212
|
+
refCurrentKey,
|
|
15213
|
+
retainedFunction
|
|
15214
|
+
});
|
|
15215
|
+
};
|
|
15216
|
+
collectAssignment(disposerFunction);
|
|
15217
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(disposerFunction);
|
|
15218
|
+
const symbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
|
|
15219
|
+
for (const reference of symbol?.references ?? []) collectAssignment(reference.identifier);
|
|
15220
|
+
walkAst(usageFunction.body, (child) => {
|
|
15221
|
+
if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
|
|
15222
|
+
if (isNodeOfType(child, "AssignmentExpression") && resolveStableValue(child.right, context) === disposerFunction) collectAssignment(child.right);
|
|
15223
|
+
});
|
|
15224
|
+
return [...assignments.values()];
|
|
15225
|
+
};
|
|
15226
|
+
const isRetainedDisposerStorageEstablished = (storage, usage, context) => doMatchingNodesCoverEveryPathBeforeUsage(usage.node, [storage.assignmentNode], storage.retainedFunction, context) || doMatchingNodesCoverEveryPathAfterUsage(usage.node, [storage.assignmentNode], context);
|
|
15227
|
+
const hasUnsafeRetainedDisposerOverwrite = (storage, usage, context) => {
|
|
15228
|
+
let hasUnsafeOverwrite = false;
|
|
15229
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15230
|
+
if (hasUnsafeOverwrite) return false;
|
|
15231
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15232
|
+
if (!isNodeOfType(child, "AssignmentExpression") || child === storage.assignmentNode || resolveExpressionKey(child.left, context) !== storage.refCurrentKey || !canNodeReachLaterNodeWithinFunction(usage.node, child, storage.retainedFunction, context)) return;
|
|
15233
|
+
const storedValue = resolveStableValue(child.right, context);
|
|
15234
|
+
if (!storedValue || !isFunctionLike$1(storedValue) || !doesCleanupFunctionReleaseUsage(storedValue, usage, context)) {
|
|
15235
|
+
hasUnsafeOverwrite = true;
|
|
15236
|
+
return false;
|
|
15237
|
+
}
|
|
15238
|
+
});
|
|
15239
|
+
return hasUnsafeOverwrite;
|
|
15240
|
+
};
|
|
15241
|
+
const hasEffectCleanupInvocation = (storage, usage, context) => {
|
|
15242
|
+
const componentFunction = findEnclosingFunction$1(storage.retainedFunction);
|
|
15243
|
+
if (!componentFunction || !isFunctionLike$1(componentFunction)) return false;
|
|
15244
|
+
const cleanupFunctionInvokesRef = (cleanupFunction) => {
|
|
15245
|
+
if (!isFunctionLike$1(cleanupFunction)) return false;
|
|
15246
|
+
let didFindCleanupCall = false;
|
|
15247
|
+
walkAst(cleanupFunction.body, (child) => {
|
|
15248
|
+
if (didFindCleanupCall) return false;
|
|
15249
|
+
if (child !== cleanupFunction.body && isFunctionLike$1(child)) return false;
|
|
15250
|
+
if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) {
|
|
15251
|
+
const callRoot = findTransparentExpressionRoot(child);
|
|
15252
|
+
const callStatement = callRoot.parent;
|
|
15253
|
+
const isDirectBlockStatement = isNodeOfType(cleanupFunction.body, "BlockStatement") && isNodeOfType(callStatement, "ExpressionStatement") && callStatement.parent === cleanupFunction.body;
|
|
15254
|
+
const isConciseBody = cleanupFunction.body === callRoot;
|
|
15255
|
+
if ((isDirectBlockStatement || isConciseBody) && !hasUnprovenReturnBeforeRefOwnedRelease(cleanupFunction, child, storage.refCurrentKey, context)) {
|
|
15256
|
+
didFindCleanupCall = true;
|
|
15257
|
+
return false;
|
|
15258
|
+
}
|
|
15259
|
+
}
|
|
15260
|
+
});
|
|
15261
|
+
return didFindCleanupCall;
|
|
15262
|
+
};
|
|
15263
|
+
const effectReturnsCleanup = (effectCallback) => {
|
|
15264
|
+
if (!isFunctionLike$1(effectCallback)) return false;
|
|
15265
|
+
if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
|
|
15266
|
+
const cleanupFunction = resolveRefOwnedCleanupFunction(effectCallback.body, context);
|
|
15267
|
+
return Boolean(cleanupFunction && cleanupFunctionInvokesRef(cleanupFunction));
|
|
15268
|
+
}
|
|
15269
|
+
const matchingReturns = [];
|
|
15270
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
15271
|
+
if (!isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
15272
|
+
const cleanupFunction = resolveRefOwnedCleanupFunction(child.argument, context);
|
|
15273
|
+
if (!cleanupFunction || !cleanupFunctionInvokesRef(cleanupFunction)) return;
|
|
15274
|
+
matchingReturns.push(child);
|
|
15275
|
+
});
|
|
15276
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(effectCallback, matchingReturns, context);
|
|
15277
|
+
};
|
|
15278
|
+
let didFindInvocation = false;
|
|
15279
|
+
walkAst(componentFunction.body, (child) => {
|
|
15280
|
+
if (didFindInvocation) return false;
|
|
15281
|
+
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction || !isReactApiCall(child, "useEffect", context.scopes)) return;
|
|
15282
|
+
const effectCallback = getEffectCallback(child);
|
|
15283
|
+
if (effectCallback && effectReturnsCleanup(effectCallback)) {
|
|
15284
|
+
didFindInvocation = true;
|
|
15285
|
+
return false;
|
|
15286
|
+
}
|
|
15287
|
+
});
|
|
15288
|
+
return didFindInvocation;
|
|
15289
|
+
};
|
|
15290
|
+
const hasCallbackRefReplacementInvocation = (storage, usage, context) => {
|
|
15291
|
+
const isReturnedCallbackRefShape = () => {
|
|
15292
|
+
if (!isFunctionLike$1(storage.retainedFunction)) return false;
|
|
15293
|
+
const callbackCall = findTransparentExpressionRoot(storage.retainedFunction).parent;
|
|
15294
|
+
if (!isNodeOfType(callbackCall, "CallExpression") || !isReactApiCall(callbackCall, "useCallback", context.scopes)) return false;
|
|
15295
|
+
const nodeParameter = storage.retainedFunction.params?.[0];
|
|
15296
|
+
const nodeParameterKey = resolveExpressionKey(nodeParameter, context);
|
|
15297
|
+
if (!nodeParameterKey || usage.receiverKey !== nodeParameterKey) return false;
|
|
15298
|
+
if (!isFunctionReturnedFromReactHook(storage.retainedFunction, context, false)) return false;
|
|
15299
|
+
const usageStart = getRangeStart(usage.node);
|
|
15300
|
+
if (usageStart === null) return false;
|
|
15301
|
+
let hasNullExit = false;
|
|
15302
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15303
|
+
if (hasNullExit) return false;
|
|
15304
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15305
|
+
if (!isNodeOfType(child, "IfStatement") || (getRangeStart(child) ?? usageStart) >= usageStart) return;
|
|
15306
|
+
const test = stripParenExpression(child.test);
|
|
15307
|
+
if (!isNodeOfType(test, "UnaryExpression") || test.operator !== "!" || resolveExpressionKey(test.argument, context) !== nodeParameterKey) return;
|
|
15308
|
+
const consequent = child.consequent;
|
|
15309
|
+
hasNullExit = isNodeOfType(consequent, "ReturnStatement") || isNodeOfType(consequent, "BlockStatement") && consequent.body.some((statement) => isNodeOfType(statement, "ReturnStatement"));
|
|
15310
|
+
if (hasNullExit) return false;
|
|
15311
|
+
});
|
|
15312
|
+
return hasNullExit;
|
|
15313
|
+
};
|
|
15314
|
+
if (!isFunctionForwardedToReactRef(storage.retainedFunction, context) && !isReturnedCallbackRefShape()) return false;
|
|
15315
|
+
const cleanupCalls = [];
|
|
15316
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15317
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15318
|
+
if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) cleanupCalls.push(child);
|
|
15319
|
+
});
|
|
15320
|
+
return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, cleanupCalls, storage.retainedFunction, context);
|
|
15321
|
+
};
|
|
15322
|
+
const isRetainedDisposerRefRelease = (releaseNode, usage, context) => {
|
|
15323
|
+
const disposerFunction = findEnclosingFunction$1(releaseNode);
|
|
15324
|
+
if (!disposerFunction) return false;
|
|
15325
|
+
return findRetainedDisposerStorages(disposerFunction, usage, context).some((storage) => isRetainedDisposerStorageEstablished(storage, usage, context) && !hasUnsafeRetainedDisposerOverwrite(storage, usage, context) && (hasEffectCleanupInvocation(storage, usage, context) || hasCallbackRefReplacementInvocation(storage, usage, context)));
|
|
15326
|
+
};
|
|
15327
|
+
const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
|
|
15328
|
+
if (usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.receiverKey === null || usage.eventKey === null || !isNodeOfType(usage.node, "CallExpression") || !isFunctionLike$1(releaseFunction) || releaseFunction.async || releaseFunction.generator || !isNodeOfType(releaseFunction.body, "BlockStatement") || !doMatchingNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseNode], context)) return false;
|
|
15329
|
+
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15330
|
+
const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
|
|
15331
|
+
if (!isNodeOfType(releaseCall, "CallExpression")) return false;
|
|
15332
|
+
const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15333
|
+
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15334
|
+
const ownerFunction = findEnclosingFunction$1(releaseFunction);
|
|
15335
|
+
if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
|
|
15336
|
+
const triggerRegistrations = [];
|
|
15337
|
+
walkAst(ownerFunction.body, (child) => {
|
|
15338
|
+
if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
|
|
15339
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
15340
|
+
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
15341
|
+
if (registrationDetails.registrationVerbName === "addEventListener" && registrationDetails.receiverKey === usage.receiverKey && resolveStableValue(child.arguments?.[1], context) === releaseFunction) triggerRegistrations.push(child);
|
|
15342
|
+
});
|
|
15343
|
+
if (triggerRegistrations.some((triggerRegistration) => triggerRegistration === usage.node)) return true;
|
|
15344
|
+
return doMatchingNodesCoverEveryPathAfterUsage(usage.node, triggerRegistrations, context) || doMatchingNodesCoverEveryPathBeforeUsage(usage.node, triggerRegistrations, ownerFunction, context);
|
|
15345
|
+
};
|
|
14834
15346
|
const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
14835
15347
|
if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
|
|
14836
15348
|
const releaseFunction = findEnclosingFunction$1(releaseNode);
|
|
14837
15349
|
if (!releaseFunction) return true;
|
|
14838
15350
|
if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
|
|
15351
|
+
if (isRetainedDisposerRefRelease(releaseNode, usage, context)) return true;
|
|
14839
15352
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14840
15353
|
if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
|
|
15354
|
+
if (isSelfReleasingListenerRelease(releaseNode, releaseFunction, usage, context)) return true;
|
|
14841
15355
|
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
14842
15356
|
};
|
|
14843
15357
|
const fileContainsReleaseForUsage = (usage, context) => {
|
|
@@ -16769,7 +17283,38 @@ const symbolHasStableImportedAlias = (symbol, scopes) => {
|
|
|
16769
17283
|
const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
|
|
16770
17284
|
return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
|
|
16771
17285
|
};
|
|
16772
|
-
const
|
|
17286
|
+
const isAssignmentTarget = (node) => {
|
|
17287
|
+
let currentNode = findTransparentExpressionRoot(node);
|
|
17288
|
+
while (currentNode.parent) {
|
|
17289
|
+
const parentNode = currentNode.parent;
|
|
17290
|
+
if (isNodeOfType(parentNode, "AssignmentExpression")) return parentNode.left === currentNode;
|
|
17291
|
+
if (isNodeOfType(parentNode, "UpdateExpression")) return parentNode.argument === currentNode;
|
|
17292
|
+
if (isNodeOfType(parentNode, "UnaryExpression")) return parentNode.operator === "delete" && parentNode.argument === currentNode;
|
|
17293
|
+
if (isNodeOfType(parentNode, "ForInStatement") || isNodeOfType(parentNode, "ForOfStatement")) return parentNode.left === currentNode;
|
|
17294
|
+
if (isNodeOfType(parentNode, "ArrayPattern") && parentNode.elements.some((element) => element === currentNode) || isNodeOfType(parentNode, "ObjectPattern") && parentNode.properties.some((property) => property === currentNode) || isNodeOfType(parentNode, "RestElement") && parentNode.argument === currentNode || isNodeOfType(parentNode, "AssignmentPattern") && parentNode.left === currentNode || isNodeOfType(parentNode, "Property") && parentNode.value === currentNode && isNodeOfType(parentNode.parent, "ObjectPattern")) {
|
|
17295
|
+
currentNode = parentNode;
|
|
17296
|
+
continue;
|
|
17297
|
+
}
|
|
17298
|
+
return false;
|
|
17299
|
+
}
|
|
17300
|
+
return false;
|
|
17301
|
+
};
|
|
17302
|
+
const symbolHasStableRefLazyInitialization = (symbol, scopes) => {
|
|
17303
|
+
if (symbol.kind !== "const" || symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
17304
|
+
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
17305
|
+
if (!isNodeOfType(initializer, "AssignmentExpression") || initializer.operator !== "??=") return false;
|
|
17306
|
+
const refSymbol = resolveReactRefSymbol(unwrapExpression$3(initializer.left), scopes);
|
|
17307
|
+
if (!refSymbol) return false;
|
|
17308
|
+
return refSymbol.references.every((reference) => {
|
|
17309
|
+
const memberExpression = findTransparentExpressionRoot(reference.identifier).parent;
|
|
17310
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || unwrapExpression$3(memberExpression.object) !== reference.identifier || getStaticPropertyName(memberExpression) !== "current") return false;
|
|
17311
|
+
const referenceRoot = findTransparentExpressionRoot(memberExpression);
|
|
17312
|
+
const parentNode = referenceRoot.parent;
|
|
17313
|
+
if (isNodeOfType(parentNode, "AssignmentExpression") && parentNode.left === referenceRoot) return parentNode === initializer;
|
|
17314
|
+
return !isAssignmentTarget(referenceRoot);
|
|
17315
|
+
});
|
|
17316
|
+
};
|
|
17317
|
+
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol, scopes) || symbolHasStableRefLazyInitialization(symbol, scopes) || symbolHasStableImportedAlias(symbol, scopes) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
16773
17318
|
//#endregion
|
|
16774
17319
|
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
16775
17320
|
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
@@ -16971,7 +17516,7 @@ const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys, allow
|
|
|
16971
17516
|
keys.add(depKey);
|
|
16972
17517
|
continue;
|
|
16973
17518
|
}
|
|
16974
|
-
const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
17519
|
+
const identitySourceKeys = resolvePureCalledFunctionSourceKeys(reference, symbol, scopes) ?? resolveRenderDerivedMutableSourceKeys(reference, symbol, scopes) ?? resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
16975
17520
|
if (identitySourceKeys) {
|
|
16976
17521
|
if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
|
|
16977
17522
|
for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
|
|
@@ -17054,6 +17599,161 @@ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
|
|
|
17054
17599
|
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
17055
17600
|
return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
|
|
17056
17601
|
};
|
|
17602
|
+
const isPureDerivedExpression = (expression) => {
|
|
17603
|
+
const candidate = unwrapExpression$3(expression);
|
|
17604
|
+
if (isNodeOfType(candidate, "Literal") || isNodeOfType(candidate, "Identifier")) return true;
|
|
17605
|
+
if (isNodeOfType(candidate, "MemberExpression")) return isPureDerivedExpression(candidate.object) && (!candidate.computed || isPureDerivedExpression(candidate.property));
|
|
17606
|
+
if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return isPureDerivedExpression(candidate.left) && isPureDerivedExpression(candidate.right);
|
|
17607
|
+
if (isNodeOfType(candidate, "UnaryExpression")) return candidate.operator !== "delete" && isPureDerivedExpression(candidate.argument);
|
|
17608
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return isPureDerivedExpression(candidate.test) && isPureDerivedExpression(candidate.consequent) && isPureDerivedExpression(candidate.alternate);
|
|
17609
|
+
if (isNodeOfType(candidate, "TemplateLiteral")) return candidate.expressions.every((nestedExpression) => isPureDerivedExpression(nestedExpression));
|
|
17610
|
+
return false;
|
|
17611
|
+
};
|
|
17612
|
+
const isPureDerivedStatement = (statement) => {
|
|
17613
|
+
if (isNodeOfType(statement, "BlockStatement")) return statement.body.every((nestedStatement) => isPureDerivedStatement(nestedStatement));
|
|
17614
|
+
if (isNodeOfType(statement, "ReturnStatement")) return !statement.argument || isPureDerivedExpression(statement.argument);
|
|
17615
|
+
if (isNodeOfType(statement, "IfStatement")) return isPureDerivedExpression(statement.test) && isPureDerivedStatement(statement.consequent) && (!statement.alternate || isPureDerivedStatement(statement.alternate));
|
|
17616
|
+
return false;
|
|
17617
|
+
};
|
|
17618
|
+
const isPureDerivedFunction = (functionNode) => {
|
|
17619
|
+
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return false;
|
|
17620
|
+
if (functionNode.async || functionNode.generator) return false;
|
|
17621
|
+
return isNodeOfType(functionNode.body, "BlockStatement") ? isPureDerivedStatement(functionNode.body) : isPureDerivedExpression(functionNode.body);
|
|
17622
|
+
};
|
|
17623
|
+
const resolvePureCalledFunctionSourceKeys = (reference, symbol, scopes) => {
|
|
17624
|
+
if (symbol.references.some((symbolReference) => symbolReference.flag !== "read")) return null;
|
|
17625
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
17626
|
+
const callExpression = referenceRoot.parent;
|
|
17627
|
+
if (!isNodeOfType(callExpression, "CallExpression") || callExpression.callee !== referenceRoot) return null;
|
|
17628
|
+
const functionNode = getFunctionValueNode(symbol);
|
|
17629
|
+
if (!functionNode || !isPureDerivedFunction(functionNode)) return null;
|
|
17630
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17631
|
+
for (const capturedReference of closureCaptures(functionNode, scopes)) {
|
|
17632
|
+
const capturedSymbol = capturedReference.resolvedSymbol;
|
|
17633
|
+
if (!capturedSymbol || capturedSymbol.id === symbol.id) continue;
|
|
17634
|
+
if (isOutsideAllFunctions(capturedSymbol) || symbolHasStableValue(capturedSymbol, scopes)) continue;
|
|
17635
|
+
const capturedKey = computeDepKey(capturedReference);
|
|
17636
|
+
if (!capturedKey) return null;
|
|
17637
|
+
if (capturedKey === capturedSymbol.name) {
|
|
17638
|
+
const nestedSourceKeys = resolveReactiveIdentitySourceKeys(capturedSymbol, scopes);
|
|
17639
|
+
if (nestedSourceKeys) {
|
|
17640
|
+
for (const nestedSourceKey of nestedSourceKeys) sourceKeys.add(nestedSourceKey);
|
|
17641
|
+
continue;
|
|
17642
|
+
}
|
|
17643
|
+
}
|
|
17644
|
+
sourceKeys.add(capturedKey);
|
|
17645
|
+
}
|
|
17646
|
+
return sourceKeys.size > 0 ? sourceKeys : null;
|
|
17647
|
+
};
|
|
17648
|
+
const mergeDerivedExpressionSourceKeys = (expressions, scopes, visitedSymbolIds) => {
|
|
17649
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17650
|
+
for (const expression of expressions) {
|
|
17651
|
+
const expressionSourceKeys = resolveDerivedExpressionSourceKeys(expression, scopes, visitedSymbolIds);
|
|
17652
|
+
if (!expressionSourceKeys) return null;
|
|
17653
|
+
for (const expressionSourceKey of expressionSourceKeys) sourceKeys.add(expressionSourceKey);
|
|
17654
|
+
}
|
|
17655
|
+
return sourceKeys;
|
|
17656
|
+
};
|
|
17657
|
+
const resolveDerivedExpressionSourceKeys = (expression, scopes, visitedSymbolIds) => {
|
|
17658
|
+
const candidate = unwrapExpression$3(expression);
|
|
17659
|
+
if (isNodeOfType(candidate, "Literal")) return /* @__PURE__ */ new Set();
|
|
17660
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
17661
|
+
if (scopes.isGlobalReference(candidate)) return /* @__PURE__ */ new Set();
|
|
17662
|
+
const sourceSymbol = scopes.symbolFor(candidate);
|
|
17663
|
+
if (!sourceSymbol) return null;
|
|
17664
|
+
if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
17665
|
+
if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((sourceReference) => sourceReference.flag === "read") && !visitedSymbolIds.has(sourceSymbol.id)) {
|
|
17666
|
+
visitedSymbolIds.add(sourceSymbol.id);
|
|
17667
|
+
const sourceKeys = resolveDerivedExpressionSourceKeys(sourceSymbol.initializer, scopes, visitedSymbolIds);
|
|
17668
|
+
visitedSymbolIds.delete(sourceSymbol.id);
|
|
17669
|
+
if (sourceKeys) return sourceKeys;
|
|
17670
|
+
}
|
|
17671
|
+
return new Set([sourceSymbol.name]);
|
|
17672
|
+
}
|
|
17673
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
17674
|
+
if (hasComputedMemberExpression(candidate)) return null;
|
|
17675
|
+
const sourceKey = stringifyMemberChain(candidate);
|
|
17676
|
+
const rootIdentifier = getMemberRootIdentifier(candidate);
|
|
17677
|
+
const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
17678
|
+
if (!sourceKey || !rootSymbol) return null;
|
|
17679
|
+
if (isOutsideAllFunctions(rootSymbol) || symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
17680
|
+
return new Set([sourceKey]);
|
|
17681
|
+
}
|
|
17682
|
+
if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return mergeDerivedExpressionSourceKeys([candidate.left, candidate.right], scopes, visitedSymbolIds);
|
|
17683
|
+
if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator !== "delete") return resolveDerivedExpressionSourceKeys(candidate.argument, scopes, visitedSymbolIds);
|
|
17684
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return mergeDerivedExpressionSourceKeys([
|
|
17685
|
+
candidate.test,
|
|
17686
|
+
candidate.consequent,
|
|
17687
|
+
candidate.alternate
|
|
17688
|
+
], scopes, visitedSymbolIds);
|
|
17689
|
+
if (isNodeOfType(candidate, "TemplateLiteral")) return mergeDerivedExpressionSourceKeys(candidate.expressions, scopes, visitedSymbolIds);
|
|
17690
|
+
if (isNodeOfType(candidate, "NewExpression")) {
|
|
17691
|
+
const callee = unwrapExpression$3(candidate.callee);
|
|
17692
|
+
if (!isNodeOfType(callee, "Identifier") || callee.name !== "Error" || !scopes.isGlobalReference(callee)) return null;
|
|
17693
|
+
const argumentsToAnalyze = [];
|
|
17694
|
+
for (const argument of candidate.arguments) {
|
|
17695
|
+
if (!isAstNode(argument) || isNodeOfType(argument, "SpreadElement")) return null;
|
|
17696
|
+
argumentsToAnalyze.push(argument);
|
|
17697
|
+
}
|
|
17698
|
+
return mergeDerivedExpressionSourceKeys(argumentsToAnalyze, scopes, visitedSymbolIds);
|
|
17699
|
+
}
|
|
17700
|
+
return null;
|
|
17701
|
+
};
|
|
17702
|
+
const resolveWriteControlSourceKeys = (assignment, boundaryFunction, scopes) => {
|
|
17703
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17704
|
+
let currentNode = assignment;
|
|
17705
|
+
while (currentNode.parent && currentNode.parent !== boundaryFunction) {
|
|
17706
|
+
const parentNode = currentNode.parent;
|
|
17707
|
+
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
17708
|
+
if (parentNode.test === currentNode) return null;
|
|
17709
|
+
const testSourceKeys = resolveDerivedExpressionSourceKeys(parentNode.test, scopes, /* @__PURE__ */ new Set());
|
|
17710
|
+
if (!testSourceKeys) return null;
|
|
17711
|
+
for (const testSourceKey of testSourceKeys) sourceKeys.add(testSourceKey);
|
|
17712
|
+
} else if (!isNodeOfType(parentNode, "ExpressionStatement") && !isNodeOfType(parentNode, "BlockStatement")) return null;
|
|
17713
|
+
currentNode = parentNode;
|
|
17714
|
+
}
|
|
17715
|
+
return currentNode.parent === boundaryFunction ? sourceKeys : null;
|
|
17716
|
+
};
|
|
17717
|
+
const isReadOnlyInitialStateUse = (referenceNode, scopes) => {
|
|
17718
|
+
const referenceRoot = findTransparentExpressionRoot(referenceNode);
|
|
17719
|
+
const callExpression = referenceRoot.parent;
|
|
17720
|
+
return isNodeOfType(callExpression, "CallExpression") && callExpression.arguments.some((argument) => argument === referenceRoot) && isReactApiCall(callExpression, "useState", scopes, {
|
|
17721
|
+
allowGlobalReactNamespace: true,
|
|
17722
|
+
allowUnboundBareCalls: true,
|
|
17723
|
+
resolveNamedAliases: true
|
|
17724
|
+
});
|
|
17725
|
+
};
|
|
17726
|
+
const resolveRenderDerivedMutableSourceKeys = (capturedReference, symbol, scopes) => {
|
|
17727
|
+
if (symbol.kind !== "let" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
17728
|
+
const boundaryFunction = findEnclosingFunction$1(symbol.bindingIdentifier);
|
|
17729
|
+
if (!boundaryFunction) return null;
|
|
17730
|
+
const capturingFunction = findEnclosingFunction$1(capturedReference.identifier);
|
|
17731
|
+
if (!capturingFunction || capturingFunction === boundaryFunction) return null;
|
|
17732
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17733
|
+
if (symbol.initializer) {
|
|
17734
|
+
const initializerSourceKeys = resolveDerivedExpressionSourceKeys(symbol.initializer, scopes, new Set([symbol.id]));
|
|
17735
|
+
if (!initializerSourceKeys) return null;
|
|
17736
|
+
for (const initializerSourceKey of initializerSourceKeys) sourceKeys.add(initializerSourceKey);
|
|
17737
|
+
}
|
|
17738
|
+
let writeCount = 0;
|
|
17739
|
+
for (const symbolReference of symbol.references) {
|
|
17740
|
+
if (symbolReference.flag === "read") {
|
|
17741
|
+
if (findEnclosingFunction$1(symbolReference.identifier) !== capturingFunction && !isReadOnlyInitialStateUse(symbolReference.identifier, scopes)) return null;
|
|
17742
|
+
continue;
|
|
17743
|
+
}
|
|
17744
|
+
if (symbolReference.flag !== "write") return null;
|
|
17745
|
+
const referenceRoot = findTransparentExpressionRoot(symbolReference.identifier);
|
|
17746
|
+
const assignment = referenceRoot.parent;
|
|
17747
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== referenceRoot || findEnclosingFunction$1(referenceRoot) !== boundaryFunction) return null;
|
|
17748
|
+
const assignmentSourceKeys = resolveDerivedExpressionSourceKeys(assignment.right, scopes, new Set([symbol.id]));
|
|
17749
|
+
const controlSourceKeys = resolveWriteControlSourceKeys(assignment, boundaryFunction, scopes);
|
|
17750
|
+
if (!assignmentSourceKeys || !controlSourceKeys) return null;
|
|
17751
|
+
for (const assignmentSourceKey of assignmentSourceKeys) sourceKeys.add(assignmentSourceKey);
|
|
17752
|
+
for (const controlSourceKey of controlSourceKeys) sourceKeys.add(controlSourceKey);
|
|
17753
|
+
writeCount += 1;
|
|
17754
|
+
}
|
|
17755
|
+
return writeCount > 0 && sourceKeys.size > 0 ? sourceKeys : null;
|
|
17756
|
+
};
|
|
17057
17757
|
const isUseCallbackResultDep = (node, scopes) => {
|
|
17058
17758
|
const rootSymbol = getRootSymbol(node, scopes);
|
|
17059
17759
|
const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
|
|
@@ -17797,7 +18497,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
17797
18497
|
if (!isUsed) continue;
|
|
17798
18498
|
const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
|
|
17799
18499
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
17800
|
-
if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || !isUnstableInitializer(rootSymbol.initializer)) continue;
|
|
18500
|
+
if (!rootSymbol || !hasDirectIdentifierDeclarator(rootSymbol) || symbolHasStableValue(rootSymbol, context.scopes) || !isUnstableInitializer(rootSymbol.initializer)) continue;
|
|
17801
18501
|
context.report({
|
|
17802
18502
|
node: reportNode,
|
|
17803
18503
|
message: buildUnstableDepMessage(hookName, declaredKey)
|
|
@@ -29281,7 +29981,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
29281
29981
|
//#endregion
|
|
29282
29982
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
29283
29983
|
const MESSAGE$39 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
29284
|
-
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
29984
|
+
const isUndefinedIdentifier$1 = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
29285
29985
|
const noAccessKey = defineRule({
|
|
29286
29986
|
id: "no-access-key",
|
|
29287
29987
|
title: "accessKey attribute used",
|
|
@@ -29306,7 +30006,7 @@ const noAccessKey = defineRule({
|
|
|
29306
30006
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
29307
30007
|
const expression = attributeValue.expression;
|
|
29308
30008
|
if (!expression || expression.type === "JSXEmptyExpression") return;
|
|
29309
|
-
if (isUndefinedIdentifier(expression)) return;
|
|
30009
|
+
if (isUndefinedIdentifier$1(expression)) return;
|
|
29310
30010
|
context.report({
|
|
29311
30011
|
node: accessKey,
|
|
29312
30012
|
message: MESSAGE$39
|
|
@@ -30071,6 +30771,12 @@ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.som
|
|
|
30071
30771
|
const importDeclaration = declarationNode.parent;
|
|
30072
30772
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
30073
30773
|
}));
|
|
30774
|
+
const isReactNamespaceReceiver = (analysis, node) => {
|
|
30775
|
+
const receiver = stripParenExpression(node);
|
|
30776
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
30777
|
+
const namespaceReference = getRef(analysis, receiver);
|
|
30778
|
+
return namespaceReference?.resolved ? isReactNamespaceImportReference(namespaceReference) : receiver.name === "React";
|
|
30779
|
+
};
|
|
30074
30780
|
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
30075
30781
|
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
30076
30782
|
const callee = stripParenExpression(declarator.init.callee);
|
|
@@ -30079,24 +30785,20 @@ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
|
30079
30785
|
if (!reference?.resolved) return callee.name === hookName;
|
|
30080
30786
|
return isReactNamedImportReference(reference, hookName);
|
|
30081
30787
|
}
|
|
30082
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.
|
|
30083
|
-
|
|
30084
|
-
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
30085
|
-
return isReactNamespaceImportReference(namespaceReference);
|
|
30788
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
30789
|
+
return isReactNamespaceReceiver(analysis, callee.object);
|
|
30086
30790
|
};
|
|
30087
30791
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
30088
30792
|
if (!node) return false;
|
|
30089
30793
|
if (isNodeOfType(node, "Identifier")) {
|
|
30090
30794
|
if (node.name === hookName) return true;
|
|
30091
30795
|
if (isReactNamedImportReference(getRef(analysis, node), hookName)) return true;
|
|
30092
|
-
const
|
|
30093
|
-
|
|
30796
|
+
const receiverRoot = findTransparentExpressionRoot(node);
|
|
30797
|
+
const parent = receiverRoot.parent;
|
|
30798
|
+
if (parent && isNodeOfType(parent, "MemberExpression") && parent.object === receiverRoot && isReactNamespaceReceiver(analysis, node) && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
30094
30799
|
return false;
|
|
30095
30800
|
}
|
|
30096
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
30097
|
-
const receiver = stripParenExpression(node.object);
|
|
30098
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30099
|
-
}
|
|
30801
|
+
if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30100
30802
|
return false;
|
|
30101
30803
|
};
|
|
30102
30804
|
const isUseEffect = (node) => {
|
|
@@ -30204,7 +30906,27 @@ const isRefCurrent = (ref) => {
|
|
|
30204
30906
|
if (!isNodeOfType(parent.property, "Identifier")) return false;
|
|
30205
30907
|
return parent.property.name === "current";
|
|
30206
30908
|
};
|
|
30207
|
-
const
|
|
30909
|
+
const resolveStateSetterReference = (analysis, ref) => {
|
|
30910
|
+
const visitedReferences = /* @__PURE__ */ new Set();
|
|
30911
|
+
let currentReference = ref;
|
|
30912
|
+
while (currentReference && !visitedReferences.has(currentReference)) {
|
|
30913
|
+
if (isStateSetter(analysis, currentReference)) return currentReference;
|
|
30914
|
+
visitedReferences.add(currentReference);
|
|
30915
|
+
const definitions = currentReference.resolved?.defs ?? [];
|
|
30916
|
+
if (definitions.length !== 1) return null;
|
|
30917
|
+
const definitionNode = definitions[0].node;
|
|
30918
|
+
if (!isNodeOfType(definitionNode, "VariableDeclarator")) return null;
|
|
30919
|
+
if (!isNodeOfType(definitionNode.id, "Identifier")) return null;
|
|
30920
|
+
const declaration = definitionNode.parent;
|
|
30921
|
+
if (!isNodeOfType(declaration, "VariableDeclaration") || declaration.kind !== "const") return null;
|
|
30922
|
+
if (!definitionNode.init) return null;
|
|
30923
|
+
const initializer = stripParenExpression(definitionNode.init);
|
|
30924
|
+
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
30925
|
+
currentReference = getRef(analysis, initializer);
|
|
30926
|
+
}
|
|
30927
|
+
return null;
|
|
30928
|
+
};
|
|
30929
|
+
const isStateSetterCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => resolveStateSetterReference(analysis, innerRef) !== null);
|
|
30208
30930
|
const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(analysis, ref) && isSynchronous(ref.identifier, effectFn) && !resolvesToAsyncFunction(ref);
|
|
30209
30931
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
30210
30932
|
const SYNCHRONOUS_CALLBACK_ARGUMENT_INDEX_BY_METHOD = new Map([
|
|
@@ -30355,9 +31077,11 @@ const isPropCallbackInvocationRef = (analysis, ref, options = {}) => {
|
|
|
30355
31077
|
};
|
|
30356
31078
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
30357
31079
|
const getUseStateDecl = (analysis, ref) => {
|
|
30358
|
-
|
|
30359
|
-
|
|
30360
|
-
|
|
31080
|
+
const definition = getUpstreamRefs(analysis, ref).find((upstreamReference) => isState(analysis, upstreamReference) || isStateSetter(analysis, upstreamReference))?.resolved?.defs.find((candidateDefinition) => {
|
|
31081
|
+
const definitionNode = candidateDefinition.node;
|
|
31082
|
+
return isNodeOfType(definitionNode, "VariableDeclarator") && isNodeOfType(definitionNode.init, "CallExpression") && isHookCallee$1(analysis, definitionNode.init.callee, "useState");
|
|
31083
|
+
});
|
|
31084
|
+
return definition ? definition.node : null;
|
|
30361
31085
|
};
|
|
30362
31086
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
30363
31087
|
if (isFunctionLike$1(node)) return true;
|
|
@@ -30520,7 +31244,88 @@ const isIndependentWriterIdentifier = (componentFunction, identifier, includeDef
|
|
|
30520
31244
|
if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
|
|
30521
31245
|
return isSetterWiredToJsxHandler(componentFunction, bindingName);
|
|
30522
31246
|
};
|
|
30523
|
-
const
|
|
31247
|
+
const isSynchronousFunction = (functionNode) => {
|
|
31248
|
+
const functionMetadata = functionNode;
|
|
31249
|
+
return functionMetadata.async !== true && functionMetadata.generator !== true;
|
|
31250
|
+
};
|
|
31251
|
+
const findBindingVariable = (analysis, bindingIdentifier) => {
|
|
31252
|
+
for (const scope of analysis.scopeManager.scopes) for (const variable of scope.variables) if (variable.identifiers.includes(bindingIdentifier)) return variable;
|
|
31253
|
+
return null;
|
|
31254
|
+
};
|
|
31255
|
+
const getImmutableFunctionVariable = (analysis, componentFunction, functionNode) => {
|
|
31256
|
+
if (!isSynchronousFunction(functionNode) || !isAstDescendant(functionNode, componentFunction)) return null;
|
|
31257
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
31258
|
+
if (!bindingIdentifier) return null;
|
|
31259
|
+
const variable = findBindingVariable(analysis, bindingIdentifier);
|
|
31260
|
+
if (!variable || variable.defs.length !== 1 || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
|
|
31261
|
+
const definition = variable.defs[0];
|
|
31262
|
+
if (definition.type === "FunctionName") return definition.node === functionNode ? variable : null;
|
|
31263
|
+
if (definition.type !== "Variable") return null;
|
|
31264
|
+
const declarator = definition.node;
|
|
31265
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return null;
|
|
31266
|
+
if (declarator.init === functionNode) return variable;
|
|
31267
|
+
if (isNodeOfType(declarator.init, "CallExpression") && declarator.init.arguments?.[0] === functionNode && isGenuineReactHookDeclarator(analysis, declarator, "useCallback")) return variable;
|
|
31268
|
+
return null;
|
|
31269
|
+
};
|
|
31270
|
+
const getJsxEventValueAttribute = (identifier) => {
|
|
31271
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
31272
|
+
const expressionContainer = expression.parent;
|
|
31273
|
+
if (!isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== expression) return null;
|
|
31274
|
+
const attribute = expressionContainer.parent;
|
|
31275
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) return null;
|
|
31276
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
31277
|
+
return attributeName && isEventHandlerName(attributeName) ? attribute : null;
|
|
31278
|
+
};
|
|
31279
|
+
const getInlineJsxEventCallbackAttribute = (callExpression) => {
|
|
31280
|
+
const callbackFunction = findEnclosingFunction$1(callExpression);
|
|
31281
|
+
if (!callbackFunction || !isSynchronousFunction(callbackFunction)) return null;
|
|
31282
|
+
return getJsxEventValueAttribute(callbackFunction);
|
|
31283
|
+
};
|
|
31284
|
+
const isReactHookDependencyReference = (identifier) => {
|
|
31285
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
31286
|
+
const dependencyArray = expression.parent;
|
|
31287
|
+
if (!isNodeOfType(dependencyArray, "ArrayExpression") || !(dependencyArray.elements ?? []).includes(expression)) return false;
|
|
31288
|
+
const hookCall = dependencyArray.parent;
|
|
31289
|
+
if (!isNodeOfType(hookCall, "CallExpression") || hookCall.arguments?.[1] !== dependencyArray) return false;
|
|
31290
|
+
const callee = hookCall.callee;
|
|
31291
|
+
if (isNodeOfType(callee, "Identifier")) return /^use[A-Z0-9]/.test(callee.name);
|
|
31292
|
+
return Boolean(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && /^use[A-Z0-9]/.test(callee.property.name));
|
|
31293
|
+
};
|
|
31294
|
+
const hasReachableJsxEventCallPath = (analysis, context, componentFunction, functionVariable, visitedVariables) => {
|
|
31295
|
+
if (visitedVariables.has(functionVariable)) return false;
|
|
31296
|
+
const nextVisitedVariables = new Set(visitedVariables).add(functionVariable);
|
|
31297
|
+
const callExpressions = [];
|
|
31298
|
+
let hasDirectJsxEventReference = false;
|
|
31299
|
+
for (const reference of functionVariable.references) {
|
|
31300
|
+
if (reference.init) continue;
|
|
31301
|
+
const identifier = reference.identifier;
|
|
31302
|
+
if (reference.isWrite()) return false;
|
|
31303
|
+
const jsxEventValueAttribute = getJsxEventValueAttribute(identifier);
|
|
31304
|
+
if (jsxEventValueAttribute) {
|
|
31305
|
+
if (isNodeReachableWithinFunction(jsxEventValueAttribute, context)) hasDirectJsxEventReference = true;
|
|
31306
|
+
continue;
|
|
31307
|
+
}
|
|
31308
|
+
if (isReactHookDependencyReference(identifier)) continue;
|
|
31309
|
+
const callExpression = getCallExpr(reference);
|
|
31310
|
+
if (!callExpression) return false;
|
|
31311
|
+
const jsxEventCallbackAttribute = getInlineJsxEventCallbackAttribute(callExpression);
|
|
31312
|
+
if (jsxEventCallbackAttribute) {
|
|
31313
|
+
if (isNodeReachableWithinFunction(callExpression, context) && isNodeReachableWithinFunction(jsxEventCallbackAttribute, context)) hasDirectJsxEventReference = true;
|
|
31314
|
+
continue;
|
|
31315
|
+
}
|
|
31316
|
+
callExpressions.push(callExpression);
|
|
31317
|
+
}
|
|
31318
|
+
if (hasDirectJsxEventReference) return true;
|
|
31319
|
+
for (const callExpression of callExpressions) {
|
|
31320
|
+
if (!isNodeReachableWithinFunction(callExpression, context)) continue;
|
|
31321
|
+
const callerFunction = findEnclosingFunction$1(callExpression);
|
|
31322
|
+
if (!callerFunction || callerFunction === componentFunction) continue;
|
|
31323
|
+
const callerVariable = getImmutableFunctionVariable(analysis, componentFunction, callerFunction);
|
|
31324
|
+
if (callerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, callerVariable, nextVisitedVariables)) return true;
|
|
31325
|
+
}
|
|
31326
|
+
return false;
|
|
31327
|
+
};
|
|
31328
|
+
const hasUserInputSetterWriter = (analysis, context, setterRef, effectNode, includeDeferredWriters = false) => {
|
|
30524
31329
|
if (!setterRef.resolved) return false;
|
|
30525
31330
|
const componentFunction = findEnclosingFunction$1(effectNode);
|
|
30526
31331
|
if (!componentFunction) return false;
|
|
@@ -30529,6 +31334,11 @@ const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters
|
|
|
30529
31334
|
const identifier = reference.identifier;
|
|
30530
31335
|
if (isAstDescendant(identifier, effectNode)) continue;
|
|
30531
31336
|
if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
|
|
31337
|
+
if (!isNodeReachableWithinFunction(identifier, context)) continue;
|
|
31338
|
+
const writerFunction = findEnclosingFunction$1(identifier);
|
|
31339
|
+
if (!writerFunction || writerFunction === componentFunction) continue;
|
|
31340
|
+
const writerVariable = getImmutableFunctionVariable(analysis, componentFunction, writerFunction);
|
|
31341
|
+
if (writerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, writerVariable, /* @__PURE__ */ new Set())) return true;
|
|
30532
31342
|
}
|
|
30533
31343
|
return false;
|
|
30534
31344
|
};
|
|
@@ -31418,7 +32228,7 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
|
|
|
31418
32228
|
}
|
|
31419
32229
|
return false;
|
|
31420
32230
|
};
|
|
31421
|
-
const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
|
|
32231
|
+
const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFilename) => {
|
|
31422
32232
|
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
|
|
31423
32233
|
if (frames.length === 0) return [];
|
|
31424
32234
|
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
@@ -31448,7 +32258,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
|
|
|
31448
32258
|
for (const returnedExpression of returnedExpressions) mergeEvidence(valueEvidence, collectValueEvidence(analysis, returnedExpression, updaterFrame, remainingValueCallFrames));
|
|
31449
32259
|
} else valueEvidence = collectValueEvidence(analysis, writtenValue, frame, remainingValueCallFrames);
|
|
31450
32260
|
const sourceReferences = [...valueEvidence.sourceReferences].filter((sourceReference) => getUseStateDecl(analysis, sourceReference) !== stateDeclarator);
|
|
31451
|
-
const hasIndependentWriter = hasUserInputSetterWriter(setterReference, effectNode, true);
|
|
32261
|
+
const hasIndependentWriter = hasUserInputSetterWriter(analysis, context, setterReference, effectNode, true);
|
|
31452
32262
|
const doesMatchStateInitializer = matchesStateInitializer(analysis, callExpression, stateDeclarator);
|
|
31453
32263
|
if (effectHasCleanup && (frame.isDeferred || valueEvidence.hasUnknownSource || valueEvidence.hasDeferredIntroducedValue || valueEvidence.readsExternalValue)) cleanupManagedStateDeclarators.add(stateDeclarator);
|
|
31454
32264
|
const isRenderKnownCopy = sourceReferences.length > 0 && !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue && !hasIndependentWriter;
|
|
@@ -31483,13 +32293,18 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
31483
32293
|
tags: ["test-noise"],
|
|
31484
32294
|
recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
31485
32295
|
create: (context) => ({ CallExpression(node) {
|
|
31486
|
-
if (!
|
|
32296
|
+
if (!isReactApiCall(node, "useEffect", context.scopes, {
|
|
32297
|
+
allowGlobalReactNamespace: true,
|
|
32298
|
+
allowUnboundBareCalls: true,
|
|
32299
|
+
resolveConditionalAliases: true,
|
|
32300
|
+
resolveNamedAliases: true
|
|
32301
|
+
})) return;
|
|
31487
32302
|
const analysis = getProgramAnalysis(node);
|
|
31488
32303
|
if (!analysis) return;
|
|
31489
32304
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
31490
32305
|
if (!dependencyReferences) return;
|
|
31491
32306
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
31492
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
32307
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
31493
32308
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
31494
32309
|
context.report({
|
|
31495
32310
|
node: fact.callExpression,
|
|
@@ -34210,7 +35025,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34210
35025
|
if (!callExpr) continue;
|
|
34211
35026
|
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
34212
35027
|
if (!readsPostMountValueThroughLocals(callExpr, effectFn, { ignoreBareRefCurrent: true })) continue;
|
|
34213
|
-
const declarator = getUseStateDeclarator(ref);
|
|
35028
|
+
const declarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
|
|
34214
35029
|
if (declarator) domSyncedStateDeclarators.add(declarator);
|
|
34215
35030
|
}
|
|
34216
35031
|
for (const ref of effectFnRefs) {
|
|
@@ -34219,7 +35034,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34219
35034
|
if (!callExpr) continue;
|
|
34220
35035
|
if (!isReachableFromStateTrigger(callExpr)) continue;
|
|
34221
35036
|
if (getArgsUpstreamRefs(analysis, ref).some((argRef) => isState(analysis, argRef))) continue;
|
|
34222
|
-
const setterDeclarator = getUseStateDeclarator(ref);
|
|
35037
|
+
const setterDeclarator = getUseStateDeclarator(resolveStateSetterReference(analysis, ref) ?? ref);
|
|
34223
35038
|
if (setterDeclarator && domSyncedStateDeclarators.has(setterDeclarator)) continue;
|
|
34224
35039
|
const isSelfTargeting = setterDeclarator !== null && stateDepDeclarators.has(setterDeclarator);
|
|
34225
35040
|
const setterArguments = isNodeOfType(callExpr, "CallExpression") ? callExpr.arguments ?? [] : [];
|
|
@@ -36060,10 +36875,15 @@ const noDerivedState = defineRule({
|
|
|
36060
36875
|
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
36061
36876
|
} }).visitors,
|
|
36062
36877
|
CallExpression(node) {
|
|
36063
|
-
if (!
|
|
36878
|
+
if (!isReactApiCall(node, "useEffect", context.scopes, {
|
|
36879
|
+
allowGlobalReactNamespace: true,
|
|
36880
|
+
allowUnboundBareCalls: true,
|
|
36881
|
+
resolveConditionalAliases: true,
|
|
36882
|
+
resolveNamedAliases: true
|
|
36883
|
+
})) return;
|
|
36064
36884
|
const analysis = getProgramAnalysis(node);
|
|
36065
36885
|
if (!analysis) return;
|
|
36066
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
36886
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
36067
36887
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
36068
36888
|
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
36069
36889
|
}
|
|
@@ -36080,10 +36900,15 @@ const noDerivedStateEffect = defineRule({
|
|
|
36080
36900
|
tags: ["test-noise"],
|
|
36081
36901
|
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
36082
36902
|
create: (context) => ({ CallExpression(node) {
|
|
36083
|
-
if (!
|
|
36903
|
+
if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
36904
|
+
allowGlobalReactNamespace: true,
|
|
36905
|
+
allowUnboundBareCalls: true,
|
|
36906
|
+
resolveConditionalAliases: true,
|
|
36907
|
+
resolveNamedAliases: true
|
|
36908
|
+
})) return;
|
|
36084
36909
|
const analysis = getProgramAnalysis(node);
|
|
36085
36910
|
if (!analysis) return;
|
|
36086
|
-
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
36911
|
+
if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
36087
36912
|
context.report({
|
|
36088
36913
|
node,
|
|
36089
36914
|
message: "You pay an extra render for state you can derive from other values."
|
|
@@ -36559,9 +37384,20 @@ const noDidMountSetState = defineRule({
|
|
|
36559
37384
|
}
|
|
36560
37385
|
});
|
|
36561
37386
|
//#endregion
|
|
37387
|
+
//#region src/plugin/utils/find-enclosing-class.ts
|
|
37388
|
+
const findEnclosingClass = (node) => {
|
|
37389
|
+
let ancestor = node.parent;
|
|
37390
|
+
while (ancestor) {
|
|
37391
|
+
if (isNodeOfType(ancestor, "ClassDeclaration") || isNodeOfType(ancestor, "ClassExpression")) return ancestor;
|
|
37392
|
+
ancestor = ancestor.parent ?? null;
|
|
37393
|
+
}
|
|
37394
|
+
return null;
|
|
37395
|
+
};
|
|
37396
|
+
//#endregion
|
|
36562
37397
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
36563
37398
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
36564
37399
|
const MESSAGE$27 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
37400
|
+
const DIFFERENCE_OPERATORS = new Set(["!=", "!=="]);
|
|
36565
37401
|
const EQUALITY_OPERATORS = new Set([
|
|
36566
37402
|
"==",
|
|
36567
37403
|
"===",
|
|
@@ -36573,6 +37409,8 @@ const FUNCTION_NODE_TYPES = new Set([
|
|
|
36573
37409
|
"FunctionExpression",
|
|
36574
37410
|
"ArrowFunctionExpression"
|
|
36575
37411
|
]);
|
|
37412
|
+
const CLASS_NODE_TYPES = new Set(["ClassDeclaration", "ClassExpression"]);
|
|
37413
|
+
const callbackRefFieldNamesByClass = /* @__PURE__ */ new WeakMap();
|
|
36576
37414
|
const isLifecycleMethodFunction = (node) => {
|
|
36577
37415
|
if (!FUNCTION_NODE_TYPES.has(node.type)) return false;
|
|
36578
37416
|
const parent = node.parent;
|
|
@@ -36628,6 +37466,187 @@ const getStaticMemberName = (node) => {
|
|
|
36628
37466
|
if (!isNodeOfType(node, "MemberExpression") || node.computed === true) return null;
|
|
36629
37467
|
return isNodeOfType(node.property, "Identifier") ? node.property.name : null;
|
|
36630
37468
|
};
|
|
37469
|
+
const getMemberIdentity = (property) => {
|
|
37470
|
+
const propertyName = getPropertyKeyName$2(property);
|
|
37471
|
+
if (propertyName !== void 0) return isNodeOfType(property, "PrivateIdentifier") ? `#${propertyName}` : propertyName;
|
|
37472
|
+
return isNodeOfType(property, "Literal") && typeof property.value === "string" ? property.value : null;
|
|
37473
|
+
};
|
|
37474
|
+
const collectPreviousSourcePaths = (pattern, domain, members, previousSourcePaths) => {
|
|
37475
|
+
if (!pattern) return;
|
|
37476
|
+
const unwrappedPattern = stripParenExpression(pattern);
|
|
37477
|
+
if (isNodeOfType(unwrappedPattern, "Identifier")) {
|
|
37478
|
+
previousSourcePaths.set(unwrappedPattern.name, {
|
|
37479
|
+
domain,
|
|
37480
|
+
members: [...members],
|
|
37481
|
+
source: "previous"
|
|
37482
|
+
});
|
|
37483
|
+
return;
|
|
37484
|
+
}
|
|
37485
|
+
if (isNodeOfType(unwrappedPattern, "AssignmentPattern")) {
|
|
37486
|
+
collectPreviousSourcePaths(unwrappedPattern.left, domain, members, previousSourcePaths);
|
|
37487
|
+
return;
|
|
37488
|
+
}
|
|
37489
|
+
if (!isNodeOfType(unwrappedPattern, "ObjectPattern")) return;
|
|
37490
|
+
for (const property of unwrappedPattern.properties) {
|
|
37491
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
37492
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
37493
|
+
if (!propertyName) continue;
|
|
37494
|
+
collectPreviousSourcePaths(property.value, domain, [...members, propertyName], previousSourcePaths);
|
|
37495
|
+
}
|
|
37496
|
+
};
|
|
37497
|
+
const getStateSourcePath = (node, previousSourcePaths) => {
|
|
37498
|
+
let currentNode = stripParenExpression(node);
|
|
37499
|
+
const members = [];
|
|
37500
|
+
while (isNodeOfType(currentNode, "MemberExpression")) {
|
|
37501
|
+
const memberName = getStaticMemberName(currentNode);
|
|
37502
|
+
if (!memberName) return null;
|
|
37503
|
+
members.unshift(memberName);
|
|
37504
|
+
currentNode = stripParenExpression(currentNode.object);
|
|
37505
|
+
}
|
|
37506
|
+
if (isNodeOfType(currentNode, "ThisExpression")) {
|
|
37507
|
+
const [domain, ...pathMembers] = members;
|
|
37508
|
+
if (domain !== "props" && domain !== "state") return null;
|
|
37509
|
+
return {
|
|
37510
|
+
domain,
|
|
37511
|
+
members: pathMembers,
|
|
37512
|
+
source: "current"
|
|
37513
|
+
};
|
|
37514
|
+
}
|
|
37515
|
+
if (!isNodeOfType(currentNode, "Identifier")) return null;
|
|
37516
|
+
const previousSourcePath = previousSourcePaths.get(currentNode.name);
|
|
37517
|
+
return previousSourcePath ? {
|
|
37518
|
+
...previousSourcePath,
|
|
37519
|
+
members: [...previousSourcePath.members, ...members]
|
|
37520
|
+
} : null;
|
|
37521
|
+
};
|
|
37522
|
+
const haveMatchingStateSourcePaths = (left, right) => left.domain === right.domain && left.members.length === right.members.length && left.members.every((member, index) => member === right.members[index]);
|
|
37523
|
+
const collectConjunctiveStateSourceComparisons = (test, previousSourcePaths, comparisons) => {
|
|
37524
|
+
const expression = stripParenExpression(test);
|
|
37525
|
+
if (isNodeOfType(expression, "LogicalExpression") && expression.operator === "&&") {
|
|
37526
|
+
collectConjunctiveStateSourceComparisons(expression.left, previousSourcePaths, comparisons);
|
|
37527
|
+
collectConjunctiveStateSourceComparisons(expression.right, previousSourcePaths, comparisons);
|
|
37528
|
+
return;
|
|
37529
|
+
}
|
|
37530
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !EQUALITY_OPERATORS.has(expression.operator)) return;
|
|
37531
|
+
const leftPath = getStateSourcePath(expression.left, previousSourcePaths);
|
|
37532
|
+
const rightPath = getStateSourcePath(expression.right, previousSourcePaths);
|
|
37533
|
+
if (Boolean(leftPath) === Boolean(rightPath)) return;
|
|
37534
|
+
const path = leftPath ?? rightPath;
|
|
37535
|
+
if (!path) return;
|
|
37536
|
+
comparisons.push({
|
|
37537
|
+
comparedValue: leftPath ? expression.right : expression.left,
|
|
37538
|
+
isDifference: DIFFERENCE_OPERATORS.has(expression.operator),
|
|
37539
|
+
path
|
|
37540
|
+
});
|
|
37541
|
+
};
|
|
37542
|
+
const isHistoricalToCurrentTransitionGuard = (test, previousSourcePaths) => {
|
|
37543
|
+
const expression = stripParenExpression(test);
|
|
37544
|
+
if (isNodeOfType(expression, "LogicalExpression") && expression.operator === "||") return isHistoricalToCurrentTransitionGuard(expression.left, previousSourcePaths) && isHistoricalToCurrentTransitionGuard(expression.right, previousSourcePaths);
|
|
37545
|
+
const comparisons = [];
|
|
37546
|
+
collectConjunctiveStateSourceComparisons(expression, previousSourcePaths, comparisons);
|
|
37547
|
+
return comparisons.some((comparison, index) => comparisons.slice(index + 1).some((candidate) => comparison.path.source !== candidate.path.source && comparison.isDifference !== candidate.isDifference && haveMatchingStateSourcePaths(comparison.path, candidate.path) && areExpressionsStructurallyEqual(comparison.comparedValue, candidate.comparedValue)));
|
|
37548
|
+
};
|
|
37549
|
+
const getThisFieldName = (node) => {
|
|
37550
|
+
const unwrappedNode = stripParenExpression(node);
|
|
37551
|
+
if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed === true || !isNodeOfType(stripParenExpression(unwrappedNode.object), "ThisExpression")) return null;
|
|
37552
|
+
return getMemberIdentity(unwrappedNode.property);
|
|
37553
|
+
};
|
|
37554
|
+
const isUndefinedIdentifier = (node) => {
|
|
37555
|
+
const unwrappedNode = stripParenExpression(node);
|
|
37556
|
+
return isNodeOfType(unwrappedNode, "Identifier") && unwrappedNode.name === "undefined";
|
|
37557
|
+
};
|
|
37558
|
+
const isDirectRefParameterValue = (node, parameterSymbolId, scopes) => {
|
|
37559
|
+
const unwrappedNode = stripParenExpression(node);
|
|
37560
|
+
if (isNodeOfType(unwrappedNode, "Identifier")) return scopes.symbolFor(unwrappedNode)?.id === parameterSymbolId;
|
|
37561
|
+
if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "??") return false;
|
|
37562
|
+
const left = stripParenExpression(unwrappedNode.left);
|
|
37563
|
+
return isNodeOfType(left, "Identifier") && scopes.symbolFor(left)?.id === parameterSymbolId && isUndefinedIdentifier(unwrappedNode.right);
|
|
37564
|
+
};
|
|
37565
|
+
const getCallbackRefAssignedFields = (callback, scopes) => {
|
|
37566
|
+
const firstParameter = (callback.params ?? [])[0];
|
|
37567
|
+
if (!firstParameter) return /* @__PURE__ */ new Set();
|
|
37568
|
+
const parameterIdentifier = isNodeOfType(firstParameter, "AssignmentPattern") ? firstParameter.left : firstParameter;
|
|
37569
|
+
if (!isNodeOfType(parameterIdentifier, "Identifier")) return /* @__PURE__ */ new Set();
|
|
37570
|
+
const parameterSymbolId = scopes.symbolFor(parameterIdentifier)?.id;
|
|
37571
|
+
if (parameterSymbolId === void 0) return /* @__PURE__ */ new Set();
|
|
37572
|
+
const body = callback.body;
|
|
37573
|
+
if (!body) return /* @__PURE__ */ new Set();
|
|
37574
|
+
const assignedFieldNames = /* @__PURE__ */ new Set();
|
|
37575
|
+
walkAst(body, (node) => {
|
|
37576
|
+
if (node !== body && (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node) || CLASS_NODE_TYPES.has(node.type))) return false;
|
|
37577
|
+
const assignmentTarget = isNodeOfType(node, "AssignmentExpression") && node.left || isNodeOfType(node, "UpdateExpression") && node.argument || isNodeOfType(node, "UnaryExpression") && node.operator === "delete" && node.argument || null;
|
|
37578
|
+
if (!assignmentTarget) return;
|
|
37579
|
+
const fieldName = getThisFieldName(assignmentTarget);
|
|
37580
|
+
if (!fieldName) return;
|
|
37581
|
+
if (isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isDirectRefParameterValue(node.right, parameterSymbolId, scopes)) {
|
|
37582
|
+
assignedFieldNames.add(fieldName);
|
|
37583
|
+
return;
|
|
37584
|
+
}
|
|
37585
|
+
assignedFieldNames.delete(fieldName);
|
|
37586
|
+
});
|
|
37587
|
+
return assignedFieldNames;
|
|
37588
|
+
};
|
|
37589
|
+
const getClassMemberCallback = (classNode, memberName) => {
|
|
37590
|
+
const classBody = classNode.body?.body ?? [];
|
|
37591
|
+
for (const member of classBody) {
|
|
37592
|
+
if (!isNodeOfType(member, "MethodDefinition") && !isNodeOfType(member, "PropertyDefinition")) continue;
|
|
37593
|
+
if (member.static === true) continue;
|
|
37594
|
+
const key = member.key;
|
|
37595
|
+
if (getMemberIdentity(key) !== memberName) continue;
|
|
37596
|
+
const value = member.value;
|
|
37597
|
+
return value && FUNCTION_NODE_TYPES.has(value.type) ? value : null;
|
|
37598
|
+
}
|
|
37599
|
+
return null;
|
|
37600
|
+
};
|
|
37601
|
+
const collectCallbackRefFieldsFromExpression = (expression, classNode, fieldNames, scopes) => {
|
|
37602
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
37603
|
+
if (FUNCTION_NODE_TYPES.has(unwrappedExpression.type)) {
|
|
37604
|
+
for (const fieldName of getCallbackRefAssignedFields(unwrappedExpression, scopes)) fieldNames.add(fieldName);
|
|
37605
|
+
return;
|
|
37606
|
+
}
|
|
37607
|
+
const handlerName = getThisFieldName(unwrappedExpression);
|
|
37608
|
+
if (handlerName) {
|
|
37609
|
+
const callback = getClassMemberCallback(classNode, handlerName);
|
|
37610
|
+
if (callback) for (const fieldName of getCallbackRefAssignedFields(callback, scopes)) fieldNames.add(fieldName);
|
|
37611
|
+
return;
|
|
37612
|
+
}
|
|
37613
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) {
|
|
37614
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.consequent, classNode, fieldNames, scopes);
|
|
37615
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.alternate, classNode, fieldNames, scopes);
|
|
37616
|
+
return;
|
|
37617
|
+
}
|
|
37618
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
37619
|
+
if (unwrappedExpression.operator !== "&&") collectCallbackRefFieldsFromExpression(unwrappedExpression.left, classNode, fieldNames, scopes);
|
|
37620
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.right, classNode, fieldNames, scopes);
|
|
37621
|
+
}
|
|
37622
|
+
};
|
|
37623
|
+
const getCallbackRefFieldNames = (classNode, scopes) => {
|
|
37624
|
+
if (!classNode) return /* @__PURE__ */ new Set();
|
|
37625
|
+
const cachedFieldNames = callbackRefFieldNamesByClass.get(classNode);
|
|
37626
|
+
if (cachedFieldNames) return cachedFieldNames;
|
|
37627
|
+
const fieldNames = /* @__PURE__ */ new Set();
|
|
37628
|
+
const classBody = classNode.body;
|
|
37629
|
+
if (classBody) walkAst(classBody, (node) => {
|
|
37630
|
+
if (node !== classBody && CLASS_NODE_TYPES.has(node.type)) return false;
|
|
37631
|
+
if (!isNodeOfType(node, "JSXAttribute") || !isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "ref" || !node.value || !isNodeOfType(node.value, "JSXExpressionContainer") || !node.value.expression) return;
|
|
37632
|
+
collectCallbackRefFieldsFromExpression(node.value.expression, classNode, fieldNames, scopes);
|
|
37633
|
+
});
|
|
37634
|
+
callbackRefFieldNamesByClass.set(classNode, fieldNames);
|
|
37635
|
+
return fieldNames;
|
|
37636
|
+
};
|
|
37637
|
+
const collectLifecycleWrittenFieldNames = (lifecycleFunction) => {
|
|
37638
|
+
const fieldNames = /* @__PURE__ */ new Set();
|
|
37639
|
+
const body = lifecycleFunction.body;
|
|
37640
|
+
if (!body) return fieldNames;
|
|
37641
|
+
walkAst(body, (node) => {
|
|
37642
|
+
if (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node)) return false;
|
|
37643
|
+
const target = isNodeOfType(node, "AssignmentExpression") && node.left || isNodeOfType(node, "UpdateExpression") && node.argument || null;
|
|
37644
|
+
if (!target) return;
|
|
37645
|
+
const fieldName = getThisFieldName(target);
|
|
37646
|
+
if (fieldName) fieldNames.add(fieldName);
|
|
37647
|
+
});
|
|
37648
|
+
return fieldNames;
|
|
37649
|
+
};
|
|
36631
37650
|
const getThisStateFieldName = (node) => {
|
|
36632
37651
|
const unwrappedNode = stripParenExpression(node);
|
|
36633
37652
|
if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
|
|
@@ -36645,15 +37664,17 @@ const collectLocalInitializers = (lifecycleFunction) => {
|
|
|
36645
37664
|
});
|
|
36646
37665
|
return initializers;
|
|
36647
37666
|
};
|
|
36648
|
-
const derivesFromPostMountValue = (node, localInitializers, visitedNames = /* @__PURE__ */ new Set()) => {
|
|
37667
|
+
const derivesFromPostMountValue = (node, localInitializers, callbackRefFieldNames, visitedNames = /* @__PURE__ */ new Set()) => {
|
|
36649
37668
|
if (readsPostMountValue(node)) return true;
|
|
37669
|
+
const fieldName = getThisFieldName(node);
|
|
37670
|
+
if (fieldName && callbackRefFieldNames.has(fieldName)) return true;
|
|
36650
37671
|
const referencedNames = /* @__PURE__ */ new Set();
|
|
36651
37672
|
collectReferenceIdentifierNames(node, referencedNames);
|
|
36652
37673
|
for (const referencedName of referencedNames) {
|
|
36653
37674
|
if (visitedNames.has(referencedName)) continue;
|
|
36654
37675
|
const initializer = localInitializers.get(referencedName);
|
|
36655
37676
|
if (!initializer) continue;
|
|
36656
|
-
if (derivesFromPostMountValue(initializer, localInitializers, new Set([...visitedNames, referencedName]))) return true;
|
|
37677
|
+
if (derivesFromPostMountValue(initializer, localInitializers, callbackRefFieldNames, new Set([...visitedNames, referencedName]))) return true;
|
|
36657
37678
|
}
|
|
36658
37679
|
return false;
|
|
36659
37680
|
};
|
|
@@ -36667,50 +37688,84 @@ const getSetStateFieldValue = (setStateCall, fieldName) => {
|
|
|
36667
37688
|
}
|
|
36668
37689
|
return null;
|
|
36669
37690
|
};
|
|
36670
|
-
const isConvergentPostMountGuard = (test, setStateCall, localInitializers) => {
|
|
36671
|
-
|
|
36672
|
-
|
|
36673
|
-
if (
|
|
36674
|
-
|
|
36675
|
-
const
|
|
36676
|
-
|
|
36677
|
-
|
|
36678
|
-
|
|
36679
|
-
|
|
36680
|
-
|
|
36681
|
-
|
|
36682
|
-
|
|
36683
|
-
|
|
36684
|
-
|
|
36685
|
-
|
|
36686
|
-
return
|
|
36687
|
-
};
|
|
36688
|
-
const
|
|
36689
|
-
|
|
36690
|
-
|
|
36691
|
-
|
|
36692
|
-
|
|
36693
|
-
|
|
36694
|
-
|
|
36695
|
-
|
|
36696
|
-
|
|
36697
|
-
|
|
36698
|
-
|
|
36699
|
-
|
|
36700
|
-
|
|
37691
|
+
const isConvergentPostMountGuard = (test, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch) => {
|
|
37692
|
+
const expression = stripParenExpression(test);
|
|
37693
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
37694
|
+
if (expression.operator !== "&&" && expression.operator !== "||") return false;
|
|
37695
|
+
const leftIsConvergent = isConvergentPostMountGuard(expression.left, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch);
|
|
37696
|
+
const rightIsConvergent = isConvergentPostMountGuard(expression.right, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch);
|
|
37697
|
+
return isTruthfulBranch && expression.operator === "||" || !isTruthfulBranch && expression.operator === "&&" ? leftIsConvergent && rightIsConvergent : leftIsConvergent || rightIsConvergent;
|
|
37698
|
+
}
|
|
37699
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !(isTruthfulBranch ? DIFFERENCE_OPERATORS.has(expression.operator) : EQUALITY_OPERATORS.has(expression.operator) && !DIFFERENCE_OPERATORS.has(expression.operator))) return false;
|
|
37700
|
+
const leftFieldName = getThisStateFieldName(expression.left);
|
|
37701
|
+
const rightFieldName = getThisStateFieldName(expression.right);
|
|
37702
|
+
const fieldName = leftFieldName ?? rightFieldName;
|
|
37703
|
+
const comparedValue = leftFieldName ? expression.right : expression.left;
|
|
37704
|
+
if (!fieldName) return false;
|
|
37705
|
+
const assignedValue = getSetStateFieldValue(setStateCall, fieldName);
|
|
37706
|
+
if (!assignedValue || !areExpressionsStructurallyEqual(comparedValue, assignedValue)) return false;
|
|
37707
|
+
return isUndefinedIdentifier(comparedValue) || derivesFromPostMountValue(comparedValue, localInitializers, callbackRefFieldNames);
|
|
37708
|
+
};
|
|
37709
|
+
const containsPositiveStateFieldTest = (test, fieldName) => {
|
|
37710
|
+
const unwrappedTest = stripParenExpression(test);
|
|
37711
|
+
if (getThisStateFieldName(unwrappedTest) === fieldName) return true;
|
|
37712
|
+
return isNodeOfType(unwrappedTest, "LogicalExpression") && unwrappedTest.operator === "&&" && (containsPositiveStateFieldTest(unwrappedTest.left, fieldName) || containsPositiveStateFieldTest(unwrappedTest.right, fieldName));
|
|
37713
|
+
};
|
|
37714
|
+
const isConvergentUndefinedClearGuard = (test, setStateCall) => {
|
|
37715
|
+
if (!isNodeOfType(setStateCall, "CallExpression")) return false;
|
|
37716
|
+
const argument = setStateCall.arguments?.[0];
|
|
37717
|
+
if (!argument || !isNodeOfType(argument, "ObjectExpression")) return false;
|
|
37718
|
+
for (const property of argument.properties ?? []) {
|
|
37719
|
+
if (!isNodeOfType(property, "Property") || property.computed === true || !isUndefinedIdentifier(property.value)) continue;
|
|
37720
|
+
const fieldName = isNodeOfType(property.key, "Identifier") && property.key.name || isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" && property.key.value || null;
|
|
37721
|
+
if (fieldName && containsPositiveStateFieldTest(test, fieldName)) return true;
|
|
37722
|
+
}
|
|
37723
|
+
return false;
|
|
37724
|
+
};
|
|
37725
|
+
const isDiffGuardTest = (test, paramNames, derivedNames, isTruthfulBranch) => {
|
|
37726
|
+
const expression = stripParenExpression(test);
|
|
37727
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
37728
|
+
if (expression.operator !== "&&" && expression.operator !== "||") return false;
|
|
37729
|
+
const leftIsDiffGuard = isDiffGuardTest(expression.left, paramNames, derivedNames, isTruthfulBranch);
|
|
37730
|
+
const rightIsDiffGuard = isDiffGuardTest(expression.right, paramNames, derivedNames, isTruthfulBranch);
|
|
37731
|
+
return isTruthfulBranch && expression.operator === "||" || !isTruthfulBranch && expression.operator === "&&" ? leftIsDiffGuard && rightIsDiffGuard : leftIsDiffGuard || rightIsDiffGuard;
|
|
37732
|
+
}
|
|
37733
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !(isTruthfulBranch ? DIFFERENCE_OPERATORS.has(expression.operator) : EQUALITY_OPERATORS.has(expression.operator) && !DIFFERENCE_OPERATORS.has(expression.operator))) return false;
|
|
37734
|
+
return isStatefulOperand(expression.left, paramNames, derivedNames) && isStatefulOperand(expression.right, paramNames, derivedNames) && (referencesAnyName(expression.left, paramNames) || referencesAnyName(expression.right, paramNames) || referencesAnyName(expression.left, derivedNames) || referencesAnyName(expression.right, derivedNames));
|
|
36701
37735
|
};
|
|
36702
|
-
const isInsideDiffGuard = (setStateCall) => {
|
|
37736
|
+
const isInsideDiffGuard = (setStateCall, scopes) => {
|
|
36703
37737
|
const lifecycleFunction = findEnclosingLifecycleFunction(setStateCall);
|
|
36704
37738
|
if (!lifecycleFunction) return false;
|
|
36705
37739
|
const paramNames = /* @__PURE__ */ new Set();
|
|
36706
|
-
|
|
37740
|
+
const parameters = lifecycleFunction.params ?? [];
|
|
37741
|
+
for (const param of parameters) collectPatternNames(param, paramNames);
|
|
37742
|
+
const previousSourcePaths = /* @__PURE__ */ new Map();
|
|
37743
|
+
const [previousPropsParameter, previousStateParameter] = parameters;
|
|
37744
|
+
collectPreviousSourcePaths(previousPropsParameter, "props", [], previousSourcePaths);
|
|
37745
|
+
collectPreviousSourcePaths(previousStateParameter, "state", [], previousSourcePaths);
|
|
36707
37746
|
const derivedNames = collectDiffSourceLocalNames(lifecycleFunction, paramNames);
|
|
36708
37747
|
const localInitializers = collectLocalInitializers(lifecycleFunction);
|
|
37748
|
+
const lifecycleWrittenFieldNames = collectLifecycleWrittenFieldNames(lifecycleFunction);
|
|
37749
|
+
const callbackRefFieldNames = new Set([...getCallbackRefFieldNames(findEnclosingClass(lifecycleFunction), scopes)].filter((fieldName) => !lifecycleWrittenFieldNames.has(fieldName)));
|
|
36709
37750
|
let child = setStateCall;
|
|
36710
37751
|
let ancestor = setStateCall.parent;
|
|
36711
37752
|
while (ancestor && ancestor !== lifecycleFunction) {
|
|
36712
|
-
|
|
36713
|
-
|
|
37753
|
+
let guardTest = null;
|
|
37754
|
+
let isTruthfulBranch = true;
|
|
37755
|
+
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
37756
|
+
if (child === ancestor.consequent) guardTest = ancestor.test;
|
|
37757
|
+
else if (child === ancestor.alternate) {
|
|
37758
|
+
guardTest = ancestor.test;
|
|
37759
|
+
isTruthfulBranch = false;
|
|
37760
|
+
}
|
|
37761
|
+
} else if (isNodeOfType(ancestor, "ConditionalExpression")) {
|
|
37762
|
+
if (child === ancestor.consequent) guardTest = ancestor.test;
|
|
37763
|
+
else if (child === ancestor.alternate) {
|
|
37764
|
+
guardTest = ancestor.test;
|
|
37765
|
+
isTruthfulBranch = false;
|
|
37766
|
+
}
|
|
37767
|
+
} else if (isNodeOfType(ancestor, "LogicalExpression") && ancestor.operator === "&&" && child === ancestor.right) guardTest = ancestor.left;
|
|
37768
|
+
if (guardTest && (isDiffGuardTest(guardTest, paramNames, derivedNames, isTruthfulBranch) || isTruthfulBranch && isHistoricalToCurrentTransitionGuard(guardTest, previousSourcePaths) || isConvergentPostMountGuard(guardTest, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch) || isTruthfulBranch && isConvergentUndefinedClearGuard(guardTest, setStateCall))) return true;
|
|
36714
37769
|
child = ancestor;
|
|
36715
37770
|
ancestor = ancestor.parent ?? null;
|
|
36716
37771
|
}
|
|
@@ -36732,7 +37787,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
36732
37787
|
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36733
37788
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36734
37789
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36735
|
-
if (isInsideDiffGuard(node)) return;
|
|
37790
|
+
if (isInsideDiffGuard(node, context.scopes)) return;
|
|
36736
37791
|
context.report({
|
|
36737
37792
|
node: node.callee,
|
|
36738
37793
|
message: MESSAGE$27
|
|
@@ -36802,7 +37857,7 @@ const noDirectMutationState = defineRule({
|
|
|
36802
37857
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
36803
37858
|
//#endregion
|
|
36804
37859
|
//#region src/plugin/rules/state-and-effects/utils/collect-use-state-bindings.ts
|
|
36805
|
-
const collectUseStateBindings = (componentBody) => {
|
|
37860
|
+
const collectUseStateBindings = (componentBody, scopes) => {
|
|
36806
37861
|
const bindings = [];
|
|
36807
37862
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
36808
37863
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -36815,7 +37870,12 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
36815
37870
|
const setterElement = elements[1];
|
|
36816
37871
|
if (!isNodeOfType(valueElement, "Identifier") || !isNodeOfType(setterElement, "Identifier") || !isSetterIdentifier(setterElement.name)) continue;
|
|
36817
37872
|
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
36818
|
-
if (!
|
|
37873
|
+
if (!(scopes ? isReactApiCall(declarator.init, "useState", scopes, {
|
|
37874
|
+
allowGlobalReactNamespace: true,
|
|
37875
|
+
allowUnboundBareCalls: true,
|
|
37876
|
+
resolveConditionalAliases: true,
|
|
37877
|
+
resolveNamedAliases: true
|
|
37878
|
+
}) : isHookCall$2(declarator.init, "useState"))) continue;
|
|
36819
37879
|
bindings.push({
|
|
36820
37880
|
valueName: valueElement.name,
|
|
36821
37881
|
setterName: setterElement.name,
|
|
@@ -37542,25 +38602,36 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
37542
38602
|
};
|
|
37543
38603
|
//#endregion
|
|
37544
38604
|
//#region src/plugin/rules/state-and-effects/no-effect-chain.ts
|
|
37545
|
-
const findTopLevelEffectCalls = (componentBody) => {
|
|
38605
|
+
const findTopLevelEffectCalls = (componentBody, scopes) => {
|
|
37546
38606
|
const effectCalls = [];
|
|
37547
38607
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
37548
38608
|
for (const statement of componentBody.body ?? []) {
|
|
37549
38609
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
37550
38610
|
const expression = unwrapDiscardedExpression(statement);
|
|
37551
38611
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
37552
|
-
if (!
|
|
38612
|
+
if (!isReactApiCall(expression, EFFECT_HOOK_NAMES$1, scopes, {
|
|
38613
|
+
allowGlobalReactNamespace: true,
|
|
38614
|
+
allowUnboundBareCalls: true,
|
|
38615
|
+
resolveConditionalAliases: true,
|
|
38616
|
+
resolveNamedAliases: true
|
|
38617
|
+
})) continue;
|
|
37553
38618
|
effectCalls.push(expression);
|
|
37554
38619
|
}
|
|
37555
38620
|
return effectCalls;
|
|
37556
38621
|
};
|
|
37557
|
-
const
|
|
37558
|
-
const
|
|
37559
|
-
if (!isNodeOfType(effectNode, "CallExpression")) return
|
|
38622
|
+
const collectDependencyStateSymbolIds = (effectNode, stateSymbolIds, scopes) => {
|
|
38623
|
+
const dependencyStateSymbolIds = /* @__PURE__ */ new Set();
|
|
38624
|
+
if (!isNodeOfType(effectNode, "CallExpression")) return dependencyStateSymbolIds;
|
|
37560
38625
|
const depsNode = effectNode.arguments?.[1];
|
|
37561
|
-
if (!isNodeOfType(depsNode, "ArrayExpression")) return
|
|
37562
|
-
for (const element of depsNode.elements ?? [])
|
|
37563
|
-
|
|
38626
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) return dependencyStateSymbolIds;
|
|
38627
|
+
for (const element of depsNode.elements ?? []) {
|
|
38628
|
+
if (!element || isNodeOfType(element, "SpreadElement")) continue;
|
|
38629
|
+
const rootIdentifier = getRootIdentifier$1(element);
|
|
38630
|
+
if (!isNodeOfType(rootIdentifier, "Identifier")) continue;
|
|
38631
|
+
const symbol = resolveConstIdentifierAlias(rootIdentifier, scopes, true);
|
|
38632
|
+
if (symbol && stateSymbolIds.has(symbol.id)) dependencyStateSymbolIds.add(symbol.id);
|
|
38633
|
+
}
|
|
38634
|
+
return dependencyStateSymbolIds;
|
|
37564
38635
|
};
|
|
37565
38636
|
const collectSynchronouslyInvokedFunctions = (effectCallback, scopes) => {
|
|
37566
38637
|
const analysisFunctions = new Set([effectCallback]);
|
|
@@ -37666,12 +38737,13 @@ const readStaticSetterValue = (setterCall, scopes) => {
|
|
|
37666
38737
|
if (updater) return readStaticUpdaterReturnValue(updater, scopes);
|
|
37667
38738
|
return readStaticEffectValue(argument, scopes, null, null);
|
|
37668
38739
|
};
|
|
37669
|
-
const collectStateWritesInEffect = (analysisFunctions,
|
|
38740
|
+
const collectStateWritesInEffect = (analysisFunctions, setterSymbolIdToStateName, scopes) => {
|
|
37670
38741
|
const stateWrites = /* @__PURE__ */ new Map();
|
|
37671
38742
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
37672
38743
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
37673
38744
|
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
37674
|
-
const
|
|
38745
|
+
const setterSymbol = resolveConstIdentifierAlias(child.callee, scopes, true);
|
|
38746
|
+
const stateName = setterSymbol ? setterSymbolIdToStateName.get(setterSymbol.id) : void 0;
|
|
37675
38747
|
if (!stateName) return;
|
|
37676
38748
|
const writeInfo = stateWrites.get(stateName) ?? {
|
|
37677
38749
|
values: /* @__PURE__ */ new Set(),
|
|
@@ -37757,11 +38829,12 @@ const NON_CONTAMINATING_MAP_METHOD_NAMES = new Set([
|
|
|
37757
38829
|
"keys",
|
|
37758
38830
|
"values"
|
|
37759
38831
|
]);
|
|
37760
|
-
const isFunctionShapedReturn = (returnedValue, setterToStateName, isExplicitReturnStatement) => {
|
|
38832
|
+
const isFunctionShapedReturn = (returnedValue, setterToStateName, setterSymbolIdToStateName, scopes, isExplicitReturnStatement) => {
|
|
37761
38833
|
if (isNodeOfType(returnedValue, "ArrowFunctionExpression") || isNodeOfType(returnedValue, "FunctionExpression")) return true;
|
|
37762
38834
|
if (isNodeOfType(returnedValue, "CallExpression")) {
|
|
37763
38835
|
if (isNodeOfType(returnedValue.callee, "Identifier")) {
|
|
37764
|
-
|
|
38836
|
+
const setterSymbol = resolveConstIdentifierAlias(returnedValue.callee, scopes, true);
|
|
38837
|
+
if (setterToStateName.has(returnedValue.callee.name) || setterSymbol && setterSymbolIdToStateName.has(setterSymbol.id)) return false;
|
|
37765
38838
|
if (isSetterIdentifier(returnedValue.callee.name)) return true;
|
|
37766
38839
|
}
|
|
37767
38840
|
return isCleanupReturn(returnedValue, EMPTY_CLEANUP_NAME_SET, EMPTY_CLEANUP_NAME_SET, { allowOpaqueReturn: isExplicitReturnStatement });
|
|
@@ -37928,11 +39001,11 @@ const isExternalSyncNode = (node) => {
|
|
|
37928
39001
|
const receiverRootName = getRootIdentifierName(node.callee.object);
|
|
37929
39002
|
return receiverRootName !== null && EXTERNAL_SYNC_HTTP_CLIENT_RECEIVERS.has(receiverRootName);
|
|
37930
39003
|
};
|
|
37931
|
-
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, scopes, allowCommittedDomSync) => {
|
|
39004
|
+
const isExternalSyncEffect = (effectCallback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, scopes, allowCommittedDomSync) => {
|
|
37932
39005
|
if (!isFunctionLike$1(effectCallback)) return false;
|
|
37933
39006
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
|
|
37934
|
-
if (isFunctionShapedReturn(effectCallback.body, setterToStateName, false)) return true;
|
|
37935
|
-
} else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, true)) return true;
|
|
39007
|
+
if (isFunctionShapedReturn(effectCallback.body, setterToStateName, setterSymbolIdToStateName, scopes, false)) return true;
|
|
39008
|
+
} else for (const statement of effectCallback.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument && isFunctionShapedReturn(statement.argument, setterToStateName, setterSymbolIdToStateName, scopes, true)) return true;
|
|
37936
39009
|
let didFindExternalCall = false;
|
|
37937
39010
|
visitSynchronousFunctionBodies(analysisFunctions, (child) => {
|
|
37938
39011
|
if (isExternalSyncNode(child) || allowCommittedDomSync && isCommittedDomSyncNode(child, scopes)) didFindExternalCall = true;
|
|
@@ -37948,10 +39021,11 @@ const noEffectChain = defineRule({
|
|
|
37948
39021
|
create: (context) => {
|
|
37949
39022
|
const checkComponent = (componentBody) => {
|
|
37950
39023
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
37951
|
-
const useStateBindings = collectUseStateBindings(componentBody);
|
|
39024
|
+
const useStateBindings = collectUseStateBindings(componentBody, context.scopes);
|
|
37952
39025
|
if (useStateBindings.length === 0) return;
|
|
37953
39026
|
const setterToStateName = /* @__PURE__ */ new Map();
|
|
37954
39027
|
const stateSymbolIds = /* @__PURE__ */ new Map();
|
|
39028
|
+
const setterSymbolIdToStateName = /* @__PURE__ */ new Map();
|
|
37955
39029
|
for (const binding of useStateBindings) {
|
|
37956
39030
|
setterToStateName.set(binding.setterName, binding.valueName);
|
|
37957
39031
|
if (!isNodeOfType(binding.declarator.id, "ArrayPattern")) continue;
|
|
@@ -37960,21 +39034,27 @@ const noEffectChain = defineRule({
|
|
|
37960
39034
|
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
37961
39035
|
if (stateSymbol) stateSymbolIds.set(binding.valueName, stateSymbol.id);
|
|
37962
39036
|
}
|
|
39037
|
+
const setterIdentifier = binding.declarator.id.elements[1];
|
|
39038
|
+
if (isNodeOfType(setterIdentifier, "Identifier")) {
|
|
39039
|
+
const setterSymbol = context.scopes.symbolFor(setterIdentifier);
|
|
39040
|
+
if (setterSymbol) setterSymbolIdToStateName.set(setterSymbol.id, binding.valueName);
|
|
39041
|
+
}
|
|
37963
39042
|
}
|
|
37964
39043
|
const storageSetterNames = collectStorageHookSetterNames(componentBody);
|
|
39044
|
+
const stateSymbolIdSet = new Set(stateSymbolIds.values());
|
|
37965
39045
|
const effectInfos = [];
|
|
37966
|
-
for (const effectCall of findTopLevelEffectCalls(componentBody)) {
|
|
39046
|
+
for (const effectCall of findTopLevelEffectCalls(componentBody, context.scopes)) {
|
|
37967
39047
|
const callback = getEffectCallback(effectCall, context.scopes);
|
|
37968
39048
|
if (!callback || !isFunctionLike$1(callback) || callback.async) continue;
|
|
37969
39049
|
const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
|
|
37970
|
-
const stateWrites = collectStateWritesInEffect(analysisFunctions,
|
|
39050
|
+
const stateWrites = collectStateWritesInEffect(analysisFunctions, setterSymbolIdToStateName, context.scopes);
|
|
37971
39051
|
const writtenStateNames = new Set(stateWrites.keys());
|
|
37972
39052
|
effectInfos.push({
|
|
37973
39053
|
node: effectCall,
|
|
37974
|
-
|
|
39054
|
+
dependencyStateSymbolIds: collectDependencyStateSymbolIds(effectCall, stateSymbolIdSet, context.scopes),
|
|
37975
39055
|
stateWrites,
|
|
37976
39056
|
analysisFunctions,
|
|
37977
|
-
isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
|
|
39057
|
+
isExternalSync: isExternalSyncEffect(callback, analysisFunctions, setterToStateName, setterSymbolIdToStateName, context.scopes, writtenStateNames.size === 0) || callsStorageHookSetter(analysisFunctions, storageSetterNames) || writtenStateNames.size === 0 && callsOpaqueExternalSetter(analysisFunctions, setterToStateName)
|
|
37978
39058
|
});
|
|
37979
39059
|
}
|
|
37980
39060
|
if (effectInfos.length < 2) return;
|
|
@@ -37985,10 +39065,11 @@ const noEffectChain = defineRule({
|
|
|
37985
39065
|
for (const readerEffect of effectInfos) {
|
|
37986
39066
|
if (readerEffect === writerEffect) continue;
|
|
37987
39067
|
if (readerEffect.isExternalSync) continue;
|
|
37988
|
-
if (readerEffect.
|
|
39068
|
+
if (readerEffect.dependencyStateSymbolIds.size === 0) continue;
|
|
37989
39069
|
let chainedStateName = null;
|
|
37990
39070
|
for (const [writtenName, writeInfo] of writerEffect.stateWrites) {
|
|
37991
|
-
|
|
39071
|
+
const writtenStateSymbolId = stateSymbolIds.get(writtenName);
|
|
39072
|
+
if (writtenStateSymbolId === void 0 || !readerEffect.dependencyStateSymbolIds.has(writtenStateSymbolId)) continue;
|
|
37992
39073
|
if (!canStateWriteReachReaderWork(writeInfo, readerEffect, stateSymbolIds.get(writtenName) ?? null, context.scopes)) continue;
|
|
37993
39074
|
chainedStateName = writtenName;
|
|
37994
39075
|
break;
|
|
@@ -39804,6 +40885,17 @@ const DOM_MEASUREMENT_NAMES = new Set([
|
|
|
39804
40885
|
"scrollHeight"
|
|
39805
40886
|
]);
|
|
39806
40887
|
const MEASUREMENT_HELPER_CALLEE_PATTERN = /^(?:get|measure|read)\w*(?:Width|Height|Rect|Rects|Size|Bounds|Position)$/;
|
|
40888
|
+
const IMPERATIVE_DOM_MUTATION_NAMES = new Set([
|
|
40889
|
+
"blur",
|
|
40890
|
+
"focus",
|
|
40891
|
+
"restoreSelection",
|
|
40892
|
+
"scroll",
|
|
40893
|
+
"scrollBy",
|
|
40894
|
+
"scrollIntoView",
|
|
40895
|
+
"scrollTo",
|
|
40896
|
+
"setRangeText",
|
|
40897
|
+
"setSelectionRange"
|
|
40898
|
+
]);
|
|
39807
40899
|
const subtreeReadsDomMeasurement = (root) => {
|
|
39808
40900
|
if (!root) return false;
|
|
39809
40901
|
let found = false;
|
|
@@ -39822,29 +40914,66 @@ const subtreeReadsDomMeasurement = (root) => {
|
|
|
39822
40914
|
});
|
|
39823
40915
|
return found;
|
|
39824
40916
|
};
|
|
39825
|
-
const
|
|
40917
|
+
const collectFunctionNamesMatchingBody = (program, matchesBody) => {
|
|
39826
40918
|
const names = /* @__PURE__ */ new Set();
|
|
39827
40919
|
walkAst(program, (child) => {
|
|
39828
40920
|
if (isNodeOfType(child, "FunctionDeclaration")) {
|
|
39829
|
-
if (child.id && isNodeOfType(child.id, "Identifier") &&
|
|
40921
|
+
if (child.id && isNodeOfType(child.id, "Identifier") && matchesBody(child.body)) names.add(child.id.name);
|
|
39830
40922
|
return;
|
|
39831
40923
|
}
|
|
39832
40924
|
if (!isNodeOfType(child, "VariableDeclarator") || !isNodeOfType(child.id, "Identifier")) return;
|
|
39833
40925
|
let functionValue = child.init;
|
|
39834
40926
|
if (functionValue && isNodeOfType(functionValue, "CallExpression") && isNodeOfType(functionValue.callee, "Identifier") && /^use[A-Z]/.test(functionValue.callee.name)) functionValue = functionValue.arguments?.[0];
|
|
39835
|
-
if (functionValue && isFunctionLike$1(functionValue) &&
|
|
40927
|
+
if (functionValue && isFunctionLike$1(functionValue) && matchesBody(functionValue.body)) names.add(child.id.name);
|
|
39836
40928
|
});
|
|
39837
40929
|
return names;
|
|
39838
40930
|
};
|
|
39839
|
-
const
|
|
39840
|
-
|
|
40931
|
+
const collectMeasuringFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeReadsDomMeasurement);
|
|
40932
|
+
const subtreeMutatesDomImperatively = (root) => {
|
|
40933
|
+
if (!root || isFunctionLike$1(root)) return false;
|
|
39841
40934
|
let found = false;
|
|
39842
40935
|
walkAst(root, (child) => {
|
|
39843
40936
|
if (found) return false;
|
|
40937
|
+
if (child !== root && isFunctionLike$1(child)) return false;
|
|
40938
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
40939
|
+
const callee = stripParenExpression(child.callee);
|
|
40940
|
+
const propertyName = isNodeOfType(callee, "MemberExpression") ? getStaticPropertyName(callee) : null;
|
|
40941
|
+
if (propertyName !== null && IMPERATIVE_DOM_MUTATION_NAMES.has(propertyName)) {
|
|
40942
|
+
found = true;
|
|
40943
|
+
return false;
|
|
40944
|
+
}
|
|
40945
|
+
});
|
|
40946
|
+
return found;
|
|
40947
|
+
};
|
|
40948
|
+
const collectImperativeDomFunctionNames = (program) => collectFunctionNamesMatchingBody(program, subtreeMutatesDomImperatively);
|
|
40949
|
+
const callsAnyName = (root, names, shouldSkipNestedFunctions = false) => {
|
|
40950
|
+
if (!root || names.size === 0 || shouldSkipNestedFunctions && isFunctionLike$1(root)) return false;
|
|
40951
|
+
let found = false;
|
|
40952
|
+
walkAst(root, (child) => {
|
|
40953
|
+
if (found) return false;
|
|
40954
|
+
if (shouldSkipNestedFunctions && child !== root && isFunctionLike$1(child)) return false;
|
|
39844
40955
|
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && names.has(child.callee.name)) found = true;
|
|
39845
40956
|
});
|
|
39846
40957
|
return found;
|
|
39847
40958
|
};
|
|
40959
|
+
const isFollowedByImperativeDomMutation = (call, imperativeDomFunctionNames) => {
|
|
40960
|
+
let statement = call;
|
|
40961
|
+
let parent = statement.parent;
|
|
40962
|
+
while (parent) {
|
|
40963
|
+
const statements = isNodeOfType(parent, "BlockStatement") || isNodeOfType(parent, "Program") || isNodeOfType(parent, "StaticBlock") ? parent.body : isNodeOfType(parent, "SwitchCase") ? parent.consequent : null;
|
|
40964
|
+
if (statements) {
|
|
40965
|
+
const statementIndex = statements.findIndex((siblingStatement) => siblingStatement === statement);
|
|
40966
|
+
if (statementIndex >= 0) {
|
|
40967
|
+
const nextStatement = statements[statementIndex + 1];
|
|
40968
|
+
return subtreeMutatesDomImperatively(nextStatement) || callsAnyName(nextStatement, imperativeDomFunctionNames, true);
|
|
40969
|
+
}
|
|
40970
|
+
}
|
|
40971
|
+
if (isFunctionLike$1(parent) || parent.type.endsWith("Statement") && !isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
40972
|
+
statement = parent;
|
|
40973
|
+
parent = parent.parent;
|
|
40974
|
+
}
|
|
40975
|
+
return false;
|
|
40976
|
+
};
|
|
39848
40977
|
const isInsideStartViewTransition = (node) => {
|
|
39849
40978
|
let cursor = node.parent;
|
|
39850
40979
|
while (cursor) {
|
|
@@ -39885,11 +41014,12 @@ const importsImperativeDomLibrary = (program) => {
|
|
|
39885
41014
|
};
|
|
39886
41015
|
const hasExemptFlushSyncCall = (program, localName) => {
|
|
39887
41016
|
const measuringFunctionNames = collectMeasuringFunctionNames(program);
|
|
41017
|
+
const imperativeDomFunctionNames = collectImperativeDomFunctionNames(program);
|
|
39888
41018
|
let exempt = false;
|
|
39889
41019
|
walkAst(program, (child) => {
|
|
39890
41020
|
if (exempt) return false;
|
|
39891
41021
|
if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "Identifier") || child.callee.name !== localName) return;
|
|
39892
|
-
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames)) {
|
|
41022
|
+
if (isInsideStartViewTransition(child) || enclosingFunctionChainReadsMeasurement(child, measuringFunctionNames) || isFollowedByImperativeDomMutation(child, imperativeDomFunctionNames)) {
|
|
39893
41023
|
exempt = true;
|
|
39894
41024
|
return false;
|
|
39895
41025
|
}
|
|
@@ -40454,41 +41584,223 @@ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
|
|
|
40454
41584
|
if (leftResult === false && rightResult === false) return false;
|
|
40455
41585
|
return null;
|
|
40456
41586
|
};
|
|
40457
|
-
const readHydrationConditionResult = (expression, context, runtime) => {
|
|
41587
|
+
const readHydrationConditionResult = (expression, context, runtime, state) => {
|
|
40458
41588
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40459
41589
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40460
41590
|
if (predicateMatch) return predicateMatch[`${runtime}Result`];
|
|
40461
41591
|
const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
|
|
40462
41592
|
if (staticResult !== null) return staticResult;
|
|
41593
|
+
const expressionSymbol = isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression) : null;
|
|
41594
|
+
const parameterValue = expressionSymbol ? state.parameterValuesBySymbolId.get(expressionSymbol.id) : null;
|
|
41595
|
+
if (expressionSymbol && parameterValue && !state.visitedSymbolIds.has(expressionSymbol.id)) {
|
|
41596
|
+
state.visitedSymbolIds.add(expressionSymbol.id);
|
|
41597
|
+
const result = readHydrationConditionResult(parameterValue, context, runtime, state);
|
|
41598
|
+
state.visitedSymbolIds.delete(expressionSymbol.id);
|
|
41599
|
+
return result;
|
|
41600
|
+
}
|
|
41601
|
+
if (expressionSymbol && expressionSymbol.kind === "const" && expressionSymbol.initializer && expressionSymbol.references.every((reference) => reference.flag === "read") && !state.visitedSymbolIds.has(expressionSymbol.id)) {
|
|
41602
|
+
state.visitedSymbolIds.add(expressionSymbol.id);
|
|
41603
|
+
const result = readHydrationConditionResult(expressionSymbol.initializer, context, runtime, state);
|
|
41604
|
+
state.visitedSymbolIds.delete(expressionSymbol.id);
|
|
41605
|
+
return result;
|
|
41606
|
+
}
|
|
41607
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
41608
|
+
const callArguments = unwrappedExpression.arguments ?? [];
|
|
41609
|
+
if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
|
|
41610
|
+
allowGlobalReactNamespace: true,
|
|
41611
|
+
resolveNamedAliases: true
|
|
41612
|
+
})) {
|
|
41613
|
+
const callbackArgument = callArguments[0];
|
|
41614
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
|
|
41615
|
+
const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
|
|
41616
|
+
return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? readHydrationFunctionResult(callbackFunction, context, runtime, state) : null;
|
|
41617
|
+
}
|
|
41618
|
+
const callee = stripParenExpression(unwrappedExpression.callee);
|
|
41619
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return readHydrationConditionResult(callArguments[0], context, runtime, state);
|
|
41620
|
+
const helperFunction = resolveExactLocalFunction(callee, context.scopes);
|
|
41621
|
+
if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
|
|
41622
|
+
const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
|
|
41623
|
+
for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
|
|
41624
|
+
const parameter = helperFunction.params[parameterIndex];
|
|
41625
|
+
const argument = callArguments[parameterIndex];
|
|
41626
|
+
if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
|
|
41627
|
+
const parameterSymbol = context.scopes.symbolFor(parameter);
|
|
41628
|
+
if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
|
|
41629
|
+
}
|
|
41630
|
+
return readHydrationFunctionResult(helperFunction, context, runtime, {
|
|
41631
|
+
...state,
|
|
41632
|
+
parameterValuesBySymbolId
|
|
41633
|
+
});
|
|
41634
|
+
}
|
|
40463
41635
|
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
40464
|
-
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
|
|
41636
|
+
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime, state);
|
|
40465
41637
|
return argumentResult === null ? null : !argumentResult;
|
|
40466
41638
|
}
|
|
40467
41639
|
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
40468
|
-
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
|
|
41640
|
+
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime, state), readHydrationConditionResult(unwrappedExpression.right, context, runtime, state));
|
|
41641
|
+
};
|
|
41642
|
+
const readHydrationStatementResult = (statement, context, runtime, state) => {
|
|
41643
|
+
if (isNodeOfType(statement, "ReturnStatement")) return {
|
|
41644
|
+
didReturn: true,
|
|
41645
|
+
value: statement.argument ? readHydrationConditionResult(statement.argument, context, runtime, state) : null
|
|
41646
|
+
};
|
|
41647
|
+
if (isNodeOfType(statement, "BlockStatement")) {
|
|
41648
|
+
for (const childStatement of statement.body) {
|
|
41649
|
+
const result = readHydrationStatementResult(childStatement, context, runtime, state);
|
|
41650
|
+
if (result.didReturn) return result;
|
|
41651
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
41652
|
+
}
|
|
41653
|
+
return {
|
|
41654
|
+
didReturn: false,
|
|
41655
|
+
value: null
|
|
41656
|
+
};
|
|
41657
|
+
}
|
|
41658
|
+
if (!isNodeOfType(statement, "IfStatement")) return {
|
|
41659
|
+
didReturn: false,
|
|
41660
|
+
value: null
|
|
41661
|
+
};
|
|
41662
|
+
const conditionResult = readHydrationConditionResult(statement.test, context, runtime, state);
|
|
41663
|
+
if (conditionResult !== null) {
|
|
41664
|
+
const selectedBranch = conditionResult ? statement.consequent : statement.alternate;
|
|
41665
|
+
return selectedBranch ? readHydrationStatementResult(selectedBranch, context, runtime, state) : {
|
|
41666
|
+
didReturn: false,
|
|
41667
|
+
value: null
|
|
41668
|
+
};
|
|
41669
|
+
}
|
|
41670
|
+
const consequentResult = readHydrationStatementResult(statement.consequent, context, runtime, state);
|
|
41671
|
+
const alternateResult = statement.alternate ? readHydrationStatementResult(statement.alternate, context, runtime, state) : {
|
|
41672
|
+
didReturn: false,
|
|
41673
|
+
value: null
|
|
41674
|
+
};
|
|
41675
|
+
return consequentResult.didReturn && alternateResult.didReturn && consequentResult.value !== null && consequentResult.value === alternateResult.value ? consequentResult : {
|
|
41676
|
+
didReturn: consequentResult.didReturn || alternateResult.didReturn,
|
|
41677
|
+
value: null
|
|
41678
|
+
};
|
|
41679
|
+
};
|
|
41680
|
+
const readHydrationFunctionResult = (functionNode, context, runtime, state) => {
|
|
41681
|
+
if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
|
|
41682
|
+
state.visitedFunctionNodes.add(functionNode);
|
|
41683
|
+
const result = isNodeOfType(functionNode.body, "BlockStatement") ? readHydrationStatementResult(functionNode.body, context, runtime, state).value : readHydrationConditionResult(functionNode.body, context, runtime, state);
|
|
41684
|
+
state.visitedFunctionNodes.delete(functionNode);
|
|
41685
|
+
return result;
|
|
41686
|
+
};
|
|
41687
|
+
const doEquivalentExpressionBindingsMatch = (leftExpression, rightExpression, scopes) => {
|
|
41688
|
+
const left = stripParenExpression(leftExpression);
|
|
41689
|
+
const right = stripParenExpression(rightExpression);
|
|
41690
|
+
if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) {
|
|
41691
|
+
const leftSymbol = scopes.symbolFor(left);
|
|
41692
|
+
const rightSymbol = scopes.symbolFor(right);
|
|
41693
|
+
return leftSymbol || rightSymbol ? leftSymbol?.id === rightSymbol?.id : true;
|
|
41694
|
+
}
|
|
41695
|
+
if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) return doEquivalentExpressionBindingsMatch(left.object, right.object, scopes) && (!left.computed || doEquivalentExpressionBindingsMatch(left.property, right.property, scopes));
|
|
41696
|
+
if (isNodeOfType(left, "CallExpression") && isNodeOfType(right, "CallExpression")) {
|
|
41697
|
+
const rightArguments = right.arguments ?? [];
|
|
41698
|
+
return doEquivalentExpressionBindingsMatch(left.callee, right.callee, scopes) && (left.arguments ?? []).every((argument, index) => {
|
|
41699
|
+
const rightArgument = rightArguments[index];
|
|
41700
|
+
return Boolean(rightArgument && doEquivalentExpressionBindingsMatch(argument, rightArgument, scopes));
|
|
41701
|
+
});
|
|
41702
|
+
}
|
|
41703
|
+
return true;
|
|
40469
41704
|
};
|
|
40470
|
-
const
|
|
41705
|
+
const areHelperReturnValuesEquivalent = (leftValue, rightValue, context) => {
|
|
41706
|
+
if (areExpressionsStructurallyEqual(leftValue, rightValue)) return doEquivalentExpressionBindingsMatch(leftValue, rightValue, context.scopes);
|
|
41707
|
+
const leftBoolean = readInitialStateBoolean(leftValue, context.scopes);
|
|
41708
|
+
const rightBoolean = readInitialStateBoolean(rightValue, context.scopes);
|
|
41709
|
+
return leftBoolean !== null && rightBoolean !== null && leftBoolean === rightBoolean;
|
|
41710
|
+
};
|
|
41711
|
+
const doHelperReturnValuesDiffer = (leftValues, rightValues, context) => {
|
|
41712
|
+
const everyValueHasEquivalent = (values, candidateValues) => values.every((value) => candidateValues.some((candidateValue) => areHelperReturnValuesEquivalent(value, candidateValue, context)));
|
|
41713
|
+
return !everyValueHasEquivalent(leftValues, rightValues) || !everyValueHasEquivalent(rightValues, leftValues);
|
|
41714
|
+
};
|
|
41715
|
+
const matchHydrationConditionInternal = (expression, context, state) => {
|
|
40471
41716
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40472
41717
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40473
41718
|
if (predicateMatch) return {
|
|
40474
41719
|
predicateMatch,
|
|
40475
41720
|
predicateNode: unwrappedExpression
|
|
40476
41721
|
};
|
|
40477
|
-
if (isNodeOfType(unwrappedExpression, "
|
|
40478
|
-
|
|
40479
|
-
|
|
40480
|
-
|
|
40481
|
-
|
|
40482
|
-
|
|
40483
|
-
|
|
40484
|
-
|
|
41722
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
41723
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
41724
|
+
const parameterValue = symbol ? state.parameterValuesBySymbolId.get(symbol.id) : null;
|
|
41725
|
+
if (symbol && parameterValue && !state.visitedSymbolIds.has(symbol.id)) {
|
|
41726
|
+
state.visitedSymbolIds.add(symbol.id);
|
|
41727
|
+
const match = matchHydrationConditionInternal(parameterValue, context, state);
|
|
41728
|
+
state.visitedSymbolIds.delete(symbol.id);
|
|
41729
|
+
return match;
|
|
41730
|
+
}
|
|
41731
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || state.visitedSymbolIds.has(symbol.id)) return null;
|
|
41732
|
+
state.visitedSymbolIds.add(symbol.id);
|
|
41733
|
+
const match = matchHydrationConditionInternal(symbol.initializer, context, state);
|
|
41734
|
+
state.visitedSymbolIds.delete(symbol.id);
|
|
41735
|
+
return match;
|
|
40485
41736
|
}
|
|
41737
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
41738
|
+
const callArguments = unwrappedExpression.arguments ?? [];
|
|
41739
|
+
if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
|
|
41740
|
+
allowGlobalReactNamespace: true,
|
|
41741
|
+
resolveNamedAliases: true
|
|
41742
|
+
})) {
|
|
41743
|
+
const callbackArgument = callArguments[0];
|
|
41744
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
|
|
41745
|
+
const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
|
|
41746
|
+
return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? matchHydrationFunctionResult(callbackFunction, context, state) : null;
|
|
41747
|
+
}
|
|
41748
|
+
const callee = stripParenExpression(unwrappedExpression.callee);
|
|
41749
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return matchHydrationConditionInternal(callArguments[0], context, state);
|
|
41750
|
+
const helperFunction = resolveExactLocalFunction(callee, context.scopes);
|
|
41751
|
+
if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
|
|
41752
|
+
const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
|
|
41753
|
+
for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
|
|
41754
|
+
const parameter = helperFunction.params[parameterIndex];
|
|
41755
|
+
const argument = callArguments[parameterIndex];
|
|
41756
|
+
if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
|
|
41757
|
+
const parameterSymbol = context.scopes.symbolFor(parameter);
|
|
41758
|
+
if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
|
|
41759
|
+
}
|
|
41760
|
+
return matchHydrationFunctionResult(helperFunction, context, {
|
|
41761
|
+
...state,
|
|
41762
|
+
parameterValuesBySymbolId
|
|
41763
|
+
});
|
|
41764
|
+
}
|
|
41765
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationConditionInternal(unwrappedExpression.argument, context, state);
|
|
41766
|
+
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
41767
|
+
const leftMatch = matchHydrationConditionInternal(unwrappedExpression.left, context, state);
|
|
41768
|
+
const rightMatch = matchHydrationConditionInternal(unwrappedExpression.right, context, state);
|
|
40486
41769
|
const nestedMatch = leftMatch ?? rightMatch;
|
|
40487
41770
|
if (!nestedMatch) return null;
|
|
40488
|
-
const
|
|
40489
|
-
|
|
40490
|
-
return nestedMatch;
|
|
41771
|
+
const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client", state);
|
|
41772
|
+
const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server", state);
|
|
41773
|
+
return clientResult !== null && serverResult !== null && clientResult === serverResult ? null : nestedMatch;
|
|
40491
41774
|
};
|
|
41775
|
+
const matchHydrationReturningStatement = (statement, context, state) => {
|
|
41776
|
+
if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? matchHydrationConditionInternal(statement.argument, context, state) : null;
|
|
41777
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
41778
|
+
const conditionMatch = matchHydrationConditionInternal(statement.test, context, state);
|
|
41779
|
+
const consequentValues = getReturnedValues(statement.consequent);
|
|
41780
|
+
const alternateValues = statement.alternate ? getReturnedValues(statement.alternate) : findFollowingReturnedValues(statement);
|
|
41781
|
+
if (conditionMatch && consequentValues.length > 0 && alternateValues.length > 0 && doHelperReturnValuesDiffer(consequentValues, alternateValues, context)) return conditionMatch;
|
|
41782
|
+
return matchHydrationReturningStatement(statement.consequent, context, state) ?? (statement.alternate ? matchHydrationReturningStatement(statement.alternate, context, state) : null);
|
|
41783
|
+
}
|
|
41784
|
+
if (!isNodeOfType(statement, "BlockStatement")) return null;
|
|
41785
|
+
for (const childStatement of statement.body) {
|
|
41786
|
+
const match = matchHydrationReturningStatement(childStatement, context, state);
|
|
41787
|
+
if (match) return match;
|
|
41788
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
41789
|
+
}
|
|
41790
|
+
return null;
|
|
41791
|
+
};
|
|
41792
|
+
const matchHydrationFunctionResult = (functionNode, context, state) => {
|
|
41793
|
+
if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
|
|
41794
|
+
state.visitedFunctionNodes.add(functionNode);
|
|
41795
|
+
const match = isNodeOfType(functionNode.body, "BlockStatement") ? matchHydrationReturningStatement(functionNode.body, context, state) : matchHydrationConditionInternal(functionNode.body, context, state);
|
|
41796
|
+
state.visitedFunctionNodes.delete(functionNode);
|
|
41797
|
+
return match;
|
|
41798
|
+
};
|
|
41799
|
+
const matchHydrationCondition = (expression, context) => matchHydrationConditionInternal(expression, context, {
|
|
41800
|
+
parameterValuesBySymbolId: /* @__PURE__ */ new Map(),
|
|
41801
|
+
visitedFunctionNodes: /* @__PURE__ */ new Set(),
|
|
41802
|
+
visitedSymbolIds: /* @__PURE__ */ new Set()
|
|
41803
|
+
});
|
|
40492
41804
|
const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
|
|
40493
41805
|
const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
|
|
40494
41806
|
if (!leftNode || !rightNode) return leftNode === rightNode;
|
|
@@ -40631,17 +41943,17 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
|
40631
41943
|
const { predicateMatch, predicateNode } = conditionMatch;
|
|
40632
41944
|
if (reportedNodes.has(predicateNode)) return;
|
|
40633
41945
|
if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
|
|
40634
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(
|
|
41946
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(conditionNode, context.scopes);
|
|
40635
41947
|
if (!componentOrHookNode) return;
|
|
40636
41948
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
40637
|
-
if (requiresRenderedContext && !isInRenderedOutput(
|
|
41949
|
+
if (requiresRenderedContext && !isInRenderedOutput(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
40638
41950
|
if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
|
|
40639
|
-
const attribute = findEnclosingJsxAttribute(
|
|
41951
|
+
const attribute = findEnclosingJsxAttribute(conditionNode);
|
|
40640
41952
|
if (!attribute || isEventHandlerAttribute(attribute)) return;
|
|
40641
41953
|
}
|
|
40642
|
-
if (fileIsEmailTemplate || isGatedByFalsyInitialState(
|
|
40643
|
-
if (isAfterClientOnlyEarlyReturn(
|
|
40644
|
-
const openingElement = findEnclosingJsxOpeningElement(
|
|
41954
|
+
if (fileIsEmailTemplate || isGatedByFalsyInitialState(conditionNode, context.scopes)) return;
|
|
41955
|
+
if (isAfterClientOnlyEarlyReturn(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
41956
|
+
const openingElement = findEnclosingJsxOpeningElement(conditionNode);
|
|
40645
41957
|
if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
|
|
40646
41958
|
if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
|
|
40647
41959
|
if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
|
|
@@ -41023,7 +42335,7 @@ const noInitializeState = defineRule({
|
|
|
41023
42335
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
41024
42336
|
const analysis = getProgramAnalysis(node);
|
|
41025
42337
|
if (!analysis) return;
|
|
41026
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
42338
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
41027
42339
|
if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
|
|
41028
42340
|
const stateName = getStateName(fact.stateDeclarator);
|
|
41029
42341
|
context.report({
|
|
@@ -41381,7 +42693,8 @@ const noJsxElementType = defineRule({
|
|
|
41381
42693
|
create: (context) => {
|
|
41382
42694
|
let isJsxImported = false;
|
|
41383
42695
|
const flaggedAnnotations = [];
|
|
41384
|
-
const
|
|
42696
|
+
const collectComponentReturnType = (functionNode, returnType) => {
|
|
42697
|
+
if (!(isNodeOfType(functionNode, "TSDeclareFunction") ? Boolean(functionNode.id && isReactComponentName(functionNode.id.name)) : isComponentFunction$1(functionNode))) return;
|
|
41385
42698
|
const typeAnnotation = extractReturnTypeAnnotation(returnType);
|
|
41386
42699
|
if (!typeAnnotation) return;
|
|
41387
42700
|
if (isJsxElementTypeReference(typeAnnotation)) flaggedAnnotations.push(typeAnnotation);
|
|
@@ -41391,19 +42704,16 @@ const noJsxElementType = defineRule({
|
|
|
41391
42704
|
if (isJsxImportBinding(node)) isJsxImported = true;
|
|
41392
42705
|
},
|
|
41393
42706
|
FunctionDeclaration(node) {
|
|
41394
|
-
|
|
42707
|
+
collectComponentReturnType(node, node.returnType);
|
|
41395
42708
|
},
|
|
41396
42709
|
ArrowFunctionExpression(node) {
|
|
41397
|
-
|
|
42710
|
+
collectComponentReturnType(node, node.returnType);
|
|
41398
42711
|
},
|
|
41399
42712
|
FunctionExpression(node) {
|
|
41400
|
-
|
|
42713
|
+
collectComponentReturnType(node, node.returnType);
|
|
41401
42714
|
},
|
|
41402
42715
|
TSDeclareFunction(node) {
|
|
41403
|
-
|
|
41404
|
-
},
|
|
41405
|
-
TSMethodSignature(node) {
|
|
41406
|
-
checkReturnType(node.returnType);
|
|
42716
|
+
collectComponentReturnType(node, node.returnType);
|
|
41407
42717
|
},
|
|
41408
42718
|
"Program:exit"() {
|
|
41409
42719
|
if (isJsxImported) return;
|
|
@@ -44430,6 +45740,114 @@ const DATA_SINK_METHOD_NAMES = new Set([
|
|
|
44430
45740
|
"deserialize"
|
|
44431
45741
|
]);
|
|
44432
45742
|
//#endregion
|
|
45743
|
+
//#region src/plugin/utils/get-transparent-react-callback-wrapper-argument.ts
|
|
45744
|
+
const getTransparentReactCallbackWrapperArgument = (initializer, resultSymbol, scopes) => {
|
|
45745
|
+
const callExpression = stripParenExpression(initializer);
|
|
45746
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
45747
|
+
const callbackArgument = callExpression.arguments[0];
|
|
45748
|
+
if (!callbackArgument) return null;
|
|
45749
|
+
if (resultSymbol && symbolHasReactUseEffectEventOrigin(resultSymbol, scopes)) return callbackArgument;
|
|
45750
|
+
return isReactApiCall(callExpression, "useCallback", scopes, {
|
|
45751
|
+
allowGlobalReactNamespace: true,
|
|
45752
|
+
allowUnboundBareCalls: true
|
|
45753
|
+
}) ? callbackArgument : null;
|
|
45754
|
+
};
|
|
45755
|
+
//#endregion
|
|
45756
|
+
//#region src/plugin/rules/state-and-effects/utils/resolve-parent-callback-provenance.ts
|
|
45757
|
+
const getDeclarationKind$1 = (declarator) => {
|
|
45758
|
+
const declaration = declarator.parent;
|
|
45759
|
+
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
45760
|
+
};
|
|
45761
|
+
const hasMutableBindingWrite$2 = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
45762
|
+
const mergeRequiredBranches = (leftNames, rightNames) => {
|
|
45763
|
+
if (!leftNames || !rightNames) return null;
|
|
45764
|
+
return new Set([...leftNames, ...rightNames]);
|
|
45765
|
+
};
|
|
45766
|
+
const getPropReferenceName = (analysis, identifier) => {
|
|
45767
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
45768
|
+
const reference = getRef(analysis, identifier);
|
|
45769
|
+
if (!reference || !isProp(analysis, reference) || isWholePropsObjectReference(analysis, reference)) return null;
|
|
45770
|
+
const bindingIdentifier = (reference.resolved?.defs.find((definition) => definition.type === "Parameter"))?.name;
|
|
45771
|
+
return (bindingIdentifier && getDestructuredBindingPropertyName(bindingIdentifier)) ?? identifier.name;
|
|
45772
|
+
};
|
|
45773
|
+
const getSingleConstDeclarator = (reference) => {
|
|
45774
|
+
if (!reference.resolved || hasMutableBindingWrite$2(reference)) return null;
|
|
45775
|
+
const declarators = reference.resolved.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
45776
|
+
if (declarators.length !== 1) return null;
|
|
45777
|
+
const declarator = declarators[0];
|
|
45778
|
+
if (!declarator || getDeclarationKind$1(declarator) !== "const") return null;
|
|
45779
|
+
return declarator;
|
|
45780
|
+
};
|
|
45781
|
+
const resolveParentCallbackPropNames = (analysis, expression, scopes, visitedReferences, allowFunctionForwarder = false) => {
|
|
45782
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
45783
|
+
if (isFunctionLike$1(unwrappedExpression)) {
|
|
45784
|
+
if (!allowFunctionForwarder || Boolean(unwrappedExpression.async)) return null;
|
|
45785
|
+
const callbackNames = /* @__PURE__ */ new Set();
|
|
45786
|
+
walkInsideStatementBlocks(unwrappedExpression.body, (child) => {
|
|
45787
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
45788
|
+
const resolvedNames = resolveParentCallbackPropNames(analysis, child.callee, scopes, new Set(visitedReferences), false);
|
|
45789
|
+
if (!resolvedNames) return;
|
|
45790
|
+
for (const resolvedName of resolvedNames) callbackNames.add(resolvedName);
|
|
45791
|
+
});
|
|
45792
|
+
return callbackNames.size > 0 ? callbackNames : null;
|
|
45793
|
+
}
|
|
45794
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return mergeRequiredBranches(resolveParentCallbackPropNames(analysis, unwrappedExpression.consequent, scopes, new Set(visitedReferences), false), resolveParentCallbackPropNames(analysis, unwrappedExpression.alternate, scopes, new Set(visitedReferences), false));
|
|
45795
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return mergeRequiredBranches(resolveParentCallbackPropNames(analysis, unwrappedExpression.left, scopes, new Set(visitedReferences), false), resolveParentCallbackPropNames(analysis, unwrappedExpression.right, scopes, new Set(visitedReferences)));
|
|
45796
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
45797
|
+
const propName = getPropReferenceName(analysis, unwrappedExpression);
|
|
45798
|
+
if (propName) return new Set([propName]);
|
|
45799
|
+
const reference = getRef(analysis, unwrappedExpression);
|
|
45800
|
+
if (!reference?.resolved || visitedReferences.has(reference.resolved)) return null;
|
|
45801
|
+
const declarator = getSingleConstDeclarator(reference);
|
|
45802
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
45803
|
+
visitedReferences.add(reference.resolved);
|
|
45804
|
+
const wrappedArgument = getTransparentReactCallbackWrapperArgument(declarator.init, scopes.symbolFor(unwrappedExpression), scopes);
|
|
45805
|
+
const allowsFunctionForwarder = Boolean(wrappedArgument && !isReactApiCall(declarator.init, "useCallback", scopes, {
|
|
45806
|
+
allowGlobalReactNamespace: true,
|
|
45807
|
+
allowUnboundBareCalls: true
|
|
45808
|
+
}));
|
|
45809
|
+
return resolveParentCallbackPropNames(analysis, wrappedArgument ?? declarator.init, scopes, visitedReferences, allowsFunctionForwarder);
|
|
45810
|
+
}
|
|
45811
|
+
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
|
|
45812
|
+
const propertyName = getStaticMemberPropertyName(unwrappedExpression);
|
|
45813
|
+
if (!propertyName) return null;
|
|
45814
|
+
const receiver = stripParenExpression(unwrappedExpression.object);
|
|
45815
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
45816
|
+
const receiverReference = getRef(analysis, receiver);
|
|
45817
|
+
if (!receiverReference?.resolved || visitedReferences.has(receiverReference.resolved)) return null;
|
|
45818
|
+
if (isWholePropsObjectReference(analysis, receiverReference)) return new Set([propertyName]);
|
|
45819
|
+
const declarator = getSingleConstDeclarator(receiverReference);
|
|
45820
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
45821
|
+
visitedReferences.add(receiverReference.resolved);
|
|
45822
|
+
const initializer = stripParenExpression(declarator.init);
|
|
45823
|
+
if (propertyName === "current" && isNodeOfType(initializer, "CallExpression")) {
|
|
45824
|
+
if (!isReactApiCall(initializer, "useRef", scopes, {
|
|
45825
|
+
allowGlobalReactNamespace: true,
|
|
45826
|
+
allowUnboundBareCalls: true
|
|
45827
|
+
})) return null;
|
|
45828
|
+
const callbackArgument = initializer.arguments[0];
|
|
45829
|
+
if (!callbackArgument) return null;
|
|
45830
|
+
let callbackNames = resolveParentCallbackPropNames(analysis, callbackArgument, scopes, new Set(visitedReferences), false);
|
|
45831
|
+
if (!callbackNames) return null;
|
|
45832
|
+
for (const candidateReference of receiverReference.resolved.references) {
|
|
45833
|
+
const candidateIdentifier = candidateReference.identifier;
|
|
45834
|
+
const candidateMember = candidateIdentifier.parent;
|
|
45835
|
+
if (!candidateMember || !isNodeOfType(candidateMember, "MemberExpression") || candidateMember.object !== candidateIdentifier || getStaticMemberPropertyName(candidateMember) !== "current") continue;
|
|
45836
|
+
const assignment = candidateMember.parent;
|
|
45837
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== candidateMember) continue;
|
|
45838
|
+
if (assignment.operator !== "=") return null;
|
|
45839
|
+
callbackNames = mergeRequiredBranches(callbackNames, resolveParentCallbackPropNames(analysis, assignment.right, scopes, new Set(visitedReferences), false));
|
|
45840
|
+
if (!callbackNames) return null;
|
|
45841
|
+
}
|
|
45842
|
+
return callbackNames;
|
|
45843
|
+
}
|
|
45844
|
+
if (!isNodeOfType(initializer, "ObjectExpression")) return null;
|
|
45845
|
+
const property = initializer.properties.find((candidateProperty) => isNodeOfType(candidateProperty, "Property") && getStaticPropertyKeyName(candidateProperty, { allowComputedString: true }) === propertyName);
|
|
45846
|
+
if (!property || !isNodeOfType(property, "Property")) return null;
|
|
45847
|
+
return resolveParentCallbackPropNames(analysis, property.value, scopes, visitedReferences, false);
|
|
45848
|
+
};
|
|
45849
|
+
const getParentCallbackPropNames = ({ analysis, expression, scopes }) => resolveParentCallbackPropNames(analysis, expression, scopes, /* @__PURE__ */ new Set(), false);
|
|
45850
|
+
//#endregion
|
|
44433
45851
|
//#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
|
|
44434
45852
|
const isUseStateIdentifier = (identifier) => {
|
|
44435
45853
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
@@ -44458,14 +45876,18 @@ const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
|
|
|
44458
45876
|
"useStableCallback",
|
|
44459
45877
|
"useCallbackRef"
|
|
44460
45878
|
]);
|
|
44461
|
-
const getWrapperHookWrappedFunction = (initializer) => {
|
|
45879
|
+
const getWrapperHookWrappedFunction = (initializer, resultSymbol, scopes) => {
|
|
44462
45880
|
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
45881
|
+
const transparentReactArgument = getTransparentReactCallbackWrapperArgument(initializer, resultSymbol, scopes);
|
|
45882
|
+
if (transparentReactArgument) return transparentReactArgument;
|
|
44463
45883
|
const callee = initializer.callee;
|
|
44464
45884
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
|
|
44465
45885
|
if (!calleeName || !FUNCTION_WRAPPER_HOOK_NAMES$1.has(calleeName)) return null;
|
|
44466
45886
|
const wrapped = initializer.arguments?.[0];
|
|
44467
|
-
if (!wrapped
|
|
44468
|
-
return
|
|
45887
|
+
if (!wrapped) return null;
|
|
45888
|
+
if (calleeName === "useEffectEvent") return null;
|
|
45889
|
+
if (isFunctionLike$1(wrapped)) return wrapped;
|
|
45890
|
+
return null;
|
|
44469
45891
|
};
|
|
44470
45892
|
const HANDLER_NAMED_PROP_PATTERN = /^(on|handle)[A-Z]/;
|
|
44471
45893
|
const wrappedFunctionNotifiesParent = (analysis, wrappedFunction) => getDownstreamRefs(analysis, wrappedFunction).some((innerRef) => {
|
|
@@ -44475,16 +45897,29 @@ const wrappedFunctionNotifiesParent = (analysis, wrappedFunction) => getDownstre
|
|
|
44475
45897
|
const innerParent = innerIdentifier.parent;
|
|
44476
45898
|
return Boolean(innerParent && isNodeOfType(innerParent, "CallExpression") && innerParent.callee === innerIdentifier);
|
|
44477
45899
|
});
|
|
44478
|
-
const isDirectParentCallbackRef = (analysis, ref) => {
|
|
45900
|
+
const isDirectParentCallbackRef = (analysis, ref, scopes) => {
|
|
44479
45901
|
if (isProp(analysis, ref)) return true;
|
|
45902
|
+
if (hasMutableBindingWrite$1(ref)) {
|
|
45903
|
+
if (!(ref.resolved?.references.filter((candidateReference) => candidateReference.isWrite() && !candidateReference.init) ?? []).every((candidateReference) => {
|
|
45904
|
+
const candidateIdentifier = candidateReference.identifier;
|
|
45905
|
+
const assignment = candidateIdentifier.parent;
|
|
45906
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== candidateIdentifier) return false;
|
|
45907
|
+
const assignedReferences = getDownstreamRefs(analysis, assignment.right);
|
|
45908
|
+
return assignedReferences.length > 0 && assignedReferences.every((assignedReference) => isProp(analysis, assignedReference));
|
|
45909
|
+
})) return false;
|
|
45910
|
+
}
|
|
44480
45911
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
44481
45912
|
const node = def.node;
|
|
44482
45913
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44483
45914
|
const initializer = unwrapChainExpression(node.init);
|
|
44484
|
-
const wrappedFunction = getWrapperHookWrappedFunction(initializer);
|
|
45915
|
+
const wrappedFunction = getWrapperHookWrappedFunction(initializer, isNodeOfType(node.id, "Identifier") ? scopes.symbolFor(node.id) ?? null : null, scopes);
|
|
44485
45916
|
if (wrappedFunction) {
|
|
44486
45917
|
if (wrappedFunction.async) return false;
|
|
44487
|
-
return wrappedFunctionNotifiesParent(analysis, wrappedFunction);
|
|
45918
|
+
if (isFunctionLike$1(wrappedFunction)) return wrappedFunctionNotifiesParent(analysis, wrappedFunction);
|
|
45919
|
+
const directName = getParentCallbackPropName(analysis, wrappedFunction);
|
|
45920
|
+
const downstreamReferences = getDownstreamRefs(analysis, wrappedFunction);
|
|
45921
|
+
if (directName !== null) return true;
|
|
45922
|
+
return downstreamReferences.some((wrappedReference) => !hasMutableBindingWrite$1(wrappedReference) && getUpstreamRefs(analysis, wrappedReference).some((upstreamReference) => isProp(analysis, upstreamReference)));
|
|
44488
45923
|
}
|
|
44489
45924
|
if (!isNodeOfType(initializer, "Identifier") && !isNodeOfType(initializer, "MemberExpression")) return false;
|
|
44490
45925
|
return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
|
|
@@ -44494,7 +45929,7 @@ const getDeclarationKind = (declarator) => {
|
|
|
44494
45929
|
const declaration = declarator.parent;
|
|
44495
45930
|
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
44496
45931
|
};
|
|
44497
|
-
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
45932
|
+
const hasMutableBindingWrite$1 = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
44498
45933
|
const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
|
|
44499
45934
|
const unwrappedExpression = stripParenExpression(expression);
|
|
44500
45935
|
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
@@ -44506,7 +45941,7 @@ const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @
|
|
|
44506
45941
|
const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
|
|
44507
45942
|
return (bindingIdentifier && getDestructuredBindingPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
|
|
44508
45943
|
}
|
|
44509
|
-
if (hasMutableBindingWrite(callbackReference)) return null;
|
|
45944
|
+
if (hasMutableBindingWrite$1(callbackReference)) return null;
|
|
44510
45945
|
const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
44511
45946
|
if (definitions.length !== 1) return null;
|
|
44512
45947
|
const declarator = definitions[0];
|
|
@@ -44572,7 +46007,7 @@ const getRefAliasDeclarator = (identifier) => {
|
|
|
44572
46007
|
const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
44573
46008
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
44574
46009
|
const receiverReference = getRef(analysis, receiver);
|
|
44575
|
-
if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
|
|
46010
|
+
if (!receiverReference?.resolved || hasMutableBindingWrite$1(receiverReference)) return null;
|
|
44576
46011
|
const variables = /* @__PURE__ */ new Set();
|
|
44577
46012
|
let currentVariable = receiverReference.resolved;
|
|
44578
46013
|
let refCall = null;
|
|
@@ -44588,7 +46023,7 @@ const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
|
44588
46023
|
}
|
|
44589
46024
|
if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
|
|
44590
46025
|
const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
|
|
44591
|
-
if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
|
|
46026
|
+
if (!upstreamReference?.resolved || hasMutableBindingWrite$1(upstreamReference)) return null;
|
|
44592
46027
|
currentVariable = upstreamReference.resolved;
|
|
44593
46028
|
}
|
|
44594
46029
|
if (!refCall) return null;
|
|
@@ -44663,7 +46098,7 @@ const isParentPropsContextMerge = (analysis, expression) => {
|
|
|
44663
46098
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44664
46099
|
const currentReference = getRef(analysis, currentExpression);
|
|
44665
46100
|
const currentVariable = currentReference?.resolved;
|
|
44666
|
-
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite(currentReference)) return false;
|
|
46101
|
+
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite$1(currentReference)) return false;
|
|
44667
46102
|
visitedVariables.add(currentVariable);
|
|
44668
46103
|
const definitions = currentVariable.defs.filter((definition) => isNodeOfType(definition.node, "VariableDeclarator"));
|
|
44669
46104
|
if (definitions.length !== 1) return false;
|
|
@@ -44677,11 +46112,11 @@ const isParentPropsContextMerge = (analysis, expression) => {
|
|
|
44677
46112
|
const propsExpression = stripParenExpression(propsSpread.argument);
|
|
44678
46113
|
if (!isNodeOfType(propsExpression, "Identifier")) return false;
|
|
44679
46114
|
const propsReference = getRef(analysis, propsExpression);
|
|
44680
|
-
if (!propsReference?.resolved || !isWholePropsObjectReference(analysis, propsReference) || hasMutableBindingWrite(propsReference) || propsReference.resolved.references.some((candidateReference) => candidateReference !== propsReference)) return false;
|
|
46115
|
+
if (!propsReference?.resolved || !isWholePropsObjectReference(analysis, propsReference) || hasMutableBindingWrite$1(propsReference) || propsReference.resolved.references.some((candidateReference) => candidateReference !== propsReference)) return false;
|
|
44681
46116
|
const contextExpression = stripParenExpression(contextSpread.argument);
|
|
44682
46117
|
if (!isNodeOfType(contextExpression, "Identifier")) return false;
|
|
44683
46118
|
const contextReference = getRef(analysis, contextExpression);
|
|
44684
|
-
if (!contextReference?.resolved || hasMutableBindingWrite(contextReference) || contextReference.resolved.references.some((candidateReference) => !candidateReference.init && candidateReference !== contextReference)) return false;
|
|
46119
|
+
if (!contextReference?.resolved || hasMutableBindingWrite$1(contextReference) || contextReference.resolved.references.some((candidateReference) => !candidateReference.init && candidateReference !== contextReference)) return false;
|
|
44685
46120
|
const contextInitializer = contextReference.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
44686
46121
|
if (!contextInitializer || !isNodeOfType(contextInitializer, "VariableDeclarator") || getDeclarationKind(contextInitializer) !== "const" || !contextInitializer.init || !isNodeOfType(contextInitializer.init, "CallExpression")) return false;
|
|
44687
46122
|
const contextHook = stripParenExpression(contextInitializer.init.callee);
|
|
@@ -44695,7 +46130,7 @@ const getImmutableParentCallbackPropName = (analysis, expression) => {
|
|
|
44695
46130
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44696
46131
|
const currentReference = getRef(analysis, currentExpression);
|
|
44697
46132
|
const currentVariable = currentReference?.resolved;
|
|
44698
|
-
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite(currentReference)) return null;
|
|
46133
|
+
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite$1(currentReference)) return null;
|
|
44699
46134
|
visitedVariables.add(currentVariable);
|
|
44700
46135
|
const definition = currentVariable.defs.length === 1 ? currentVariable.defs[0] : null;
|
|
44701
46136
|
const bindingIdentifier = definition?.name;
|
|
@@ -44764,7 +46199,7 @@ const getCommandCallbackPropName = (analysis, expression, isReactUseRefCall) =>
|
|
|
44764
46199
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44765
46200
|
const callbackReference = getRef(analysis, currentExpression);
|
|
44766
46201
|
const callbackVariable = callbackReference?.resolved;
|
|
44767
|
-
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable) || hasMutableBindingWrite(callbackReference)) return null;
|
|
46202
|
+
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable) || hasMutableBindingWrite$1(callbackReference)) return null;
|
|
44768
46203
|
visitedVariables.add(callbackVariable);
|
|
44769
46204
|
const definition = callbackVariable.defs.length === 1 ? callbackVariable.defs[0] : null;
|
|
44770
46205
|
const declarator = definition?.node;
|
|
@@ -44784,10 +46219,11 @@ const getCommandCallbackPropName = (analysis, expression, isReactUseRefCall) =>
|
|
|
44784
46219
|
if (!propertyName || !COMMAND_PROP_NAME_PATTERN.test(propertyName)) return null;
|
|
44785
46220
|
return refCurrentObjectPreservesCallbackProperty(analysis, currentExpression.object, propertyName, isReactUseRefCall) ? propertyName : null;
|
|
44786
46221
|
};
|
|
44787
|
-
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
46222
|
+
const isWrapperHookCallbackRef = (analysis, ref, scopes) => Boolean(ref.resolved?.defs.some((def) => {
|
|
44788
46223
|
const node = def.node;
|
|
44789
46224
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44790
|
-
|
|
46225
|
+
const resultSymbol = isNodeOfType(node.id, "Identifier") ? scopes.symbolFor(node.id) ?? null : null;
|
|
46226
|
+
return getWrapperHookWrappedFunction(unwrapChainExpression(node.init), resultSymbol, scopes) !== null;
|
|
44791
46227
|
}));
|
|
44792
46228
|
const isHandlerBagArgument = (analysis, argument) => {
|
|
44793
46229
|
if (!isNodeOfType(argument, "ObjectExpression")) return false;
|
|
@@ -44806,14 +46242,26 @@ const isHandlerBagArgument = (analysis, argument) => {
|
|
|
44806
46242
|
};
|
|
44807
46243
|
const getFunctionalUpdaterDataRefs = (analysis, updater) => getDownstreamRefs(analysis, updater).filter((updaterRef) => !updaterRef.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updater));
|
|
44808
46244
|
const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
44809
|
-
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
|
|
46245
|
+
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
|
|
44810
46246
|
"useIntersectionObserver",
|
|
44811
46247
|
"useMatchMedia",
|
|
46248
|
+
"useMediaJobProgress",
|
|
44812
46249
|
"useMediaQuery",
|
|
46250
|
+
"useMediaQueryState",
|
|
44813
46251
|
"useResizeObserver",
|
|
44814
46252
|
"useVisibility",
|
|
44815
46253
|
"useWindowSize"
|
|
44816
46254
|
]);
|
|
46255
|
+
const isCallbackPropReference = (analysis, ref) => {
|
|
46256
|
+
if (!isProp(analysis, ref)) return false;
|
|
46257
|
+
const identifier = ref.identifier;
|
|
46258
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
46259
|
+
if (!isWholePropsObjectReference(analysis, ref)) return HANDLER_NAMED_PROP_PATTERN.test(identifier.name);
|
|
46260
|
+
const member = identifier.parent;
|
|
46261
|
+
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier) return false;
|
|
46262
|
+
const propertyName = getStaticMemberPropertyName(member);
|
|
46263
|
+
return Boolean(propertyName && HANDLER_NAMED_PROP_PATTERN.test(propertyName));
|
|
46264
|
+
};
|
|
44817
46265
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
44818
46266
|
const node = def.node;
|
|
44819
46267
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
@@ -44821,7 +46269,7 @@ const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs
|
|
|
44821
46269
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
44822
46270
|
const callee = init.callee;
|
|
44823
46271
|
if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
|
|
44824
|
-
return (init.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) =>
|
|
46272
|
+
return (init.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
44825
46273
|
}));
|
|
44826
46274
|
const isParentWiredHookResultArgument = (analysis, argument) => {
|
|
44827
46275
|
if (!isNodeOfType(argument, "Identifier")) return false;
|
|
@@ -44834,19 +46282,40 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
44834
46282
|
if (!isNodeOfType(identifier, "Identifier") || !HOOK_NAME_PATTERN$1.test(identifier.name)) return false;
|
|
44835
46283
|
const parent = identifier.parent;
|
|
44836
46284
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
44837
|
-
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) =>
|
|
46285
|
+
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
44838
46286
|
};
|
|
44839
|
-
const
|
|
46287
|
+
const getLocalHookExternalStateProof = (analysis, ref) => {
|
|
46288
|
+
let hookFunction = resolveToFunction(ref);
|
|
46289
|
+
if (!hookFunction) for (const definition of ref.resolved?.defs ?? []) {
|
|
46290
|
+
const definitionNode = definition.node;
|
|
46291
|
+
if (!isNodeOfType(definitionNode, "VariableDeclarator") || !definitionNode.init) continue;
|
|
46292
|
+
const initializer = stripParenExpression(definitionNode.init);
|
|
46293
|
+
if (!isNodeOfType(initializer, "CallExpression")) continue;
|
|
46294
|
+
const callee = stripParenExpression(initializer.callee);
|
|
46295
|
+
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
46296
|
+
const calleeReference = getRef(analysis, callee);
|
|
46297
|
+
if (!calleeReference) continue;
|
|
46298
|
+
hookFunction = resolveToFunction(calleeReference);
|
|
46299
|
+
if (hookFunction) break;
|
|
46300
|
+
}
|
|
46301
|
+
if (!hookFunction) return null;
|
|
46302
|
+
const returnedReferences = collectFunctionReturnStatements(hookFunction).flatMap((returnStatement) => returnStatement.argument ? getDownstreamRefs(analysis, returnStatement.argument) : []);
|
|
46303
|
+
if (returnedReferences.length === 0) return null;
|
|
46304
|
+
return returnedReferences.every((returnedReference) => isState(analysis, returnedReference) && isExternallyDrivenState(analysis, returnedReference));
|
|
46305
|
+
};
|
|
46306
|
+
const isExternalSubscriptionHookRef = (analysis, ref) => {
|
|
44840
46307
|
const identifier = ref.identifier;
|
|
44841
46308
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
44842
|
-
|
|
46309
|
+
const localHookProof = getLocalHookExternalStateProof(analysis, ref);
|
|
46310
|
+
if (localHookProof !== null) return localHookProof;
|
|
46311
|
+
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
44843
46312
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
44844
46313
|
const node = def.node;
|
|
44845
46314
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44846
46315
|
const initializer = stripParenExpression(node.init);
|
|
44847
46316
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
44848
46317
|
const callee = stripParenExpression(initializer.callee);
|
|
44849
|
-
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(callee.name);
|
|
46318
|
+
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(callee.name);
|
|
44850
46319
|
}));
|
|
44851
46320
|
};
|
|
44852
46321
|
const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
|
|
@@ -44882,16 +46351,22 @@ const noPassDataToParent = defineRule({
|
|
|
44882
46351
|
const callExpr = getCallExpr(ref);
|
|
44883
46352
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
44884
46353
|
const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall, isReactUseEffectCall);
|
|
44885
|
-
if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
|
|
44886
46354
|
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
44887
46355
|
const calleeNode = unwrapChainExpression(callExpr.callee);
|
|
44888
46356
|
const identifier = ref.identifier;
|
|
44889
|
-
|
|
44890
|
-
|
|
46357
|
+
const resolvedCallbackPropNames = isNodeOfType(calleeNode, "MemberExpression") && getStaticMemberPropertyName(calleeNode) === "current" ? null : getParentCallbackPropNames({
|
|
46358
|
+
analysis,
|
|
46359
|
+
expression: calleeNode,
|
|
46360
|
+
scopes: context.scopes
|
|
46361
|
+
});
|
|
46362
|
+
const callbackPropNames = callbackRefProvenance?.callbackPropNames ?? resolvedCallbackPropNames;
|
|
46363
|
+
if (isRefCall(analysis, ref) && !callbackPropNames) continue;
|
|
46364
|
+
if (callbackPropNames) {
|
|
46365
|
+
if ([...callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
|
|
44891
46366
|
} else if (calleeNode === identifier) {
|
|
44892
46367
|
const callbackPropName = getCommandCallbackPropName(analysis, identifier, isReactUseRefCall);
|
|
44893
46368
|
if (callbackPropName && COMMAND_PROP_NAME_PATTERN.test(callbackPropName)) continue;
|
|
44894
|
-
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
46369
|
+
if (!isDirectParentCallbackRef(analysis, ref, context.scopes)) continue;
|
|
44895
46370
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
44896
46371
|
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
44897
46372
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
@@ -44899,10 +46374,10 @@ const noPassDataToParent = defineRule({
|
|
|
44899
46374
|
} else continue;
|
|
44900
46375
|
const methodName = getCallMethodName(calleeNode);
|
|
44901
46376
|
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
44902
|
-
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
46377
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead && !callbackPropNames) continue;
|
|
44903
46378
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
44904
|
-
if (!
|
|
44905
|
-
const isSetterNamedCallee =
|
|
46379
|
+
if (!callbackPropNames && isNamespacedApiCallee(calleeNode)) continue;
|
|
46380
|
+
const isSetterNamedCallee = callbackPropNames ? [...callbackPropNames].every((callbackPropName) => SETTER_NAMED_PROP_PATTERN.test(callbackPropName)) : Boolean((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) && SETTER_NAMED_PROP_PATTERN.test((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) ?? ""));
|
|
44906
46381
|
const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
|
|
44907
46382
|
const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
|
|
44908
46383
|
if (isFunctionLike$1(argument)) {
|
|
@@ -44916,11 +46391,11 @@ const noPassDataToParent = defineRule({
|
|
|
44916
46391
|
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
44917
46392
|
}
|
|
44918
46393
|
return getDownstreamRefs(analysis, argument);
|
|
44919
|
-
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
44920
|
-
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
46394
|
+
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) || isExternalSubscriptionHookRef(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
46395
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
44921
46396
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
44922
46397
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
44923
|
-
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
46398
|
+
if (isExternalSubscriptionHookRef(analysis, argRef)) return false;
|
|
44924
46399
|
if (isProp(analysis, argRef)) return false;
|
|
44925
46400
|
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
44926
46401
|
if (isRefCurrent(argRef)) return false;
|
|
@@ -44955,9 +46430,47 @@ const isCallResultConsumedAsArgument = (callExpression) => {
|
|
|
44955
46430
|
return false;
|
|
44956
46431
|
};
|
|
44957
46432
|
//#endregion
|
|
46433
|
+
//#region src/plugin/rules/state-and-effects/utils/is-custom-hook-state-result-reference.ts
|
|
46434
|
+
const NON_STATE_CUSTOM_HOOK_NAMES = new Set([
|
|
46435
|
+
"useCallbackRef",
|
|
46436
|
+
"useEffectEvent",
|
|
46437
|
+
"useEvent",
|
|
46438
|
+
"useEventCallback",
|
|
46439
|
+
"useLatest",
|
|
46440
|
+
"useMemoizedFn",
|
|
46441
|
+
"useStableCallback"
|
|
46442
|
+
]);
|
|
46443
|
+
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
|
|
46444
|
+
"useIntersectionObserver",
|
|
46445
|
+
"useMatchMedia",
|
|
46446
|
+
"useMediaJobProgress",
|
|
46447
|
+
"useMediaQuery",
|
|
46448
|
+
"useResizeObserver",
|
|
46449
|
+
"useVisibility",
|
|
46450
|
+
"useWindowSize"
|
|
46451
|
+
]);
|
|
46452
|
+
const getHookCalleeName = (initializer) => {
|
|
46453
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
46454
|
+
if (!isNodeOfType(unwrappedInitializer, "CallExpression")) return null;
|
|
46455
|
+
const callee = stripParenExpression(unwrappedInitializer.callee);
|
|
46456
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
46457
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
46458
|
+
return null;
|
|
46459
|
+
};
|
|
46460
|
+
const isCustomHookStateResultReference = (analysis, reference) => Boolean(reference.resolved?.defs.some((definition) => {
|
|
46461
|
+
const declarator = definition.node;
|
|
46462
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return false;
|
|
46463
|
+
const calleeName = getHookCalleeName(declarator.init);
|
|
46464
|
+
if (!calleeName || !HOOK_NAME_PATTERN$3.test(calleeName) || BUILTIN_HOOK_NAMES.has(calleeName) || NON_STATE_CUSTOM_HOOK_NAMES.has(calleeName) || EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(calleeName)) return false;
|
|
46465
|
+
const initializer = stripParenExpression(declarator.init);
|
|
46466
|
+
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
46467
|
+
return initializer.arguments.some((argument) => getDownstreamRefs(analysis, argument).some((argumentReference) => isProp(analysis, argumentReference)));
|
|
46468
|
+
}));
|
|
46469
|
+
//#endregion
|
|
44958
46470
|
//#region src/plugin/rules/state-and-effects/no-pass-live-state-to-parent.ts
|
|
44959
46471
|
const SETTER_NAMED_CALLBACK_PATTERN = /^set[A-Z]/;
|
|
44960
46472
|
const DATA_FETCHING_CALLBACK_PATTERN = /^(fetch|refetch|load|query|request)([A-Z_]|$)/;
|
|
46473
|
+
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
44961
46474
|
const getCallCalleeName = (callExpr) => {
|
|
44962
46475
|
if (!isNodeOfType(callExpr, "CallExpression")) return null;
|
|
44963
46476
|
const callee = callExpr.callee;
|
|
@@ -45002,6 +46515,10 @@ const collectUpstreamStateRefs = (analysis, ref, stateRefs, visited) => {
|
|
|
45002
46515
|
stateRefs.push(ref);
|
|
45003
46516
|
return;
|
|
45004
46517
|
}
|
|
46518
|
+
if (isCustomHookStateResultReference(analysis, ref)) {
|
|
46519
|
+
stateRefs.push(ref);
|
|
46520
|
+
return;
|
|
46521
|
+
}
|
|
45005
46522
|
for (const def of ref.resolved?.defs ?? []) {
|
|
45006
46523
|
if (def.type === "ImportBinding" || def.type === "Parameter") continue;
|
|
45007
46524
|
const defNode = def.node;
|
|
@@ -45031,6 +46548,32 @@ const collectPropCallbackBoundStateRefs = (analysis, ref, isPropCallbackRef) =>
|
|
|
45031
46548
|
}
|
|
45032
46549
|
return stateRefs;
|
|
45033
46550
|
};
|
|
46551
|
+
const collectDirectCallStateRefs = (analysis, callExpression) => {
|
|
46552
|
+
const stateReferences = [];
|
|
46553
|
+
for (const argument of callExpression.arguments) {
|
|
46554
|
+
if (isFunctionLike$1(argument)) continue;
|
|
46555
|
+
for (const argumentReference of getDownstreamRefs(analysis, argument)) {
|
|
46556
|
+
if (resolveToFunction(argumentReference)) continue;
|
|
46557
|
+
collectUpstreamStateRefs(analysis, argumentReference, stateReferences, /* @__PURE__ */ new Set());
|
|
46558
|
+
}
|
|
46559
|
+
}
|
|
46560
|
+
return stateReferences;
|
|
46561
|
+
};
|
|
46562
|
+
const getTransparentWrapperPropReference = (analysis, reference, context) => {
|
|
46563
|
+
for (const definition of reference.resolved?.defs ?? []) {
|
|
46564
|
+
const declarator = definition.node;
|
|
46565
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
|
|
46566
|
+
const resultSymbol = context.scopes.symbolFor(declarator.id);
|
|
46567
|
+
const callbackArgument = getTransparentReactCallbackWrapperArgument(declarator.init, resultSymbol, context.scopes);
|
|
46568
|
+
if (!callbackArgument) continue;
|
|
46569
|
+
const callbackReferences = getDownstreamRefs(analysis, callbackArgument);
|
|
46570
|
+
const callbackReference = callbackReferences.find((candidateReference) => isPropCallbackInvocationRef(analysis, candidateReference));
|
|
46571
|
+
if (callbackReference) return callbackReference;
|
|
46572
|
+
const propReference = callbackReferences.find((candidateReference) => isProp(analysis, candidateReference) && !candidateReference.resolved?.references.some((candidateUsage) => candidateUsage.isWrite() && !candidateUsage.init));
|
|
46573
|
+
if (propReference) return propReference;
|
|
46574
|
+
}
|
|
46575
|
+
return null;
|
|
46576
|
+
};
|
|
45034
46577
|
const isSetterNamedCallbackReceivingData = (callbackRef) => {
|
|
45035
46578
|
const callExpr = getCallExpr(callbackRef);
|
|
45036
46579
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) return false;
|
|
@@ -45066,6 +46609,16 @@ const resolvesToLocalHookReturnBinding = (ref) => Boolean(ref?.resolved?.defs?.s
|
|
|
45066
46609
|
const calleeName = getInitializerCalleeName(node.init);
|
|
45067
46610
|
return calleeName !== null && isReactHookName(calleeName) && !FUNCTION_WRAPPER_HOOK_NAMES.has(calleeName);
|
|
45068
46611
|
}));
|
|
46612
|
+
const getDirectLocalEffectHelper = (callExpression, effectFunction, context) => {
|
|
46613
|
+
const helperFunction = resolveExactLocalFunction(callExpression.callee, context.scopes);
|
|
46614
|
+
if (!helperFunction) return null;
|
|
46615
|
+
let ancestor = callExpression.parent;
|
|
46616
|
+
while (ancestor && ancestor !== effectFunction) {
|
|
46617
|
+
if (isFunctionLike$1(ancestor)) return null;
|
|
46618
|
+
ancestor = ancestor.parent;
|
|
46619
|
+
}
|
|
46620
|
+
return ancestor === effectFunction ? helperFunction : null;
|
|
46621
|
+
};
|
|
45069
46622
|
const noPassLiveStateToParent = defineRule({
|
|
45070
46623
|
id: "no-pass-live-state-to-parent",
|
|
45071
46624
|
title: "Live state pushed to parent via effect",
|
|
@@ -45080,20 +46633,32 @@ const noPassLiveStateToParent = defineRule({
|
|
|
45080
46633
|
if (!effectFnRefs) return;
|
|
45081
46634
|
const effectFn = getEffectFn(analysis, node);
|
|
45082
46635
|
if (!effectFn) return;
|
|
46636
|
+
const effectFunctionBody = isNodeOfType(effectFn, "ArrowFunctionExpression") || isNodeOfType(effectFn, "FunctionExpression") || isNodeOfType(effectFn, "FunctionDeclaration") ? effectFn.body : null;
|
|
45083
46637
|
for (const ref of effectFnRefs) {
|
|
45084
|
-
const propCallbackRefs = getEventualCallRefsTo(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
45085
|
-
if (propCallbackRefs.length === 0) continue;
|
|
45086
|
-
if (resolvesToLocalHookReturnBinding(ref)) continue;
|
|
45087
|
-
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
45088
46638
|
const callExpr = getCallExpr(ref);
|
|
45089
|
-
if (!callExpr) continue;
|
|
46639
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
46640
|
+
const directLocalEffectHelper = getDirectLocalEffectHelper(callExpr, effectFn, context);
|
|
46641
|
+
const callGraphReferences = directLocalEffectHelper ? [ref, ...getDownstreamRefs(analysis, directLocalEffectHelper)] : [ref];
|
|
46642
|
+
const resolvedCallbackPropNames = getParentCallbackPropNames({
|
|
46643
|
+
analysis,
|
|
46644
|
+
expression: callExpr.callee,
|
|
46645
|
+
scopes: context.scopes
|
|
46646
|
+
});
|
|
46647
|
+
const callExpressionRoot = findTransparentExpressionRoot(callExpr);
|
|
46648
|
+
const notificationCallbackPropNames = Boolean(resolvedCallbackPropNames && callExpr.arguments.length > 0 && (!isCallResultCapturedToLocal(callExpr) || isNodeOfType(callExpressionRoot.parent, "ReturnStatement") && callExpressionRoot.parent.parent === effectFunctionBody) && [...resolvedCallbackPropNames].every((callbackPropName) => !DATA_FETCHING_CALLBACK_PATTERN.test(callbackPropName))) ? resolvedCallbackPropNames : null;
|
|
46649
|
+
if (!notificationCallbackPropNames && hasMutableBindingWrite(ref)) continue;
|
|
46650
|
+
const propCallbackRefs = callGraphReferences.flatMap((callGraphReference) => getEventualCallRefsTo(analysis, callGraphReference, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef)));
|
|
46651
|
+
const transparentPropReference = propCallbackRefs.length === 0 ? getTransparentWrapperPropReference(analysis, ref, context) : null;
|
|
46652
|
+
if (propCallbackRefs.length === 0 && !transparentPropReference && !notificationCallbackPropNames) continue;
|
|
46653
|
+
if (!notificationCallbackPropNames && resolvesToLocalHookReturnBinding(ref)) continue;
|
|
46654
|
+
if (!isSynchronous(ref.identifier, effectFn) && !directLocalEffectHelper) continue;
|
|
45090
46655
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
45091
46656
|
const calleeNode = callExpr.callee;
|
|
45092
46657
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
45093
46658
|
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
45094
|
-
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
45095
|
-
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
45096
|
-
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis,
|
|
46659
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead && !notificationCallbackPropNames) continue;
|
|
46660
|
+
if (!notificationCallbackPropNames && calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
46661
|
+
const stateArgRefs = transparentPropReference || notificationCallbackPropNames ? collectDirectCallStateRefs(analysis, callExpr) : callGraphReferences.flatMap((callGraphReference) => collectPropCallbackBoundStateRefs(analysis, callGraphReference, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef)));
|
|
45097
46662
|
const handsSetterNamedCallbackData = propCallbackRefs.some(isSetterNamedCallbackReceivingData);
|
|
45098
46663
|
if (stateArgRefs.length === 0 && !handsSetterNamedCallbackData) continue;
|
|
45099
46664
|
context.report({
|
|
@@ -45486,6 +47051,7 @@ const isStateLikeDependency = (analysis, element, isPropName) => {
|
|
|
45486
47051
|
if (!analysis) return true;
|
|
45487
47052
|
const reference = getRef(analysis, element);
|
|
45488
47053
|
if (!reference) return true;
|
|
47054
|
+
if (isCustomHookStateResultReference(analysis, reference)) return true;
|
|
45489
47055
|
const upstreamReferences = getUpstreamRefs(analysis, reference);
|
|
45490
47056
|
if (upstreamReferences.some((upstreamReference) => isState(analysis, upstreamReference))) return true;
|
|
45491
47057
|
return !upstreamReferences.some((upstreamReference) => isProp(analysis, upstreamReference));
|
|
@@ -45502,6 +47068,22 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
|
45502
47068
|
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
45503
47069
|
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
45504
47070
|
};
|
|
47071
|
+
const getTransparentWrappedPropCallbackName = (callExpression, context, isPropName) => {
|
|
47072
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
47073
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
47074
|
+
const binding = findVariableInitializer(callExpression, callee.name);
|
|
47075
|
+
if (!binding?.initializer) return null;
|
|
47076
|
+
const resultSymbol = context.scopes.symbolFor(callee);
|
|
47077
|
+
const callbackArgument = getTransparentReactCallbackWrapperArgument(binding.initializer, resultSymbol, context.scopes);
|
|
47078
|
+
if (!callbackArgument) return null;
|
|
47079
|
+
const callbackSource = stripParenExpression(callbackArgument);
|
|
47080
|
+
if (isNodeOfType(callbackSource, "Identifier")) return isPropName(callbackSource.name, callbackSource) ? callbackSource.name : null;
|
|
47081
|
+
if (!isNodeOfType(callbackSource, "MemberExpression")) return null;
|
|
47082
|
+
const receiver = stripParenExpression(callbackSource.object);
|
|
47083
|
+
const propertyName = getStaticPropertyName(callbackSource);
|
|
47084
|
+
if (!isNodeOfType(receiver, "Identifier") || !propertyName) return null;
|
|
47085
|
+
return isPropName(receiver.name, receiver) ? propertyName : null;
|
|
47086
|
+
};
|
|
45505
47087
|
const noPropCallbackInEffect = defineRule({
|
|
45506
47088
|
id: "no-prop-callback-in-effect",
|
|
45507
47089
|
title: "Parent kept in sync with a callback effect",
|
|
@@ -45535,9 +47117,16 @@ const noPropCallbackInEffect = defineRule({
|
|
|
45535
47117
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
45536
47118
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
45537
47119
|
const directCallee = stripParenExpression(child.callee);
|
|
45538
|
-
const
|
|
47120
|
+
const resolvedCallbackPropNames = analysis && propStackTracker.getCurrentPropNames().size > 0 ? getParentCallbackPropNames({
|
|
47121
|
+
analysis,
|
|
47122
|
+
expression: directCallee,
|
|
47123
|
+
scopes: context.scopes
|
|
47124
|
+
}) : null;
|
|
47125
|
+
const calleeName = resolvedCallbackPropNames && [...resolvedCallbackPropNames][0] || isNodeOfType(directCallee, "Identifier") && propStackTracker.isPropName(directCallee.name) && directCallee.name || getRefHeldPropCallbackName(child, propStackTracker.isPropName) || getTransparentWrappedPropCallbackName(child, context, propStackTracker.isPropName);
|
|
45539
47126
|
if (!calleeName) return;
|
|
45540
|
-
|
|
47127
|
+
const callExpressionRoot = findTransparentExpressionRoot(child);
|
|
47128
|
+
const isDirectEffectReturn = isNodeOfType(callExpressionRoot.parent, "ReturnStatement") && callExpressionRoot.parent.parent === callback.body;
|
|
47129
|
+
if (!isResultDiscardedCall(child) && !isDirectEffectReturn) return;
|
|
45541
47130
|
if (reportedNodes.has(child)) return;
|
|
45542
47131
|
reportedNodes.add(child);
|
|
45543
47132
|
context.report({
|
|
@@ -46370,6 +47959,69 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
46370
47959
|
}
|
|
46371
47960
|
});
|
|
46372
47961
|
//#endregion
|
|
47962
|
+
//#region src/plugin/rules/correctness/no-ref-callback-cleanup-before-react-19.ts
|
|
47963
|
+
const resolveFunctionExpressions = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
47964
|
+
const expression = stripParenExpression(rawExpression);
|
|
47965
|
+
if (isFunctionLike$1(expression)) return expression.async || expression.generator ? [] : [expression];
|
|
47966
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
47967
|
+
if (isNodeOfType(expression.test, "Literal")) return resolveFunctionExpressions(expression.test.value ? expression.consequent : expression.alternate, scopes, visitedSymbolIds);
|
|
47968
|
+
return [...resolveFunctionExpressions(expression.consequent, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.alternate, scopes, visitedSymbolIds)];
|
|
47969
|
+
}
|
|
47970
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
47971
|
+
if (isNodeOfType(expression.left, "Literal")) {
|
|
47972
|
+
const isLeftTruthy = Boolean(expression.left.value);
|
|
47973
|
+
if (expression.operator === "&&" && !isLeftTruthy) return [];
|
|
47974
|
+
if (expression.operator === "||" && isLeftTruthy) return [];
|
|
47975
|
+
if (expression.operator === "??" && expression.left.value !== null) return [];
|
|
47976
|
+
}
|
|
47977
|
+
if (expression.operator === "&&") return resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds);
|
|
47978
|
+
return [...resolveFunctionExpressions(expression.left, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds)];
|
|
47979
|
+
}
|
|
47980
|
+
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
47981
|
+
const finalExpression = expression.expressions.at(-1);
|
|
47982
|
+
return finalExpression ? resolveFunctionExpressions(finalExpression, scopes, visitedSymbolIds) : [];
|
|
47983
|
+
}
|
|
47984
|
+
if (isNodeOfType(expression, "CallExpression")) {
|
|
47985
|
+
if (!isReactApiCall(expression, "useCallback", scopes)) return [];
|
|
47986
|
+
const callback = expression.arguments[0];
|
|
47987
|
+
return callback && !isNodeOfType(callback, "SpreadElement") ? resolveFunctionExpressions(callback, scopes, visitedSymbolIds) : [];
|
|
47988
|
+
}
|
|
47989
|
+
if (!isNodeOfType(expression, "Identifier")) return [];
|
|
47990
|
+
const symbol = scopes.symbolFor(expression);
|
|
47991
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return [];
|
|
47992
|
+
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration") && symbol.references.every((reference) => reference.flag === "read")) return resolveFunctionExpressions(symbol.declarationNode, scopes, new Set([...visitedSymbolIds, symbol.id]));
|
|
47993
|
+
const initializer = getDirectConstInitializer(symbol);
|
|
47994
|
+
if (!initializer) return [];
|
|
47995
|
+
return resolveFunctionExpressions(initializer, scopes, new Set([...visitedSymbolIds, symbol.id]));
|
|
47996
|
+
};
|
|
47997
|
+
const functionReturnsCleanupFunction = (functionExpression, scopes) => {
|
|
47998
|
+
if (!isFunctionLike$1(functionExpression)) return false;
|
|
47999
|
+
if (!isNodeOfType(functionExpression.body, "BlockStatement")) return resolveFunctionExpressions(functionExpression.body, scopes).length > 0;
|
|
48000
|
+
return collectFunctionReturnStatements(functionExpression).some((returnStatement) => Boolean(returnStatement.argument && resolveFunctionExpressions(returnStatement.argument, scopes).length > 0));
|
|
48001
|
+
};
|
|
48002
|
+
const callbackReturnsCleanupFunction = (callback, scopes) => {
|
|
48003
|
+
return resolveFunctionExpressions(callback, scopes).some((functionExpression) => functionReturnsCleanupFunction(functionExpression, scopes));
|
|
48004
|
+
};
|
|
48005
|
+
const noRefCallbackCleanupBeforeReact19 = defineRule({
|
|
48006
|
+
id: "no-ref-callback-cleanup-before-react-19",
|
|
48007
|
+
title: "Ref cleanup requires React 19",
|
|
48008
|
+
requires: ["react:18"],
|
|
48009
|
+
disabledWhen: ["react:19"],
|
|
48010
|
+
severity: "warn",
|
|
48011
|
+
recommendation: "React 18 ignores functions returned from ref callbacks. Handle cleanup when React calls the ref with `null`, or require React 19 before returning a cleanup function.",
|
|
48012
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
48013
|
+
if (getJsxAttributeName(node.name) !== "ref") return;
|
|
48014
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
48015
|
+
const callback = node.value.expression;
|
|
48016
|
+
if (!callback || isNodeOfType(callback, "JSXEmptyExpression")) return;
|
|
48017
|
+
if (!callbackReturnsCleanupFunction(callback, context.scopes)) return;
|
|
48018
|
+
context.report({
|
|
48019
|
+
node,
|
|
48020
|
+
message: "This ref callback returns a cleanup function, but React 18 ignores ref cleanup returns, so the cleanup never runs. Handle detachment when React calls the ref with `null`, or require React 19."
|
|
48021
|
+
});
|
|
48022
|
+
} })
|
|
48023
|
+
});
|
|
48024
|
+
//#endregion
|
|
46373
48025
|
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
46374
48026
|
const REPEATED_ANCESTOR_TYPES = new Set([
|
|
46375
48027
|
"DoWhileStatement",
|
|
@@ -53130,12 +54782,6 @@ const isInsideEs6Component$1 = (methodDefinition) => {
|
|
|
53130
54782
|
if (!owningClass) return false;
|
|
53131
54783
|
return isPreactOrReactComponentClass(owningClass);
|
|
53132
54784
|
};
|
|
53133
|
-
const stripThisParameter = (params) => {
|
|
53134
|
-
const first = params[0];
|
|
53135
|
-
if (!first) return params;
|
|
53136
|
-
if (isNodeOfType(first, "Identifier") && first.name === "this") return params.slice(1);
|
|
53137
|
-
return params;
|
|
53138
|
-
};
|
|
53139
54785
|
const preactNoRenderArguments = defineRule({
|
|
53140
54786
|
id: "preact-no-render-arguments",
|
|
53141
54787
|
title: "render() reads props from arguments",
|
|
@@ -56337,8 +57983,39 @@ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node,
|
|
|
56337
57983
|
destructureIndex: 1
|
|
56338
57984
|
});
|
|
56339
57985
|
//#endregion
|
|
57986
|
+
//#region src/plugin/utils/unwrap-return-expression.ts
|
|
57987
|
+
const unwrapReturnExpression = (node) => isNodeOfType(node, "ReturnStatement") && node.argument ? node.argument : node;
|
|
57988
|
+
//#endregion
|
|
56340
57989
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
56341
57990
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
57991
|
+
const USE_CALLBACK_ONLY = new Set(["useCallback"]);
|
|
57992
|
+
const USE_STATE_ONLY = new Set(["useState"]);
|
|
57993
|
+
const REACT_API_CALL_OPTIONS = {
|
|
57994
|
+
allowGlobalReactNamespace: true,
|
|
57995
|
+
allowUnboundBareCalls: true,
|
|
57996
|
+
resolveNamedAliases: true
|
|
57997
|
+
};
|
|
57998
|
+
const expressionReadsDerivedSymbol = (context, expression, stateDerivedSymbolIds) => {
|
|
57999
|
+
let readsDerivedSymbol = false;
|
|
58000
|
+
walkAst(expression, (node) => {
|
|
58001
|
+
if (readsDerivedSymbol) return false;
|
|
58002
|
+
if (node !== expression && isFunctionLike$1(node)) return false;
|
|
58003
|
+
if (isNodeOfType(node, "Identifier") && stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) readsDerivedSymbol = true;
|
|
58004
|
+
});
|
|
58005
|
+
return readsDerivedSymbol;
|
|
58006
|
+
};
|
|
58007
|
+
const getStaticObjectPropertyName = (property) => {
|
|
58008
|
+
if (!isNodeOfType(property, "Property") || property.computed || property.method || property.kind !== "init") return null;
|
|
58009
|
+
if (isNodeOfType(property.key, "Identifier")) return property.key.name;
|
|
58010
|
+
if (isNodeOfType(property.key, "Literal") && (typeof property.key.value === "string" || typeof property.key.value === "number")) return String(property.key.value);
|
|
58011
|
+
return null;
|
|
58012
|
+
};
|
|
58013
|
+
const isNonVisibleJsxSpreadProperty = (propertyName) => propertyName === "id" || propertyName.startsWith("aria-") || /^on[A-Z]/.test(propertyName);
|
|
58014
|
+
const isTransparentAssignmentTarget = (identifier) => {
|
|
58015
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
58016
|
+
const parent = expressionRoot.parent;
|
|
58017
|
+
return Boolean(isNodeOfType(parent, "AssignmentExpression") && parent.left === expressionRoot || isNodeOfType(parent, "UpdateExpression") && parent.argument === expressionRoot || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expressionRoot);
|
|
58018
|
+
};
|
|
56342
58019
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
56343
58020
|
let readsCurrent = false;
|
|
56344
58021
|
walkAst(argument, (child) => {
|
|
@@ -56390,6 +58067,166 @@ const isStateUsedOnlyInIdOrAriaAttributes = (setterCall, setterName) => {
|
|
|
56390
58067
|
});
|
|
56391
58068
|
return referenceCount > 0 && !nonAriaReferenceFound;
|
|
56392
58069
|
};
|
|
58070
|
+
const isGlobalWindowMember = (context, node, propertyName) => {
|
|
58071
|
+
const member = stripParenExpression(node);
|
|
58072
|
+
if (!isNodeOfType(member, "MemberExpression") || member.computed) return false;
|
|
58073
|
+
const receiver = stripParenExpression(member.object);
|
|
58074
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "window" && context.scopes.isGlobalReference(receiver) && isNodeOfType(member.property, "Identifier") && member.property.name === propertyName;
|
|
58075
|
+
};
|
|
58076
|
+
const getDirectWindowWidthSetter = (context, statement) => {
|
|
58077
|
+
const call = unwrapDiscardedExpression(statement);
|
|
58078
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 1) return null;
|
|
58079
|
+
if (!isNodeOfType(call.callee, "Identifier") || !isSetterCall(call)) return null;
|
|
58080
|
+
const argument = call.arguments[0];
|
|
58081
|
+
return isGlobalWindowMember(context, argument, "innerWidth") ? call : null;
|
|
58082
|
+
};
|
|
58083
|
+
const getResizeListenerHandler = (context, statement, methodName) => {
|
|
58084
|
+
const call = unwrapDiscardedExpression(statement);
|
|
58085
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 2) return null;
|
|
58086
|
+
if (!isGlobalWindowMember(context, call.callee, methodName)) return null;
|
|
58087
|
+
const eventName = call.arguments[0];
|
|
58088
|
+
const handler = call.arguments[1];
|
|
58089
|
+
if (!isNodeOfType(eventName, "Literal") || eventName.value !== "resize") return null;
|
|
58090
|
+
return isNodeOfType(handler, "Identifier") ? handler : null;
|
|
58091
|
+
};
|
|
58092
|
+
const getCleanupResizeHandler = (context, statement) => {
|
|
58093
|
+
if (!isNodeOfType(statement, "ReturnStatement") || !isFunctionLike$1(statement.argument)) return null;
|
|
58094
|
+
const cleanupStatements = getCallbackStatements(statement.argument);
|
|
58095
|
+
if (cleanupStatements.length !== 1) return null;
|
|
58096
|
+
return getResizeListenerHandler(context, unwrapReturnExpression(cleanupStatements[0]), "removeEventListener");
|
|
58097
|
+
};
|
|
58098
|
+
const findExactViewportState = (context, componentFunction, setterCall) => {
|
|
58099
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
58100
|
+
const componentBody = componentFunction.body;
|
|
58101
|
+
if (!isNodeOfType(setterCall.callee, "Identifier")) return null;
|
|
58102
|
+
const setterSymbol = context.scopes.symbolFor(setterCall.callee);
|
|
58103
|
+
if (!setterSymbol || setterSymbol.kind !== "const" || !isNodeOfType(setterSymbol.declarationNode, "VariableDeclarator")) return null;
|
|
58104
|
+
const declarator = setterSymbol.declarationNode;
|
|
58105
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return null;
|
|
58106
|
+
const stateIdentifier = declarator.id.elements?.[0];
|
|
58107
|
+
const setterIdentifier = declarator.id.elements?.[1];
|
|
58108
|
+
if (!isNodeOfType(stateIdentifier, "Identifier") || !isNodeOfType(setterIdentifier, "Identifier") || setterIdentifier !== setterSymbol.bindingIdentifier || !isNodeOfType(declarator.init, "CallExpression") || !isReactApiCall(declarator.init, USE_STATE_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return null;
|
|
58109
|
+
const initializer = declarator.init.arguments?.[0];
|
|
58110
|
+
if (!isNodeOfType(initializer, "Literal") || initializer.value !== 0) return null;
|
|
58111
|
+
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
58112
|
+
if (!stateSymbol) return null;
|
|
58113
|
+
const stateDerivedSymbolIds = new Set([stateSymbol.id]);
|
|
58114
|
+
let didAddDerivedSymbol = true;
|
|
58115
|
+
while (didAddDerivedSymbol) {
|
|
58116
|
+
didAddDerivedSymbol = false;
|
|
58117
|
+
for (const statement of componentBody.body ?? []) {
|
|
58118
|
+
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
58119
|
+
for (const candidateDeclarator of statement.declarations ?? []) {
|
|
58120
|
+
if (!isNodeOfType(candidateDeclarator.id, "Identifier") || !candidateDeclarator.init) continue;
|
|
58121
|
+
const candidateInitializer = stripParenExpression(candidateDeclarator.init);
|
|
58122
|
+
if (isFunctionLike$1(candidateInitializer) || isNodeOfType(candidateInitializer, "CallExpression") && isReactApiCall(candidateInitializer, USE_CALLBACK_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) continue;
|
|
58123
|
+
if (!expressionReadsDerivedSymbol(context, candidateInitializer, stateDerivedSymbolIds)) continue;
|
|
58124
|
+
const candidateSymbol = context.scopes.symbolFor(candidateDeclarator.id);
|
|
58125
|
+
if (candidateSymbol?.kind === "const" && candidateSymbol.references.every((reference) => reference.flag === "read" && !isTransparentAssignmentTarget(reference.identifier)) && !stateDerivedSymbolIds.has(candidateSymbol.id)) {
|
|
58126
|
+
stateDerivedSymbolIds.add(candidateSymbol.id);
|
|
58127
|
+
didAddDerivedSymbol = true;
|
|
58128
|
+
}
|
|
58129
|
+
}
|
|
58130
|
+
}
|
|
58131
|
+
}
|
|
58132
|
+
const staticSpreadVisibilityBySymbolId = /* @__PURE__ */ new Map();
|
|
58133
|
+
const hasOnlyStaticObjectReferences = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
58134
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
58135
|
+
if (!symbol) return false;
|
|
58136
|
+
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
58137
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
58138
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
58139
|
+
let hasUnknownReference = false;
|
|
58140
|
+
walkAst(componentBody, (node) => {
|
|
58141
|
+
if (hasUnknownReference || !isNodeOfType(node, "Identifier") || context.scopes.symbolFor(node)?.id !== symbol.id || node === symbol.bindingIdentifier) return;
|
|
58142
|
+
const referenceRoot = findTransparentExpressionRoot(node);
|
|
58143
|
+
const parent = referenceRoot.parent;
|
|
58144
|
+
if (isNodeOfType(parent, "JSXSpreadAttribute") && parent.argument === referenceRoot) return;
|
|
58145
|
+
if (isNodeOfType(parent, "VariableDeclarator") && parent.init === referenceRoot && isNodeOfType(parent.id, "Identifier") && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const" && hasOnlyStaticObjectReferences(parent.id, nextVisitedSymbolIds)) return;
|
|
58146
|
+
hasUnknownReference = true;
|
|
58147
|
+
return false;
|
|
58148
|
+
});
|
|
58149
|
+
return !hasUnknownReference;
|
|
58150
|
+
};
|
|
58151
|
+
const classifyStaticSpreadObject = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
58152
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
58153
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return "unknown";
|
|
58154
|
+
const cachedVisibility = staticSpreadVisibilityBySymbolId.get(symbol.id);
|
|
58155
|
+
if (cachedVisibility) return cachedVisibility;
|
|
58156
|
+
if (symbol.kind !== "const" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || symbol.declarationNode.id !== symbol.bindingIdentifier || !symbol.declarationNode.init) return "unknown";
|
|
58157
|
+
if (!hasOnlyStaticObjectReferences(identifier)) return "unknown";
|
|
58158
|
+
const initializer = stripParenExpression(symbol.declarationNode.init);
|
|
58159
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
58160
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
58161
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
58162
|
+
const visibility = classifyStaticSpreadObject(initializer, nextVisitedSymbolIds);
|
|
58163
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
58164
|
+
return visibility;
|
|
58165
|
+
}
|
|
58166
|
+
if (!isNodeOfType(initializer, "ObjectExpression")) return "unknown";
|
|
58167
|
+
let visibility = "non-visible";
|
|
58168
|
+
for (const property of initializer.properties ?? []) {
|
|
58169
|
+
const propertyName = getStaticObjectPropertyName(property);
|
|
58170
|
+
if (!isNodeOfType(property, "Property") || !propertyName) {
|
|
58171
|
+
visibility = "unknown";
|
|
58172
|
+
break;
|
|
58173
|
+
}
|
|
58174
|
+
if (expressionReadsDerivedSymbol(context, property.value, stateDerivedSymbolIds) && !isNonVisibleJsxSpreadProperty(propertyName)) visibility = "visible";
|
|
58175
|
+
}
|
|
58176
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
58177
|
+
return visibility;
|
|
58178
|
+
};
|
|
58179
|
+
let hasNonAriaReference = false;
|
|
58180
|
+
walkAst(componentBody, (node) => {
|
|
58181
|
+
if (hasNonAriaReference) return false;
|
|
58182
|
+
if (!isNodeOfType(node, "Identifier") || !stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) return;
|
|
58183
|
+
if (findEnclosingFunction$1(node) !== componentFunction) return;
|
|
58184
|
+
const parent = node.parent;
|
|
58185
|
+
if (parent && (isNodeOfType(parent, "MemberExpression") && parent.property === node && !parent.computed || isNodeOfType(parent, "Property") && parent.key === node && !parent.computed)) return;
|
|
58186
|
+
let cursor = parent;
|
|
58187
|
+
while (cursor && cursor !== componentBody) {
|
|
58188
|
+
if (isFunctionLike$1(cursor)) return;
|
|
58189
|
+
if (isNodeOfType(cursor, "JSXSpreadAttribute")) {
|
|
58190
|
+
if (isNodeOfType(node, "Identifier") && classifyStaticSpreadObject(node) === "visible") hasNonAriaReference = true;
|
|
58191
|
+
return;
|
|
58192
|
+
}
|
|
58193
|
+
if (isNodeOfType(cursor, "JSXAttribute")) {
|
|
58194
|
+
if (isEventHandlerAttribute(cursor)) return;
|
|
58195
|
+
if (!isInsideIdOrAriaAttribute(node)) hasNonAriaReference = true;
|
|
58196
|
+
return;
|
|
58197
|
+
}
|
|
58198
|
+
if (isNodeOfType(cursor, "ReturnStatement")) {
|
|
58199
|
+
hasNonAriaReference = true;
|
|
58200
|
+
return;
|
|
58201
|
+
}
|
|
58202
|
+
cursor = cursor.parent;
|
|
58203
|
+
}
|
|
58204
|
+
});
|
|
58205
|
+
return hasNonAriaReference ? stateIdentifier.name : null;
|
|
58206
|
+
};
|
|
58207
|
+
const isExactViewportSubscriptionEffect = (context, effectCall, callback) => {
|
|
58208
|
+
if (!isReactApiCall(effectCall, USE_EFFECT_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return false;
|
|
58209
|
+
if (!isFunctionLike$1(callback) || callback.async || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
58210
|
+
const statements = getCallbackStatements(callback);
|
|
58211
|
+
if (statements.length !== 4) return false;
|
|
58212
|
+
const handlerDeclaration = statements[0];
|
|
58213
|
+
if (!isNodeOfType(handlerDeclaration, "VariableDeclaration") || handlerDeclaration.kind !== "const" || handlerDeclaration.declarations?.length !== 1) return false;
|
|
58214
|
+
const handlerDeclarator = handlerDeclaration.declarations[0];
|
|
58215
|
+
if (!isNodeOfType(handlerDeclarator.id, "Identifier") || !isFunctionLike$1(handlerDeclarator.init)) return false;
|
|
58216
|
+
const handlerStatements = getCallbackStatements(handlerDeclarator.init);
|
|
58217
|
+
if (handlerStatements.length !== 1) return false;
|
|
58218
|
+
const handlerSetter = getDirectWindowWidthSetter(context, unwrapReturnExpression(handlerStatements[0]));
|
|
58219
|
+
const subscribedHandler = getResizeListenerHandler(context, statements[1], "addEventListener");
|
|
58220
|
+
const immediateSetter = getDirectWindowWidthSetter(context, statements[2]);
|
|
58221
|
+
const cleanupHandler = getCleanupResizeHandler(context, statements[3]);
|
|
58222
|
+
if (!handlerSetter || !subscribedHandler || !immediateSetter || !cleanupHandler) return false;
|
|
58223
|
+
const handlerSymbol = context.scopes.symbolFor(handlerDeclarator.id);
|
|
58224
|
+
if (!handlerSymbol || context.scopes.symbolFor(subscribedHandler) !== handlerSymbol || context.scopes.symbolFor(cleanupHandler) !== handlerSymbol) return false;
|
|
58225
|
+
if (!isNodeOfType(handlerSetter.callee, "Identifier") || !isNodeOfType(immediateSetter.callee, "Identifier") || context.scopes.symbolFor(handlerSetter.callee) !== context.scopes.symbolFor(immediateSetter.callee)) return false;
|
|
58226
|
+
const componentFunction = findEnclosingFunction$1(effectCall);
|
|
58227
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return false;
|
|
58228
|
+
return findExactViewportState(context, componentFunction, immediateSetter) !== null;
|
|
58229
|
+
};
|
|
56393
58230
|
const renderingHydrationNoFlicker = defineRule({
|
|
56394
58231
|
id: "rendering-hydration-no-flicker",
|
|
56395
58232
|
title: "useEffect setState flashes on mount",
|
|
@@ -56402,7 +58239,14 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
56402
58239
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
56403
58240
|
const callback = getEffectCallback(node);
|
|
56404
58241
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
56405
|
-
|
|
58242
|
+
if (isExactViewportSubscriptionEffect(context, node, callback)) {
|
|
58243
|
+
context.report({
|
|
58244
|
+
node,
|
|
58245
|
+
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
58246
|
+
});
|
|
58247
|
+
return;
|
|
58248
|
+
}
|
|
58249
|
+
const bodyStatements = getCallbackStatements(callback);
|
|
56406
58250
|
if (bodyStatements.length !== 1) return;
|
|
56407
58251
|
const soleStatement = bodyStatements[0];
|
|
56408
58252
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
@@ -56565,6 +58409,125 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
56565
58409
|
const RESOURCE_LOAD_EVENT_ATTRIBUTE_PATTERN = /^on(?:Load|Error|Abort|Progress|CanPlay|Stalled|Suspend|Waiting|Ended)/;
|
|
56566
58410
|
const JSX_EVENT_HANDLER_ATTRIBUTE_PATTERN = /^on[A-Z]/;
|
|
56567
58411
|
const REDUX_DISPATCH_HOOK_PATTERN = /^use\w*Dispatch$/;
|
|
58412
|
+
const FILE_READER_READ_METHOD_NAMES = new Set([
|
|
58413
|
+
"readAsArrayBuffer",
|
|
58414
|
+
"readAsBinaryString",
|
|
58415
|
+
"readAsDataURL",
|
|
58416
|
+
"readAsText"
|
|
58417
|
+
]);
|
|
58418
|
+
const isGlobalFileReaderConstruction = (expression, context) => {
|
|
58419
|
+
if (!expression) return false;
|
|
58420
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
58421
|
+
if (!isNodeOfType(unwrappedExpression, "NewExpression") || !isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
58422
|
+
return unwrappedExpression.callee.name === "FileReader" && context.scopes.isGlobalReference(unwrappedExpression.callee);
|
|
58423
|
+
};
|
|
58424
|
+
const getFileReaderOriginStartBefore = (readerSymbol, readCall, context) => {
|
|
58425
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58426
|
+
let latestValue = null;
|
|
58427
|
+
let latestStart = null;
|
|
58428
|
+
if (readerSymbol.initializer && findEnclosingFunction$1(readerSymbol.declarationNode) === readFunction && readerSymbol.declarationNode.range[0] < readCall.range[0]) {
|
|
58429
|
+
latestValue = readerSymbol.initializer;
|
|
58430
|
+
latestStart = readerSymbol.declarationNode.range[0];
|
|
58431
|
+
}
|
|
58432
|
+
for (const reference of readerSymbol.references) {
|
|
58433
|
+
if (reference.flag === "read" || reference.identifier.range[0] >= readCall.range[0] || latestStart !== null && reference.identifier.range[0] <= latestStart || findEnclosingFunction$1(reference.identifier) !== readFunction) continue;
|
|
58434
|
+
const assignment = reference.identifier.parent;
|
|
58435
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== reference.identifier) continue;
|
|
58436
|
+
latestValue = assignment.right;
|
|
58437
|
+
latestStart = reference.identifier.range[0];
|
|
58438
|
+
}
|
|
58439
|
+
return isGlobalFileReaderConstruction(latestValue, context) ? latestStart : null;
|
|
58440
|
+
};
|
|
58441
|
+
const resolveLoadingCompletionFunction = (expression, context) => {
|
|
58442
|
+
const directFunction = resolveExactLocalFunction(expression, context.scopes);
|
|
58443
|
+
if (directFunction) return directFunction;
|
|
58444
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
58445
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
58446
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
58447
|
+
const initializer = symbol ? getDirectUnreassignedInitializer(symbol) : null;
|
|
58448
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression") || !isReactApiCall(initializer, "useCallback", context.scopes)) return null;
|
|
58449
|
+
const callback = initializer.arguments?.[0];
|
|
58450
|
+
return callback && isFunctionLike$1(callback) ? callback : null;
|
|
58451
|
+
};
|
|
58452
|
+
const isSetterBooleanCall = (node, setterSymbol, value, context) => {
|
|
58453
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
58454
|
+
const callee = stripParenExpression(node.callee);
|
|
58455
|
+
const argument = node.arguments?.[0];
|
|
58456
|
+
const unwrappedArgument = argument ? stripParenExpression(argument) : null;
|
|
58457
|
+
return Boolean(isNodeOfType(callee, "Identifier") && context.scopes.symbolFor(callee) === setterSymbol && unwrappedArgument && isNodeOfType(unwrappedArgument, "Literal") && unwrappedArgument.value === value);
|
|
58458
|
+
};
|
|
58459
|
+
const functionClearsLoadingState = (functionNode, setterSymbol, context, visitedFunctions) => {
|
|
58460
|
+
if (visitedFunctions.has(functionNode) || !isFunctionLike$1(functionNode)) return false;
|
|
58461
|
+
visitedFunctions.add(functionNode);
|
|
58462
|
+
let didClearLoadingState = false;
|
|
58463
|
+
walkAst(functionNode.body, (child) => {
|
|
58464
|
+
if (didClearLoadingState) return false;
|
|
58465
|
+
if (child !== functionNode.body && isFunctionLike$1(child)) return false;
|
|
58466
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
58467
|
+
if (isSetterBooleanCall(child, setterSymbol, false, context)) {
|
|
58468
|
+
didClearLoadingState = true;
|
|
58469
|
+
return false;
|
|
58470
|
+
}
|
|
58471
|
+
const helperFunction = resolveLoadingCompletionFunction(child.callee, context);
|
|
58472
|
+
if (helperFunction && functionClearsLoadingState(helperFunction, setterSymbol, context, visitedFunctions)) {
|
|
58473
|
+
didClearLoadingState = true;
|
|
58474
|
+
return false;
|
|
58475
|
+
}
|
|
58476
|
+
});
|
|
58477
|
+
return didClearLoadingState;
|
|
58478
|
+
};
|
|
58479
|
+
const getLatestFileReaderCallbackBefore = (readCall, readerSymbol, propertyName, originStart, context) => {
|
|
58480
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58481
|
+
if (!readFunction || !isFunctionLike$1(readFunction)) return null;
|
|
58482
|
+
let callback = null;
|
|
58483
|
+
let callbackStart = originStart;
|
|
58484
|
+
walkAst(readFunction.body, (child) => {
|
|
58485
|
+
if (child !== readFunction.body && isFunctionLike$1(child)) return false;
|
|
58486
|
+
if (!isNodeOfType(child, "AssignmentExpression") || child.operator !== "=" || child.range[0] >= readCall.range[0] || child.range[0] <= callbackStart || !isNodeOfType(child.left, "MemberExpression") || getStaticPropertyName(child.left) !== propertyName) return;
|
|
58487
|
+
const receiver = stripParenExpression(child.left.object);
|
|
58488
|
+
if (!isNodeOfType(receiver, "Identifier") || context.scopes.symbolFor(receiver) !== readerSymbol) return;
|
|
58489
|
+
callback = child.right;
|
|
58490
|
+
callbackStart = child.range[0];
|
|
58491
|
+
});
|
|
58492
|
+
return callback;
|
|
58493
|
+
};
|
|
58494
|
+
const setterStartsLoadingBefore = (readCall, setterSymbol, context) => {
|
|
58495
|
+
const readFunction = findEnclosingFunction$1(readCall);
|
|
58496
|
+
if (!readFunction || !isFunctionLike$1(readFunction)) return false;
|
|
58497
|
+
let didStartLoading = false;
|
|
58498
|
+
walkAst(readFunction.body, (child) => {
|
|
58499
|
+
if (didStartLoading) return false;
|
|
58500
|
+
if (child !== readFunction.body && isFunctionLike$1(child)) return false;
|
|
58501
|
+
if (!isNodeOfType(child, "CallExpression") || child.range[0] >= readCall.range[0]) return;
|
|
58502
|
+
if (isSetterBooleanCall(child, setterSymbol, true, context)) {
|
|
58503
|
+
didStartLoading = true;
|
|
58504
|
+
return false;
|
|
58505
|
+
}
|
|
58506
|
+
});
|
|
58507
|
+
return didStartLoading;
|
|
58508
|
+
};
|
|
58509
|
+
const setterTracksFileReader = (functionBody, setterSymbol, context) => {
|
|
58510
|
+
let didFindFileReaderLifecycle = false;
|
|
58511
|
+
walkAst(functionBody, (child) => {
|
|
58512
|
+
if (didFindFileReaderLifecycle) return false;
|
|
58513
|
+
if (!isNodeOfType(child, "CallExpression") || !isNodeOfType(child.callee, "MemberExpression") || !FILE_READER_READ_METHOD_NAMES.has(getStaticPropertyName(child.callee) ?? "")) return;
|
|
58514
|
+
const receiver = stripParenExpression(child.callee.object);
|
|
58515
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
58516
|
+
const readerSymbol = context.scopes.symbolFor(receiver);
|
|
58517
|
+
if (!readerSymbol) return;
|
|
58518
|
+
const originStart = getFileReaderOriginStartBefore(readerSymbol, child, context);
|
|
58519
|
+
if (originStart === null || !setterStartsLoadingBefore(child, setterSymbol, context)) return;
|
|
58520
|
+
const loadCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onload", originStart, context);
|
|
58521
|
+
const errorCallback = getLatestFileReaderCallbackBefore(child, readerSymbol, "onerror", originStart, context);
|
|
58522
|
+
const loadFunction = loadCallback ? resolveLoadingCompletionFunction(loadCallback, context) : null;
|
|
58523
|
+
const errorFunction = errorCallback ? resolveLoadingCompletionFunction(errorCallback, context) : null;
|
|
58524
|
+
if (loadFunction && errorFunction && functionClearsLoadingState(loadFunction, setterSymbol, context, /* @__PURE__ */ new Set()) && functionClearsLoadingState(errorFunction, setterSymbol, context, /* @__PURE__ */ new Set())) {
|
|
58525
|
+
didFindFileReaderLifecycle = true;
|
|
58526
|
+
return false;
|
|
58527
|
+
}
|
|
58528
|
+
});
|
|
58529
|
+
return didFindFileReaderLifecycle;
|
|
58530
|
+
};
|
|
56568
58531
|
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
56569
58532
|
let found = false;
|
|
56570
58533
|
walkAst(fnBody, (child) => {
|
|
@@ -56738,6 +58701,8 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
56738
58701
|
const fnBody = enclosingFunctionBody(node);
|
|
56739
58702
|
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
56740
58703
|
if (fnBody && setterName) {
|
|
58704
|
+
const setterSymbol = isNodeOfType(secondBinding, "Identifier") ? context.scopes.symbolFor(secondBinding) : null;
|
|
58705
|
+
if (setterSymbol && setterTracksFileReader(fnBody, setterSymbol, context)) return;
|
|
56741
58706
|
if (setterEscapes(fnBody, setterName, node)) return;
|
|
56742
58707
|
if (setterCalledAlongsideAsyncSignal(fnBody, setterName)) return;
|
|
56743
58708
|
if (setterCalledInEventListenerHandler(fnBody, setterName)) return;
|
|
@@ -66234,14 +68199,6 @@ const isStateKey = (key) => {
|
|
|
66234
68199
|
if (isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value === "state";
|
|
66235
68200
|
return false;
|
|
66236
68201
|
};
|
|
66237
|
-
const findEnclosingClass = (node) => {
|
|
66238
|
-
let ancestor = node.parent;
|
|
66239
|
-
while (ancestor) {
|
|
66240
|
-
if (isNodeOfType(ancestor, "ClassDeclaration") || isNodeOfType(ancestor, "ClassExpression")) return ancestor;
|
|
66241
|
-
ancestor = ancestor.parent ?? null;
|
|
66242
|
-
}
|
|
66243
|
-
return null;
|
|
66244
|
-
};
|
|
66245
68202
|
const isInConstructor = (node) => {
|
|
66246
68203
|
let ancestor = node.parent;
|
|
66247
68204
|
while (ancestor) {
|
|
@@ -70953,6 +72910,17 @@ const reactDoctorRules = [
|
|
|
70953
72910
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
70954
72911
|
}
|
|
70955
72912
|
},
|
|
72913
|
+
{
|
|
72914
|
+
key: "react-doctor/no-ref-callback-cleanup-before-react-19",
|
|
72915
|
+
id: "no-ref-callback-cleanup-before-react-19",
|
|
72916
|
+
source: "react-doctor",
|
|
72917
|
+
originallyExternal: false,
|
|
72918
|
+
rule: {
|
|
72919
|
+
...noRefCallbackCleanupBeforeReact19,
|
|
72920
|
+
framework: "global",
|
|
72921
|
+
category: "Bugs"
|
|
72922
|
+
}
|
|
72923
|
+
},
|
|
70956
72924
|
{
|
|
70957
72925
|
key: "react-doctor/no-ref-current-in-render",
|
|
70958
72926
|
id: "no-ref-current-in-render",
|