oxlint-plugin-react-doctor 0.7.9-dev.b392093 → 0.7.9-dev.bd3655c
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.js +1056 -150
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6736,6 +6736,28 @@ const asyncParallel = defineRule({
|
|
|
6736
6736
|
}
|
|
6737
6737
|
});
|
|
6738
6738
|
//#endregion
|
|
6739
|
+
//#region src/plugin/utils/collect-function-return-statements.ts
|
|
6740
|
+
const collectFunctionReturnStatements = (functionNode) => {
|
|
6741
|
+
if (!isFunctionLike$1(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return [];
|
|
6742
|
+
const returnStatements = [];
|
|
6743
|
+
walkAst(functionNode.body, (node) => {
|
|
6744
|
+
if (node !== functionNode.body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
6745
|
+
if (isNodeOfType(node, "ReturnStatement")) returnStatements.push(node);
|
|
6746
|
+
});
|
|
6747
|
+
return returnStatements;
|
|
6748
|
+
};
|
|
6749
|
+
//#endregion
|
|
6750
|
+
//#region src/plugin/utils/is-nullish-expression.ts
|
|
6751
|
+
const isNullishExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined" || isNodeOfType(expression, "UnaryExpression") && expression.operator === "void";
|
|
6752
|
+
//#endregion
|
|
6753
|
+
//#region src/plugin/utils/strip-this-parameter.ts
|
|
6754
|
+
const stripThisParameter = (parameters) => {
|
|
6755
|
+
const firstParameter = parameters[0];
|
|
6756
|
+
if (!firstParameter) return parameters;
|
|
6757
|
+
if (isNodeOfType(firstParameter, "Identifier") && firstParameter.name === "this") return parameters.slice(1);
|
|
6758
|
+
return parameters;
|
|
6759
|
+
};
|
|
6760
|
+
//#endregion
|
|
6739
6761
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
6740
6762
|
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
6763
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
@@ -6747,12 +6769,8 @@ const STORAGE_GLOBALS = new Set([
|
|
|
6747
6769
|
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
6748
6770
|
const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
|
|
6749
6771
|
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
6772
|
const isAuthCredentialKey = (key) => {
|
|
6753
|
-
if (PRODUCT_API_KEY_RECORDS_PATTERN.test(key)) return false;
|
|
6754
6773
|
if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
|
|
6755
|
-
if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
|
|
6756
6774
|
if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
|
|
6757
6775
|
return true;
|
|
6758
6776
|
};
|
|
@@ -6761,11 +6779,63 @@ const isDirectWebStorageObject = (node) => {
|
|
|
6761
6779
|
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
6780
|
return false;
|
|
6763
6781
|
};
|
|
6764
|
-
const
|
|
6765
|
-
if (
|
|
6766
|
-
|
|
6767
|
-
const binding = findVariableInitializer(
|
|
6768
|
-
|
|
6782
|
+
const immutableInitializer = (identifier, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
6783
|
+
if (visitedIdentifiers.has(identifier)) return null;
|
|
6784
|
+
visitedIdentifiers.add(identifier);
|
|
6785
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
6786
|
+
if (!binding?.initializer) return null;
|
|
6787
|
+
if (isNodeOfType(binding.initializer, "FunctionDeclaration")) return binding.initializer;
|
|
6788
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
6789
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
6790
|
+
const declaration = declarator.parent;
|
|
6791
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
6792
|
+
if (declaration.kind !== "const") return null;
|
|
6793
|
+
const initializer = stripParenExpression(binding.initializer);
|
|
6794
|
+
if (!isNodeOfType(initializer, "Identifier")) return initializer;
|
|
6795
|
+
return immutableInitializer(initializer, visitedIdentifiers) ?? initializer;
|
|
6796
|
+
};
|
|
6797
|
+
const isWebStorageFactoryResult = (node, visitedNodes) => {
|
|
6798
|
+
const expression = stripParenExpression(node);
|
|
6799
|
+
if (isWebStorageObject(expression, new Set(visitedNodes))) return true;
|
|
6800
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
6801
|
+
const consequent = stripParenExpression(expression.consequent);
|
|
6802
|
+
const alternate = stripParenExpression(expression.alternate);
|
|
6803
|
+
return (isNullishExpression(consequent) || isWebStorageFactoryResult(consequent, visitedNodes)) && (isNullishExpression(alternate) || isWebStorageFactoryResult(alternate, visitedNodes)) && (!isNullishExpression(consequent) || !isNullishExpression(alternate));
|
|
6804
|
+
}
|
|
6805
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
6806
|
+
if (expression.operator === "&&") return isWebStorageFactoryResult(expression.right, visitedNodes);
|
|
6807
|
+
const left = stripParenExpression(expression.left);
|
|
6808
|
+
const right = stripParenExpression(expression.right);
|
|
6809
|
+
const isLeftStorage = isWebStorageFactoryResult(left, visitedNodes);
|
|
6810
|
+
const isRightStorage = isWebStorageFactoryResult(right, visitedNodes);
|
|
6811
|
+
return (isLeftStorage || isNullishExpression(left)) && (isRightStorage || isNullishExpression(right)) && (isLeftStorage || isRightStorage);
|
|
6812
|
+
}
|
|
6813
|
+
return false;
|
|
6814
|
+
};
|
|
6815
|
+
const isWebStorageObject = (node, visitedNodes = /* @__PURE__ */ new Set()) => {
|
|
6816
|
+
const expression = stripParenExpression(node);
|
|
6817
|
+
if (visitedNodes.has(expression)) return false;
|
|
6818
|
+
visitedNodes.add(expression);
|
|
6819
|
+
if (isDirectWebStorageObject(expression)) return true;
|
|
6820
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
6821
|
+
const initializer = immutableInitializer(expression);
|
|
6822
|
+
return initializer ? isWebStorageObject(initializer, new Set(visitedNodes)) : false;
|
|
6823
|
+
}
|
|
6824
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
6825
|
+
const callee = stripParenExpression(expression.callee);
|
|
6826
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
6827
|
+
const factory = immutableInitializer(callee);
|
|
6828
|
+
if (!isFunctionLike$1(factory)) return false;
|
|
6829
|
+
if (isNodeOfType(factory, "ArrowFunctionExpression") && !isNodeOfType(factory.body, "BlockStatement")) return isWebStorageFactoryResult(factory.body, visitedNodes);
|
|
6830
|
+
let didReturnWebStorage = false;
|
|
6831
|
+
for (const returnStatement of collectFunctionReturnStatements(factory)) {
|
|
6832
|
+
if (!returnStatement.argument) continue;
|
|
6833
|
+
const strippedReturn = stripParenExpression(returnStatement.argument);
|
|
6834
|
+
if (isNullishExpression(strippedReturn)) continue;
|
|
6835
|
+
if (!isWebStorageFactoryResult(strippedReturn, visitedNodes)) return false;
|
|
6836
|
+
didReturnWebStorage = true;
|
|
6837
|
+
}
|
|
6838
|
+
return didReturnWebStorage;
|
|
6769
6839
|
};
|
|
6770
6840
|
const resolveStaticKeyString = (node) => {
|
|
6771
6841
|
if (isNodeOfType(node, "Literal") && typeof node.value === "string") return node.value;
|
|
@@ -6785,6 +6855,56 @@ const staticMemberName = (member) => {
|
|
|
6785
6855
|
if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
|
|
6786
6856
|
return null;
|
|
6787
6857
|
};
|
|
6858
|
+
const parameterIndex = (expression, parameterSymbolIds, scopes, canUnwrapSerialization, visitedNodes = /* @__PURE__ */ new Set()) => {
|
|
6859
|
+
const strippedExpression = stripParenExpression(expression);
|
|
6860
|
+
if (visitedNodes.has(strippedExpression)) return null;
|
|
6861
|
+
visitedNodes.add(strippedExpression);
|
|
6862
|
+
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
6863
|
+
const directSymbolId = scopes.symbolFor(strippedExpression)?.id;
|
|
6864
|
+
const directIndex = directSymbolId === void 0 ? -1 : parameterSymbolIds.indexOf(directSymbolId);
|
|
6865
|
+
if (directIndex !== -1) return directIndex;
|
|
6866
|
+
const initializer = immutableInitializer(strippedExpression);
|
|
6867
|
+
return initializer ? parameterIndex(initializer, parameterSymbolIds, scopes, canUnwrapSerialization, visitedNodes) : null;
|
|
6868
|
+
}
|
|
6869
|
+
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") {
|
|
6870
|
+
const serializedArgument = strippedExpression.arguments[0];
|
|
6871
|
+
return serializedArgument ? parameterIndex(serializedArgument, parameterSymbolIds, scopes, true, visitedNodes) : null;
|
|
6872
|
+
}
|
|
6873
|
+
return null;
|
|
6874
|
+
};
|
|
6875
|
+
const storageHelperSinkCache = /* @__PURE__ */ new WeakMap();
|
|
6876
|
+
const findStorageHelperSinks = (functionNode, scopes) => {
|
|
6877
|
+
const cachedSinks = storageHelperSinkCache.get(functionNode);
|
|
6878
|
+
if (cachedSinks) return cachedSinks;
|
|
6879
|
+
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) {
|
|
6880
|
+
storageHelperSinkCache.set(functionNode, []);
|
|
6881
|
+
return [];
|
|
6882
|
+
}
|
|
6883
|
+
const parameterSymbolIds = stripThisParameter(functionNode.params).map((parameter) => {
|
|
6884
|
+
const strippedParameter = stripParenExpression(parameter);
|
|
6885
|
+
const identifier = isNodeOfType(strippedParameter, "Identifier") ? strippedParameter : isNodeOfType(strippedParameter, "AssignmentPattern") && isNodeOfType(strippedParameter.left, "Identifier") ? strippedParameter.left : null;
|
|
6886
|
+
return identifier ? scopes.symbolFor(identifier)?.id ?? null : null;
|
|
6887
|
+
});
|
|
6888
|
+
const helperSinks = [];
|
|
6889
|
+
walkAst(functionNode.body, (child) => {
|
|
6890
|
+
if (child !== functionNode.body && isFunctionLike$1(child)) return false;
|
|
6891
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
6892
|
+
const callee = stripParenExpression(child.callee);
|
|
6893
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem" || !isWebStorageObject(callee.object)) return;
|
|
6894
|
+
const keyExpression = child.arguments[0];
|
|
6895
|
+
const valueExpression = child.arguments[1];
|
|
6896
|
+
if (!keyExpression || !valueExpression) return;
|
|
6897
|
+
const keyParameterIndex = parameterIndex(keyExpression, parameterSymbolIds, scopes, false);
|
|
6898
|
+
const valueParameterIndex = parameterIndex(valueExpression, parameterSymbolIds, scopes, true);
|
|
6899
|
+
if (keyParameterIndex === null || valueParameterIndex === null) return;
|
|
6900
|
+
helperSinks.push({
|
|
6901
|
+
keyParameterIndex,
|
|
6902
|
+
valueParameterIndex
|
|
6903
|
+
});
|
|
6904
|
+
});
|
|
6905
|
+
storageHelperSinkCache.set(functionNode, helperSinks);
|
|
6906
|
+
return helperSinks;
|
|
6907
|
+
};
|
|
6788
6908
|
const authTokenInWebStorage = defineRule({
|
|
6789
6909
|
id: "auth-token-in-web-storage",
|
|
6790
6910
|
title: "Auth token in web storage",
|
|
@@ -6792,14 +6912,23 @@ const authTokenInWebStorage = defineRule({
|
|
|
6792
6912
|
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
6913
|
create: skipNonProductionFiles((context) => ({
|
|
6794
6914
|
CallExpression(node) {
|
|
6795
|
-
const callee = node.callee;
|
|
6796
|
-
|
|
6797
|
-
if (!isNodeOfType(callee.property, "Identifier")
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
if (
|
|
6801
|
-
|
|
6802
|
-
|
|
6915
|
+
const callee = stripParenExpression(node.callee);
|
|
6916
|
+
const keyArguments = [];
|
|
6917
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "setItem" && isWebStorageObject(stripParenExpression(callee.object))) {
|
|
6918
|
+
const keyArgument = node.arguments[0];
|
|
6919
|
+
if (keyArgument) keyArguments.push(keyArgument);
|
|
6920
|
+
} else if (isNodeOfType(callee, "Identifier")) {
|
|
6921
|
+
const helperFunction = immutableInitializer(callee);
|
|
6922
|
+
const helperSinks = helperFunction ? findStorageHelperSinks(helperFunction, context.scopes) : [];
|
|
6923
|
+
for (const helperSink of helperSinks) {
|
|
6924
|
+
const keyArgument = node.arguments[helperSink.keyParameterIndex];
|
|
6925
|
+
if (keyArgument && node.arguments[helperSink.valueParameterIndex]) keyArguments.push(keyArgument);
|
|
6926
|
+
}
|
|
6927
|
+
}
|
|
6928
|
+
if (!keyArguments.some((keyArgument) => {
|
|
6929
|
+
const keyString = resolveStaticKeyString(keyArgument);
|
|
6930
|
+
return keyString !== null && isAuthCredentialKey(keyString);
|
|
6931
|
+
})) return;
|
|
6803
6932
|
context.report({
|
|
6804
6933
|
node,
|
|
6805
6934
|
message: MESSAGE$60
|
|
@@ -7047,9 +7176,6 @@ const isCreateElementCall = (node) => {
|
|
|
7047
7176
|
return false;
|
|
7048
7177
|
};
|
|
7049
7178
|
//#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
7179
|
//#region src/plugin/rules/react-builtins/button-has-type.ts
|
|
7054
7180
|
const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
|
|
7055
7181
|
const INVALID_MESSAGE = "This button has an invalid `type`, so the browser may treat it like a submit button.";
|
|
@@ -7511,6 +7637,7 @@ const areExpressionsStructurallyEqual = (a, b) => {
|
|
|
7511
7637
|
if (a.type !== b.type) return false;
|
|
7512
7638
|
if (isNodeOfType(a, "ThisExpression")) return true;
|
|
7513
7639
|
if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
|
|
7640
|
+
if (isNodeOfType(a, "PrivateIdentifier") && isNodeOfType(b, "PrivateIdentifier")) return a.name === b.name;
|
|
7514
7641
|
if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
|
|
7515
7642
|
if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
|
|
7516
7643
|
if (a.computed !== b.computed) return false;
|
|
@@ -8566,17 +8693,6 @@ const createMethodMutationAnalysis = (context) => {
|
|
|
8566
8693
|
//#region src/plugin/utils/is-member-property.ts
|
|
8567
8694
|
const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === propertyName);
|
|
8568
8695
|
//#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
8696
|
//#region src/plugin/utils/statement-always-exits.ts
|
|
8581
8697
|
const statementAlwaysExits = (statement) => {
|
|
8582
8698
|
if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
|
|
@@ -8843,7 +8959,7 @@ const isReactNamespaceImport = (identifier, scopes) => {
|
|
|
8843
8959
|
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
8844
8960
|
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
8845
8961
|
};
|
|
8846
|
-
const isReactNamespaceReceiver = (receiver, scopes, options) => {
|
|
8962
|
+
const isReactNamespaceReceiver$1 = (receiver, scopes, options) => {
|
|
8847
8963
|
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
8848
8964
|
if (isReactNamespaceImport(receiver, scopes)) return true;
|
|
8849
8965
|
return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
|
|
@@ -8856,7 +8972,7 @@ const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) =>
|
|
|
8856
8972
|
for (const property of pattern.properties) {
|
|
8857
8973
|
if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
|
|
8858
8974
|
const propertyName = getStaticPropertyKeyName(property);
|
|
8859
|
-
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver(stripParenExpression(symbol.initializer), scopes, options));
|
|
8975
|
+
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver$1(stripParenExpression(symbol.initializer), scopes, options));
|
|
8860
8976
|
}
|
|
8861
8977
|
return false;
|
|
8862
8978
|
};
|
|
@@ -8880,7 +8996,7 @@ const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds
|
|
|
8880
8996
|
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
8881
8997
|
}
|
|
8882
8998
|
if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
|
|
8883
|
-
return isReactNamespaceReceiver(stripParenExpression(callee.object), scopes, options);
|
|
8999
|
+
return isReactNamespaceReceiver$1(stripParenExpression(callee.object), scopes, options);
|
|
8884
9000
|
};
|
|
8885
9001
|
//#endregion
|
|
8886
9002
|
//#region src/plugin/utils/is-proven-browser-api-receiver.ts
|
|
@@ -14751,6 +14867,29 @@ const getReleaseVerbName = (node) => {
|
|
|
14751
14867
|
}
|
|
14752
14868
|
return null;
|
|
14753
14869
|
};
|
|
14870
|
+
const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) => {
|
|
14871
|
+
const releaseFunction = findEnclosingFunction$1(releaseReceiver);
|
|
14872
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14873
|
+
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !hasReactRefCurrentOrigin(releaseReceiver, context.scopes)) return false;
|
|
14874
|
+
const controllerKey = getListenerAbortControllerKey(usage, context);
|
|
14875
|
+
const refCurrentKey = resolveExpressionKey(releaseReceiver, context);
|
|
14876
|
+
if (controllerKey === null || refCurrentKey === null) return false;
|
|
14877
|
+
const usageFunctionBody = usageFunction.body;
|
|
14878
|
+
const previousAbortCalls = [];
|
|
14879
|
+
const ownershipAssignments = [];
|
|
14880
|
+
walkAst(usageFunctionBody, (child) => {
|
|
14881
|
+
if (child !== usageFunctionBody && isFunctionLike$1(child)) return false;
|
|
14882
|
+
if (isNodeOfType(child, "AssignmentExpression") && resolveExpressionKey(child.left, context) === refCurrentKey && resolveExpressionKey(child.right, context) === controllerKey) {
|
|
14883
|
+
ownershipAssignments.push(child);
|
|
14884
|
+
return;
|
|
14885
|
+
}
|
|
14886
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
14887
|
+
const childCallee = isNodeOfType(child.callee, "ChainExpression") ? child.callee.expression : stripParenExpression(child.callee);
|
|
14888
|
+
if (isNodeOfType(childCallee, "MemberExpression") && !childCallee.computed && isNodeOfType(childCallee.property, "Identifier") && childCallee.property.name === "abort" && resolveExpressionKey(childCallee.object, context) === refCurrentKey) previousAbortCalls.push(child);
|
|
14889
|
+
});
|
|
14890
|
+
const safeOwnershipAssignments = ownershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, previousAbortCalls, usageFunction, context));
|
|
14891
|
+
return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
|
|
14892
|
+
};
|
|
14754
14893
|
const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
14755
14894
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
14756
14895
|
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
@@ -14770,6 +14909,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14770
14909
|
if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
14771
14910
|
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
14911
|
if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
|
|
14912
|
+
if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
|
|
14773
14913
|
if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
|
|
14774
14914
|
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
14775
14915
|
if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
|
|
@@ -14803,9 +14943,11 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14803
14943
|
return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
|
|
14804
14944
|
}
|
|
14805
14945
|
if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
|
|
14806
|
-
const
|
|
14946
|
+
const usesUnaryListenerSignature = usage.registrationVerbName === "addListener" && isNodeOfType(usage.node, "CallExpression") && usage.node.arguments?.length === 1 && callNode.arguments?.length === 1;
|
|
14947
|
+
const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
|
|
14807
14948
|
if (!releaseHandler) return releaseVerbName === "off";
|
|
14808
|
-
|
|
14949
|
+
const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
|
|
14950
|
+
return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey;
|
|
14809
14951
|
}
|
|
14810
14952
|
if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
|
|
14811
14953
|
return true;
|
|
@@ -14818,8 +14960,7 @@ const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
|
14818
14960
|
currentNode = parentNode;
|
|
14819
14961
|
parentNode = currentNode.parent;
|
|
14820
14962
|
}
|
|
14821
|
-
|
|
14822
|
-
const effectCallback = findEnclosingFunction$1(parentNode);
|
|
14963
|
+
const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
|
|
14823
14964
|
const effectCall = effectCallback?.parent;
|
|
14824
14965
|
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
|
|
14825
14966
|
};
|
|
@@ -15105,6 +15246,11 @@ const doesResourceResultEscape = (resourceNode, allowReturnedResourceEscape, all
|
|
|
15105
15246
|
parentNode = currentNode.parent;
|
|
15106
15247
|
continue;
|
|
15107
15248
|
}
|
|
15249
|
+
if (isNodeOfType(parentNode, "ConditionalExpression") && (parentNode.consequent === currentNode || parentNode.alternate === currentNode) || isNodeOfType(parentNode, "LogicalExpression") && (parentNode.right === currentNode || parentNode.left === currentNode && parentNode.operator !== "&&")) {
|
|
15250
|
+
currentNode = parentNode;
|
|
15251
|
+
parentNode = currentNode.parent;
|
|
15252
|
+
continue;
|
|
15253
|
+
}
|
|
15108
15254
|
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode && isNodeOfType(parentNode.id, "Identifier") && isNodeOfType(parentNode.parent, "VariableDeclaration") && parentNode.parent.kind === "const") {
|
|
15109
15255
|
const ownerFunction = findEnclosingFunction$1(resourceNode);
|
|
15110
15256
|
const resourceSymbol = context.scopes.symbolFor(parentNode.id);
|
|
@@ -29276,7 +29422,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
29276
29422
|
//#endregion
|
|
29277
29423
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
29278
29424
|
const MESSAGE$39 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
29279
|
-
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
29425
|
+
const isUndefinedIdentifier$1 = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
29280
29426
|
const noAccessKey = defineRule({
|
|
29281
29427
|
id: "no-access-key",
|
|
29282
29428
|
title: "accessKey attribute used",
|
|
@@ -29301,7 +29447,7 @@ const noAccessKey = defineRule({
|
|
|
29301
29447
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
29302
29448
|
const expression = attributeValue.expression;
|
|
29303
29449
|
if (!expression || expression.type === "JSXEmptyExpression") return;
|
|
29304
|
-
if (isUndefinedIdentifier(expression)) return;
|
|
29450
|
+
if (isUndefinedIdentifier$1(expression)) return;
|
|
29305
29451
|
context.report({
|
|
29306
29452
|
node: accessKey,
|
|
29307
29453
|
message: MESSAGE$39
|
|
@@ -30066,6 +30212,12 @@ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.som
|
|
|
30066
30212
|
const importDeclaration = declarationNode.parent;
|
|
30067
30213
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
30068
30214
|
}));
|
|
30215
|
+
const isReactNamespaceReceiver = (analysis, node) => {
|
|
30216
|
+
const receiver = stripParenExpression(node);
|
|
30217
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
30218
|
+
const namespaceReference = getRef(analysis, receiver);
|
|
30219
|
+
return namespaceReference?.resolved ? isReactNamespaceImportReference(namespaceReference) : receiver.name === "React";
|
|
30220
|
+
};
|
|
30069
30221
|
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
30070
30222
|
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
30071
30223
|
const callee = stripParenExpression(declarator.init.callee);
|
|
@@ -30074,24 +30226,20 @@ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
|
30074
30226
|
if (!reference?.resolved) return callee.name === hookName;
|
|
30075
30227
|
return isReactNamedImportReference(reference, hookName);
|
|
30076
30228
|
}
|
|
30077
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.
|
|
30078
|
-
|
|
30079
|
-
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
30080
|
-
return isReactNamespaceImportReference(namespaceReference);
|
|
30229
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
30230
|
+
return isReactNamespaceReceiver(analysis, callee.object);
|
|
30081
30231
|
};
|
|
30082
30232
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
30083
30233
|
if (!node) return false;
|
|
30084
30234
|
if (isNodeOfType(node, "Identifier")) {
|
|
30085
30235
|
if (node.name === hookName) return true;
|
|
30086
30236
|
if (isReactNamedImportReference(getRef(analysis, node), hookName)) return true;
|
|
30087
|
-
const
|
|
30088
|
-
|
|
30237
|
+
const receiverRoot = findTransparentExpressionRoot(node);
|
|
30238
|
+
const parent = receiverRoot.parent;
|
|
30239
|
+
if (parent && isNodeOfType(parent, "MemberExpression") && parent.object === receiverRoot && isReactNamespaceReceiver(analysis, node) && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
30089
30240
|
return false;
|
|
30090
30241
|
}
|
|
30091
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
30092
|
-
const receiver = stripParenExpression(node.object);
|
|
30093
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30094
|
-
}
|
|
30242
|
+
if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30095
30243
|
return false;
|
|
30096
30244
|
};
|
|
30097
30245
|
const isUseEffect = (node) => {
|
|
@@ -30515,7 +30663,88 @@ const isIndependentWriterIdentifier = (componentFunction, identifier, includeDef
|
|
|
30515
30663
|
if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
|
|
30516
30664
|
return isSetterWiredToJsxHandler(componentFunction, bindingName);
|
|
30517
30665
|
};
|
|
30518
|
-
const
|
|
30666
|
+
const isSynchronousFunction = (functionNode) => {
|
|
30667
|
+
const functionMetadata = functionNode;
|
|
30668
|
+
return functionMetadata.async !== true && functionMetadata.generator !== true;
|
|
30669
|
+
};
|
|
30670
|
+
const findBindingVariable = (analysis, bindingIdentifier) => {
|
|
30671
|
+
for (const scope of analysis.scopeManager.scopes) for (const variable of scope.variables) if (variable.identifiers.includes(bindingIdentifier)) return variable;
|
|
30672
|
+
return null;
|
|
30673
|
+
};
|
|
30674
|
+
const getImmutableFunctionVariable = (analysis, componentFunction, functionNode) => {
|
|
30675
|
+
if (!isSynchronousFunction(functionNode) || !isAstDescendant(functionNode, componentFunction)) return null;
|
|
30676
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
30677
|
+
if (!bindingIdentifier) return null;
|
|
30678
|
+
const variable = findBindingVariable(analysis, bindingIdentifier);
|
|
30679
|
+
if (!variable || variable.defs.length !== 1 || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
|
|
30680
|
+
const definition = variable.defs[0];
|
|
30681
|
+
if (definition.type === "FunctionName") return definition.node === functionNode ? variable : null;
|
|
30682
|
+
if (definition.type !== "Variable") return null;
|
|
30683
|
+
const declarator = definition.node;
|
|
30684
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return null;
|
|
30685
|
+
if (declarator.init === functionNode) return variable;
|
|
30686
|
+
if (isNodeOfType(declarator.init, "CallExpression") && declarator.init.arguments?.[0] === functionNode && isGenuineReactHookDeclarator(analysis, declarator, "useCallback")) return variable;
|
|
30687
|
+
return null;
|
|
30688
|
+
};
|
|
30689
|
+
const getJsxEventValueAttribute = (identifier) => {
|
|
30690
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
30691
|
+
const expressionContainer = expression.parent;
|
|
30692
|
+
if (!isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== expression) return null;
|
|
30693
|
+
const attribute = expressionContainer.parent;
|
|
30694
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) return null;
|
|
30695
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
30696
|
+
return attributeName && isEventHandlerName(attributeName) ? attribute : null;
|
|
30697
|
+
};
|
|
30698
|
+
const getInlineJsxEventCallbackAttribute = (callExpression) => {
|
|
30699
|
+
const callbackFunction = findEnclosingFunction$1(callExpression);
|
|
30700
|
+
if (!callbackFunction || !isSynchronousFunction(callbackFunction)) return null;
|
|
30701
|
+
return getJsxEventValueAttribute(callbackFunction);
|
|
30702
|
+
};
|
|
30703
|
+
const isReactHookDependencyReference = (identifier) => {
|
|
30704
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
30705
|
+
const dependencyArray = expression.parent;
|
|
30706
|
+
if (!isNodeOfType(dependencyArray, "ArrayExpression") || !(dependencyArray.elements ?? []).includes(expression)) return false;
|
|
30707
|
+
const hookCall = dependencyArray.parent;
|
|
30708
|
+
if (!isNodeOfType(hookCall, "CallExpression") || hookCall.arguments?.[1] !== dependencyArray) return false;
|
|
30709
|
+
const callee = hookCall.callee;
|
|
30710
|
+
if (isNodeOfType(callee, "Identifier")) return /^use[A-Z0-9]/.test(callee.name);
|
|
30711
|
+
return Boolean(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && /^use[A-Z0-9]/.test(callee.property.name));
|
|
30712
|
+
};
|
|
30713
|
+
const hasReachableJsxEventCallPath = (analysis, context, componentFunction, functionVariable, visitedVariables) => {
|
|
30714
|
+
if (visitedVariables.has(functionVariable)) return false;
|
|
30715
|
+
const nextVisitedVariables = new Set(visitedVariables).add(functionVariable);
|
|
30716
|
+
const callExpressions = [];
|
|
30717
|
+
let hasDirectJsxEventReference = false;
|
|
30718
|
+
for (const reference of functionVariable.references) {
|
|
30719
|
+
if (reference.init) continue;
|
|
30720
|
+
const identifier = reference.identifier;
|
|
30721
|
+
if (reference.isWrite()) return false;
|
|
30722
|
+
const jsxEventValueAttribute = getJsxEventValueAttribute(identifier);
|
|
30723
|
+
if (jsxEventValueAttribute) {
|
|
30724
|
+
if (isNodeReachableWithinFunction(jsxEventValueAttribute, context)) hasDirectJsxEventReference = true;
|
|
30725
|
+
continue;
|
|
30726
|
+
}
|
|
30727
|
+
if (isReactHookDependencyReference(identifier)) continue;
|
|
30728
|
+
const callExpression = getCallExpr(reference);
|
|
30729
|
+
if (!callExpression) return false;
|
|
30730
|
+
const jsxEventCallbackAttribute = getInlineJsxEventCallbackAttribute(callExpression);
|
|
30731
|
+
if (jsxEventCallbackAttribute) {
|
|
30732
|
+
if (isNodeReachableWithinFunction(callExpression, context) && isNodeReachableWithinFunction(jsxEventCallbackAttribute, context)) hasDirectJsxEventReference = true;
|
|
30733
|
+
continue;
|
|
30734
|
+
}
|
|
30735
|
+
callExpressions.push(callExpression);
|
|
30736
|
+
}
|
|
30737
|
+
if (hasDirectJsxEventReference) return true;
|
|
30738
|
+
for (const callExpression of callExpressions) {
|
|
30739
|
+
if (!isNodeReachableWithinFunction(callExpression, context)) continue;
|
|
30740
|
+
const callerFunction = findEnclosingFunction$1(callExpression);
|
|
30741
|
+
if (!callerFunction || callerFunction === componentFunction) continue;
|
|
30742
|
+
const callerVariable = getImmutableFunctionVariable(analysis, componentFunction, callerFunction);
|
|
30743
|
+
if (callerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, callerVariable, nextVisitedVariables)) return true;
|
|
30744
|
+
}
|
|
30745
|
+
return false;
|
|
30746
|
+
};
|
|
30747
|
+
const hasUserInputSetterWriter = (analysis, context, setterRef, effectNode, includeDeferredWriters = false) => {
|
|
30519
30748
|
if (!setterRef.resolved) return false;
|
|
30520
30749
|
const componentFunction = findEnclosingFunction$1(effectNode);
|
|
30521
30750
|
if (!componentFunction) return false;
|
|
@@ -30524,6 +30753,11 @@ const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters
|
|
|
30524
30753
|
const identifier = reference.identifier;
|
|
30525
30754
|
if (isAstDescendant(identifier, effectNode)) continue;
|
|
30526
30755
|
if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
|
|
30756
|
+
if (!isNodeReachableWithinFunction(identifier, context)) continue;
|
|
30757
|
+
const writerFunction = findEnclosingFunction$1(identifier);
|
|
30758
|
+
if (!writerFunction || writerFunction === componentFunction) continue;
|
|
30759
|
+
const writerVariable = getImmutableFunctionVariable(analysis, componentFunction, writerFunction);
|
|
30760
|
+
if (writerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, writerVariable, /* @__PURE__ */ new Set())) return true;
|
|
30527
30761
|
}
|
|
30528
30762
|
return false;
|
|
30529
30763
|
};
|
|
@@ -31413,7 +31647,7 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
|
|
|
31413
31647
|
}
|
|
31414
31648
|
return false;
|
|
31415
31649
|
};
|
|
31416
|
-
const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
|
|
31650
|
+
const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFilename) => {
|
|
31417
31651
|
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
|
|
31418
31652
|
if (frames.length === 0) return [];
|
|
31419
31653
|
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
@@ -31443,7 +31677,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
|
|
|
31443
31677
|
for (const returnedExpression of returnedExpressions) mergeEvidence(valueEvidence, collectValueEvidence(analysis, returnedExpression, updaterFrame, remainingValueCallFrames));
|
|
31444
31678
|
} else valueEvidence = collectValueEvidence(analysis, writtenValue, frame, remainingValueCallFrames);
|
|
31445
31679
|
const sourceReferences = [...valueEvidence.sourceReferences].filter((sourceReference) => getUseStateDecl(analysis, sourceReference) !== stateDeclarator);
|
|
31446
|
-
const hasIndependentWriter = hasUserInputSetterWriter(setterReference, effectNode, true);
|
|
31680
|
+
const hasIndependentWriter = hasUserInputSetterWriter(analysis, context, setterReference, effectNode, true);
|
|
31447
31681
|
const doesMatchStateInitializer = matchesStateInitializer(analysis, callExpression, stateDeclarator);
|
|
31448
31682
|
if (effectHasCleanup && (frame.isDeferred || valueEvidence.hasUnknownSource || valueEvidence.hasDeferredIntroducedValue || valueEvidence.readsExternalValue)) cleanupManagedStateDeclarators.add(stateDeclarator);
|
|
31449
31683
|
const isRenderKnownCopy = sourceReferences.length > 0 && !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue && !hasIndependentWriter;
|
|
@@ -31484,7 +31718,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
31484
31718
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
31485
31719
|
if (!dependencyReferences) return;
|
|
31486
31720
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
31487
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
31721
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
31488
31722
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
31489
31723
|
context.report({
|
|
31490
31724
|
node: fact.callExpression,
|
|
@@ -34171,6 +34405,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34171
34405
|
id: "no-chain-state-updates",
|
|
34172
34406
|
title: "State updates chained through effects",
|
|
34173
34407
|
severity: "warn",
|
|
34408
|
+
disabledWhen: ["react:18"],
|
|
34174
34409
|
tags: ["test-noise"],
|
|
34175
34410
|
recommendation: "Set all the related state together in the event handler that starts it, instead of having one useEffect react to a state change and set more state. See https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations",
|
|
34176
34411
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -36057,7 +36292,7 @@ const noDerivedState = defineRule({
|
|
|
36057
36292
|
if (!isUseEffect(node)) return;
|
|
36058
36293
|
const analysis = getProgramAnalysis(node);
|
|
36059
36294
|
if (!analysis) return;
|
|
36060
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
36295
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
36061
36296
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
36062
36297
|
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
36063
36298
|
}
|
|
@@ -36077,7 +36312,7 @@ const noDerivedStateEffect = defineRule({
|
|
|
36077
36312
|
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
36078
36313
|
const analysis = getProgramAnalysis(node);
|
|
36079
36314
|
if (!analysis) return;
|
|
36080
|
-
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
36315
|
+
if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
36081
36316
|
context.report({
|
|
36082
36317
|
node,
|
|
36083
36318
|
message: "You pay an extra render for state you can derive from other values."
|
|
@@ -36553,9 +36788,20 @@ const noDidMountSetState = defineRule({
|
|
|
36553
36788
|
}
|
|
36554
36789
|
});
|
|
36555
36790
|
//#endregion
|
|
36791
|
+
//#region src/plugin/utils/find-enclosing-class.ts
|
|
36792
|
+
const findEnclosingClass = (node) => {
|
|
36793
|
+
let ancestor = node.parent;
|
|
36794
|
+
while (ancestor) {
|
|
36795
|
+
if (isNodeOfType(ancestor, "ClassDeclaration") || isNodeOfType(ancestor, "ClassExpression")) return ancestor;
|
|
36796
|
+
ancestor = ancestor.parent ?? null;
|
|
36797
|
+
}
|
|
36798
|
+
return null;
|
|
36799
|
+
};
|
|
36800
|
+
//#endregion
|
|
36556
36801
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
36557
36802
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
36558
36803
|
const MESSAGE$27 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
36804
|
+
const DIFFERENCE_OPERATORS = new Set(["!=", "!=="]);
|
|
36559
36805
|
const EQUALITY_OPERATORS = new Set([
|
|
36560
36806
|
"==",
|
|
36561
36807
|
"===",
|
|
@@ -36567,6 +36813,8 @@ const FUNCTION_NODE_TYPES = new Set([
|
|
|
36567
36813
|
"FunctionExpression",
|
|
36568
36814
|
"ArrowFunctionExpression"
|
|
36569
36815
|
]);
|
|
36816
|
+
const CLASS_NODE_TYPES = new Set(["ClassDeclaration", "ClassExpression"]);
|
|
36817
|
+
const callbackRefFieldNamesByClass = /* @__PURE__ */ new WeakMap();
|
|
36570
36818
|
const isLifecycleMethodFunction = (node) => {
|
|
36571
36819
|
if (!FUNCTION_NODE_TYPES.has(node.type)) return false;
|
|
36572
36820
|
const parent = node.parent;
|
|
@@ -36622,6 +36870,187 @@ const getStaticMemberName = (node) => {
|
|
|
36622
36870
|
if (!isNodeOfType(node, "MemberExpression") || node.computed === true) return null;
|
|
36623
36871
|
return isNodeOfType(node.property, "Identifier") ? node.property.name : null;
|
|
36624
36872
|
};
|
|
36873
|
+
const getMemberIdentity = (property) => {
|
|
36874
|
+
const propertyName = getPropertyKeyName$2(property);
|
|
36875
|
+
if (propertyName !== void 0) return isNodeOfType(property, "PrivateIdentifier") ? `#${propertyName}` : propertyName;
|
|
36876
|
+
return isNodeOfType(property, "Literal") && typeof property.value === "string" ? property.value : null;
|
|
36877
|
+
};
|
|
36878
|
+
const collectPreviousSourcePaths = (pattern, domain, members, previousSourcePaths) => {
|
|
36879
|
+
if (!pattern) return;
|
|
36880
|
+
const unwrappedPattern = stripParenExpression(pattern);
|
|
36881
|
+
if (isNodeOfType(unwrappedPattern, "Identifier")) {
|
|
36882
|
+
previousSourcePaths.set(unwrappedPattern.name, {
|
|
36883
|
+
domain,
|
|
36884
|
+
members: [...members],
|
|
36885
|
+
source: "previous"
|
|
36886
|
+
});
|
|
36887
|
+
return;
|
|
36888
|
+
}
|
|
36889
|
+
if (isNodeOfType(unwrappedPattern, "AssignmentPattern")) {
|
|
36890
|
+
collectPreviousSourcePaths(unwrappedPattern.left, domain, members, previousSourcePaths);
|
|
36891
|
+
return;
|
|
36892
|
+
}
|
|
36893
|
+
if (!isNodeOfType(unwrappedPattern, "ObjectPattern")) return;
|
|
36894
|
+
for (const property of unwrappedPattern.properties) {
|
|
36895
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
36896
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
36897
|
+
if (!propertyName) continue;
|
|
36898
|
+
collectPreviousSourcePaths(property.value, domain, [...members, propertyName], previousSourcePaths);
|
|
36899
|
+
}
|
|
36900
|
+
};
|
|
36901
|
+
const getStateSourcePath = (node, previousSourcePaths) => {
|
|
36902
|
+
let currentNode = stripParenExpression(node);
|
|
36903
|
+
const members = [];
|
|
36904
|
+
while (isNodeOfType(currentNode, "MemberExpression")) {
|
|
36905
|
+
const memberName = getStaticMemberName(currentNode);
|
|
36906
|
+
if (!memberName) return null;
|
|
36907
|
+
members.unshift(memberName);
|
|
36908
|
+
currentNode = stripParenExpression(currentNode.object);
|
|
36909
|
+
}
|
|
36910
|
+
if (isNodeOfType(currentNode, "ThisExpression")) {
|
|
36911
|
+
const [domain, ...pathMembers] = members;
|
|
36912
|
+
if (domain !== "props" && domain !== "state") return null;
|
|
36913
|
+
return {
|
|
36914
|
+
domain,
|
|
36915
|
+
members: pathMembers,
|
|
36916
|
+
source: "current"
|
|
36917
|
+
};
|
|
36918
|
+
}
|
|
36919
|
+
if (!isNodeOfType(currentNode, "Identifier")) return null;
|
|
36920
|
+
const previousSourcePath = previousSourcePaths.get(currentNode.name);
|
|
36921
|
+
return previousSourcePath ? {
|
|
36922
|
+
...previousSourcePath,
|
|
36923
|
+
members: [...previousSourcePath.members, ...members]
|
|
36924
|
+
} : null;
|
|
36925
|
+
};
|
|
36926
|
+
const haveMatchingStateSourcePaths = (left, right) => left.domain === right.domain && left.members.length === right.members.length && left.members.every((member, index) => member === right.members[index]);
|
|
36927
|
+
const collectConjunctiveStateSourceComparisons = (test, previousSourcePaths, comparisons) => {
|
|
36928
|
+
const expression = stripParenExpression(test);
|
|
36929
|
+
if (isNodeOfType(expression, "LogicalExpression") && expression.operator === "&&") {
|
|
36930
|
+
collectConjunctiveStateSourceComparisons(expression.left, previousSourcePaths, comparisons);
|
|
36931
|
+
collectConjunctiveStateSourceComparisons(expression.right, previousSourcePaths, comparisons);
|
|
36932
|
+
return;
|
|
36933
|
+
}
|
|
36934
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !EQUALITY_OPERATORS.has(expression.operator)) return;
|
|
36935
|
+
const leftPath = getStateSourcePath(expression.left, previousSourcePaths);
|
|
36936
|
+
const rightPath = getStateSourcePath(expression.right, previousSourcePaths);
|
|
36937
|
+
if (Boolean(leftPath) === Boolean(rightPath)) return;
|
|
36938
|
+
const path = leftPath ?? rightPath;
|
|
36939
|
+
if (!path) return;
|
|
36940
|
+
comparisons.push({
|
|
36941
|
+
comparedValue: leftPath ? expression.right : expression.left,
|
|
36942
|
+
isDifference: DIFFERENCE_OPERATORS.has(expression.operator),
|
|
36943
|
+
path
|
|
36944
|
+
});
|
|
36945
|
+
};
|
|
36946
|
+
const isHistoricalToCurrentTransitionGuard = (test, previousSourcePaths) => {
|
|
36947
|
+
const expression = stripParenExpression(test);
|
|
36948
|
+
if (isNodeOfType(expression, "LogicalExpression") && expression.operator === "||") return isHistoricalToCurrentTransitionGuard(expression.left, previousSourcePaths) && isHistoricalToCurrentTransitionGuard(expression.right, previousSourcePaths);
|
|
36949
|
+
const comparisons = [];
|
|
36950
|
+
collectConjunctiveStateSourceComparisons(expression, previousSourcePaths, comparisons);
|
|
36951
|
+
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)));
|
|
36952
|
+
};
|
|
36953
|
+
const getThisFieldName = (node) => {
|
|
36954
|
+
const unwrappedNode = stripParenExpression(node);
|
|
36955
|
+
if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed === true || !isNodeOfType(stripParenExpression(unwrappedNode.object), "ThisExpression")) return null;
|
|
36956
|
+
return getMemberIdentity(unwrappedNode.property);
|
|
36957
|
+
};
|
|
36958
|
+
const isUndefinedIdentifier = (node) => {
|
|
36959
|
+
const unwrappedNode = stripParenExpression(node);
|
|
36960
|
+
return isNodeOfType(unwrappedNode, "Identifier") && unwrappedNode.name === "undefined";
|
|
36961
|
+
};
|
|
36962
|
+
const isDirectRefParameterValue = (node, parameterSymbolId, scopes) => {
|
|
36963
|
+
const unwrappedNode = stripParenExpression(node);
|
|
36964
|
+
if (isNodeOfType(unwrappedNode, "Identifier")) return scopes.symbolFor(unwrappedNode)?.id === parameterSymbolId;
|
|
36965
|
+
if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "??") return false;
|
|
36966
|
+
const left = stripParenExpression(unwrappedNode.left);
|
|
36967
|
+
return isNodeOfType(left, "Identifier") && scopes.symbolFor(left)?.id === parameterSymbolId && isUndefinedIdentifier(unwrappedNode.right);
|
|
36968
|
+
};
|
|
36969
|
+
const getCallbackRefAssignedFields = (callback, scopes) => {
|
|
36970
|
+
const firstParameter = (callback.params ?? [])[0];
|
|
36971
|
+
if (!firstParameter) return /* @__PURE__ */ new Set();
|
|
36972
|
+
const parameterIdentifier = isNodeOfType(firstParameter, "AssignmentPattern") ? firstParameter.left : firstParameter;
|
|
36973
|
+
if (!isNodeOfType(parameterIdentifier, "Identifier")) return /* @__PURE__ */ new Set();
|
|
36974
|
+
const parameterSymbolId = scopes.symbolFor(parameterIdentifier)?.id;
|
|
36975
|
+
if (parameterSymbolId === void 0) return /* @__PURE__ */ new Set();
|
|
36976
|
+
const body = callback.body;
|
|
36977
|
+
if (!body) return /* @__PURE__ */ new Set();
|
|
36978
|
+
const assignedFieldNames = /* @__PURE__ */ new Set();
|
|
36979
|
+
walkAst(body, (node) => {
|
|
36980
|
+
if (node !== body && (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node) || CLASS_NODE_TYPES.has(node.type))) return false;
|
|
36981
|
+
const assignmentTarget = isNodeOfType(node, "AssignmentExpression") && node.left || isNodeOfType(node, "UpdateExpression") && node.argument || isNodeOfType(node, "UnaryExpression") && node.operator === "delete" && node.argument || null;
|
|
36982
|
+
if (!assignmentTarget) return;
|
|
36983
|
+
const fieldName = getThisFieldName(assignmentTarget);
|
|
36984
|
+
if (!fieldName) return;
|
|
36985
|
+
if (isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isDirectRefParameterValue(node.right, parameterSymbolId, scopes)) {
|
|
36986
|
+
assignedFieldNames.add(fieldName);
|
|
36987
|
+
return;
|
|
36988
|
+
}
|
|
36989
|
+
assignedFieldNames.delete(fieldName);
|
|
36990
|
+
});
|
|
36991
|
+
return assignedFieldNames;
|
|
36992
|
+
};
|
|
36993
|
+
const getClassMemberCallback = (classNode, memberName) => {
|
|
36994
|
+
const classBody = classNode.body?.body ?? [];
|
|
36995
|
+
for (const member of classBody) {
|
|
36996
|
+
if (!isNodeOfType(member, "MethodDefinition") && !isNodeOfType(member, "PropertyDefinition")) continue;
|
|
36997
|
+
if (member.static === true) continue;
|
|
36998
|
+
const key = member.key;
|
|
36999
|
+
if (getMemberIdentity(key) !== memberName) continue;
|
|
37000
|
+
const value = member.value;
|
|
37001
|
+
return value && FUNCTION_NODE_TYPES.has(value.type) ? value : null;
|
|
37002
|
+
}
|
|
37003
|
+
return null;
|
|
37004
|
+
};
|
|
37005
|
+
const collectCallbackRefFieldsFromExpression = (expression, classNode, fieldNames, scopes) => {
|
|
37006
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
37007
|
+
if (FUNCTION_NODE_TYPES.has(unwrappedExpression.type)) {
|
|
37008
|
+
for (const fieldName of getCallbackRefAssignedFields(unwrappedExpression, scopes)) fieldNames.add(fieldName);
|
|
37009
|
+
return;
|
|
37010
|
+
}
|
|
37011
|
+
const handlerName = getThisFieldName(unwrappedExpression);
|
|
37012
|
+
if (handlerName) {
|
|
37013
|
+
const callback = getClassMemberCallback(classNode, handlerName);
|
|
37014
|
+
if (callback) for (const fieldName of getCallbackRefAssignedFields(callback, scopes)) fieldNames.add(fieldName);
|
|
37015
|
+
return;
|
|
37016
|
+
}
|
|
37017
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) {
|
|
37018
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.consequent, classNode, fieldNames, scopes);
|
|
37019
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.alternate, classNode, fieldNames, scopes);
|
|
37020
|
+
return;
|
|
37021
|
+
}
|
|
37022
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
37023
|
+
if (unwrappedExpression.operator !== "&&") collectCallbackRefFieldsFromExpression(unwrappedExpression.left, classNode, fieldNames, scopes);
|
|
37024
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.right, classNode, fieldNames, scopes);
|
|
37025
|
+
}
|
|
37026
|
+
};
|
|
37027
|
+
const getCallbackRefFieldNames = (classNode, scopes) => {
|
|
37028
|
+
if (!classNode) return /* @__PURE__ */ new Set();
|
|
37029
|
+
const cachedFieldNames = callbackRefFieldNamesByClass.get(classNode);
|
|
37030
|
+
if (cachedFieldNames) return cachedFieldNames;
|
|
37031
|
+
const fieldNames = /* @__PURE__ */ new Set();
|
|
37032
|
+
const classBody = classNode.body;
|
|
37033
|
+
if (classBody) walkAst(classBody, (node) => {
|
|
37034
|
+
if (node !== classBody && CLASS_NODE_TYPES.has(node.type)) return false;
|
|
37035
|
+
if (!isNodeOfType(node, "JSXAttribute") || !isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "ref" || !node.value || !isNodeOfType(node.value, "JSXExpressionContainer") || !node.value.expression) return;
|
|
37036
|
+
collectCallbackRefFieldsFromExpression(node.value.expression, classNode, fieldNames, scopes);
|
|
37037
|
+
});
|
|
37038
|
+
callbackRefFieldNamesByClass.set(classNode, fieldNames);
|
|
37039
|
+
return fieldNames;
|
|
37040
|
+
};
|
|
37041
|
+
const collectLifecycleWrittenFieldNames = (lifecycleFunction) => {
|
|
37042
|
+
const fieldNames = /* @__PURE__ */ new Set();
|
|
37043
|
+
const body = lifecycleFunction.body;
|
|
37044
|
+
if (!body) return fieldNames;
|
|
37045
|
+
walkAst(body, (node) => {
|
|
37046
|
+
if (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node)) return false;
|
|
37047
|
+
const target = isNodeOfType(node, "AssignmentExpression") && node.left || isNodeOfType(node, "UpdateExpression") && node.argument || null;
|
|
37048
|
+
if (!target) return;
|
|
37049
|
+
const fieldName = getThisFieldName(target);
|
|
37050
|
+
if (fieldName) fieldNames.add(fieldName);
|
|
37051
|
+
});
|
|
37052
|
+
return fieldNames;
|
|
37053
|
+
};
|
|
36625
37054
|
const getThisStateFieldName = (node) => {
|
|
36626
37055
|
const unwrappedNode = stripParenExpression(node);
|
|
36627
37056
|
if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
|
|
@@ -36639,15 +37068,17 @@ const collectLocalInitializers = (lifecycleFunction) => {
|
|
|
36639
37068
|
});
|
|
36640
37069
|
return initializers;
|
|
36641
37070
|
};
|
|
36642
|
-
const derivesFromPostMountValue = (node, localInitializers, visitedNames = /* @__PURE__ */ new Set()) => {
|
|
37071
|
+
const derivesFromPostMountValue = (node, localInitializers, callbackRefFieldNames, visitedNames = /* @__PURE__ */ new Set()) => {
|
|
36643
37072
|
if (readsPostMountValue(node)) return true;
|
|
37073
|
+
const fieldName = getThisFieldName(node);
|
|
37074
|
+
if (fieldName && callbackRefFieldNames.has(fieldName)) return true;
|
|
36644
37075
|
const referencedNames = /* @__PURE__ */ new Set();
|
|
36645
37076
|
collectReferenceIdentifierNames(node, referencedNames);
|
|
36646
37077
|
for (const referencedName of referencedNames) {
|
|
36647
37078
|
if (visitedNames.has(referencedName)) continue;
|
|
36648
37079
|
const initializer = localInitializers.get(referencedName);
|
|
36649
37080
|
if (!initializer) continue;
|
|
36650
|
-
if (derivesFromPostMountValue(initializer, localInitializers, new Set([...visitedNames, referencedName]))) return true;
|
|
37081
|
+
if (derivesFromPostMountValue(initializer, localInitializers, callbackRefFieldNames, new Set([...visitedNames, referencedName]))) return true;
|
|
36651
37082
|
}
|
|
36652
37083
|
return false;
|
|
36653
37084
|
};
|
|
@@ -36661,50 +37092,84 @@ const getSetStateFieldValue = (setStateCall, fieldName) => {
|
|
|
36661
37092
|
}
|
|
36662
37093
|
return null;
|
|
36663
37094
|
};
|
|
36664
|
-
const isConvergentPostMountGuard = (test, setStateCall, localInitializers) => {
|
|
36665
|
-
|
|
36666
|
-
|
|
36667
|
-
if (
|
|
36668
|
-
|
|
36669
|
-
const
|
|
36670
|
-
|
|
36671
|
-
|
|
36672
|
-
|
|
36673
|
-
|
|
36674
|
-
|
|
36675
|
-
|
|
36676
|
-
|
|
36677
|
-
|
|
36678
|
-
|
|
36679
|
-
|
|
36680
|
-
return
|
|
36681
|
-
};
|
|
36682
|
-
const
|
|
36683
|
-
|
|
36684
|
-
|
|
36685
|
-
|
|
36686
|
-
if (qualifies) return false;
|
|
36687
|
-
if (!isNodeOfType(node, "BinaryExpression")) return;
|
|
36688
|
-
if (!EQUALITY_OPERATORS.has(node.operator)) return;
|
|
36689
|
-
if (isStatefulOperand(node.left, paramNames, derivedNames) && isStatefulOperand(node.right, paramNames, derivedNames) && (referencesAnyName(node.left, derivedNames) || referencesAnyName(node.right, derivedNames))) {
|
|
36690
|
-
qualifies = true;
|
|
36691
|
-
return false;
|
|
36692
|
-
}
|
|
36693
|
-
});
|
|
36694
|
-
return qualifies;
|
|
37095
|
+
const isConvergentPostMountGuard = (test, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch) => {
|
|
37096
|
+
const expression = stripParenExpression(test);
|
|
37097
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
37098
|
+
if (expression.operator !== "&&" && expression.operator !== "||") return false;
|
|
37099
|
+
const leftIsConvergent = isConvergentPostMountGuard(expression.left, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch);
|
|
37100
|
+
const rightIsConvergent = isConvergentPostMountGuard(expression.right, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch);
|
|
37101
|
+
return isTruthfulBranch && expression.operator === "||" || !isTruthfulBranch && expression.operator === "&&" ? leftIsConvergent && rightIsConvergent : leftIsConvergent || rightIsConvergent;
|
|
37102
|
+
}
|
|
37103
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !(isTruthfulBranch ? DIFFERENCE_OPERATORS.has(expression.operator) : EQUALITY_OPERATORS.has(expression.operator) && !DIFFERENCE_OPERATORS.has(expression.operator))) return false;
|
|
37104
|
+
const leftFieldName = getThisStateFieldName(expression.left);
|
|
37105
|
+
const rightFieldName = getThisStateFieldName(expression.right);
|
|
37106
|
+
const fieldName = leftFieldName ?? rightFieldName;
|
|
37107
|
+
const comparedValue = leftFieldName ? expression.right : expression.left;
|
|
37108
|
+
if (!fieldName) return false;
|
|
37109
|
+
const assignedValue = getSetStateFieldValue(setStateCall, fieldName);
|
|
37110
|
+
if (!assignedValue || !areExpressionsStructurallyEqual(comparedValue, assignedValue)) return false;
|
|
37111
|
+
return isUndefinedIdentifier(comparedValue) || derivesFromPostMountValue(comparedValue, localInitializers, callbackRefFieldNames);
|
|
37112
|
+
};
|
|
37113
|
+
const containsPositiveStateFieldTest = (test, fieldName) => {
|
|
37114
|
+
const unwrappedTest = stripParenExpression(test);
|
|
37115
|
+
if (getThisStateFieldName(unwrappedTest) === fieldName) return true;
|
|
37116
|
+
return isNodeOfType(unwrappedTest, "LogicalExpression") && unwrappedTest.operator === "&&" && (containsPositiveStateFieldTest(unwrappedTest.left, fieldName) || containsPositiveStateFieldTest(unwrappedTest.right, fieldName));
|
|
36695
37117
|
};
|
|
36696
|
-
const
|
|
37118
|
+
const isConvergentUndefinedClearGuard = (test, setStateCall) => {
|
|
37119
|
+
if (!isNodeOfType(setStateCall, "CallExpression")) return false;
|
|
37120
|
+
const argument = setStateCall.arguments?.[0];
|
|
37121
|
+
if (!argument || !isNodeOfType(argument, "ObjectExpression")) return false;
|
|
37122
|
+
for (const property of argument.properties ?? []) {
|
|
37123
|
+
if (!isNodeOfType(property, "Property") || property.computed === true || !isUndefinedIdentifier(property.value)) continue;
|
|
37124
|
+
const fieldName = isNodeOfType(property.key, "Identifier") && property.key.name || isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" && property.key.value || null;
|
|
37125
|
+
if (fieldName && containsPositiveStateFieldTest(test, fieldName)) return true;
|
|
37126
|
+
}
|
|
37127
|
+
return false;
|
|
37128
|
+
};
|
|
37129
|
+
const isDiffGuardTest = (test, paramNames, derivedNames, isTruthfulBranch) => {
|
|
37130
|
+
const expression = stripParenExpression(test);
|
|
37131
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
37132
|
+
if (expression.operator !== "&&" && expression.operator !== "||") return false;
|
|
37133
|
+
const leftIsDiffGuard = isDiffGuardTest(expression.left, paramNames, derivedNames, isTruthfulBranch);
|
|
37134
|
+
const rightIsDiffGuard = isDiffGuardTest(expression.right, paramNames, derivedNames, isTruthfulBranch);
|
|
37135
|
+
return isTruthfulBranch && expression.operator === "||" || !isTruthfulBranch && expression.operator === "&&" ? leftIsDiffGuard && rightIsDiffGuard : leftIsDiffGuard || rightIsDiffGuard;
|
|
37136
|
+
}
|
|
37137
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !(isTruthfulBranch ? DIFFERENCE_OPERATORS.has(expression.operator) : EQUALITY_OPERATORS.has(expression.operator) && !DIFFERENCE_OPERATORS.has(expression.operator))) return false;
|
|
37138
|
+
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));
|
|
37139
|
+
};
|
|
37140
|
+
const isInsideDiffGuard = (setStateCall, scopes) => {
|
|
36697
37141
|
const lifecycleFunction = findEnclosingLifecycleFunction(setStateCall);
|
|
36698
37142
|
if (!lifecycleFunction) return false;
|
|
36699
37143
|
const paramNames = /* @__PURE__ */ new Set();
|
|
36700
|
-
|
|
37144
|
+
const parameters = lifecycleFunction.params ?? [];
|
|
37145
|
+
for (const param of parameters) collectPatternNames(param, paramNames);
|
|
37146
|
+
const previousSourcePaths = /* @__PURE__ */ new Map();
|
|
37147
|
+
const [previousPropsParameter, previousStateParameter] = parameters;
|
|
37148
|
+
collectPreviousSourcePaths(previousPropsParameter, "props", [], previousSourcePaths);
|
|
37149
|
+
collectPreviousSourcePaths(previousStateParameter, "state", [], previousSourcePaths);
|
|
36701
37150
|
const derivedNames = collectDiffSourceLocalNames(lifecycleFunction, paramNames);
|
|
36702
37151
|
const localInitializers = collectLocalInitializers(lifecycleFunction);
|
|
37152
|
+
const lifecycleWrittenFieldNames = collectLifecycleWrittenFieldNames(lifecycleFunction);
|
|
37153
|
+
const callbackRefFieldNames = new Set([...getCallbackRefFieldNames(findEnclosingClass(lifecycleFunction), scopes)].filter((fieldName) => !lifecycleWrittenFieldNames.has(fieldName)));
|
|
36703
37154
|
let child = setStateCall;
|
|
36704
37155
|
let ancestor = setStateCall.parent;
|
|
36705
37156
|
while (ancestor && ancestor !== lifecycleFunction) {
|
|
36706
|
-
|
|
36707
|
-
|
|
37157
|
+
let guardTest = null;
|
|
37158
|
+
let isTruthfulBranch = true;
|
|
37159
|
+
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
37160
|
+
if (child === ancestor.consequent) guardTest = ancestor.test;
|
|
37161
|
+
else if (child === ancestor.alternate) {
|
|
37162
|
+
guardTest = ancestor.test;
|
|
37163
|
+
isTruthfulBranch = false;
|
|
37164
|
+
}
|
|
37165
|
+
} else if (isNodeOfType(ancestor, "ConditionalExpression")) {
|
|
37166
|
+
if (child === ancestor.consequent) guardTest = ancestor.test;
|
|
37167
|
+
else if (child === ancestor.alternate) {
|
|
37168
|
+
guardTest = ancestor.test;
|
|
37169
|
+
isTruthfulBranch = false;
|
|
37170
|
+
}
|
|
37171
|
+
} else if (isNodeOfType(ancestor, "LogicalExpression") && ancestor.operator === "&&" && child === ancestor.right) guardTest = ancestor.left;
|
|
37172
|
+
if (guardTest && (isDiffGuardTest(guardTest, paramNames, derivedNames, isTruthfulBranch) || isTruthfulBranch && isHistoricalToCurrentTransitionGuard(guardTest, previousSourcePaths) || isConvergentPostMountGuard(guardTest, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch) || isTruthfulBranch && isConvergentUndefinedClearGuard(guardTest, setStateCall))) return true;
|
|
36708
37173
|
child = ancestor;
|
|
36709
37174
|
ancestor = ancestor.parent ?? null;
|
|
36710
37175
|
}
|
|
@@ -36726,7 +37191,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
36726
37191
|
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36727
37192
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36728
37193
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36729
|
-
if (isInsideDiffGuard(node)) return;
|
|
37194
|
+
if (isInsideDiffGuard(node, context.scopes)) return;
|
|
36730
37195
|
context.report({
|
|
36731
37196
|
node: node.callee,
|
|
36732
37197
|
message: MESSAGE$27
|
|
@@ -41017,7 +41482,7 @@ const noInitializeState = defineRule({
|
|
|
41017
41482
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
41018
41483
|
const analysis = getProgramAnalysis(node);
|
|
41019
41484
|
if (!analysis) return;
|
|
41020
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
41485
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
41021
41486
|
if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
|
|
41022
41487
|
const stateName = getStateName(fact.stateDeclarator);
|
|
41023
41488
|
context.report({
|
|
@@ -44424,6 +44889,114 @@ const DATA_SINK_METHOD_NAMES = new Set([
|
|
|
44424
44889
|
"deserialize"
|
|
44425
44890
|
]);
|
|
44426
44891
|
//#endregion
|
|
44892
|
+
//#region src/plugin/utils/get-transparent-react-callback-wrapper-argument.ts
|
|
44893
|
+
const getTransparentReactCallbackWrapperArgument = (initializer, resultSymbol, scopes) => {
|
|
44894
|
+
const callExpression = stripParenExpression(initializer);
|
|
44895
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
44896
|
+
const callbackArgument = callExpression.arguments[0];
|
|
44897
|
+
if (!callbackArgument) return null;
|
|
44898
|
+
if (resultSymbol && symbolHasReactUseEffectEventOrigin(resultSymbol, scopes)) return callbackArgument;
|
|
44899
|
+
return isReactApiCall(callExpression, "useCallback", scopes, {
|
|
44900
|
+
allowGlobalReactNamespace: true,
|
|
44901
|
+
allowUnboundBareCalls: true
|
|
44902
|
+
}) ? callbackArgument : null;
|
|
44903
|
+
};
|
|
44904
|
+
//#endregion
|
|
44905
|
+
//#region src/plugin/rules/state-and-effects/utils/resolve-parent-callback-provenance.ts
|
|
44906
|
+
const getDeclarationKind$1 = (declarator) => {
|
|
44907
|
+
const declaration = declarator.parent;
|
|
44908
|
+
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
44909
|
+
};
|
|
44910
|
+
const hasMutableBindingWrite$2 = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
44911
|
+
const mergeRequiredBranches = (leftNames, rightNames) => {
|
|
44912
|
+
if (!leftNames || !rightNames) return null;
|
|
44913
|
+
return new Set([...leftNames, ...rightNames]);
|
|
44914
|
+
};
|
|
44915
|
+
const getPropReferenceName = (analysis, identifier) => {
|
|
44916
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
44917
|
+
const reference = getRef(analysis, identifier);
|
|
44918
|
+
if (!reference || !isProp(analysis, reference) || isWholePropsObjectReference(analysis, reference)) return null;
|
|
44919
|
+
const bindingIdentifier = (reference.resolved?.defs.find((definition) => definition.type === "Parameter"))?.name;
|
|
44920
|
+
return (bindingIdentifier && getDestructuredBindingPropertyName(bindingIdentifier)) ?? identifier.name;
|
|
44921
|
+
};
|
|
44922
|
+
const getSingleConstDeclarator = (reference) => {
|
|
44923
|
+
if (!reference.resolved || hasMutableBindingWrite$2(reference)) return null;
|
|
44924
|
+
const declarators = reference.resolved.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
44925
|
+
if (declarators.length !== 1) return null;
|
|
44926
|
+
const declarator = declarators[0];
|
|
44927
|
+
if (!declarator || getDeclarationKind$1(declarator) !== "const") return null;
|
|
44928
|
+
return declarator;
|
|
44929
|
+
};
|
|
44930
|
+
const resolveParentCallbackPropNames = (analysis, expression, scopes, visitedReferences, allowFunctionForwarder = false) => {
|
|
44931
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
44932
|
+
if (isFunctionLike$1(unwrappedExpression)) {
|
|
44933
|
+
if (!allowFunctionForwarder || Boolean(unwrappedExpression.async)) return null;
|
|
44934
|
+
const callbackNames = /* @__PURE__ */ new Set();
|
|
44935
|
+
walkInsideStatementBlocks(unwrappedExpression.body, (child) => {
|
|
44936
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
44937
|
+
const resolvedNames = resolveParentCallbackPropNames(analysis, child.callee, scopes, new Set(visitedReferences), false);
|
|
44938
|
+
if (!resolvedNames) return;
|
|
44939
|
+
for (const resolvedName of resolvedNames) callbackNames.add(resolvedName);
|
|
44940
|
+
});
|
|
44941
|
+
return callbackNames.size > 0 ? callbackNames : null;
|
|
44942
|
+
}
|
|
44943
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return mergeRequiredBranches(resolveParentCallbackPropNames(analysis, unwrappedExpression.consequent, scopes, new Set(visitedReferences), false), resolveParentCallbackPropNames(analysis, unwrappedExpression.alternate, scopes, new Set(visitedReferences), false));
|
|
44944
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return mergeRequiredBranches(resolveParentCallbackPropNames(analysis, unwrappedExpression.left, scopes, new Set(visitedReferences), false), resolveParentCallbackPropNames(analysis, unwrappedExpression.right, scopes, new Set(visitedReferences)));
|
|
44945
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
44946
|
+
const propName = getPropReferenceName(analysis, unwrappedExpression);
|
|
44947
|
+
if (propName) return new Set([propName]);
|
|
44948
|
+
const reference = getRef(analysis, unwrappedExpression);
|
|
44949
|
+
if (!reference?.resolved || visitedReferences.has(reference.resolved)) return null;
|
|
44950
|
+
const declarator = getSingleConstDeclarator(reference);
|
|
44951
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
44952
|
+
visitedReferences.add(reference.resolved);
|
|
44953
|
+
const wrappedArgument = getTransparentReactCallbackWrapperArgument(declarator.init, scopes.symbolFor(unwrappedExpression), scopes);
|
|
44954
|
+
const allowsFunctionForwarder = Boolean(wrappedArgument && !isReactApiCall(declarator.init, "useCallback", scopes, {
|
|
44955
|
+
allowGlobalReactNamespace: true,
|
|
44956
|
+
allowUnboundBareCalls: true
|
|
44957
|
+
}));
|
|
44958
|
+
return resolveParentCallbackPropNames(analysis, wrappedArgument ?? declarator.init, scopes, visitedReferences, allowsFunctionForwarder);
|
|
44959
|
+
}
|
|
44960
|
+
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
|
|
44961
|
+
const propertyName = getStaticMemberPropertyName(unwrappedExpression);
|
|
44962
|
+
if (!propertyName) return null;
|
|
44963
|
+
const receiver = stripParenExpression(unwrappedExpression.object);
|
|
44964
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
44965
|
+
const receiverReference = getRef(analysis, receiver);
|
|
44966
|
+
if (!receiverReference?.resolved || visitedReferences.has(receiverReference.resolved)) return null;
|
|
44967
|
+
if (isWholePropsObjectReference(analysis, receiverReference)) return new Set([propertyName]);
|
|
44968
|
+
const declarator = getSingleConstDeclarator(receiverReference);
|
|
44969
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
44970
|
+
visitedReferences.add(receiverReference.resolved);
|
|
44971
|
+
const initializer = stripParenExpression(declarator.init);
|
|
44972
|
+
if (propertyName === "current" && isNodeOfType(initializer, "CallExpression")) {
|
|
44973
|
+
if (!isReactApiCall(initializer, "useRef", scopes, {
|
|
44974
|
+
allowGlobalReactNamespace: true,
|
|
44975
|
+
allowUnboundBareCalls: true
|
|
44976
|
+
})) return null;
|
|
44977
|
+
const callbackArgument = initializer.arguments[0];
|
|
44978
|
+
if (!callbackArgument) return null;
|
|
44979
|
+
let callbackNames = resolveParentCallbackPropNames(analysis, callbackArgument, scopes, new Set(visitedReferences), false);
|
|
44980
|
+
if (!callbackNames) return null;
|
|
44981
|
+
for (const candidateReference of receiverReference.resolved.references) {
|
|
44982
|
+
const candidateIdentifier = candidateReference.identifier;
|
|
44983
|
+
const candidateMember = candidateIdentifier.parent;
|
|
44984
|
+
if (!candidateMember || !isNodeOfType(candidateMember, "MemberExpression") || candidateMember.object !== candidateIdentifier || getStaticMemberPropertyName(candidateMember) !== "current") continue;
|
|
44985
|
+
const assignment = candidateMember.parent;
|
|
44986
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== candidateMember) continue;
|
|
44987
|
+
if (assignment.operator !== "=") return null;
|
|
44988
|
+
callbackNames = mergeRequiredBranches(callbackNames, resolveParentCallbackPropNames(analysis, assignment.right, scopes, new Set(visitedReferences), false));
|
|
44989
|
+
if (!callbackNames) return null;
|
|
44990
|
+
}
|
|
44991
|
+
return callbackNames;
|
|
44992
|
+
}
|
|
44993
|
+
if (!isNodeOfType(initializer, "ObjectExpression")) return null;
|
|
44994
|
+
const property = initializer.properties.find((candidateProperty) => isNodeOfType(candidateProperty, "Property") && getStaticPropertyKeyName(candidateProperty, { allowComputedString: true }) === propertyName);
|
|
44995
|
+
if (!property || !isNodeOfType(property, "Property")) return null;
|
|
44996
|
+
return resolveParentCallbackPropNames(analysis, property.value, scopes, visitedReferences, false);
|
|
44997
|
+
};
|
|
44998
|
+
const getParentCallbackPropNames = ({ analysis, expression, scopes }) => resolveParentCallbackPropNames(analysis, expression, scopes, /* @__PURE__ */ new Set(), false);
|
|
44999
|
+
//#endregion
|
|
44427
45000
|
//#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
|
|
44428
45001
|
const isUseStateIdentifier = (identifier) => {
|
|
44429
45002
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
@@ -44452,14 +45025,18 @@ const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
|
|
|
44452
45025
|
"useStableCallback",
|
|
44453
45026
|
"useCallbackRef"
|
|
44454
45027
|
]);
|
|
44455
|
-
const getWrapperHookWrappedFunction = (initializer) => {
|
|
45028
|
+
const getWrapperHookWrappedFunction = (initializer, resultSymbol, scopes) => {
|
|
44456
45029
|
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
45030
|
+
const transparentReactArgument = getTransparentReactCallbackWrapperArgument(initializer, resultSymbol, scopes);
|
|
45031
|
+
if (transparentReactArgument) return transparentReactArgument;
|
|
44457
45032
|
const callee = initializer.callee;
|
|
44458
45033
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
|
|
44459
45034
|
if (!calleeName || !FUNCTION_WRAPPER_HOOK_NAMES$1.has(calleeName)) return null;
|
|
44460
45035
|
const wrapped = initializer.arguments?.[0];
|
|
44461
|
-
if (!wrapped
|
|
44462
|
-
return
|
|
45036
|
+
if (!wrapped) return null;
|
|
45037
|
+
if (calleeName === "useEffectEvent") return null;
|
|
45038
|
+
if (isFunctionLike$1(wrapped)) return wrapped;
|
|
45039
|
+
return null;
|
|
44463
45040
|
};
|
|
44464
45041
|
const HANDLER_NAMED_PROP_PATTERN = /^(on|handle)[A-Z]/;
|
|
44465
45042
|
const wrappedFunctionNotifiesParent = (analysis, wrappedFunction) => getDownstreamRefs(analysis, wrappedFunction).some((innerRef) => {
|
|
@@ -44469,16 +45046,29 @@ const wrappedFunctionNotifiesParent = (analysis, wrappedFunction) => getDownstre
|
|
|
44469
45046
|
const innerParent = innerIdentifier.parent;
|
|
44470
45047
|
return Boolean(innerParent && isNodeOfType(innerParent, "CallExpression") && innerParent.callee === innerIdentifier);
|
|
44471
45048
|
});
|
|
44472
|
-
const isDirectParentCallbackRef = (analysis, ref) => {
|
|
45049
|
+
const isDirectParentCallbackRef = (analysis, ref, scopes) => {
|
|
44473
45050
|
if (isProp(analysis, ref)) return true;
|
|
45051
|
+
if (hasMutableBindingWrite$1(ref)) {
|
|
45052
|
+
if (!(ref.resolved?.references.filter((candidateReference) => candidateReference.isWrite() && !candidateReference.init) ?? []).every((candidateReference) => {
|
|
45053
|
+
const candidateIdentifier = candidateReference.identifier;
|
|
45054
|
+
const assignment = candidateIdentifier.parent;
|
|
45055
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== candidateIdentifier) return false;
|
|
45056
|
+
const assignedReferences = getDownstreamRefs(analysis, assignment.right);
|
|
45057
|
+
return assignedReferences.length > 0 && assignedReferences.every((assignedReference) => isProp(analysis, assignedReference));
|
|
45058
|
+
})) return false;
|
|
45059
|
+
}
|
|
44474
45060
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
44475
45061
|
const node = def.node;
|
|
44476
45062
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44477
45063
|
const initializer = unwrapChainExpression(node.init);
|
|
44478
|
-
const wrappedFunction = getWrapperHookWrappedFunction(initializer);
|
|
45064
|
+
const wrappedFunction = getWrapperHookWrappedFunction(initializer, isNodeOfType(node.id, "Identifier") ? scopes.symbolFor(node.id) ?? null : null, scopes);
|
|
44479
45065
|
if (wrappedFunction) {
|
|
44480
45066
|
if (wrappedFunction.async) return false;
|
|
44481
|
-
return wrappedFunctionNotifiesParent(analysis, wrappedFunction);
|
|
45067
|
+
if (isFunctionLike$1(wrappedFunction)) return wrappedFunctionNotifiesParent(analysis, wrappedFunction);
|
|
45068
|
+
const directName = getParentCallbackPropName(analysis, wrappedFunction);
|
|
45069
|
+
const downstreamReferences = getDownstreamRefs(analysis, wrappedFunction);
|
|
45070
|
+
if (directName !== null) return true;
|
|
45071
|
+
return downstreamReferences.some((wrappedReference) => !hasMutableBindingWrite$1(wrappedReference) && getUpstreamRefs(analysis, wrappedReference).some((upstreamReference) => isProp(analysis, upstreamReference)));
|
|
44482
45072
|
}
|
|
44483
45073
|
if (!isNodeOfType(initializer, "Identifier") && !isNodeOfType(initializer, "MemberExpression")) return false;
|
|
44484
45074
|
return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
|
|
@@ -44488,7 +45078,7 @@ const getDeclarationKind = (declarator) => {
|
|
|
44488
45078
|
const declaration = declarator.parent;
|
|
44489
45079
|
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
44490
45080
|
};
|
|
44491
|
-
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
45081
|
+
const hasMutableBindingWrite$1 = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
44492
45082
|
const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
|
|
44493
45083
|
const unwrappedExpression = stripParenExpression(expression);
|
|
44494
45084
|
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
@@ -44500,7 +45090,7 @@ const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @
|
|
|
44500
45090
|
const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
|
|
44501
45091
|
return (bindingIdentifier && getDestructuredBindingPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
|
|
44502
45092
|
}
|
|
44503
|
-
if (hasMutableBindingWrite(callbackReference)) return null;
|
|
45093
|
+
if (hasMutableBindingWrite$1(callbackReference)) return null;
|
|
44504
45094
|
const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
44505
45095
|
if (definitions.length !== 1) return null;
|
|
44506
45096
|
const declarator = definitions[0];
|
|
@@ -44566,7 +45156,7 @@ const getRefAliasDeclarator = (identifier) => {
|
|
|
44566
45156
|
const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
44567
45157
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
44568
45158
|
const receiverReference = getRef(analysis, receiver);
|
|
44569
|
-
if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
|
|
45159
|
+
if (!receiverReference?.resolved || hasMutableBindingWrite$1(receiverReference)) return null;
|
|
44570
45160
|
const variables = /* @__PURE__ */ new Set();
|
|
44571
45161
|
let currentVariable = receiverReference.resolved;
|
|
44572
45162
|
let refCall = null;
|
|
@@ -44582,7 +45172,7 @@ const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
|
44582
45172
|
}
|
|
44583
45173
|
if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
|
|
44584
45174
|
const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
|
|
44585
|
-
if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
|
|
45175
|
+
if (!upstreamReference?.resolved || hasMutableBindingWrite$1(upstreamReference)) return null;
|
|
44586
45176
|
currentVariable = upstreamReference.resolved;
|
|
44587
45177
|
}
|
|
44588
45178
|
if (!refCall) return null;
|
|
@@ -44657,7 +45247,7 @@ const isParentPropsContextMerge = (analysis, expression) => {
|
|
|
44657
45247
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44658
45248
|
const currentReference = getRef(analysis, currentExpression);
|
|
44659
45249
|
const currentVariable = currentReference?.resolved;
|
|
44660
|
-
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite(currentReference)) return false;
|
|
45250
|
+
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite$1(currentReference)) return false;
|
|
44661
45251
|
visitedVariables.add(currentVariable);
|
|
44662
45252
|
const definitions = currentVariable.defs.filter((definition) => isNodeOfType(definition.node, "VariableDeclarator"));
|
|
44663
45253
|
if (definitions.length !== 1) return false;
|
|
@@ -44671,11 +45261,11 @@ const isParentPropsContextMerge = (analysis, expression) => {
|
|
|
44671
45261
|
const propsExpression = stripParenExpression(propsSpread.argument);
|
|
44672
45262
|
if (!isNodeOfType(propsExpression, "Identifier")) return false;
|
|
44673
45263
|
const propsReference = getRef(analysis, propsExpression);
|
|
44674
|
-
if (!propsReference?.resolved || !isWholePropsObjectReference(analysis, propsReference) || hasMutableBindingWrite(propsReference) || propsReference.resolved.references.some((candidateReference) => candidateReference !== propsReference)) return false;
|
|
45264
|
+
if (!propsReference?.resolved || !isWholePropsObjectReference(analysis, propsReference) || hasMutableBindingWrite$1(propsReference) || propsReference.resolved.references.some((candidateReference) => candidateReference !== propsReference)) return false;
|
|
44675
45265
|
const contextExpression = stripParenExpression(contextSpread.argument);
|
|
44676
45266
|
if (!isNodeOfType(contextExpression, "Identifier")) return false;
|
|
44677
45267
|
const contextReference = getRef(analysis, contextExpression);
|
|
44678
|
-
if (!contextReference?.resolved || hasMutableBindingWrite(contextReference) || contextReference.resolved.references.some((candidateReference) => !candidateReference.init && candidateReference !== contextReference)) return false;
|
|
45268
|
+
if (!contextReference?.resolved || hasMutableBindingWrite$1(contextReference) || contextReference.resolved.references.some((candidateReference) => !candidateReference.init && candidateReference !== contextReference)) return false;
|
|
44679
45269
|
const contextInitializer = contextReference.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
44680
45270
|
if (!contextInitializer || !isNodeOfType(contextInitializer, "VariableDeclarator") || getDeclarationKind(contextInitializer) !== "const" || !contextInitializer.init || !isNodeOfType(contextInitializer.init, "CallExpression")) return false;
|
|
44681
45271
|
const contextHook = stripParenExpression(contextInitializer.init.callee);
|
|
@@ -44689,7 +45279,7 @@ const getImmutableParentCallbackPropName = (analysis, expression) => {
|
|
|
44689
45279
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44690
45280
|
const currentReference = getRef(analysis, currentExpression);
|
|
44691
45281
|
const currentVariable = currentReference?.resolved;
|
|
44692
|
-
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite(currentReference)) return null;
|
|
45282
|
+
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite$1(currentReference)) return null;
|
|
44693
45283
|
visitedVariables.add(currentVariable);
|
|
44694
45284
|
const definition = currentVariable.defs.length === 1 ? currentVariable.defs[0] : null;
|
|
44695
45285
|
const bindingIdentifier = definition?.name;
|
|
@@ -44758,7 +45348,7 @@ const getCommandCallbackPropName = (analysis, expression, isReactUseRefCall) =>
|
|
|
44758
45348
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44759
45349
|
const callbackReference = getRef(analysis, currentExpression);
|
|
44760
45350
|
const callbackVariable = callbackReference?.resolved;
|
|
44761
|
-
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable) || hasMutableBindingWrite(callbackReference)) return null;
|
|
45351
|
+
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable) || hasMutableBindingWrite$1(callbackReference)) return null;
|
|
44762
45352
|
visitedVariables.add(callbackVariable);
|
|
44763
45353
|
const definition = callbackVariable.defs.length === 1 ? callbackVariable.defs[0] : null;
|
|
44764
45354
|
const declarator = definition?.node;
|
|
@@ -44778,10 +45368,11 @@ const getCommandCallbackPropName = (analysis, expression, isReactUseRefCall) =>
|
|
|
44778
45368
|
if (!propertyName || !COMMAND_PROP_NAME_PATTERN.test(propertyName)) return null;
|
|
44779
45369
|
return refCurrentObjectPreservesCallbackProperty(analysis, currentExpression.object, propertyName, isReactUseRefCall) ? propertyName : null;
|
|
44780
45370
|
};
|
|
44781
|
-
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
45371
|
+
const isWrapperHookCallbackRef = (analysis, ref, scopes) => Boolean(ref.resolved?.defs.some((def) => {
|
|
44782
45372
|
const node = def.node;
|
|
44783
45373
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44784
|
-
|
|
45374
|
+
const resultSymbol = isNodeOfType(node.id, "Identifier") ? scopes.symbolFor(node.id) ?? null : null;
|
|
45375
|
+
return getWrapperHookWrappedFunction(unwrapChainExpression(node.init), resultSymbol, scopes) !== null;
|
|
44785
45376
|
}));
|
|
44786
45377
|
const isHandlerBagArgument = (analysis, argument) => {
|
|
44787
45378
|
if (!isNodeOfType(argument, "ObjectExpression")) return false;
|
|
@@ -44800,14 +45391,25 @@ const isHandlerBagArgument = (analysis, argument) => {
|
|
|
44800
45391
|
};
|
|
44801
45392
|
const getFunctionalUpdaterDataRefs = (analysis, updater) => getDownstreamRefs(analysis, updater).filter((updaterRef) => !updaterRef.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updater));
|
|
44802
45393
|
const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
44803
|
-
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
|
|
45394
|
+
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
|
|
44804
45395
|
"useIntersectionObserver",
|
|
44805
45396
|
"useMatchMedia",
|
|
45397
|
+
"useMediaJobProgress",
|
|
44806
45398
|
"useMediaQuery",
|
|
44807
45399
|
"useResizeObserver",
|
|
44808
45400
|
"useVisibility",
|
|
44809
45401
|
"useWindowSize"
|
|
44810
45402
|
]);
|
|
45403
|
+
const isCallbackPropReference = (analysis, ref) => {
|
|
45404
|
+
if (!isProp(analysis, ref)) return false;
|
|
45405
|
+
const identifier = ref.identifier;
|
|
45406
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
45407
|
+
if (!isWholePropsObjectReference(analysis, ref)) return HANDLER_NAMED_PROP_PATTERN.test(identifier.name);
|
|
45408
|
+
const member = identifier.parent;
|
|
45409
|
+
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier) return false;
|
|
45410
|
+
const propertyName = getStaticMemberPropertyName(member);
|
|
45411
|
+
return Boolean(propertyName && HANDLER_NAMED_PROP_PATTERN.test(propertyName));
|
|
45412
|
+
};
|
|
44811
45413
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
44812
45414
|
const node = def.node;
|
|
44813
45415
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
@@ -44815,7 +45417,7 @@ const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs
|
|
|
44815
45417
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
44816
45418
|
const callee = init.callee;
|
|
44817
45419
|
if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
|
|
44818
|
-
return (init.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) =>
|
|
45420
|
+
return (init.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
44819
45421
|
}));
|
|
44820
45422
|
const isParentWiredHookResultArgument = (analysis, argument) => {
|
|
44821
45423
|
if (!isNodeOfType(argument, "Identifier")) return false;
|
|
@@ -44828,19 +45430,19 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
44828
45430
|
if (!isNodeOfType(identifier, "Identifier") || !HOOK_NAME_PATTERN$1.test(identifier.name)) return false;
|
|
44829
45431
|
const parent = identifier.parent;
|
|
44830
45432
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
44831
|
-
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) =>
|
|
45433
|
+
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
44832
45434
|
};
|
|
44833
45435
|
const isExternalSubscriptionHookRef = (ref) => {
|
|
44834
45436
|
const identifier = ref.identifier;
|
|
44835
45437
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
44836
|
-
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
45438
|
+
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
44837
45439
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
44838
45440
|
const node = def.node;
|
|
44839
45441
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44840
45442
|
const initializer = stripParenExpression(node.init);
|
|
44841
45443
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
44842
45444
|
const callee = stripParenExpression(initializer.callee);
|
|
44843
|
-
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(callee.name);
|
|
45445
|
+
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(callee.name);
|
|
44844
45446
|
}));
|
|
44845
45447
|
};
|
|
44846
45448
|
const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
|
|
@@ -44876,16 +45478,22 @@ const noPassDataToParent = defineRule({
|
|
|
44876
45478
|
const callExpr = getCallExpr(ref);
|
|
44877
45479
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
44878
45480
|
const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall, isReactUseEffectCall);
|
|
44879
|
-
if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
|
|
44880
45481
|
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
44881
45482
|
const calleeNode = unwrapChainExpression(callExpr.callee);
|
|
44882
45483
|
const identifier = ref.identifier;
|
|
44883
|
-
|
|
44884
|
-
|
|
45484
|
+
const resolvedCallbackPropNames = isNodeOfType(calleeNode, "MemberExpression") && getStaticMemberPropertyName(calleeNode) === "current" ? null : getParentCallbackPropNames({
|
|
45485
|
+
analysis,
|
|
45486
|
+
expression: calleeNode,
|
|
45487
|
+
scopes: context.scopes
|
|
45488
|
+
});
|
|
45489
|
+
const callbackPropNames = callbackRefProvenance?.callbackPropNames ?? resolvedCallbackPropNames;
|
|
45490
|
+
if (isRefCall(analysis, ref) && !callbackPropNames) continue;
|
|
45491
|
+
if (callbackPropNames) {
|
|
45492
|
+
if ([...callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
|
|
44885
45493
|
} else if (calleeNode === identifier) {
|
|
44886
45494
|
const callbackPropName = getCommandCallbackPropName(analysis, identifier, isReactUseRefCall);
|
|
44887
45495
|
if (callbackPropName && COMMAND_PROP_NAME_PATTERN.test(callbackPropName)) continue;
|
|
44888
|
-
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
45496
|
+
if (!isDirectParentCallbackRef(analysis, ref, context.scopes)) continue;
|
|
44889
45497
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
44890
45498
|
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
44891
45499
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
@@ -44893,10 +45501,10 @@ const noPassDataToParent = defineRule({
|
|
|
44893
45501
|
} else continue;
|
|
44894
45502
|
const methodName = getCallMethodName(calleeNode);
|
|
44895
45503
|
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
44896
|
-
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
45504
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead && !callbackPropNames) continue;
|
|
44897
45505
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
44898
|
-
if (!
|
|
44899
|
-
const isSetterNamedCallee =
|
|
45506
|
+
if (!callbackPropNames && isNamespacedApiCallee(calleeNode)) continue;
|
|
45507
|
+
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) ?? ""));
|
|
44900
45508
|
const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
|
|
44901
45509
|
const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
|
|
44902
45510
|
if (isFunctionLike$1(argument)) {
|
|
@@ -44911,7 +45519,7 @@ const noPassDataToParent = defineRule({
|
|
|
44911
45519
|
}
|
|
44912
45520
|
return getDownstreamRefs(analysis, argument);
|
|
44913
45521
|
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
44914
|
-
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
45522
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
44915
45523
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
44916
45524
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
44917
45525
|
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
@@ -44949,9 +45557,47 @@ const isCallResultConsumedAsArgument = (callExpression) => {
|
|
|
44949
45557
|
return false;
|
|
44950
45558
|
};
|
|
44951
45559
|
//#endregion
|
|
45560
|
+
//#region src/plugin/rules/state-and-effects/utils/is-custom-hook-state-result-reference.ts
|
|
45561
|
+
const NON_STATE_CUSTOM_HOOK_NAMES = new Set([
|
|
45562
|
+
"useCallbackRef",
|
|
45563
|
+
"useEffectEvent",
|
|
45564
|
+
"useEvent",
|
|
45565
|
+
"useEventCallback",
|
|
45566
|
+
"useLatest",
|
|
45567
|
+
"useMemoizedFn",
|
|
45568
|
+
"useStableCallback"
|
|
45569
|
+
]);
|
|
45570
|
+
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
|
|
45571
|
+
"useIntersectionObserver",
|
|
45572
|
+
"useMatchMedia",
|
|
45573
|
+
"useMediaJobProgress",
|
|
45574
|
+
"useMediaQuery",
|
|
45575
|
+
"useResizeObserver",
|
|
45576
|
+
"useVisibility",
|
|
45577
|
+
"useWindowSize"
|
|
45578
|
+
]);
|
|
45579
|
+
const getHookCalleeName = (initializer) => {
|
|
45580
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
45581
|
+
if (!isNodeOfType(unwrappedInitializer, "CallExpression")) return null;
|
|
45582
|
+
const callee = stripParenExpression(unwrappedInitializer.callee);
|
|
45583
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
45584
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
45585
|
+
return null;
|
|
45586
|
+
};
|
|
45587
|
+
const isCustomHookStateResultReference = (analysis, reference) => Boolean(reference.resolved?.defs.some((definition) => {
|
|
45588
|
+
const declarator = definition.node;
|
|
45589
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return false;
|
|
45590
|
+
const calleeName = getHookCalleeName(declarator.init);
|
|
45591
|
+
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;
|
|
45592
|
+
const initializer = stripParenExpression(declarator.init);
|
|
45593
|
+
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
45594
|
+
return initializer.arguments.some((argument) => getDownstreamRefs(analysis, argument).some((argumentReference) => isProp(analysis, argumentReference)));
|
|
45595
|
+
}));
|
|
45596
|
+
//#endregion
|
|
44952
45597
|
//#region src/plugin/rules/state-and-effects/no-pass-live-state-to-parent.ts
|
|
44953
45598
|
const SETTER_NAMED_CALLBACK_PATTERN = /^set[A-Z]/;
|
|
44954
45599
|
const DATA_FETCHING_CALLBACK_PATTERN = /^(fetch|refetch|load|query|request)([A-Z_]|$)/;
|
|
45600
|
+
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
44955
45601
|
const getCallCalleeName = (callExpr) => {
|
|
44956
45602
|
if (!isNodeOfType(callExpr, "CallExpression")) return null;
|
|
44957
45603
|
const callee = callExpr.callee;
|
|
@@ -44996,6 +45642,10 @@ const collectUpstreamStateRefs = (analysis, ref, stateRefs, visited) => {
|
|
|
44996
45642
|
stateRefs.push(ref);
|
|
44997
45643
|
return;
|
|
44998
45644
|
}
|
|
45645
|
+
if (isCustomHookStateResultReference(analysis, ref)) {
|
|
45646
|
+
stateRefs.push(ref);
|
|
45647
|
+
return;
|
|
45648
|
+
}
|
|
44999
45649
|
for (const def of ref.resolved?.defs ?? []) {
|
|
45000
45650
|
if (def.type === "ImportBinding" || def.type === "Parameter") continue;
|
|
45001
45651
|
const defNode = def.node;
|
|
@@ -45025,6 +45675,32 @@ const collectPropCallbackBoundStateRefs = (analysis, ref, isPropCallbackRef) =>
|
|
|
45025
45675
|
}
|
|
45026
45676
|
return stateRefs;
|
|
45027
45677
|
};
|
|
45678
|
+
const collectDirectCallStateRefs = (analysis, callExpression) => {
|
|
45679
|
+
const stateReferences = [];
|
|
45680
|
+
for (const argument of callExpression.arguments) {
|
|
45681
|
+
if (isFunctionLike$1(argument)) continue;
|
|
45682
|
+
for (const argumentReference of getDownstreamRefs(analysis, argument)) {
|
|
45683
|
+
if (resolveToFunction(argumentReference)) continue;
|
|
45684
|
+
collectUpstreamStateRefs(analysis, argumentReference, stateReferences, /* @__PURE__ */ new Set());
|
|
45685
|
+
}
|
|
45686
|
+
}
|
|
45687
|
+
return stateReferences;
|
|
45688
|
+
};
|
|
45689
|
+
const getTransparentWrapperPropReference = (analysis, reference, context) => {
|
|
45690
|
+
for (const definition of reference.resolved?.defs ?? []) {
|
|
45691
|
+
const declarator = definition.node;
|
|
45692
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
|
|
45693
|
+
const resultSymbol = context.scopes.symbolFor(declarator.id);
|
|
45694
|
+
const callbackArgument = getTransparentReactCallbackWrapperArgument(declarator.init, resultSymbol, context.scopes);
|
|
45695
|
+
if (!callbackArgument) continue;
|
|
45696
|
+
const callbackReferences = getDownstreamRefs(analysis, callbackArgument);
|
|
45697
|
+
const callbackReference = callbackReferences.find((candidateReference) => isPropCallbackInvocationRef(analysis, candidateReference));
|
|
45698
|
+
if (callbackReference) return callbackReference;
|
|
45699
|
+
const propReference = callbackReferences.find((candidateReference) => isProp(analysis, candidateReference) && !candidateReference.resolved?.references.some((candidateUsage) => candidateUsage.isWrite() && !candidateUsage.init));
|
|
45700
|
+
if (propReference) return propReference;
|
|
45701
|
+
}
|
|
45702
|
+
return null;
|
|
45703
|
+
};
|
|
45028
45704
|
const isSetterNamedCallbackReceivingData = (callbackRef) => {
|
|
45029
45705
|
const callExpr = getCallExpr(callbackRef);
|
|
45030
45706
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) return false;
|
|
@@ -45060,6 +45736,16 @@ const resolvesToLocalHookReturnBinding = (ref) => Boolean(ref?.resolved?.defs?.s
|
|
|
45060
45736
|
const calleeName = getInitializerCalleeName(node.init);
|
|
45061
45737
|
return calleeName !== null && isReactHookName(calleeName) && !FUNCTION_WRAPPER_HOOK_NAMES.has(calleeName);
|
|
45062
45738
|
}));
|
|
45739
|
+
const getDirectLocalEffectHelper = (callExpression, effectFunction, context) => {
|
|
45740
|
+
const helperFunction = resolveExactLocalFunction(callExpression.callee, context.scopes);
|
|
45741
|
+
if (!helperFunction) return null;
|
|
45742
|
+
let ancestor = callExpression.parent;
|
|
45743
|
+
while (ancestor && ancestor !== effectFunction) {
|
|
45744
|
+
if (isFunctionLike$1(ancestor)) return null;
|
|
45745
|
+
ancestor = ancestor.parent;
|
|
45746
|
+
}
|
|
45747
|
+
return ancestor === effectFunction ? helperFunction : null;
|
|
45748
|
+
};
|
|
45063
45749
|
const noPassLiveStateToParent = defineRule({
|
|
45064
45750
|
id: "no-pass-live-state-to-parent",
|
|
45065
45751
|
title: "Live state pushed to parent via effect",
|
|
@@ -45074,20 +45760,32 @@ const noPassLiveStateToParent = defineRule({
|
|
|
45074
45760
|
if (!effectFnRefs) return;
|
|
45075
45761
|
const effectFn = getEffectFn(analysis, node);
|
|
45076
45762
|
if (!effectFn) return;
|
|
45763
|
+
const effectFunctionBody = isNodeOfType(effectFn, "ArrowFunctionExpression") || isNodeOfType(effectFn, "FunctionExpression") || isNodeOfType(effectFn, "FunctionDeclaration") ? effectFn.body : null;
|
|
45077
45764
|
for (const ref of effectFnRefs) {
|
|
45078
|
-
const propCallbackRefs = getEventualCallRefsTo(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
45079
|
-
if (propCallbackRefs.length === 0) continue;
|
|
45080
|
-
if (resolvesToLocalHookReturnBinding(ref)) continue;
|
|
45081
|
-
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
45082
45765
|
const callExpr = getCallExpr(ref);
|
|
45083
|
-
if (!callExpr) continue;
|
|
45766
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
45767
|
+
const directLocalEffectHelper = getDirectLocalEffectHelper(callExpr, effectFn, context);
|
|
45768
|
+
const callGraphReferences = directLocalEffectHelper ? [ref, ...getDownstreamRefs(analysis, directLocalEffectHelper)] : [ref];
|
|
45769
|
+
const resolvedCallbackPropNames = getParentCallbackPropNames({
|
|
45770
|
+
analysis,
|
|
45771
|
+
expression: callExpr.callee,
|
|
45772
|
+
scopes: context.scopes
|
|
45773
|
+
});
|
|
45774
|
+
const callExpressionRoot = findTransparentExpressionRoot(callExpr);
|
|
45775
|
+
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;
|
|
45776
|
+
if (!notificationCallbackPropNames && hasMutableBindingWrite(ref)) continue;
|
|
45777
|
+
const propCallbackRefs = callGraphReferences.flatMap((callGraphReference) => getEventualCallRefsTo(analysis, callGraphReference, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef)));
|
|
45778
|
+
const transparentPropReference = propCallbackRefs.length === 0 ? getTransparentWrapperPropReference(analysis, ref, context) : null;
|
|
45779
|
+
if (propCallbackRefs.length === 0 && !transparentPropReference && !notificationCallbackPropNames) continue;
|
|
45780
|
+
if (!notificationCallbackPropNames && resolvesToLocalHookReturnBinding(ref)) continue;
|
|
45781
|
+
if (!isSynchronous(ref.identifier, effectFn) && !directLocalEffectHelper) continue;
|
|
45084
45782
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
45085
45783
|
const calleeNode = callExpr.callee;
|
|
45086
45784
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
45087
45785
|
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
45088
|
-
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
45089
|
-
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
45090
|
-
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis,
|
|
45786
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead && !notificationCallbackPropNames) continue;
|
|
45787
|
+
if (!notificationCallbackPropNames && calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
45788
|
+
const stateArgRefs = transparentPropReference || notificationCallbackPropNames ? collectDirectCallStateRefs(analysis, callExpr) : callGraphReferences.flatMap((callGraphReference) => collectPropCallbackBoundStateRefs(analysis, callGraphReference, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef)));
|
|
45091
45789
|
const handsSetterNamedCallbackData = propCallbackRefs.some(isSetterNamedCallbackReceivingData);
|
|
45092
45790
|
if (stateArgRefs.length === 0 && !handsSetterNamedCallbackData) continue;
|
|
45093
45791
|
context.report({
|
|
@@ -45480,6 +46178,7 @@ const isStateLikeDependency = (analysis, element, isPropName) => {
|
|
|
45480
46178
|
if (!analysis) return true;
|
|
45481
46179
|
const reference = getRef(analysis, element);
|
|
45482
46180
|
if (!reference) return true;
|
|
46181
|
+
if (isCustomHookStateResultReference(analysis, reference)) return true;
|
|
45483
46182
|
const upstreamReferences = getUpstreamRefs(analysis, reference);
|
|
45484
46183
|
if (upstreamReferences.some((upstreamReference) => isState(analysis, upstreamReference))) return true;
|
|
45485
46184
|
return !upstreamReferences.some((upstreamReference) => isProp(analysis, upstreamReference));
|
|
@@ -45496,6 +46195,22 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
|
45496
46195
|
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
45497
46196
|
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
45498
46197
|
};
|
|
46198
|
+
const getTransparentWrappedPropCallbackName = (callExpression, context, isPropName) => {
|
|
46199
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
46200
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
46201
|
+
const binding = findVariableInitializer(callExpression, callee.name);
|
|
46202
|
+
if (!binding?.initializer) return null;
|
|
46203
|
+
const resultSymbol = context.scopes.symbolFor(callee);
|
|
46204
|
+
const callbackArgument = getTransparentReactCallbackWrapperArgument(binding.initializer, resultSymbol, context.scopes);
|
|
46205
|
+
if (!callbackArgument) return null;
|
|
46206
|
+
const callbackSource = stripParenExpression(callbackArgument);
|
|
46207
|
+
if (isNodeOfType(callbackSource, "Identifier")) return isPropName(callbackSource.name, callbackSource) ? callbackSource.name : null;
|
|
46208
|
+
if (!isNodeOfType(callbackSource, "MemberExpression")) return null;
|
|
46209
|
+
const receiver = stripParenExpression(callbackSource.object);
|
|
46210
|
+
const propertyName = getStaticPropertyName(callbackSource);
|
|
46211
|
+
if (!isNodeOfType(receiver, "Identifier") || !propertyName) return null;
|
|
46212
|
+
return isPropName(receiver.name, receiver) ? propertyName : null;
|
|
46213
|
+
};
|
|
45499
46214
|
const noPropCallbackInEffect = defineRule({
|
|
45500
46215
|
id: "no-prop-callback-in-effect",
|
|
45501
46216
|
title: "Parent kept in sync with a callback effect",
|
|
@@ -45529,9 +46244,16 @@ const noPropCallbackInEffect = defineRule({
|
|
|
45529
46244
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
45530
46245
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
45531
46246
|
const directCallee = stripParenExpression(child.callee);
|
|
45532
|
-
const
|
|
46247
|
+
const resolvedCallbackPropNames = analysis && propStackTracker.getCurrentPropNames().size > 0 ? getParentCallbackPropNames({
|
|
46248
|
+
analysis,
|
|
46249
|
+
expression: directCallee,
|
|
46250
|
+
scopes: context.scopes
|
|
46251
|
+
}) : null;
|
|
46252
|
+
const calleeName = resolvedCallbackPropNames && [...resolvedCallbackPropNames][0] || isNodeOfType(directCallee, "Identifier") && propStackTracker.isPropName(directCallee.name) && directCallee.name || getRefHeldPropCallbackName(child, propStackTracker.isPropName) || getTransparentWrappedPropCallbackName(child, context, propStackTracker.isPropName);
|
|
45533
46253
|
if (!calleeName) return;
|
|
45534
|
-
|
|
46254
|
+
const callExpressionRoot = findTransparentExpressionRoot(child);
|
|
46255
|
+
const isDirectEffectReturn = isNodeOfType(callExpressionRoot.parent, "ReturnStatement") && callExpressionRoot.parent.parent === callback.body;
|
|
46256
|
+
if (!isResultDiscardedCall(child) && !isDirectEffectReturn) return;
|
|
45535
46257
|
if (reportedNodes.has(child)) return;
|
|
45536
46258
|
reportedNodes.add(child);
|
|
45537
46259
|
context.report({
|
|
@@ -53124,12 +53846,6 @@ const isInsideEs6Component$1 = (methodDefinition) => {
|
|
|
53124
53846
|
if (!owningClass) return false;
|
|
53125
53847
|
return isPreactOrReactComponentClass(owningClass);
|
|
53126
53848
|
};
|
|
53127
|
-
const stripThisParameter = (params) => {
|
|
53128
|
-
const first = params[0];
|
|
53129
|
-
if (!first) return params;
|
|
53130
|
-
if (isNodeOfType(first, "Identifier") && first.name === "this") return params.slice(1);
|
|
53131
|
-
return params;
|
|
53132
|
-
};
|
|
53133
53849
|
const preactNoRenderArguments = defineRule({
|
|
53134
53850
|
id: "preact-no-render-arguments",
|
|
53135
53851
|
title: "render() reads props from arguments",
|
|
@@ -56331,8 +57047,39 @@ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node,
|
|
|
56331
57047
|
destructureIndex: 1
|
|
56332
57048
|
});
|
|
56333
57049
|
//#endregion
|
|
57050
|
+
//#region src/plugin/utils/unwrap-return-expression.ts
|
|
57051
|
+
const unwrapReturnExpression = (node) => isNodeOfType(node, "ReturnStatement") && node.argument ? node.argument : node;
|
|
57052
|
+
//#endregion
|
|
56334
57053
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
56335
57054
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
57055
|
+
const USE_CALLBACK_ONLY = new Set(["useCallback"]);
|
|
57056
|
+
const USE_STATE_ONLY = new Set(["useState"]);
|
|
57057
|
+
const REACT_API_CALL_OPTIONS = {
|
|
57058
|
+
allowGlobalReactNamespace: true,
|
|
57059
|
+
allowUnboundBareCalls: true,
|
|
57060
|
+
resolveNamedAliases: true
|
|
57061
|
+
};
|
|
57062
|
+
const expressionReadsDerivedSymbol = (context, expression, stateDerivedSymbolIds) => {
|
|
57063
|
+
let readsDerivedSymbol = false;
|
|
57064
|
+
walkAst(expression, (node) => {
|
|
57065
|
+
if (readsDerivedSymbol) return false;
|
|
57066
|
+
if (node !== expression && isFunctionLike$1(node)) return false;
|
|
57067
|
+
if (isNodeOfType(node, "Identifier") && stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) readsDerivedSymbol = true;
|
|
57068
|
+
});
|
|
57069
|
+
return readsDerivedSymbol;
|
|
57070
|
+
};
|
|
57071
|
+
const getStaticObjectPropertyName = (property) => {
|
|
57072
|
+
if (!isNodeOfType(property, "Property") || property.computed || property.method || property.kind !== "init") return null;
|
|
57073
|
+
if (isNodeOfType(property.key, "Identifier")) return property.key.name;
|
|
57074
|
+
if (isNodeOfType(property.key, "Literal") && (typeof property.key.value === "string" || typeof property.key.value === "number")) return String(property.key.value);
|
|
57075
|
+
return null;
|
|
57076
|
+
};
|
|
57077
|
+
const isNonVisibleJsxSpreadProperty = (propertyName) => propertyName === "id" || propertyName.startsWith("aria-") || /^on[A-Z]/.test(propertyName);
|
|
57078
|
+
const isTransparentAssignmentTarget = (identifier) => {
|
|
57079
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
57080
|
+
const parent = expressionRoot.parent;
|
|
57081
|
+
return Boolean(isNodeOfType(parent, "AssignmentExpression") && parent.left === expressionRoot || isNodeOfType(parent, "UpdateExpression") && parent.argument === expressionRoot || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expressionRoot);
|
|
57082
|
+
};
|
|
56336
57083
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
56337
57084
|
let readsCurrent = false;
|
|
56338
57085
|
walkAst(argument, (child) => {
|
|
@@ -56384,6 +57131,166 @@ const isStateUsedOnlyInIdOrAriaAttributes = (setterCall, setterName) => {
|
|
|
56384
57131
|
});
|
|
56385
57132
|
return referenceCount > 0 && !nonAriaReferenceFound;
|
|
56386
57133
|
};
|
|
57134
|
+
const isGlobalWindowMember = (context, node, propertyName) => {
|
|
57135
|
+
const member = stripParenExpression(node);
|
|
57136
|
+
if (!isNodeOfType(member, "MemberExpression") || member.computed) return false;
|
|
57137
|
+
const receiver = stripParenExpression(member.object);
|
|
57138
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "window" && context.scopes.isGlobalReference(receiver) && isNodeOfType(member.property, "Identifier") && member.property.name === propertyName;
|
|
57139
|
+
};
|
|
57140
|
+
const getDirectWindowWidthSetter = (context, statement) => {
|
|
57141
|
+
const call = unwrapDiscardedExpression(statement);
|
|
57142
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 1) return null;
|
|
57143
|
+
if (!isNodeOfType(call.callee, "Identifier") || !isSetterCall(call)) return null;
|
|
57144
|
+
const argument = call.arguments[0];
|
|
57145
|
+
return isGlobalWindowMember(context, argument, "innerWidth") ? call : null;
|
|
57146
|
+
};
|
|
57147
|
+
const getResizeListenerHandler = (context, statement, methodName) => {
|
|
57148
|
+
const call = unwrapDiscardedExpression(statement);
|
|
57149
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 2) return null;
|
|
57150
|
+
if (!isGlobalWindowMember(context, call.callee, methodName)) return null;
|
|
57151
|
+
const eventName = call.arguments[0];
|
|
57152
|
+
const handler = call.arguments[1];
|
|
57153
|
+
if (!isNodeOfType(eventName, "Literal") || eventName.value !== "resize") return null;
|
|
57154
|
+
return isNodeOfType(handler, "Identifier") ? handler : null;
|
|
57155
|
+
};
|
|
57156
|
+
const getCleanupResizeHandler = (context, statement) => {
|
|
57157
|
+
if (!isNodeOfType(statement, "ReturnStatement") || !isFunctionLike$1(statement.argument)) return null;
|
|
57158
|
+
const cleanupStatements = getCallbackStatements(statement.argument);
|
|
57159
|
+
if (cleanupStatements.length !== 1) return null;
|
|
57160
|
+
return getResizeListenerHandler(context, unwrapReturnExpression(cleanupStatements[0]), "removeEventListener");
|
|
57161
|
+
};
|
|
57162
|
+
const findExactViewportState = (context, componentFunction, setterCall) => {
|
|
57163
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
57164
|
+
const componentBody = componentFunction.body;
|
|
57165
|
+
if (!isNodeOfType(setterCall.callee, "Identifier")) return null;
|
|
57166
|
+
const setterSymbol = context.scopes.symbolFor(setterCall.callee);
|
|
57167
|
+
if (!setterSymbol || setterSymbol.kind !== "const" || !isNodeOfType(setterSymbol.declarationNode, "VariableDeclarator")) return null;
|
|
57168
|
+
const declarator = setterSymbol.declarationNode;
|
|
57169
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return null;
|
|
57170
|
+
const stateIdentifier = declarator.id.elements?.[0];
|
|
57171
|
+
const setterIdentifier = declarator.id.elements?.[1];
|
|
57172
|
+
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;
|
|
57173
|
+
const initializer = declarator.init.arguments?.[0];
|
|
57174
|
+
if (!isNodeOfType(initializer, "Literal") || initializer.value !== 0) return null;
|
|
57175
|
+
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
57176
|
+
if (!stateSymbol) return null;
|
|
57177
|
+
const stateDerivedSymbolIds = new Set([stateSymbol.id]);
|
|
57178
|
+
let didAddDerivedSymbol = true;
|
|
57179
|
+
while (didAddDerivedSymbol) {
|
|
57180
|
+
didAddDerivedSymbol = false;
|
|
57181
|
+
for (const statement of componentBody.body ?? []) {
|
|
57182
|
+
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
57183
|
+
for (const candidateDeclarator of statement.declarations ?? []) {
|
|
57184
|
+
if (!isNodeOfType(candidateDeclarator.id, "Identifier") || !candidateDeclarator.init) continue;
|
|
57185
|
+
const candidateInitializer = stripParenExpression(candidateDeclarator.init);
|
|
57186
|
+
if (isFunctionLike$1(candidateInitializer) || isNodeOfType(candidateInitializer, "CallExpression") && isReactApiCall(candidateInitializer, USE_CALLBACK_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) continue;
|
|
57187
|
+
if (!expressionReadsDerivedSymbol(context, candidateInitializer, stateDerivedSymbolIds)) continue;
|
|
57188
|
+
const candidateSymbol = context.scopes.symbolFor(candidateDeclarator.id);
|
|
57189
|
+
if (candidateSymbol?.kind === "const" && candidateSymbol.references.every((reference) => reference.flag === "read" && !isTransparentAssignmentTarget(reference.identifier)) && !stateDerivedSymbolIds.has(candidateSymbol.id)) {
|
|
57190
|
+
stateDerivedSymbolIds.add(candidateSymbol.id);
|
|
57191
|
+
didAddDerivedSymbol = true;
|
|
57192
|
+
}
|
|
57193
|
+
}
|
|
57194
|
+
}
|
|
57195
|
+
}
|
|
57196
|
+
const staticSpreadVisibilityBySymbolId = /* @__PURE__ */ new Map();
|
|
57197
|
+
const hasOnlyStaticObjectReferences = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
57198
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
57199
|
+
if (!symbol) return false;
|
|
57200
|
+
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
57201
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
57202
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
57203
|
+
let hasUnknownReference = false;
|
|
57204
|
+
walkAst(componentBody, (node) => {
|
|
57205
|
+
if (hasUnknownReference || !isNodeOfType(node, "Identifier") || context.scopes.symbolFor(node)?.id !== symbol.id || node === symbol.bindingIdentifier) return;
|
|
57206
|
+
const referenceRoot = findTransparentExpressionRoot(node);
|
|
57207
|
+
const parent = referenceRoot.parent;
|
|
57208
|
+
if (isNodeOfType(parent, "JSXSpreadAttribute") && parent.argument === referenceRoot) return;
|
|
57209
|
+
if (isNodeOfType(parent, "VariableDeclarator") && parent.init === referenceRoot && isNodeOfType(parent.id, "Identifier") && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const" && hasOnlyStaticObjectReferences(parent.id, nextVisitedSymbolIds)) return;
|
|
57210
|
+
hasUnknownReference = true;
|
|
57211
|
+
return false;
|
|
57212
|
+
});
|
|
57213
|
+
return !hasUnknownReference;
|
|
57214
|
+
};
|
|
57215
|
+
const classifyStaticSpreadObject = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
57216
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
57217
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return "unknown";
|
|
57218
|
+
const cachedVisibility = staticSpreadVisibilityBySymbolId.get(symbol.id);
|
|
57219
|
+
if (cachedVisibility) return cachedVisibility;
|
|
57220
|
+
if (symbol.kind !== "const" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || symbol.declarationNode.id !== symbol.bindingIdentifier || !symbol.declarationNode.init) return "unknown";
|
|
57221
|
+
if (!hasOnlyStaticObjectReferences(identifier)) return "unknown";
|
|
57222
|
+
const initializer = stripParenExpression(symbol.declarationNode.init);
|
|
57223
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
57224
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
57225
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
57226
|
+
const visibility = classifyStaticSpreadObject(initializer, nextVisitedSymbolIds);
|
|
57227
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
57228
|
+
return visibility;
|
|
57229
|
+
}
|
|
57230
|
+
if (!isNodeOfType(initializer, "ObjectExpression")) return "unknown";
|
|
57231
|
+
let visibility = "non-visible";
|
|
57232
|
+
for (const property of initializer.properties ?? []) {
|
|
57233
|
+
const propertyName = getStaticObjectPropertyName(property);
|
|
57234
|
+
if (!isNodeOfType(property, "Property") || !propertyName) {
|
|
57235
|
+
visibility = "unknown";
|
|
57236
|
+
break;
|
|
57237
|
+
}
|
|
57238
|
+
if (expressionReadsDerivedSymbol(context, property.value, stateDerivedSymbolIds) && !isNonVisibleJsxSpreadProperty(propertyName)) visibility = "visible";
|
|
57239
|
+
}
|
|
57240
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
57241
|
+
return visibility;
|
|
57242
|
+
};
|
|
57243
|
+
let hasNonAriaReference = false;
|
|
57244
|
+
walkAst(componentBody, (node) => {
|
|
57245
|
+
if (hasNonAriaReference) return false;
|
|
57246
|
+
if (!isNodeOfType(node, "Identifier") || !stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) return;
|
|
57247
|
+
if (findEnclosingFunction$1(node) !== componentFunction) return;
|
|
57248
|
+
const parent = node.parent;
|
|
57249
|
+
if (parent && (isNodeOfType(parent, "MemberExpression") && parent.property === node && !parent.computed || isNodeOfType(parent, "Property") && parent.key === node && !parent.computed)) return;
|
|
57250
|
+
let cursor = parent;
|
|
57251
|
+
while (cursor && cursor !== componentBody) {
|
|
57252
|
+
if (isFunctionLike$1(cursor)) return;
|
|
57253
|
+
if (isNodeOfType(cursor, "JSXSpreadAttribute")) {
|
|
57254
|
+
if (isNodeOfType(node, "Identifier") && classifyStaticSpreadObject(node) === "visible") hasNonAriaReference = true;
|
|
57255
|
+
return;
|
|
57256
|
+
}
|
|
57257
|
+
if (isNodeOfType(cursor, "JSXAttribute")) {
|
|
57258
|
+
if (isEventHandlerAttribute(cursor)) return;
|
|
57259
|
+
if (!isInsideIdOrAriaAttribute(node)) hasNonAriaReference = true;
|
|
57260
|
+
return;
|
|
57261
|
+
}
|
|
57262
|
+
if (isNodeOfType(cursor, "ReturnStatement")) {
|
|
57263
|
+
hasNonAriaReference = true;
|
|
57264
|
+
return;
|
|
57265
|
+
}
|
|
57266
|
+
cursor = cursor.parent;
|
|
57267
|
+
}
|
|
57268
|
+
});
|
|
57269
|
+
return hasNonAriaReference ? stateIdentifier.name : null;
|
|
57270
|
+
};
|
|
57271
|
+
const isExactViewportSubscriptionEffect = (context, effectCall, callback) => {
|
|
57272
|
+
if (!isReactApiCall(effectCall, USE_EFFECT_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return false;
|
|
57273
|
+
if (!isFunctionLike$1(callback) || callback.async || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
57274
|
+
const statements = getCallbackStatements(callback);
|
|
57275
|
+
if (statements.length !== 4) return false;
|
|
57276
|
+
const handlerDeclaration = statements[0];
|
|
57277
|
+
if (!isNodeOfType(handlerDeclaration, "VariableDeclaration") || handlerDeclaration.kind !== "const" || handlerDeclaration.declarations?.length !== 1) return false;
|
|
57278
|
+
const handlerDeclarator = handlerDeclaration.declarations[0];
|
|
57279
|
+
if (!isNodeOfType(handlerDeclarator.id, "Identifier") || !isFunctionLike$1(handlerDeclarator.init)) return false;
|
|
57280
|
+
const handlerStatements = getCallbackStatements(handlerDeclarator.init);
|
|
57281
|
+
if (handlerStatements.length !== 1) return false;
|
|
57282
|
+
const handlerSetter = getDirectWindowWidthSetter(context, unwrapReturnExpression(handlerStatements[0]));
|
|
57283
|
+
const subscribedHandler = getResizeListenerHandler(context, statements[1], "addEventListener");
|
|
57284
|
+
const immediateSetter = getDirectWindowWidthSetter(context, statements[2]);
|
|
57285
|
+
const cleanupHandler = getCleanupResizeHandler(context, statements[3]);
|
|
57286
|
+
if (!handlerSetter || !subscribedHandler || !immediateSetter || !cleanupHandler) return false;
|
|
57287
|
+
const handlerSymbol = context.scopes.symbolFor(handlerDeclarator.id);
|
|
57288
|
+
if (!handlerSymbol || context.scopes.symbolFor(subscribedHandler) !== handlerSymbol || context.scopes.symbolFor(cleanupHandler) !== handlerSymbol) return false;
|
|
57289
|
+
if (!isNodeOfType(handlerSetter.callee, "Identifier") || !isNodeOfType(immediateSetter.callee, "Identifier") || context.scopes.symbolFor(handlerSetter.callee) !== context.scopes.symbolFor(immediateSetter.callee)) return false;
|
|
57290
|
+
const componentFunction = findEnclosingFunction$1(effectCall);
|
|
57291
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return false;
|
|
57292
|
+
return findExactViewportState(context, componentFunction, immediateSetter) !== null;
|
|
57293
|
+
};
|
|
56387
57294
|
const renderingHydrationNoFlicker = defineRule({
|
|
56388
57295
|
id: "rendering-hydration-no-flicker",
|
|
56389
57296
|
title: "useEffect setState flashes on mount",
|
|
@@ -56396,7 +57303,14 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
56396
57303
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
56397
57304
|
const callback = getEffectCallback(node);
|
|
56398
57305
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
56399
|
-
|
|
57306
|
+
if (isExactViewportSubscriptionEffect(context, node, callback)) {
|
|
57307
|
+
context.report({
|
|
57308
|
+
node,
|
|
57309
|
+
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
57310
|
+
});
|
|
57311
|
+
return;
|
|
57312
|
+
}
|
|
57313
|
+
const bodyStatements = getCallbackStatements(callback);
|
|
56400
57314
|
if (bodyStatements.length !== 1) return;
|
|
56401
57315
|
const soleStatement = bodyStatements[0];
|
|
56402
57316
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
@@ -66228,14 +67142,6 @@ const isStateKey = (key) => {
|
|
|
66228
67142
|
if (isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value === "state";
|
|
66229
67143
|
return false;
|
|
66230
67144
|
};
|
|
66231
|
-
const findEnclosingClass = (node) => {
|
|
66232
|
-
let ancestor = node.parent;
|
|
66233
|
-
while (ancestor) {
|
|
66234
|
-
if (isNodeOfType(ancestor, "ClassDeclaration") || isNodeOfType(ancestor, "ClassExpression")) return ancestor;
|
|
66235
|
-
ancestor = ancestor.parent ?? null;
|
|
66236
|
-
}
|
|
66237
|
-
return null;
|
|
66238
|
-
};
|
|
66239
67145
|
const isInConstructor = (node) => {
|
|
66240
67146
|
let ancestor = node.parent;
|
|
66241
67147
|
while (ancestor) {
|