oxlint-plugin-react-doctor 0.7.9-dev.b392093 → 0.7.9-dev.b51022f
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 +181 -41
- 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.";
|
|
@@ -8566,17 +8692,6 @@ const createMethodMutationAnalysis = (context) => {
|
|
|
8566
8692
|
//#region src/plugin/utils/is-member-property.ts
|
|
8567
8693
|
const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === propertyName);
|
|
8568
8694
|
//#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
8695
|
//#region src/plugin/utils/statement-always-exits.ts
|
|
8581
8696
|
const statementAlwaysExits = (statement) => {
|
|
8582
8697
|
if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
|
|
@@ -14751,6 +14866,29 @@ const getReleaseVerbName = (node) => {
|
|
|
14751
14866
|
}
|
|
14752
14867
|
return null;
|
|
14753
14868
|
};
|
|
14869
|
+
const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) => {
|
|
14870
|
+
const releaseFunction = findEnclosingFunction$1(releaseReceiver);
|
|
14871
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14872
|
+
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !hasReactRefCurrentOrigin(releaseReceiver, context.scopes)) return false;
|
|
14873
|
+
const controllerKey = getListenerAbortControllerKey(usage, context);
|
|
14874
|
+
const refCurrentKey = resolveExpressionKey(releaseReceiver, context);
|
|
14875
|
+
if (controllerKey === null || refCurrentKey === null) return false;
|
|
14876
|
+
const usageFunctionBody = usageFunction.body;
|
|
14877
|
+
const previousAbortCalls = [];
|
|
14878
|
+
const ownershipAssignments = [];
|
|
14879
|
+
walkAst(usageFunctionBody, (child) => {
|
|
14880
|
+
if (child !== usageFunctionBody && isFunctionLike$1(child)) return false;
|
|
14881
|
+
if (isNodeOfType(child, "AssignmentExpression") && resolveExpressionKey(child.left, context) === refCurrentKey && resolveExpressionKey(child.right, context) === controllerKey) {
|
|
14882
|
+
ownershipAssignments.push(child);
|
|
14883
|
+
return;
|
|
14884
|
+
}
|
|
14885
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
14886
|
+
const childCallee = isNodeOfType(child.callee, "ChainExpression") ? child.callee.expression : stripParenExpression(child.callee);
|
|
14887
|
+
if (isNodeOfType(childCallee, "MemberExpression") && !childCallee.computed && isNodeOfType(childCallee.property, "Identifier") && childCallee.property.name === "abort" && resolveExpressionKey(childCallee.object, context) === refCurrentKey) previousAbortCalls.push(child);
|
|
14888
|
+
});
|
|
14889
|
+
const safeOwnershipAssignments = ownershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, previousAbortCalls, usageFunction, context));
|
|
14890
|
+
return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
|
|
14891
|
+
};
|
|
14754
14892
|
const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
14755
14893
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
14756
14894
|
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
@@ -14770,6 +14908,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14770
14908
|
if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
14771
14909
|
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
14910
|
if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
|
|
14911
|
+
if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
|
|
14773
14912
|
if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
|
|
14774
14913
|
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
14775
14914
|
if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
|
|
@@ -14803,9 +14942,11 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14803
14942
|
return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
|
|
14804
14943
|
}
|
|
14805
14944
|
if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
|
|
14806
|
-
const
|
|
14945
|
+
const usesUnaryListenerSignature = usage.registrationVerbName === "addListener" && isNodeOfType(usage.node, "CallExpression") && usage.node.arguments?.length === 1 && callNode.arguments?.length === 1;
|
|
14946
|
+
const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
|
|
14807
14947
|
if (!releaseHandler) return releaseVerbName === "off";
|
|
14808
|
-
|
|
14948
|
+
const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
|
|
14949
|
+
return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey;
|
|
14809
14950
|
}
|
|
14810
14951
|
if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
|
|
14811
14952
|
return true;
|
|
@@ -14818,8 +14959,7 @@ const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
|
14818
14959
|
currentNode = parentNode;
|
|
14819
14960
|
parentNode = currentNode.parent;
|
|
14820
14961
|
}
|
|
14821
|
-
|
|
14822
|
-
const effectCallback = findEnclosingFunction$1(parentNode);
|
|
14962
|
+
const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
|
|
14823
14963
|
const effectCall = effectCallback?.parent;
|
|
14824
14964
|
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
|
|
14825
14965
|
};
|
|
@@ -15105,6 +15245,11 @@ const doesResourceResultEscape = (resourceNode, allowReturnedResourceEscape, all
|
|
|
15105
15245
|
parentNode = currentNode.parent;
|
|
15106
15246
|
continue;
|
|
15107
15247
|
}
|
|
15248
|
+
if (isNodeOfType(parentNode, "ConditionalExpression") && (parentNode.consequent === currentNode || parentNode.alternate === currentNode) || isNodeOfType(parentNode, "LogicalExpression") && (parentNode.right === currentNode || parentNode.left === currentNode && parentNode.operator !== "&&")) {
|
|
15249
|
+
currentNode = parentNode;
|
|
15250
|
+
parentNode = currentNode.parent;
|
|
15251
|
+
continue;
|
|
15252
|
+
}
|
|
15108
15253
|
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode && isNodeOfType(parentNode.id, "Identifier") && isNodeOfType(parentNode.parent, "VariableDeclaration") && parentNode.parent.kind === "const") {
|
|
15109
15254
|
const ownerFunction = findEnclosingFunction$1(resourceNode);
|
|
15110
15255
|
const resourceSymbol = context.scopes.symbolFor(parentNode.id);
|
|
@@ -34171,6 +34316,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34171
34316
|
id: "no-chain-state-updates",
|
|
34172
34317
|
title: "State updates chained through effects",
|
|
34173
34318
|
severity: "warn",
|
|
34319
|
+
disabledWhen: ["react:18"],
|
|
34174
34320
|
tags: ["test-noise"],
|
|
34175
34321
|
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
34322
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -53124,12 +53270,6 @@ const isInsideEs6Component$1 = (methodDefinition) => {
|
|
|
53124
53270
|
if (!owningClass) return false;
|
|
53125
53271
|
return isPreactOrReactComponentClass(owningClass);
|
|
53126
53272
|
};
|
|
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
53273
|
const preactNoRenderArguments = defineRule({
|
|
53134
53274
|
id: "preact-no-render-arguments",
|
|
53135
53275
|
title: "render() reads props from arguments",
|