oxlint-plugin-react-doctor 0.7.6-dev.1d7b6e2 → 0.7.6-dev.3f22fe3
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/README.md +1 -1
- package/dist/index.js +751 -434
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6004,6 +6004,170 @@ const getStaticPropertyKeyName = (node, options = {}) => {
|
|
|
6004
6004
|
return null;
|
|
6005
6005
|
};
|
|
6006
6006
|
//#endregion
|
|
6007
|
+
//#region src/plugin/utils/are-expressions-structurally-equal.ts
|
|
6008
|
+
const areExpressionsStructurallyEqual = (a, b) => {
|
|
6009
|
+
if (!a || !b) return a === b;
|
|
6010
|
+
const unwrappedA = stripParenExpression(a);
|
|
6011
|
+
const unwrappedB = stripParenExpression(b);
|
|
6012
|
+
if (unwrappedA !== a || unwrappedB !== b) return areExpressionsStructurallyEqual(unwrappedA, unwrappedB);
|
|
6013
|
+
if (a.type !== b.type) return false;
|
|
6014
|
+
if (isNodeOfType(a, "ThisExpression")) return true;
|
|
6015
|
+
if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
|
|
6016
|
+
if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
|
|
6017
|
+
if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
|
|
6018
|
+
if (a.computed !== b.computed) return false;
|
|
6019
|
+
return areExpressionsStructurallyEqual(a.object, b.object) && areExpressionsStructurallyEqual(a.property, b.property);
|
|
6020
|
+
}
|
|
6021
|
+
if (isNodeOfType(a, "CallExpression") && isNodeOfType(b, "CallExpression")) {
|
|
6022
|
+
if (!areExpressionsStructurallyEqual(a.callee, b.callee)) return false;
|
|
6023
|
+
const argumentsA = a.arguments ?? [];
|
|
6024
|
+
const argumentsB = b.arguments ?? [];
|
|
6025
|
+
if (argumentsA.length !== argumentsB.length) return false;
|
|
6026
|
+
return argumentsA.every((argument, index) => areExpressionsStructurallyEqual(argument, argumentsB[index]));
|
|
6027
|
+
}
|
|
6028
|
+
return false;
|
|
6029
|
+
};
|
|
6030
|
+
//#endregion
|
|
6031
|
+
//#region src/plugin/utils/has-keyboard-activatable-descendant.ts
|
|
6032
|
+
const NATIVE_KEYBOARD_ACTIVATABLE_TAGS = new Set([
|
|
6033
|
+
"a",
|
|
6034
|
+
"button",
|
|
6035
|
+
"input",
|
|
6036
|
+
"select",
|
|
6037
|
+
"summary",
|
|
6038
|
+
"textarea"
|
|
6039
|
+
]);
|
|
6040
|
+
const KEYBOARD_ACTIVATABLE_COMPONENT_NAME_PATTERN = /button|link|nav|anchor/i;
|
|
6041
|
+
const EQUIVALENT_ACTION_COMPONENT_NAME_PATTERN = /(?:button|link|anchor)$/i;
|
|
6042
|
+
const UPPERCASE_COMPONENT_NAME_PATTERN = /^[A-Z]/;
|
|
6043
|
+
const DESCENDANT_ACTION_PROP_NAMES = ["onClick", "onPress"];
|
|
6044
|
+
const isStaticallyNullish = (expression) => isNullishExpression(stripParenExpression(expression));
|
|
6045
|
+
const isStaticallyNullishHandlerExpression = (expression, scopes) => {
|
|
6046
|
+
const strippedExpression = stripParenExpression(expression);
|
|
6047
|
+
if (isNullishExpression(strippedExpression)) return true;
|
|
6048
|
+
const symbol = resolveConstIdentifierAlias(strippedExpression, scopes);
|
|
6049
|
+
if (symbol?.kind !== "const" || !symbol.initializer) return false;
|
|
6050
|
+
return isNullishExpression(stripParenExpression(symbol.initializer));
|
|
6051
|
+
};
|
|
6052
|
+
const resolveSingleHandlerAction = (expression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
6053
|
+
const strippedExpression = stripParenExpression(expression);
|
|
6054
|
+
if (isStaticallyNullishHandlerExpression(strippedExpression, scopes)) return null;
|
|
6055
|
+
if (isNodeOfType(strippedExpression, "ConditionalExpression")) {
|
|
6056
|
+
const consequent = strippedExpression.consequent;
|
|
6057
|
+
const alternate = strippedExpression.alternate;
|
|
6058
|
+
if (isStaticallyNullishHandlerExpression(consequent, scopes)) return resolveSingleHandlerAction(alternate, scopes, visitedSymbolIds);
|
|
6059
|
+
if (isStaticallyNullishHandlerExpression(alternate, scopes)) return resolveSingleHandlerAction(consequent, scopes, visitedSymbolIds);
|
|
6060
|
+
return null;
|
|
6061
|
+
}
|
|
6062
|
+
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
6063
|
+
const symbol = scopes.symbolFor(strippedExpression);
|
|
6064
|
+
if (symbol?.kind === "const" && symbol.initializer && isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") && !visitedSymbolIds.has(symbol.id)) {
|
|
6065
|
+
visitedSymbolIds.add(symbol.id);
|
|
6066
|
+
return resolveSingleHandlerAction(symbol.initializer, scopes, visitedSymbolIds);
|
|
6067
|
+
}
|
|
6068
|
+
return strippedExpression;
|
|
6069
|
+
}
|
|
6070
|
+
if (isNodeOfType(strippedExpression, "ArrowFunctionExpression") || isNodeOfType(strippedExpression, "FunctionExpression") || isNodeOfType(strippedExpression, "FunctionDeclaration")) {
|
|
6071
|
+
const body = stripParenExpression(strippedExpression.body);
|
|
6072
|
+
if (!isNodeOfType(body, "BlockStatement")) return body;
|
|
6073
|
+
if (body.body.length !== 1) return null;
|
|
6074
|
+
const statement = body.body[0];
|
|
6075
|
+
if (isNodeOfType(statement, "ExpressionStatement")) return statement.expression;
|
|
6076
|
+
if (isNodeOfType(statement, "ReturnStatement") && statement.argument) return stripParenExpression(statement.argument);
|
|
6077
|
+
return null;
|
|
6078
|
+
}
|
|
6079
|
+
return strippedExpression;
|
|
6080
|
+
};
|
|
6081
|
+
const getAttributeAction = (attribute, scopes) => {
|
|
6082
|
+
if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
|
|
6083
|
+
return resolveSingleHandlerAction(attribute.value.expression, scopes);
|
|
6084
|
+
};
|
|
6085
|
+
const hasPotentiallyTruthyAttribute = (openingElement, attributeName) => {
|
|
6086
|
+
const attribute = hasJsxPropIgnoreCase(openingElement.attributes, attributeName);
|
|
6087
|
+
if (!attribute) return false;
|
|
6088
|
+
if (!attribute.value) return true;
|
|
6089
|
+
if (isNodeOfType(attribute.value, "Literal")) return attribute.value.value === true || attribute.value.value === "true";
|
|
6090
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return true;
|
|
6091
|
+
const expression = stripParenExpression(attribute.value.expression);
|
|
6092
|
+
return !isNodeOfType(expression, "Literal") || expression.value !== false;
|
|
6093
|
+
};
|
|
6094
|
+
const hasPotentiallyNonEmptyAttribute = (openingElement, attributeName) => {
|
|
6095
|
+
const attribute = hasJsxPropIgnoreCase(openingElement.attributes, attributeName);
|
|
6096
|
+
if (!attribute?.value) return false;
|
|
6097
|
+
if (isNodeOfType(attribute.value, "Literal")) return typeof attribute.value.value === "string" && attribute.value.value.trim().length > 0;
|
|
6098
|
+
if (!isNodeOfType(attribute.value, "JSXExpressionContainer")) return true;
|
|
6099
|
+
const expression = stripParenExpression(attribute.value.expression);
|
|
6100
|
+
if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" && expression.value.trim().length > 0;
|
|
6101
|
+
return !isStaticallyNullish(expression);
|
|
6102
|
+
};
|
|
6103
|
+
const childMayProvideAccessibleName = (child) => {
|
|
6104
|
+
if (isNodeOfType(child, "JSXText")) return child.value.trim().length > 0;
|
|
6105
|
+
if (isNodeOfType(child, "JSXElement")) return hasAccessibleNameEvidence(child);
|
|
6106
|
+
if (isNodeOfType(child, "JSXFragment")) return child.children.some((nestedChild) => childMayProvideAccessibleName(nestedChild));
|
|
6107
|
+
if (!isNodeOfType(child, "JSXExpressionContainer")) return false;
|
|
6108
|
+
const expression = stripParenExpression(child.expression);
|
|
6109
|
+
if (isNodeOfType(expression, "Literal")) {
|
|
6110
|
+
if (typeof expression.value === "string") return expression.value.trim().length > 0;
|
|
6111
|
+
return typeof expression.value === "number";
|
|
6112
|
+
}
|
|
6113
|
+
if (isStaticallyNullish(expression)) return false;
|
|
6114
|
+
if (isNodeOfType(expression, "JSXElement")) return hasAccessibleNameEvidence(expression);
|
|
6115
|
+
if (isNodeOfType(expression, "JSXFragment")) return expression.children.some((nestedChild) => childMayProvideAccessibleName(nestedChild));
|
|
6116
|
+
return true;
|
|
6117
|
+
};
|
|
6118
|
+
const hasAccessibleNameEvidence = (element) => {
|
|
6119
|
+
if (hasPotentiallyNonEmptyAttribute(element.openingElement, "aria-label") || hasPotentiallyNonEmptyAttribute(element.openingElement, "aria-labelledby")) return true;
|
|
6120
|
+
return element.children.some((child) => childMayProvideAccessibleName(child));
|
|
6121
|
+
};
|
|
6122
|
+
const hasNegativeStaticTabIndex = (openingElement) => {
|
|
6123
|
+
const tabIndexAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "tabIndex");
|
|
6124
|
+
if (!tabIndexAttribute?.value) return false;
|
|
6125
|
+
const tabIndexValue = parseJsxValue(tabIndexAttribute.value);
|
|
6126
|
+
return tabIndexValue !== null && tabIndexValue < 0;
|
|
6127
|
+
};
|
|
6128
|
+
const isHiddenSubtreeRoot = (openingElement, settings) => isHiddenFromScreenReader(openingElement, settings) || hasPotentiallyTruthyAttribute(openingElement, "hidden");
|
|
6129
|
+
const isKeyboardActivatableElement = (element, requiresAccessibleName) => {
|
|
6130
|
+
const openingElement = element.openingElement;
|
|
6131
|
+
const elementName = flattenJsxName$1(openingElement.name);
|
|
6132
|
+
if (!elementName) return false;
|
|
6133
|
+
if (HTML_TAGS.has(elementName)) {
|
|
6134
|
+
if (!NATIVE_KEYBOARD_ACTIVATABLE_TAGS.has(elementName)) return false;
|
|
6135
|
+
} else if (requiresAccessibleName) {
|
|
6136
|
+
if (!UPPERCASE_COMPONENT_NAME_PATTERN.test(elementName) || !EQUIVALENT_ACTION_COMPONENT_NAME_PATTERN.test(elementName)) return false;
|
|
6137
|
+
} else if (!UPPERCASE_COMPONENT_NAME_PATTERN.test(elementName) || !KEYBOARD_ACTIVATABLE_COMPONENT_NAME_PATTERN.test(elementName)) return false;
|
|
6138
|
+
if (!requiresAccessibleName) return true;
|
|
6139
|
+
if (elementName === "a" && !hasJsxPropIgnoreCase(openingElement.attributes, "href")) return false;
|
|
6140
|
+
if (hasPotentiallyTruthyAttribute(openingElement, "disabled") || hasPotentiallyTruthyAttribute(openingElement, "isDisabled") || hasPotentiallyTruthyAttribute(openingElement, "aria-disabled") || hasNegativeStaticTabIndex(openingElement)) return false;
|
|
6141
|
+
return hasAccessibleNameEvidence(element);
|
|
6142
|
+
};
|
|
6143
|
+
const findKeyboardActivatableDescendant = (node, expectedAction, scopes, settings) => {
|
|
6144
|
+
const walk = (descendant) => findKeyboardActivatableDescendant(descendant, expectedAction, scopes, settings);
|
|
6145
|
+
if (isNodeOfType(node, "JSXElement")) {
|
|
6146
|
+
if (expectedAction && isHiddenSubtreeRoot(node.openingElement, settings)) return false;
|
|
6147
|
+
if (isKeyboardActivatableElement(node, expectedAction !== null)) {
|
|
6148
|
+
if (!expectedAction) return true;
|
|
6149
|
+
for (const actionPropName of DESCENDANT_ACTION_PROP_NAMES) {
|
|
6150
|
+
const attribute = hasJsxPropIgnoreCase(node.openingElement.attributes, actionPropName);
|
|
6151
|
+
const action = attribute ? getAttributeAction(attribute, scopes) : null;
|
|
6152
|
+
if (action && areExpressionsStructurallyEqual(expectedAction, action)) return true;
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
return node.children.some((child) => walk(child));
|
|
6156
|
+
}
|
|
6157
|
+
if (isNodeOfType(node, "JSXFragment")) return node.children.some((child) => walk(child));
|
|
6158
|
+
if (!expectedAction) return false;
|
|
6159
|
+
if (isNodeOfType(node, "JSXExpressionContainer")) return walk(node.expression);
|
|
6160
|
+
if (isNodeOfType(node, "LogicalExpression")) return walk(node.left) || walk(node.right);
|
|
6161
|
+
if (isNodeOfType(node, "ConditionalExpression")) return walk(node.consequent) || walk(node.alternate);
|
|
6162
|
+
return false;
|
|
6163
|
+
};
|
|
6164
|
+
const hasKeyboardActivatableDescendant = (element, interactionAttribute, scopes, settings) => {
|
|
6165
|
+
if (!element || !isNodeOfType(element, "JSXElement")) return false;
|
|
6166
|
+
const expectedAction = interactionAttribute ? getAttributeAction(interactionAttribute, scopes) : null;
|
|
6167
|
+
if (interactionAttribute && !expectedAction) return false;
|
|
6168
|
+
return element.children.some((child) => findKeyboardActivatableDescendant(child, expectedAction, scopes, settings));
|
|
6169
|
+
};
|
|
6170
|
+
//#endregion
|
|
6007
6171
|
//#region src/plugin/utils/is-presentation-role.ts
|
|
6008
6172
|
const isPresentationRole = (openingElement) => {
|
|
6009
6173
|
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
@@ -6012,6 +6176,53 @@ const isPresentationRole = (openingElement) => {
|
|
|
6012
6176
|
return value !== null && PRESENTATION_ROLES$1.has(value);
|
|
6013
6177
|
};
|
|
6014
6178
|
//#endregion
|
|
6179
|
+
//#region src/plugin/utils/get-property-key-name.ts
|
|
6180
|
+
const getPropertyKeyName$2 = (propertyKey) => {
|
|
6181
|
+
if (isNodeOfType(propertyKey, "Identifier") || isNodeOfType(propertyKey, "PrivateIdentifier")) return propertyKey.name;
|
|
6182
|
+
};
|
|
6183
|
+
//#endregion
|
|
6184
|
+
//#region src/plugin/utils/resolve-member-handler-function.ts
|
|
6185
|
+
const asHandlerFunction = (value) => {
|
|
6186
|
+
if (isNodeOfType(value, "FunctionExpression") || isNodeOfType(value, "ArrowFunctionExpression")) return value;
|
|
6187
|
+
};
|
|
6188
|
+
const resolveFromClassBody = (classBody, propertyName) => {
|
|
6189
|
+
for (const classElement of classBody.body) {
|
|
6190
|
+
if (!isNodeOfType(classElement, "MethodDefinition") && !isNodeOfType(classElement, "PropertyDefinition")) continue;
|
|
6191
|
+
if (getPropertyKeyName$2(classElement.key) !== propertyName) continue;
|
|
6192
|
+
const resolvedHandler = asHandlerFunction(classElement.value);
|
|
6193
|
+
if (resolvedHandler) return resolvedHandler;
|
|
6194
|
+
}
|
|
6195
|
+
};
|
|
6196
|
+
const resolveFromObjectExpression = (objectExpression, propertyName) => {
|
|
6197
|
+
for (const objectProperty of objectExpression.properties) {
|
|
6198
|
+
if (!isNodeOfType(objectProperty, "Property")) continue;
|
|
6199
|
+
if (getPropertyKeyName$2(objectProperty.key) !== propertyName) continue;
|
|
6200
|
+
const resolvedHandler = asHandlerFunction(objectProperty.value);
|
|
6201
|
+
if (resolvedHandler) return resolvedHandler;
|
|
6202
|
+
}
|
|
6203
|
+
};
|
|
6204
|
+
const resolveMemberHandlerFunction = (handler) => {
|
|
6205
|
+
const propertyName = getPropertyKeyName$2(handler.property);
|
|
6206
|
+
if (propertyName === void 0) return void 0;
|
|
6207
|
+
const objectNode = handler.object;
|
|
6208
|
+
if (isNodeOfType(objectNode, "ThisExpression")) {
|
|
6209
|
+
let ancestor = handler.parent;
|
|
6210
|
+
while (ancestor) {
|
|
6211
|
+
if (isNodeOfType(ancestor, "ClassBody")) return resolveFromClassBody(ancestor, propertyName);
|
|
6212
|
+
if (isNodeOfType(ancestor, "ObjectExpression")) {
|
|
6213
|
+
const resolvedHandler = resolveFromObjectExpression(ancestor, propertyName);
|
|
6214
|
+
if (resolvedHandler) return resolvedHandler;
|
|
6215
|
+
}
|
|
6216
|
+
ancestor = ancestor.parent;
|
|
6217
|
+
}
|
|
6218
|
+
return;
|
|
6219
|
+
}
|
|
6220
|
+
if (isNodeOfType(objectNode, "Identifier")) {
|
|
6221
|
+
const binding = findVariableInitializer(objectNode, objectNode.name);
|
|
6222
|
+
if (isNodeOfType(binding?.initializer, "ObjectExpression")) return resolveFromObjectExpression(binding.initializer, propertyName);
|
|
6223
|
+
}
|
|
6224
|
+
};
|
|
6225
|
+
//#endregion
|
|
6015
6226
|
//#region src/plugin/utils/is-pure-event-blocker-handler.ts
|
|
6016
6227
|
const BLOCKER_METHOD_NAMES = new Set([
|
|
6017
6228
|
"stopPropagation",
|
|
@@ -6043,9 +6254,10 @@ const isPureEventBlockerBody = (body) => {
|
|
|
6043
6254
|
};
|
|
6044
6255
|
const isPureEventBlockerHandler = (attribute) => {
|
|
6045
6256
|
if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return false;
|
|
6046
|
-
const expression = attribute.value.expression;
|
|
6257
|
+
const expression = stripParenExpression(attribute.value.expression);
|
|
6047
6258
|
if (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression")) return isPureEventBlockerBody(expression.body);
|
|
6048
|
-
return false;
|
|
6259
|
+
if (!isNodeOfType(expression, "MemberExpression")) return false;
|
|
6260
|
+
return isPureEventBlockerBody(resolveMemberHandlerFunction(expression)?.body);
|
|
6049
6261
|
};
|
|
6050
6262
|
//#endregion
|
|
6051
6263
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
@@ -6197,29 +6409,6 @@ const hasCompositeItemRole = (node) => {
|
|
|
6197
6409
|
const firstRole = roleValue.value.split(/\s+/)[0];
|
|
6198
6410
|
return Boolean(firstRole && COMPOSITE_ITEM_ROLES$1.has(firstRole.toLowerCase()));
|
|
6199
6411
|
};
|
|
6200
|
-
const NATIVE_ACTIVATABLE_TAGS = new Set([
|
|
6201
|
-
"a",
|
|
6202
|
-
"button",
|
|
6203
|
-
"input",
|
|
6204
|
-
"select",
|
|
6205
|
-
"textarea",
|
|
6206
|
-
"summary"
|
|
6207
|
-
]);
|
|
6208
|
-
const INTERACTIVE_COMPONENT_NAME_PATTERN = /button|link|nav|anchor/i;
|
|
6209
|
-
const containsKeyboardActivatableDescendant = (element) => {
|
|
6210
|
-
if (!element || !isNodeOfType(element, "JSXElement")) return false;
|
|
6211
|
-
for (const child of element.children) {
|
|
6212
|
-
const childNode = child;
|
|
6213
|
-
if (!isNodeOfType(childNode, "JSXElement")) continue;
|
|
6214
|
-
const name = flattenJsxName$1(childNode.openingElement.name);
|
|
6215
|
-
if (name) {
|
|
6216
|
-
if (NATIVE_ACTIVATABLE_TAGS.has(name)) return true;
|
|
6217
|
-
if (/^[A-Z]/.test(name) && INTERACTIVE_COMPONENT_NAME_PATTERN.test(name)) return true;
|
|
6218
|
-
}
|
|
6219
|
-
if (containsKeyboardActivatableDescendant(childNode)) return true;
|
|
6220
|
-
}
|
|
6221
|
-
return false;
|
|
6222
|
-
};
|
|
6223
6412
|
const isTargetCurrentTargetComparison = (node) => {
|
|
6224
6413
|
if (!isNodeOfType(node, "BinaryExpression")) return false;
|
|
6225
6414
|
if (node.operator !== "===" && node.operator !== "==" && node.operator !== "!==") return false;
|
|
@@ -6276,7 +6465,8 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
6276
6465
|
if (hasCompositeItemRole(node)) return;
|
|
6277
6466
|
if (isHoverSelectionListItem(tag, node)) return;
|
|
6278
6467
|
if (onClick && isBackdropDismissHandler(onClick)) return;
|
|
6279
|
-
if (
|
|
6468
|
+
if (hasKeyboardActivatableDescendant(node.parent, null, context.scopes, context.settings)) return;
|
|
6469
|
+
if (onClick && hasKeyboardActivatableDescendant(node.parent, onClick, context.scopes, context.settings)) return;
|
|
6280
6470
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
6281
6471
|
if (isPresentationRole(node)) return;
|
|
6282
6472
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler) || spreadEventValues.has(handler.toLowerCase()))) return;
|
|
@@ -8159,50 +8349,6 @@ const assignedHandlerCallsPreventDefault = (scopeOwner, handlerName) => {
|
|
|
8159
8349
|
});
|
|
8160
8350
|
return didFindPreventDefault;
|
|
8161
8351
|
};
|
|
8162
|
-
const asHandlerFunction = (value) => {
|
|
8163
|
-
if (!value) return void 0;
|
|
8164
|
-
if (isNodeOfType(value, "FunctionExpression") || isNodeOfType(value, "ArrowFunctionExpression")) return value;
|
|
8165
|
-
};
|
|
8166
|
-
const memberKeyName = (keyNode) => {
|
|
8167
|
-
if (isNodeOfType(keyNode, "Identifier") || isNodeOfType(keyNode, "PrivateIdentifier")) return keyNode.name;
|
|
8168
|
-
};
|
|
8169
|
-
const resolveFromClassBody = (classBody, propertyName) => {
|
|
8170
|
-
for (const element of classBody.body ?? []) {
|
|
8171
|
-
if (!isNodeOfType(element, "MethodDefinition") && !isNodeOfType(element, "PropertyDefinition")) continue;
|
|
8172
|
-
if (memberKeyName(element.key) !== propertyName) continue;
|
|
8173
|
-
const resolved = asHandlerFunction(element.value);
|
|
8174
|
-
if (resolved) return resolved;
|
|
8175
|
-
}
|
|
8176
|
-
};
|
|
8177
|
-
const resolveFromObjectExpression = (objectExpression, propertyName) => {
|
|
8178
|
-
for (const objectProperty of objectExpression.properties ?? []) {
|
|
8179
|
-
if (!isNodeOfType(objectProperty, "Property")) continue;
|
|
8180
|
-
if (memberKeyName(objectProperty.key) !== propertyName) continue;
|
|
8181
|
-
const resolved = asHandlerFunction(objectProperty.value);
|
|
8182
|
-
if (resolved) return resolved;
|
|
8183
|
-
}
|
|
8184
|
-
};
|
|
8185
|
-
const resolveMemberHandlerFunction = (handler) => {
|
|
8186
|
-
const propertyName = memberKeyName(handler.property);
|
|
8187
|
-
if (propertyName === void 0) return void 0;
|
|
8188
|
-
const objectNode = handler.object;
|
|
8189
|
-
if (isNodeOfType(objectNode, "ThisExpression")) {
|
|
8190
|
-
let ancestor = handler.parent;
|
|
8191
|
-
while (ancestor) {
|
|
8192
|
-
if (isNodeOfType(ancestor, "ClassBody")) return resolveFromClassBody(ancestor, propertyName);
|
|
8193
|
-
if (isNodeOfType(ancestor, "ObjectExpression")) {
|
|
8194
|
-
const resolved = resolveFromObjectExpression(ancestor, propertyName);
|
|
8195
|
-
if (resolved) return resolved;
|
|
8196
|
-
}
|
|
8197
|
-
ancestor = ancestor.parent ?? null;
|
|
8198
|
-
}
|
|
8199
|
-
return;
|
|
8200
|
-
}
|
|
8201
|
-
if (isNodeOfType(objectNode, "Identifier")) {
|
|
8202
|
-
const initializer = findVariableInitializer(objectNode, objectNode.name)?.initializer;
|
|
8203
|
-
if (initializer && isNodeOfType(initializer, "ObjectExpression")) return resolveFromObjectExpression(initializer, propertyName);
|
|
8204
|
-
}
|
|
8205
|
-
};
|
|
8206
8352
|
const handlerArgumentCallsPreventDefault = (handler) => {
|
|
8207
8353
|
if (!handler) return false;
|
|
8208
8354
|
if (handlerCallsPreventDefault(handler)) return true;
|
|
@@ -8364,7 +8510,7 @@ const handlerMayExposeEvent = (handler, context) => {
|
|
|
8364
8510
|
}
|
|
8365
8511
|
const propertyName = getStaticPropertyName(expression);
|
|
8366
8512
|
if (isNodeOfType(memberObject, "ObjectExpression") && propertyName) {
|
|
8367
|
-
const property = memberObject.properties.find((candidate) => isNodeOfType(candidate, "Property") &&
|
|
8513
|
+
const property = memberObject.properties.find((candidate) => isNodeOfType(candidate, "Property") && getPropertyKeyName$2(candidate.key) === propertyName);
|
|
8368
8514
|
return Boolean(isNodeOfType(property, "Property") && expressionContainsParameterAlias(property.value));
|
|
8369
8515
|
}
|
|
8370
8516
|
if (isNodeOfType(memberObject, "ArrayExpression") && isNodeOfType(expression.property, "Literal") && typeof expression.property.value === "number") {
|
|
@@ -8405,9 +8551,9 @@ const handlerMayExposeEvent = (handler, context) => {
|
|
|
8405
8551
|
continue;
|
|
8406
8552
|
}
|
|
8407
8553
|
if (!isNodeOfType(patternProperty, "Property")) continue;
|
|
8408
|
-
const propertyName =
|
|
8554
|
+
const propertyName = getPropertyKeyName$2(patternProperty.key);
|
|
8409
8555
|
if (!propertyName) continue;
|
|
8410
|
-
const sourceProperty = source.properties.find((property) => isNodeOfType(property, "Property") &&
|
|
8556
|
+
const sourceProperty = source.properties.find((property) => isNodeOfType(property, "Property") && getPropertyKeyName$2(property.key) === propertyName);
|
|
8411
8557
|
if (isNodeOfType(sourceProperty, "Property")) addAliasesFromPattern(patternProperty.value, sourceProperty.value);
|
|
8412
8558
|
}
|
|
8413
8559
|
return;
|
|
@@ -10279,21 +10425,29 @@ const isListenerPathAmbiguous = (node, bodyNode, allowAmbiguousChild) => {
|
|
|
10279
10425
|
};
|
|
10280
10426
|
//#endregion
|
|
10281
10427
|
//#region src/plugin/rules/state-and-effects/utils/resolve-event-listener-capture.ts
|
|
10282
|
-
const resolveEventListenerCapture = (optionsNode) => {
|
|
10428
|
+
const resolveEventListenerCapture = (optionsNode, { allowComputedString = false, allowIndeterminateEntries = false } = {}) => {
|
|
10283
10429
|
if (!optionsNode) return false;
|
|
10284
10430
|
const unwrappedOptions = stripParenExpression(optionsNode);
|
|
10285
10431
|
if (isNodeOfType(unwrappedOptions, "Literal")) return typeof unwrappedOptions.value === "boolean" ? unwrappedOptions.value : null;
|
|
10286
10432
|
if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return null;
|
|
10287
10433
|
let capture = false;
|
|
10288
|
-
for (const property of unwrappedOptions.properties) {
|
|
10434
|
+
for (const property of unwrappedOptions.properties ?? []) {
|
|
10435
|
+
if (allowIndeterminateEntries && isNodeOfType(property, "SpreadElement")) {
|
|
10436
|
+
capture = null;
|
|
10437
|
+
continue;
|
|
10438
|
+
}
|
|
10289
10439
|
if (!isNodeOfType(property, "Property")) return null;
|
|
10290
|
-
const propertyName = getStaticPropertyKeyName(property, { allowComputedString
|
|
10440
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString });
|
|
10291
10441
|
if (propertyName === null) return null;
|
|
10292
10442
|
if (!property.computed && propertyName === "__proto__") return null;
|
|
10293
10443
|
if (propertyName !== "capture") continue;
|
|
10294
10444
|
const propertyValue = stripParenExpression(property.value);
|
|
10295
|
-
if (
|
|
10296
|
-
|
|
10445
|
+
if (isNodeOfType(propertyValue, "Literal") && typeof propertyValue.value === "boolean") {
|
|
10446
|
+
capture = propertyValue.value;
|
|
10447
|
+
continue;
|
|
10448
|
+
}
|
|
10449
|
+
if (!allowIndeterminateEntries) return null;
|
|
10450
|
+
capture = null;
|
|
10297
10451
|
}
|
|
10298
10452
|
return capture;
|
|
10299
10453
|
};
|
|
@@ -10536,7 +10690,7 @@ const readListenerCandidate = (node, methodName, context) => {
|
|
|
10536
10690
|
const targetKey = resolveTargetKey(targetNode, context);
|
|
10537
10691
|
const eventName = resolveStaticEventName(node.arguments?.[0], context);
|
|
10538
10692
|
const callbackIdentity = resolveCallbackIdentity(node.arguments?.[1], context);
|
|
10539
|
-
const capture = resolveEventListenerCapture(node.arguments?.[2]);
|
|
10693
|
+
const capture = resolveEventListenerCapture(node.arguments?.[2], { allowComputedString: true });
|
|
10540
10694
|
if (targetKey === null || eventName === null) return null;
|
|
10541
10695
|
return {
|
|
10542
10696
|
node,
|
|
@@ -10570,7 +10724,7 @@ const readDestructuredRemovalCandidate = (node, context) => {
|
|
|
10570
10724
|
targetKey,
|
|
10571
10725
|
eventName,
|
|
10572
10726
|
callbackIdentity: resolveCallbackIdentity(node.arguments?.[2], context),
|
|
10573
|
-
capture: resolveEventListenerCapture(node.arguments?.[3])
|
|
10727
|
+
capture: resolveEventListenerCapture(node.arguments?.[3], { allowComputedString: true })
|
|
10574
10728
|
};
|
|
10575
10729
|
};
|
|
10576
10730
|
const resolveReturnedCleanupBody = (returnedValue, context) => {
|
|
@@ -10948,7 +11102,7 @@ const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
|
10948
11102
|
};
|
|
10949
11103
|
//#endregion
|
|
10950
11104
|
//#region src/plugin/utils/executes-during-render.ts
|
|
10951
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
11105
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
10952
11106
|
"map",
|
|
10953
11107
|
"filter",
|
|
10954
11108
|
"forEach",
|
|
@@ -10984,7 +11138,7 @@ const executesDuringRender = (functionNode, scopes) => {
|
|
|
10984
11138
|
if (parent.callee === functionNode) return true;
|
|
10985
11139
|
if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === functionNode) return true;
|
|
10986
11140
|
if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
|
|
10987
|
-
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
11141
|
+
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
|
|
10988
11142
|
};
|
|
10989
11143
|
//#endregion
|
|
10990
11144
|
//#region src/plugin/utils/find-render-phase-component-or-hook.ts
|
|
@@ -11015,6 +11169,26 @@ const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(
|
|
|
11015
11169
|
//#region src/plugin/utils/is-event-handler-attribute.ts
|
|
11016
11170
|
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
|
|
11017
11171
|
//#endregion
|
|
11172
|
+
//#region src/plugin/utils/react-ref-origin.ts
|
|
11173
|
+
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
11174
|
+
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
11175
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticPropertyName(memberExpression) !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
11176
|
+
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
11177
|
+
if (!symbol?.initializer) return null;
|
|
11178
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
11179
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
11180
|
+
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
11181
|
+
};
|
|
11182
|
+
const hasReactRefCurrentOrigin = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
11183
|
+
const expression = stripParenExpression(node);
|
|
11184
|
+
if (resolveReactRefSymbol(expression, scopes)) return true;
|
|
11185
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
11186
|
+
const symbol = resolveConstIdentifierAlias(expression, scopes);
|
|
11187
|
+
if (!symbol?.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
11188
|
+
visitedSymbolIds.add(symbol.id);
|
|
11189
|
+
return hasReactRefCurrentOrigin(symbol.initializer, scopes, visitedSymbolIds);
|
|
11190
|
+
};
|
|
11191
|
+
//#endregion
|
|
11018
11192
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
11019
11193
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
11020
11194
|
if (!node || typeof node !== "object") return;
|
|
@@ -11051,6 +11225,7 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
|
11051
11225
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
11052
11226
|
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
11053
11227
|
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
11228
|
+
const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
|
|
11054
11229
|
const RESOURCE_NOUN_BY_KIND = {
|
|
11055
11230
|
subscribe: "subscription",
|
|
11056
11231
|
timer: "timer",
|
|
@@ -11280,9 +11455,108 @@ const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
|
|
|
11280
11455
|
return !doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReleaseCalls, context);
|
|
11281
11456
|
});
|
|
11282
11457
|
};
|
|
11458
|
+
const findForOfStatementForIteratorExpression = (expression, context) => {
|
|
11459
|
+
if (!expression) return null;
|
|
11460
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
11461
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
11462
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
11463
|
+
const bindingDeclarator = symbol?.bindingIdentifier.parent;
|
|
11464
|
+
const bindingDeclaration = bindingDeclarator?.parent;
|
|
11465
|
+
const forOfStatement = bindingDeclaration?.parent;
|
|
11466
|
+
const isStableIteratorBinding = isNodeOfType(bindingDeclaration, "VariableDeclaration") && symbol?.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier));
|
|
11467
|
+
return symbol && isNodeOfType(bindingDeclarator, "VariableDeclarator") && bindingDeclarator.id === symbol.bindingIdentifier && isStableIteratorBinding && bindingDeclaration.declarations.length === 1 && isNodeOfType(forOfStatement, "ForOfStatement") && forOfStatement.left === bindingDeclaration && forOfStatement.await !== true ? forOfStatement : null;
|
|
11468
|
+
};
|
|
11469
|
+
const isAssignmentFormForOfIteratorReference = (expression, context) => {
|
|
11470
|
+
if (!expression) return false;
|
|
11471
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
11472
|
+
const referencedSymbolIds = /* @__PURE__ */ new Set();
|
|
11473
|
+
walkAst(unwrappedExpression, (expressionChild) => {
|
|
11474
|
+
if (!isNodeOfType(expressionChild, "Identifier")) return;
|
|
11475
|
+
const symbol = context.scopes.symbolFor(expressionChild);
|
|
11476
|
+
if (symbol) referencedSymbolIds.add(symbol.id);
|
|
11477
|
+
});
|
|
11478
|
+
if (referencedSymbolIds.size === 0) return false;
|
|
11479
|
+
const assignsReferencedSymbol = (root, requireAssignmentTarget) => {
|
|
11480
|
+
let didAssignReferencedSymbol = false;
|
|
11481
|
+
walkAst(root, (child) => {
|
|
11482
|
+
if (!isNodeOfType(child, "Identifier")) return;
|
|
11483
|
+
if (requireAssignmentTarget && !isWithinAssignmentTarget(child)) return;
|
|
11484
|
+
const childSymbol = context.scopes.symbolFor(child);
|
|
11485
|
+
if (childSymbol && referencedSymbolIds.has(childSymbol.id)) {
|
|
11486
|
+
didAssignReferencedSymbol = true;
|
|
11487
|
+
return false;
|
|
11488
|
+
}
|
|
11489
|
+
});
|
|
11490
|
+
return didAssignReferencedSymbol;
|
|
11491
|
+
};
|
|
11492
|
+
let currentNode = unwrappedExpression.parent;
|
|
11493
|
+
while (currentNode && !isFunctionLike$2(currentNode)) {
|
|
11494
|
+
if (isNodeOfType(currentNode, "ForOfStatement")) {
|
|
11495
|
+
const loopTarget = stripParenExpression(currentNode.left);
|
|
11496
|
+
if (!isNodeOfType(loopTarget, "VariableDeclaration") && (assignsReferencedSymbol(loopTarget, false) || assignsReferencedSymbol(currentNode.body, true))) return true;
|
|
11497
|
+
}
|
|
11498
|
+
currentNode = currentNode.parent;
|
|
11499
|
+
}
|
|
11500
|
+
return false;
|
|
11501
|
+
};
|
|
11502
|
+
const isPrivatePlainConstIdentifier = (identifier, context) => {
|
|
11503
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
11504
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
11505
|
+
if (!symbol || symbol.kind !== "const" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
|
|
11506
|
+
return !isNodeOfType(symbol.declarationNode.parent?.parent, "ExportNamedDeclaration") && !isNodeOfType(symbol.declarationNode.parent?.parent, "ExportDefaultDeclaration");
|
|
11507
|
+
};
|
|
11508
|
+
const hasOnlyReplayableCollectionReferences = (identifier, context, visitedSymbolIds) => {
|
|
11509
|
+
if (!isNodeOfType(identifier, "Identifier") || !isPrivatePlainConstIdentifier(identifier, context)) return false;
|
|
11510
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
11511
|
+
if (!symbol) return false;
|
|
11512
|
+
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
11513
|
+
visitedSymbolIds.add(symbol.id);
|
|
11514
|
+
return symbol.references.every((reference) => {
|
|
11515
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
11516
|
+
const parent = referenceRoot.parent;
|
|
11517
|
+
if (isNodeOfType(parent, "ForOfStatement") && parent.right === referenceRoot) return true;
|
|
11518
|
+
if (isNodeOfType(parent, "VariableDeclarator") && parent.init === referenceRoot) {
|
|
11519
|
+
const declaration = parent.parent;
|
|
11520
|
+
return isNodeOfType(parent.id, "Identifier") && isNodeOfType(declaration, "VariableDeclaration") && declaration.kind === "const" ? hasOnlyReplayableCollectionReferences(parent.id, context, visitedSymbolIds) : false;
|
|
11521
|
+
}
|
|
11522
|
+
return false;
|
|
11523
|
+
});
|
|
11524
|
+
};
|
|
11525
|
+
const resolveReplayableIteratorCollectionKeyUncached = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
11526
|
+
if (!expression) return null;
|
|
11527
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
11528
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier") || !isPrivatePlainConstIdentifier(unwrappedExpression, context)) return null;
|
|
11529
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
11530
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return null;
|
|
11531
|
+
visitedSymbolIds.add(symbol.id);
|
|
11532
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
11533
|
+
if (isNodeOfType(initializer, "ArrayExpression")) return (initializer.elements ?? []).every((element) => element === null || isNodeOfType(element, "Literal") && (element.value === null || typeof element.value === "boolean" || typeof element.value === "number" || typeof element.value === "string")) && hasOnlyReplayableCollectionReferences(symbol.bindingIdentifier, context, /* @__PURE__ */ new Set()) ? `symbol:${symbol.id}` : null;
|
|
11534
|
+
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
11535
|
+
return resolveReplayableIteratorCollectionKeyUncached(initializer, context, visitedSymbolIds);
|
|
11536
|
+
};
|
|
11537
|
+
const resolveReplayableIteratorCollectionKey = (expression, context) => {
|
|
11538
|
+
if (!expression) return null;
|
|
11539
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
11540
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
11541
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
11542
|
+
if (!symbol) return null;
|
|
11543
|
+
let contextCache = REPLAYABLE_ITERATOR_COLLECTION_CACHE.get(context);
|
|
11544
|
+
if (!contextCache) {
|
|
11545
|
+
contextCache = /* @__PURE__ */ new Map();
|
|
11546
|
+
REPLAYABLE_ITERATOR_COLLECTION_CACHE.set(context, contextCache);
|
|
11547
|
+
}
|
|
11548
|
+
if (contextCache.has(symbol.id)) return contextCache.get(symbol.id) ?? null;
|
|
11549
|
+
const collectionKey = resolveReplayableIteratorCollectionKeyUncached(expression, context);
|
|
11550
|
+
contextCache.set(symbol.id, collectionKey);
|
|
11551
|
+
return collectionKey;
|
|
11552
|
+
};
|
|
11283
11553
|
const resolveIteratorCollectionKey = (expression, context) => {
|
|
11284
|
-
if (!expression
|
|
11285
|
-
const
|
|
11554
|
+
if (!expression) return null;
|
|
11555
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
11556
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
11557
|
+
const forOfStatement = findForOfStatementForIteratorExpression(unwrappedExpression, context);
|
|
11558
|
+
if (forOfStatement) return resolveReplayableIteratorCollectionKey(forOfStatement.right, context);
|
|
11559
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
11286
11560
|
if (!symbol || symbol.kind !== "parameter") return null;
|
|
11287
11561
|
let callbackNode = symbol.bindingIdentifier.parent;
|
|
11288
11562
|
while (callbackNode && !isFunctionLike$2(callbackNode)) callbackNode = callbackNode.parent;
|
|
@@ -11296,6 +11570,34 @@ const resolveIteratorCollectionKey = (expression, context) => {
|
|
|
11296
11570
|
}
|
|
11297
11571
|
return null;
|
|
11298
11572
|
};
|
|
11573
|
+
const isStableLoopReceiver = (expression, context) => {
|
|
11574
|
+
if (!expression) return false;
|
|
11575
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
11576
|
+
return isNodeOfType(unwrappedExpression, "Identifier") && unwrappedExpression.name === "document" && context.scopes.isGlobalReference(unwrappedExpression);
|
|
11577
|
+
};
|
|
11578
|
+
const resolveStableLoopHandlerSymbolId = (expression, context) => {
|
|
11579
|
+
if (!expression) return null;
|
|
11580
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
11581
|
+
if (!isNodeOfType(unwrappedExpression, "Identifier")) return null;
|
|
11582
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
11583
|
+
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function" && symbol.kind !== "parameter" || !symbol.references.every((reference) => reference.flag === "read" && !isWithinAssignmentTarget(reference.identifier))) return null;
|
|
11584
|
+
return symbol.id;
|
|
11585
|
+
};
|
|
11586
|
+
const isDirectExhaustiveForOfRelease = (releaseNode, forOfStatement) => {
|
|
11587
|
+
const releaseStatement = findTransparentExpressionRoot(releaseNode).parent;
|
|
11588
|
+
if (!isNodeOfType(releaseStatement, "ExpressionStatement")) return false;
|
|
11589
|
+
if (!(isNodeOfType(forOfStatement.body, "BlockStatement") ? releaseStatement.parent === forOfStatement.body : releaseStatement === forOfStatement.body)) return false;
|
|
11590
|
+
let hasAbruptLoopExit = false;
|
|
11591
|
+
walkAst(forOfStatement.body, (child) => {
|
|
11592
|
+
if (hasAbruptLoopExit) return false;
|
|
11593
|
+
if (child !== forOfStatement.body && isFunctionLike$2(child)) return false;
|
|
11594
|
+
if (isNodeOfType(child, "BreakStatement") || isNodeOfType(child, "ContinueStatement") || isNodeOfType(child, "ReturnStatement") || isNodeOfType(child, "ThrowStatement")) {
|
|
11595
|
+
hasAbruptLoopExit = true;
|
|
11596
|
+
return false;
|
|
11597
|
+
}
|
|
11598
|
+
});
|
|
11599
|
+
return !hasAbruptLoopExit;
|
|
11600
|
+
};
|
|
11299
11601
|
const findCollectionMappingCall = (callbackNode) => {
|
|
11300
11602
|
if (!isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression") || callbackNode.async || callbackNode.generator) return null;
|
|
11301
11603
|
const callNode = callbackNode.parent;
|
|
@@ -11443,23 +11745,26 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
|
|
|
11443
11745
|
if (!isFunctionLike$2(cleanupFunction) || visitedFunctions.has(cleanupFunction)) return false;
|
|
11444
11746
|
visitedFunctions.add(cleanupFunction);
|
|
11445
11747
|
let didCleanupFunctionMatch = false;
|
|
11748
|
+
const matchingLoopOrHelperAnchors = [];
|
|
11446
11749
|
walkAst(cleanupFunction.body, (cleanupChild) => {
|
|
11447
11750
|
if (didCleanupFunctionMatch) return false;
|
|
11448
11751
|
if (cleanupChild !== cleanupFunction.body && isFunctionLike$2(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
|
|
11752
|
+
const cleanupCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
|
|
11449
11753
|
if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
|
|
11450
|
-
|
|
11451
|
-
|
|
11754
|
+
const cleanupForOfStatement = findForOfStatementForIteratorExpression(isNodeOfType(cleanupCall, "CallExpression") ? cleanupCall.arguments?.[0] : null, context);
|
|
11755
|
+
if (!cleanupForOfStatement) {
|
|
11756
|
+
didCleanupFunctionMatch = true;
|
|
11757
|
+
return false;
|
|
11758
|
+
}
|
|
11759
|
+
if (!cleanupFunction.async && !cleanupFunction.generator && isDirectExhaustiveForOfRelease(cleanupChild, cleanupForOfStatement)) matchingLoopOrHelperAnchors.push(cleanupForOfStatement);
|
|
11760
|
+
return;
|
|
11452
11761
|
}
|
|
11453
|
-
|
|
11454
|
-
|
|
11455
|
-
const stableHelperFunction = resolveStableValue(helperCall.callee, context);
|
|
11762
|
+
if (!isNodeOfType(cleanupCall, "CallExpression")) return;
|
|
11763
|
+
const stableHelperFunction = resolveStableValue(cleanupCall.callee, context);
|
|
11456
11764
|
const helperFunction = isNodeOfType(stableHelperFunction, "Identifier") ? resolveSingleAssignedCleanupFunction(stableHelperFunction, usage, context) : stableHelperFunction;
|
|
11457
|
-
if (helperFunction && isFunctionLike$2(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, visitedFunctions))
|
|
11458
|
-
didCleanupFunctionMatch = true;
|
|
11459
|
-
return false;
|
|
11460
|
-
}
|
|
11765
|
+
if (helperFunction && isFunctionLike$2(helperFunction) && !helperFunction.async && !helperFunction.generator && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, new Set(visitedFunctions))) matchingLoopOrHelperAnchors.push(cleanupCall);
|
|
11461
11766
|
});
|
|
11462
|
-
return didCleanupFunctionMatch;
|
|
11767
|
+
return didCleanupFunctionMatch || doMatchingNodesCoverEveryPathFromFunctionEntry(cleanupFunction, matchingLoopOrHelperAnchors, context);
|
|
11463
11768
|
};
|
|
11464
11769
|
const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
|
|
11465
11770
|
const callExpression = stripParenExpression(expression);
|
|
@@ -11476,6 +11781,7 @@ const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
|
|
|
11476
11781
|
};
|
|
11477
11782
|
const callbackReturnsCleanupForUsage = (callback, usage, context) => {
|
|
11478
11783
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
11784
|
+
if (callback.async) return false;
|
|
11479
11785
|
const doesReturnedValueReleaseUsage = (returnedValue) => {
|
|
11480
11786
|
if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) return true;
|
|
11481
11787
|
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
@@ -11540,6 +11846,7 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
|
11540
11846
|
const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
|
|
11541
11847
|
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
11542
11848
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
11849
|
+
if (callback.async) return false;
|
|
11543
11850
|
if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
11544
11851
|
if (!isNodeOfType(callback.body, "BlockStatement")) return callback.body === usage.node && isCleanupReturningSubscribeLikeCallExpression(callback.body);
|
|
11545
11852
|
const matchingCleanupReturns = [];
|
|
@@ -11663,10 +11970,29 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
11663
11970
|
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
11664
11971
|
if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
|
|
11665
11972
|
const releaseEventKey = resolveExpressionKey(callNode.arguments?.[0], context);
|
|
11973
|
+
const usageEventArgument = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[0] : null;
|
|
11974
|
+
const releaseEventArgument = callNode.arguments?.[0];
|
|
11975
|
+
if (isAssignmentFormForOfIteratorReference(usageEventArgument, context) || isAssignmentFormForOfIteratorReference(releaseEventArgument, context)) return false;
|
|
11666
11976
|
if (usage.eventKey !== null && releaseEventKey !== null && usage.eventKey !== releaseEventKey) {
|
|
11667
|
-
|
|
11977
|
+
if (!isNodeOfType(usage.node, "CallExpression")) return false;
|
|
11978
|
+
const usageForOfStatement = findForOfStatementForIteratorExpression(usageEventArgument, context);
|
|
11979
|
+
const releaseForOfStatement = findForOfStatementForIteratorExpression(releaseEventArgument, context);
|
|
11980
|
+
if (usageForOfStatement === null !== (releaseForOfStatement === null)) return false;
|
|
11981
|
+
const usageIteratorCollectionKey = resolveIteratorCollectionKey(usage.node.arguments?.[0], context);
|
|
11668
11982
|
const releaseIteratorCollectionKey = resolveIteratorCollectionKey(callNode.arguments?.[0], context);
|
|
11669
11983
|
if (usageIteratorCollectionKey === null || usageIteratorCollectionKey !== releaseIteratorCollectionKey) return false;
|
|
11984
|
+
if (usageForOfStatement && releaseForOfStatement) {
|
|
11985
|
+
const registrationHandlerSymbolId = resolveStableLoopHandlerSymbolId(usage.node.arguments?.[1], context);
|
|
11986
|
+
const releaseHandlerSymbolId = resolveStableLoopHandlerSymbolId(callNode.arguments?.[1], context);
|
|
11987
|
+
if (usage.registrationVerbName !== "addEventListener" || releaseVerbName !== "removeEventListener") return false;
|
|
11988
|
+
const registrationCallee = stripParenExpression(usage.node.callee);
|
|
11989
|
+
if (!isNodeOfType(registrationCallee, "MemberExpression")) return false;
|
|
11990
|
+
if (!isStableLoopReceiver(registrationCallee.object, context) || !isStableLoopReceiver(callee.object, context) || registrationHandlerSymbolId === null || registrationHandlerSymbolId !== releaseHandlerSymbolId) return false;
|
|
11991
|
+
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
11992
|
+
const releaseCapture = resolveEventListenerCapture(callNode.arguments?.[2], { allowIndeterminateEntries: true });
|
|
11993
|
+
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
11994
|
+
if (!isDirectExhaustiveForOfRelease(callNode, releaseForOfStatement)) return false;
|
|
11995
|
+
}
|
|
11670
11996
|
}
|
|
11671
11997
|
if (releaseVerbName === "on") {
|
|
11672
11998
|
const handlerArgument = callNode.arguments?.[1];
|
|
@@ -11721,6 +12047,213 @@ const fileContainsReleaseForUsage = (usage, context) => {
|
|
|
11721
12047
|
});
|
|
11722
12048
|
return didFindRelease;
|
|
11723
12049
|
};
|
|
12050
|
+
const resolveRefOwnedCleanupFunction = (expression, context) => {
|
|
12051
|
+
const resolvedExpression = resolveStableValue(expression, context);
|
|
12052
|
+
if (isFunctionLike$2(resolvedExpression)) return resolvedExpression;
|
|
12053
|
+
if (!isNodeOfType(resolvedExpression, "CallExpression") || !isReactApiCall(resolvedExpression, "useCallback", context.scopes)) return null;
|
|
12054
|
+
return getEffectCallback(resolvedExpression);
|
|
12055
|
+
};
|
|
12056
|
+
const findRefOwnedHandlerStorage = (retainedFunction, usage, context) => {
|
|
12057
|
+
if (!isFunctionLike$2(retainedFunction) || usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.handlerKey === null || !usage.receiverKey?.startsWith("global:") || !usage.eventKey?.startsWith("literal:")) return null;
|
|
12058
|
+
const usageStart = getRangeStart(usage.node);
|
|
12059
|
+
const functionCfg = context.cfg.cfgFor(retainedFunction);
|
|
12060
|
+
const usageBlock = functionCfg?.blockOf(usage.node);
|
|
12061
|
+
if (usageStart === null || !functionCfg || !usageBlock) return null;
|
|
12062
|
+
const matchingStorage = [];
|
|
12063
|
+
walkAst(retainedFunction.body, (child) => {
|
|
12064
|
+
if (child !== retainedFunction.body && isFunctionLike$2(child)) return false;
|
|
12065
|
+
if (!isNodeOfType(child, "AssignmentExpression") || child.operator !== "=") return;
|
|
12066
|
+
const assignmentStart = getRangeStart(child);
|
|
12067
|
+
const refCurrentExpression = stripParenExpression(child.left);
|
|
12068
|
+
if (assignmentStart === null || assignmentStart >= usageStart || functionCfg.blockOf(child) !== usageBlock || !isNodeOfType(refCurrentExpression, "MemberExpression") || !resolveReactRefSymbol(refCurrentExpression, context.scopes)) return;
|
|
12069
|
+
const refCurrentKey = resolveExpressionKey(refCurrentExpression, context);
|
|
12070
|
+
const refKey = resolveExpressionKey(refCurrentExpression.object, context);
|
|
12071
|
+
const storedSession = stripParenExpression(child.right);
|
|
12072
|
+
if (!refCurrentKey || !refKey || !isNodeOfType(storedSession, "ObjectExpression")) return;
|
|
12073
|
+
const storedSessionProperties = storedSession.properties ?? [];
|
|
12074
|
+
if (storedSessionProperties.some((property) => !isNodeOfType(property, "Property"))) return;
|
|
12075
|
+
for (const property of storedSessionProperties) {
|
|
12076
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
12077
|
+
const propertyName = getStaticPropertyKeyName(property);
|
|
12078
|
+
if (propertyName && resolveExpressionKey(property.value, context) === usage.handlerKey) {
|
|
12079
|
+
const handlerKey = `${refCurrentKey}.${propertyName}`;
|
|
12080
|
+
matchingStorage.push({
|
|
12081
|
+
handlerKey,
|
|
12082
|
+
refCurrentKey,
|
|
12083
|
+
refKey,
|
|
12084
|
+
assignmentNode: child
|
|
12085
|
+
});
|
|
12086
|
+
}
|
|
12087
|
+
}
|
|
12088
|
+
});
|
|
12089
|
+
return matchingStorage.length === 1 ? matchingStorage[0] : null;
|
|
12090
|
+
};
|
|
12091
|
+
const doMatchingNodesCoverEveryPathBeforeUsage = (usageNode, matchingNodes, owner, context) => {
|
|
12092
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
12093
|
+
const usageBlock = functionCfg?.blockOf(usageNode);
|
|
12094
|
+
const usageStart = getRangeStart(usageNode);
|
|
12095
|
+
if (!functionCfg || !usageBlock || usageStart === null) return false;
|
|
12096
|
+
const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
|
|
12097
|
+
if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
|
|
12098
|
+
const matchingStart = getRangeStart(matchingNode);
|
|
12099
|
+
if (matchingStart === null || matchingStart >= usageStart) return [];
|
|
12100
|
+
const matchingBlock = functionCfg.blockOf(matchingNode);
|
|
12101
|
+
return matchingBlock ? [matchingBlock] : [];
|
|
12102
|
+
}));
|
|
12103
|
+
if (matchingBlocks.size === 0) return false;
|
|
12104
|
+
if (matchingBlocks.has(usageBlock)) return true;
|
|
12105
|
+
const visitedBlocks = new Set([functionCfg.entry]);
|
|
12106
|
+
const pendingBlocks = [functionCfg.entry];
|
|
12107
|
+
while (pendingBlocks.length > 0) {
|
|
12108
|
+
const currentBlock = pendingBlocks.pop();
|
|
12109
|
+
if (!currentBlock) break;
|
|
12110
|
+
if (matchingBlocks.has(currentBlock)) continue;
|
|
12111
|
+
if (currentBlock === usageBlock) return false;
|
|
12112
|
+
for (const edge of currentBlock.successors) {
|
|
12113
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
12114
|
+
visitedBlocks.add(edge.to);
|
|
12115
|
+
pendingBlocks.push(edge.to);
|
|
12116
|
+
}
|
|
12117
|
+
}
|
|
12118
|
+
return true;
|
|
12119
|
+
};
|
|
12120
|
+
const retainedFunctionReleasesPreviousRefOwnedUsage = (retainedFunction, cleanupFunction, storageNode, context) => {
|
|
12121
|
+
if (!isFunctionLike$2(retainedFunction)) return false;
|
|
12122
|
+
const retainedFunctionBody = retainedFunction.body;
|
|
12123
|
+
const cleanupCalls = [];
|
|
12124
|
+
walkAst(retainedFunctionBody, (child) => {
|
|
12125
|
+
if (child !== retainedFunctionBody && isFunctionLike$2(child)) return false;
|
|
12126
|
+
if (isNodeOfType(child, "CallExpression") && resolveRefOwnedCleanupFunction(child.callee, context) === cleanupFunction) cleanupCalls.push(child);
|
|
12127
|
+
});
|
|
12128
|
+
return doMatchingNodesCoverEveryPathBeforeUsage(storageNode, cleanupCalls, retainedFunction, context);
|
|
12129
|
+
};
|
|
12130
|
+
const isDirectRefOwnedRelease = (releaseNode, cleanupFunction, usage, storedHandlerKey, refCurrentKey, context) => {
|
|
12131
|
+
if (!isFunctionLike$2(cleanupFunction)) return false;
|
|
12132
|
+
const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
|
|
12133
|
+
if (!isNodeOfType(releaseCall, "CallExpression") || !isNodeOfType(releaseCall.callee, "MemberExpression") || releaseCall.callee.computed || !isNodeOfType(releaseCall.callee.property, "Identifier") || releaseCall.callee.property.name !== "removeEventListener" || !isNodeOfType(usage.node, "CallExpression") || usage.node.arguments?.[2] !== void 0 || releaseCall.arguments?.[2] !== void 0) return false;
|
|
12134
|
+
const releaseStatement = findTransparentExpressionRoot(releaseNode).parent;
|
|
12135
|
+
const releaseBlock = releaseStatement?.parent;
|
|
12136
|
+
const releaseGuard = releaseBlock?.parent;
|
|
12137
|
+
const isDirectCleanupStatement = releaseBlock === cleanupFunction.body;
|
|
12138
|
+
const isRefPresenceGuardedStatement = Boolean(isNodeOfType(releaseBlock, "BlockStatement") && isNodeOfType(releaseGuard, "IfStatement") && releaseGuard.consequent === releaseBlock && releaseGuard.alternate === null && resolveExpressionKey(releaseGuard.test, context) === refCurrentKey);
|
|
12139
|
+
if (!isNodeOfType(releaseStatement, "ExpressionStatement") || !isNodeOfType(cleanupFunction.body, "BlockStatement") || !isDirectCleanupStatement && !isRefPresenceGuardedStatement) return false;
|
|
12140
|
+
return usage.receiverKey !== null && usage.receiverKey === resolveExpressionKey(releaseCall.callee.object, context) && usage.eventKey !== null && usage.eventKey === resolveExpressionKey(releaseCall.arguments?.[0], context) && resolveExpressionKey(releaseCall.arguments?.[1], context) === storedHandlerKey;
|
|
12141
|
+
};
|
|
12142
|
+
const isRefPresenceGuardedEarlyReturn = (returnStatement, refCurrentKey, context) => {
|
|
12143
|
+
const returnBranch = returnStatement.parent;
|
|
12144
|
+
const guardStatement = isNodeOfType(returnBranch, "BlockStatement") ? returnBranch.parent : returnBranch;
|
|
12145
|
+
const guardedConsequent = isNodeOfType(returnBranch, "BlockStatement") ? returnBranch : returnStatement;
|
|
12146
|
+
if (!isNodeOfType(guardStatement, "IfStatement") || guardStatement.consequent !== guardedConsequent || guardStatement.alternate !== null) return false;
|
|
12147
|
+
const guardTest = stripParenExpression(guardStatement.test);
|
|
12148
|
+
return isNodeOfType(guardTest, "UnaryExpression") && guardTest.operator === "!" && resolveExpressionKey(guardTest.argument, context) === refCurrentKey;
|
|
12149
|
+
};
|
|
12150
|
+
const hasUnprovenReturnBeforeRefOwnedRelease = (cleanupFunction, releaseNode, refCurrentKey, context) => {
|
|
12151
|
+
if (!isFunctionLike$2(cleanupFunction)) return true;
|
|
12152
|
+
const releaseStart = getRangeStart(releaseNode);
|
|
12153
|
+
if (releaseStart === null) return true;
|
|
12154
|
+
let hasUnprovenEarlyReturn = false;
|
|
12155
|
+
walkAst(cleanupFunction.body, (child) => {
|
|
12156
|
+
if (hasUnprovenEarlyReturn) return false;
|
|
12157
|
+
if (child !== cleanupFunction.body && isFunctionLike$2(child)) return false;
|
|
12158
|
+
if (!isNodeOfType(child, "ReturnStatement")) return;
|
|
12159
|
+
const returnStart = getRangeStart(child);
|
|
12160
|
+
if ((returnStart === null || returnStart < releaseStart) && !isRefPresenceGuardedEarlyReturn(child, refCurrentKey, context)) {
|
|
12161
|
+
hasUnprovenEarlyReturn = true;
|
|
12162
|
+
return false;
|
|
12163
|
+
}
|
|
12164
|
+
});
|
|
12165
|
+
return hasUnprovenEarlyReturn;
|
|
12166
|
+
};
|
|
12167
|
+
const cleanupFunctionReleasesRefOwnedUsage = (cleanupFunction, componentFunction, retainedFunction, usage, context) => {
|
|
12168
|
+
if (!isFunctionLike$2(cleanupFunction) || !isFunctionLike$2(componentFunction) || cleanupFunction.async || cleanupFunction.generator) return false;
|
|
12169
|
+
const storage = findRefOwnedHandlerStorage(retainedFunction, usage, context);
|
|
12170
|
+
if (!storage) return false;
|
|
12171
|
+
if (!retainedFunctionReleasesPreviousRefOwnedUsage(retainedFunction, cleanupFunction, storage.assignmentNode, context)) return false;
|
|
12172
|
+
let releaseNode = null;
|
|
12173
|
+
walkAst(cleanupFunction.body, (child) => {
|
|
12174
|
+
if (releaseNode) return false;
|
|
12175
|
+
if (child !== cleanupFunction.body && isFunctionLike$2(child)) return false;
|
|
12176
|
+
if (isDirectRefOwnedRelease(child, cleanupFunction, usage, storage.handlerKey, storage.refCurrentKey, context)) {
|
|
12177
|
+
releaseNode = child;
|
|
12178
|
+
return false;
|
|
12179
|
+
}
|
|
12180
|
+
});
|
|
12181
|
+
if (!releaseNode) return false;
|
|
12182
|
+
if (hasUnprovenReturnBeforeRefOwnedRelease(cleanupFunction, releaseNode, storage.refCurrentKey, context)) return false;
|
|
12183
|
+
let hasUnsafeRefWrite = false;
|
|
12184
|
+
walkAst(componentFunction.body, (child) => {
|
|
12185
|
+
if (hasUnsafeRefWrite) return false;
|
|
12186
|
+
if (isNodeOfType(child, "UnaryExpression") && child.operator === "delete") {
|
|
12187
|
+
const deleteTarget = stripParenExpression(child.argument);
|
|
12188
|
+
if (resolveExpressionKey(deleteTarget, context) === storage.handlerKey || isNodeOfType(deleteTarget, "MemberExpression") && resolveExpressionKey(deleteTarget.object, context) === storage.refCurrentKey) {
|
|
12189
|
+
hasUnsafeRefWrite = true;
|
|
12190
|
+
return false;
|
|
12191
|
+
}
|
|
12192
|
+
return;
|
|
12193
|
+
}
|
|
12194
|
+
if (isNodeOfType(child, "CallExpression")) {
|
|
12195
|
+
if ((child.arguments ?? []).some((argumentNode) => {
|
|
12196
|
+
const argumentKey = resolveExpressionKey(argumentNode, context);
|
|
12197
|
+
return argumentKey !== null && (argumentKey === storage.refKey || argumentKey === storage.refCurrentKey);
|
|
12198
|
+
})) {
|
|
12199
|
+
hasUnsafeRefWrite = true;
|
|
12200
|
+
return false;
|
|
12201
|
+
}
|
|
12202
|
+
return;
|
|
12203
|
+
}
|
|
12204
|
+
if (!isNodeOfType(child, "AssignmentExpression")) return;
|
|
12205
|
+
const assignmentTarget = stripParenExpression(child.left);
|
|
12206
|
+
if (isNodeOfType(assignmentTarget, "MemberExpression") && assignmentTarget.computed && resolveExpressionKey(assignmentTarget.object, context) === storage.refCurrentKey) {
|
|
12207
|
+
hasUnsafeRefWrite = true;
|
|
12208
|
+
return false;
|
|
12209
|
+
}
|
|
12210
|
+
const assignedKey = resolveExpressionKey(child.left, context);
|
|
12211
|
+
if (assignedKey === storage.handlerKey) {
|
|
12212
|
+
hasUnsafeRefWrite = true;
|
|
12213
|
+
return false;
|
|
12214
|
+
}
|
|
12215
|
+
if (assignedKey !== storage.refCurrentKey) return;
|
|
12216
|
+
const assignedValue = stripParenExpression(child.right);
|
|
12217
|
+
if (isNodeOfType(assignedValue, "Literal") && assignedValue.value === null && findEnclosingFunction(child) === cleanupFunction) return;
|
|
12218
|
+
const assignedSessionProperties = (isNodeOfType(assignedValue, "ObjectExpression") ? assignedValue : null)?.properties ?? [];
|
|
12219
|
+
const storesMatchingHandler = assignedSessionProperties.every((property) => isNodeOfType(property, "Property")) && assignedSessionProperties.some((property) => isNodeOfType(property, "Property") && `${storage.refCurrentKey}.${getStaticPropertyKeyName(property) ?? ""}` === storage.handlerKey && resolveExpressionKey(property.value, context) === usage.handlerKey);
|
|
12220
|
+
if (findEnclosingFunction(child) !== retainedFunction || !storesMatchingHandler) {
|
|
12221
|
+
hasUnsafeRefWrite = true;
|
|
12222
|
+
return false;
|
|
12223
|
+
}
|
|
12224
|
+
});
|
|
12225
|
+
return !hasUnsafeRefWrite;
|
|
12226
|
+
};
|
|
12227
|
+
const effectReturnsRefOwnedCleanup = (effectCallback, componentFunction, retainedFunction, usage, context) => {
|
|
12228
|
+
const matchesReturnedCleanup = (returnedValue) => {
|
|
12229
|
+
const cleanupFunction = resolveRefOwnedCleanupFunction(returnedValue, context);
|
|
12230
|
+
return Boolean(cleanupFunction && cleanupFunctionReleasesRefOwnedUsage(cleanupFunction, componentFunction, retainedFunction, usage, context));
|
|
12231
|
+
};
|
|
12232
|
+
if (!isFunctionLike$2(effectCallback)) return false;
|
|
12233
|
+
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return matchesReturnedCleanup(stripParenExpression(effectCallback.body));
|
|
12234
|
+
const matchingReturns = [];
|
|
12235
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
12236
|
+
if (isNodeOfType(child, "ReturnStatement") && child.argument && matchesReturnedCleanup(stripParenExpression(child.argument))) matchingReturns.push(child);
|
|
12237
|
+
});
|
|
12238
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(effectCallback, matchingReturns, context);
|
|
12239
|
+
};
|
|
12240
|
+
const hasGuaranteedRefOwnedUnmountCleanup = (retainedFunction, usage, context) => {
|
|
12241
|
+
const componentFunction = findEnclosingFunction(retainedFunction);
|
|
12242
|
+
if (!componentFunction || !isFunctionLike$2(componentFunction)) return false;
|
|
12243
|
+
let didFindCleanupEffect = false;
|
|
12244
|
+
walkAst(componentFunction.body, (child) => {
|
|
12245
|
+
if (didFindCleanupEffect) return false;
|
|
12246
|
+
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction(child) !== componentFunction || !isReactApiCall(child, "useEffect", context.scopes)) return;
|
|
12247
|
+
const effectStatement = findTransparentExpressionRoot(child).parent;
|
|
12248
|
+
if (!isNodeOfType(effectStatement, "ExpressionStatement") || effectStatement.parent !== componentFunction.body) return;
|
|
12249
|
+
const effectCallback = getEffectCallback(child);
|
|
12250
|
+
if (effectCallback && effectReturnsRefOwnedCleanup(effectCallback, componentFunction, retainedFunction, usage, context)) {
|
|
12251
|
+
didFindCleanupEffect = true;
|
|
12252
|
+
return false;
|
|
12253
|
+
}
|
|
12254
|
+
});
|
|
12255
|
+
return didFindCleanupEffect;
|
|
12256
|
+
};
|
|
11724
12257
|
const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
|
|
11725
12258
|
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
11726
12259
|
if (!bindingIdentifier) return false;
|
|
@@ -11761,7 +12294,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
11761
12294
|
let leak = null;
|
|
11762
12295
|
const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
|
|
11763
12296
|
const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
|
|
11764
|
-
const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context);
|
|
12297
|
+
const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context) || hasGuaranteedRefOwnedUnmountCleanup(retainedFunction, usage, context);
|
|
11765
12298
|
walkAst(body, (child) => {
|
|
11766
12299
|
if (leak !== null) return false;
|
|
11767
12300
|
if (isFunctionLike$2(child)) return false;
|
|
@@ -12970,7 +13503,7 @@ const stringifyMemberChain = (node) => {
|
|
|
12970
13503
|
}
|
|
12971
13504
|
return null;
|
|
12972
13505
|
};
|
|
12973
|
-
const collectCaptureDepKeys = (callback, scopes) => {
|
|
13506
|
+
const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys) => {
|
|
12974
13507
|
const keys = /* @__PURE__ */ new Set();
|
|
12975
13508
|
const stableCapturedNames = /* @__PURE__ */ new Set();
|
|
12976
13509
|
const moduleScopeCapturedNames = /* @__PURE__ */ new Set();
|
|
@@ -13000,6 +13533,10 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
13000
13533
|
continue;
|
|
13001
13534
|
}
|
|
13002
13535
|
if (depKey === symbol.name) {
|
|
13536
|
+
if (declaredExactBindingKeys?.has(depKey)) {
|
|
13537
|
+
keys.add(depKey);
|
|
13538
|
+
continue;
|
|
13539
|
+
}
|
|
13003
13540
|
const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
13004
13541
|
if (identitySourceKeys) {
|
|
13005
13542
|
if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
|
|
@@ -13108,13 +13645,13 @@ const isRegExpLiteral = (node) => {
|
|
|
13108
13645
|
if (!isNodeOfType(node, "Literal")) return false;
|
|
13109
13646
|
return Boolean(node.regex);
|
|
13110
13647
|
};
|
|
13111
|
-
const isUnstableInitializer = (node) => {
|
|
13648
|
+
const isUnstableInitializer = (node, isNestedInitializer = false) => {
|
|
13112
13649
|
if (!node) return false;
|
|
13113
13650
|
const stripped = unwrapExpression$3(node);
|
|
13114
13651
|
if (isRegExpLiteral(stripped)) return true;
|
|
13115
|
-
if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
|
|
13116
|
-
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
13117
|
-
return isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment") || isNodeOfType(stripped, "AssignmentExpression") || isNodeOfType(stripped, "NewExpression");
|
|
13652
|
+
if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent, true) || isUnstableInitializer(stripped.alternate, true);
|
|
13653
|
+
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left, true) || isUnstableInitializer(stripped.right, true);
|
|
13654
|
+
return isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNestedInitializer && (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "FunctionExpression")) || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment") || isNodeOfType(stripped, "AssignmentExpression") || isNodeOfType(stripped, "NewExpression");
|
|
13118
13655
|
};
|
|
13119
13656
|
const isPotentiallyFreshComparedValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
13120
13657
|
const candidate = unwrapExpression$3(node);
|
|
@@ -13567,8 +14104,6 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
13567
14104
|
});
|
|
13568
14105
|
return;
|
|
13569
14106
|
}
|
|
13570
|
-
const { keys: captureKeys, stableCapturedNames, moduleScopeCapturedNames, outerFunctionCapturedNames } = collectCaptureDepKeys(callbackToAnalyze ?? callbackArgument, context.scopes);
|
|
13571
|
-
for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
|
|
13572
14107
|
const hasLiteralDepElement = depsArgument.elements.some((element) => {
|
|
13573
14108
|
if (!element) return false;
|
|
13574
14109
|
return isLiteralOrEmptyTemplate(unwrapExpression$3(element));
|
|
@@ -13582,6 +14117,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
13582
14117
|
message: buildLiteralDepMessage(hookName)
|
|
13583
14118
|
});
|
|
13584
14119
|
const declaredKeys = /* @__PURE__ */ new Set();
|
|
14120
|
+
const declaredExactBindingKeys = /* @__PURE__ */ new Set();
|
|
13585
14121
|
const declaredKeyToReportNode = /* @__PURE__ */ new Map();
|
|
13586
14122
|
const seenDeclaredKeys = /* @__PURE__ */ new Set();
|
|
13587
14123
|
let didReportRefCurrentDep = false;
|
|
@@ -13647,8 +14183,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
13647
14183
|
}
|
|
13648
14184
|
seenDeclaredKeys.add(key);
|
|
13649
14185
|
declaredKeys.add(key);
|
|
14186
|
+
if (isNodeOfType(stripped, "Identifier")) declaredExactBindingKeys.add(key);
|
|
13650
14187
|
declaredKeyToReportNode.set(key, elementNode);
|
|
13651
14188
|
}
|
|
14189
|
+
const { keys: captureKeys, stableCapturedNames, moduleScopeCapturedNames, outerFunctionCapturedNames } = collectCaptureDepKeys(callbackToAnalyze ?? callbackArgument, context.scopes, declaredExactBindingKeys);
|
|
14190
|
+
for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
|
|
13652
14191
|
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
|
|
13653
14192
|
const missingCaptureKeys = [];
|
|
13654
14193
|
for (const captureKey of captureKeys) {
|
|
@@ -15646,7 +16185,7 @@ const jotaiTqUseRawQueryAtom = defineRule({
|
|
|
15646
16185
|
});
|
|
15647
16186
|
//#endregion
|
|
15648
16187
|
//#region src/plugin/rules/js-performance/js-async-reduce-without-awaited-acc.ts
|
|
15649
|
-
const isAsyncFunctionLike
|
|
16188
|
+
const isAsyncFunctionLike = (node) => {
|
|
15650
16189
|
if (!node) return false;
|
|
15651
16190
|
if (!isNodeOfType(node, "ArrowFunctionExpression") && !isNodeOfType(node, "FunctionExpression")) return false;
|
|
15652
16191
|
return node.async === true;
|
|
@@ -15713,7 +16252,7 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
15713
16252
|
const args = node.arguments ?? [];
|
|
15714
16253
|
if (args.length === 0) return;
|
|
15715
16254
|
const reducerCandidate = stripParenExpression(args[0]);
|
|
15716
|
-
if (!isAsyncFunctionLike
|
|
16255
|
+
if (!isAsyncFunctionLike(reducerCandidate)) return;
|
|
15717
16256
|
const reducer = reducerCandidate;
|
|
15718
16257
|
if (!containsDirectAwait(reducer.body)) return;
|
|
15719
16258
|
const firstParameter = classifyFirstParameter(reducer);
|
|
@@ -16997,30 +17536,6 @@ const jsIndexMaps = defineRule({
|
|
|
16997
17536
|
} })
|
|
16998
17537
|
});
|
|
16999
17538
|
//#endregion
|
|
17000
|
-
//#region src/plugin/utils/are-expressions-structurally-equal.ts
|
|
17001
|
-
const areExpressionsStructurallyEqual = (a, b) => {
|
|
17002
|
-
if (!a || !b) return a === b;
|
|
17003
|
-
const unwrappedA = stripParenExpression(a);
|
|
17004
|
-
const unwrappedB = stripParenExpression(b);
|
|
17005
|
-
if (unwrappedA !== a || unwrappedB !== b) return areExpressionsStructurallyEqual(unwrappedA, unwrappedB);
|
|
17006
|
-
if (a.type !== b.type) return false;
|
|
17007
|
-
if (isNodeOfType(a, "ThisExpression")) return true;
|
|
17008
|
-
if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
|
|
17009
|
-
if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
|
|
17010
|
-
if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
|
|
17011
|
-
if (a.computed !== b.computed) return false;
|
|
17012
|
-
return areExpressionsStructurallyEqual(a.object, b.object) && areExpressionsStructurallyEqual(a.property, b.property);
|
|
17013
|
-
}
|
|
17014
|
-
if (isNodeOfType(a, "CallExpression") && isNodeOfType(b, "CallExpression")) {
|
|
17015
|
-
if (!areExpressionsStructurallyEqual(a.callee, b.callee)) return false;
|
|
17016
|
-
const argumentsA = a.arguments ?? [];
|
|
17017
|
-
const argumentsB = b.arguments ?? [];
|
|
17018
|
-
if (argumentsA.length !== argumentsB.length) return false;
|
|
17019
|
-
return argumentsA.every((argument, index) => areExpressionsStructurallyEqual(argument, argumentsB[index]));
|
|
17020
|
-
}
|
|
17021
|
-
return false;
|
|
17022
|
-
};
|
|
17023
|
-
//#endregion
|
|
17024
17539
|
//#region src/plugin/utils/flatten-logical-and-chain.ts
|
|
17025
17540
|
const flattenLogicalAndChain = (node) => {
|
|
17026
17541
|
if (isNodeOfType(node, "LogicalExpression") && node.operator === "&&") return [...flattenLogicalAndChain(node.left), ...flattenLogicalAndChain(node.right)];
|
|
@@ -28299,271 +28814,21 @@ const noCallComponentAsFunction = defineRule({
|
|
|
28299
28814
|
}
|
|
28300
28815
|
});
|
|
28301
28816
|
//#endregion
|
|
28302
|
-
//#region src/plugin/utils/
|
|
28303
|
-
const
|
|
28304
|
-
|
|
28305
|
-
|
|
28306
|
-
|
|
28307
|
-
|
|
28308
|
-
//#region src/plugin/utils/is-hook-binding-in-scope.ts
|
|
28309
|
-
const isHookBindingInScope = (node, query) => {
|
|
28310
|
-
const { bindingName, hookName, destructureIndex } = query;
|
|
28311
|
-
let cursor = node;
|
|
28312
|
-
while (cursor) {
|
|
28313
|
-
if (isNodeOfType(cursor, "BlockStatement") || isNodeOfType(cursor, "Program")) for (const statement of cursor.body ?? []) {
|
|
28314
|
-
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
28315
|
-
for (const declarator of statement.declarations ?? []) {
|
|
28316
|
-
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
28317
|
-
if (!isHookCall$2(declarator.init, hookName)) continue;
|
|
28318
|
-
if (destructureIndex !== void 0) {
|
|
28319
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
|
|
28320
|
-
const elements = declarator.id.elements ?? [];
|
|
28321
|
-
if (elements.length <= destructureIndex) continue;
|
|
28322
|
-
const element = elements[destructureIndex];
|
|
28323
|
-
if (isNodeOfType(element, "Identifier") && element.name === bindingName) return true;
|
|
28324
|
-
} else if (isNodeOfType(declarator.id, "Identifier") && declarator.id.name === bindingName) return true;
|
|
28325
|
-
}
|
|
28326
|
-
}
|
|
28327
|
-
cursor = cursor.parent ?? null;
|
|
28328
|
-
}
|
|
28329
|
-
return false;
|
|
28330
|
-
};
|
|
28331
|
-
//#endregion
|
|
28332
|
-
//#region src/plugin/utils/is-use-state-setter-in-scope.ts
|
|
28333
|
-
const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node, {
|
|
28334
|
-
bindingName: setterName,
|
|
28335
|
-
hookName: "useState",
|
|
28336
|
-
destructureIndex: 1
|
|
28817
|
+
//#region src/plugin/utils/define-retired-rule.ts
|
|
28818
|
+
const defineRetiredRule = (rule) => defineRule({
|
|
28819
|
+
...rule,
|
|
28820
|
+
defaultEnabled: false,
|
|
28821
|
+
lifecycle: "retired",
|
|
28822
|
+
create: () => ({})
|
|
28337
28823
|
});
|
|
28338
28824
|
//#endregion
|
|
28339
28825
|
//#region src/plugin/rules/state-and-effects/no-cascading-set-state.ts
|
|
28340
|
-
const
|
|
28341
|
-
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
28342
|
-
return false;
|
|
28343
|
-
};
|
|
28344
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
28345
|
-
"forEach",
|
|
28346
|
-
"map",
|
|
28347
|
-
"filter",
|
|
28348
|
-
"reduce",
|
|
28349
|
-
"reduceRight",
|
|
28350
|
-
"flatMap",
|
|
28351
|
-
"some",
|
|
28352
|
-
"every",
|
|
28353
|
-
"find",
|
|
28354
|
-
"findIndex",
|
|
28355
|
-
"findLast",
|
|
28356
|
-
"findLastIndex",
|
|
28357
|
-
"sort"
|
|
28358
|
-
]);
|
|
28359
|
-
const runsOnEffectDispatch = (functionNode) => {
|
|
28360
|
-
const parent = functionNode.parent;
|
|
28361
|
-
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
28362
|
-
if (parent.callee === functionNode) return true;
|
|
28363
|
-
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
28364
|
-
const callee = parent.callee;
|
|
28365
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
28366
|
-
};
|
|
28367
|
-
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
28368
|
-
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
28369
|
-
const analyzeStatementSequence = (statements, context) => {
|
|
28370
|
-
let fallThroughCount = 0;
|
|
28371
|
-
let maxTerminatedCount = 0;
|
|
28372
|
-
for (const statement of statements) {
|
|
28373
|
-
if (isNodeOfType(statement, "IfStatement")) {
|
|
28374
|
-
fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
|
|
28375
|
-
const thenSummary = analyzeBranchStatements(statement.consequent, context);
|
|
28376
|
-
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
28377
|
-
fallThroughCount: 0,
|
|
28378
|
-
maxTerminatedCount: 0,
|
|
28379
|
-
doAllPathsTerminate: false
|
|
28380
|
-
};
|
|
28381
|
-
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
28382
|
-
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
28383
|
-
fallThroughCount: 0,
|
|
28384
|
-
maxTerminatedCount,
|
|
28385
|
-
doAllPathsTerminate: true
|
|
28386
|
-
};
|
|
28387
|
-
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
28388
|
-
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
28389
|
-
continue;
|
|
28390
|
-
}
|
|
28391
|
-
if (isTerminatingStatement(statement)) {
|
|
28392
|
-
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
28393
|
-
return {
|
|
28394
|
-
fallThroughCount: 0,
|
|
28395
|
-
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
28396
|
-
doAllPathsTerminate: true
|
|
28397
|
-
};
|
|
28398
|
-
}
|
|
28399
|
-
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
28400
|
-
}
|
|
28401
|
-
return {
|
|
28402
|
-
fallThroughCount,
|
|
28403
|
-
maxTerminatedCount,
|
|
28404
|
-
doAllPathsTerminate: false
|
|
28405
|
-
};
|
|
28406
|
-
};
|
|
28407
|
-
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
28408
|
-
const summary = analyzeStatementSequence(statements, context);
|
|
28409
|
-
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
28410
|
-
};
|
|
28411
|
-
const collectLocalHelperFunctions = (root) => {
|
|
28412
|
-
const helpersByName = /* @__PURE__ */ new Map();
|
|
28413
|
-
const visit = (node) => {
|
|
28414
|
-
if (isNodeOfType(node, "FunctionDeclaration") && node.id) helpersByName.set(node.id.name, node);
|
|
28415
|
-
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.init && isFunctionLike$2(node.init)) helpersByName.set(node.id.name, node.init);
|
|
28416
|
-
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.init && isNodeOfType(node.init, "CallExpression") && isNodeOfType(node.init.callee, "Identifier") && node.init.callee.name === "useCallback" && node.init.arguments?.[0] && isFunctionLike$2(node.init.arguments[0])) helpersByName.set(node.id.name, node.init.arguments[0]);
|
|
28417
|
-
const record = node;
|
|
28418
|
-
for (const key of Object.keys(record)) {
|
|
28419
|
-
if (key === "parent") continue;
|
|
28420
|
-
const child = record[key];
|
|
28421
|
-
if (Array.isArray(child)) {
|
|
28422
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item) visit(item);
|
|
28423
|
-
} else if (child && typeof child === "object" && "type" in child) visit(child);
|
|
28424
|
-
}
|
|
28425
|
-
};
|
|
28426
|
-
visit(root);
|
|
28427
|
-
return helpersByName;
|
|
28428
|
-
};
|
|
28429
|
-
const isWholesaleDelegationCall = (callNode, effectCallback) => {
|
|
28430
|
-
const parent = callNode.parent;
|
|
28431
|
-
if (!parent) return false;
|
|
28432
|
-
if (parent === effectCallback) return true;
|
|
28433
|
-
if (!isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
28434
|
-
return parent.parent === effectCallback.body;
|
|
28435
|
-
};
|
|
28436
|
-
const countFunctionBodySetStateCalls = (functionNode, context) => {
|
|
28437
|
-
if (isAsyncFunctionLike(functionNode)) return 0;
|
|
28438
|
-
const body = functionNode.body;
|
|
28439
|
-
if (!body) return 0;
|
|
28440
|
-
return countMaxPathSetStateCalls(body, context);
|
|
28441
|
-
};
|
|
28442
|
-
const countMaxPathSetStateCalls = (node, context) => {
|
|
28443
|
-
if (!node || typeof node !== "object") return 0;
|
|
28444
|
-
if (isFunctionLike$2(node)) return 0;
|
|
28445
|
-
if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
|
|
28446
|
-
if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
|
|
28447
|
-
const consequent = node.consequent;
|
|
28448
|
-
const alternate = node.alternate;
|
|
28449
|
-
const testCount = countMaxPathSetStateCalls(node.test, context);
|
|
28450
|
-
const thenCount = countMaxPathSetStateCalls(consequent, context);
|
|
28451
|
-
const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
|
|
28452
|
-
return testCount + Math.max(thenCount, elseCount);
|
|
28453
|
-
}
|
|
28454
|
-
if (isNodeOfType(node, "SwitchStatement")) {
|
|
28455
|
-
let maxRunSetters = 0;
|
|
28456
|
-
let currentRunSetters = 0;
|
|
28457
|
-
for (const switchCase of node.cases ?? []) {
|
|
28458
|
-
const consequent = switchCase.consequent ?? [];
|
|
28459
|
-
let caseSetters = 0;
|
|
28460
|
-
let runEnds = false;
|
|
28461
|
-
for (const statement of consequent) {
|
|
28462
|
-
caseSetters += countMaxPathSetStateCalls(statement, context);
|
|
28463
|
-
if (isTerminatingStatement(statement)) runEnds = true;
|
|
28464
|
-
}
|
|
28465
|
-
currentRunSetters += caseSetters;
|
|
28466
|
-
if (runEnds) {
|
|
28467
|
-
if (currentRunSetters > maxRunSetters) maxRunSetters = currentRunSetters;
|
|
28468
|
-
currentRunSetters = 0;
|
|
28469
|
-
}
|
|
28470
|
-
}
|
|
28471
|
-
if (currentRunSetters > maxRunSetters) maxRunSetters = currentRunSetters;
|
|
28472
|
-
return maxRunSetters;
|
|
28473
|
-
}
|
|
28474
|
-
if (isNodeOfType(node, "TryStatement")) {
|
|
28475
|
-
const tryCount = countMaxPathSetStateCalls(node.block, context);
|
|
28476
|
-
const catchCount = node.handler ? countMaxPathSetStateCalls(node.handler.body, context) : 0;
|
|
28477
|
-
const finallyCount = node.finalizer ? countMaxPathSetStateCalls(node.finalizer, context) : 0;
|
|
28478
|
-
return Math.max(tryCount, catchCount) + finallyCount;
|
|
28479
|
-
}
|
|
28480
|
-
if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
|
|
28481
|
-
let nestedSettersInArgs = 0;
|
|
28482
|
-
for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$2(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
|
|
28483
|
-
return 1 + nestedSettersInArgs;
|
|
28484
|
-
}
|
|
28485
|
-
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
|
|
28486
|
-
const helperFunction = context.helpersByName.get(node.callee.name);
|
|
28487
|
-
if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
|
|
28488
|
-
context.activeHelpers.add(helperFunction);
|
|
28489
|
-
let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
|
|
28490
|
-
context.activeHelpers.delete(helperFunction);
|
|
28491
|
-
for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
|
|
28492
|
-
return helperCount;
|
|
28493
|
-
}
|
|
28494
|
-
}
|
|
28495
|
-
const countChild = (child) => {
|
|
28496
|
-
if (isFunctionLike$2(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
|
|
28497
|
-
return countMaxPathSetStateCalls(child, context);
|
|
28498
|
-
};
|
|
28499
|
-
let total = 0;
|
|
28500
|
-
const nodeRecord = node;
|
|
28501
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
28502
|
-
if (key === "parent") continue;
|
|
28503
|
-
const child = nodeRecord[key];
|
|
28504
|
-
if (Array.isArray(child)) {
|
|
28505
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
|
|
28506
|
-
} else if (child && typeof child === "object" && "type" in child) total += countChild(child);
|
|
28507
|
-
}
|
|
28508
|
-
return total;
|
|
28509
|
-
};
|
|
28510
|
-
const isInitOnlyEffect = (node) => {
|
|
28511
|
-
const depsArg = node.arguments?.[1];
|
|
28512
|
-
if (!depsArg) return false;
|
|
28513
|
-
if (!isNodeOfType(depsArg, "ArrayExpression")) return false;
|
|
28514
|
-
return (depsArg.elements ?? []).length === 0;
|
|
28515
|
-
};
|
|
28516
|
-
const DEV_ENV_FLAG_NAMES = new Set([
|
|
28517
|
-
"DEV",
|
|
28518
|
-
"PROD",
|
|
28519
|
-
"MODE",
|
|
28520
|
-
"NODE_ENV"
|
|
28521
|
-
]);
|
|
28522
|
-
const mentionsDevEnvFlag = (node) => {
|
|
28523
|
-
if (!node || typeof node !== "object") return false;
|
|
28524
|
-
if (isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && DEV_ENV_FLAG_NAMES.has(node.property.name) && isNodeOfType(node.object, "MemberExpression") && isNodeOfType(node.object.property, "Identifier") && node.object.property.name === "env") return true;
|
|
28525
|
-
const record = node;
|
|
28526
|
-
for (const key of Object.keys(record)) {
|
|
28527
|
-
if (key === "parent") continue;
|
|
28528
|
-
const child = record[key];
|
|
28529
|
-
if (Array.isArray(child)) {
|
|
28530
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item && mentionsDevEnvFlag(item)) return true;
|
|
28531
|
-
} else if (child && typeof child === "object" && "type" in child && mentionsDevEnvFlag(child)) return true;
|
|
28532
|
-
}
|
|
28533
|
-
return false;
|
|
28534
|
-
};
|
|
28535
|
-
const isDevOnlyGuardedEffect = (callback) => {
|
|
28536
|
-
const body = callback.body;
|
|
28537
|
-
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
28538
|
-
const firstStatement = (body.body ?? [])[0];
|
|
28539
|
-
if (!firstStatement) return false;
|
|
28540
|
-
if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
|
|
28541
|
-
const consequent = firstStatement.consequent;
|
|
28542
|
-
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
28543
|
-
return mentionsDevEnvFlag(firstStatement.test);
|
|
28544
|
-
};
|
|
28545
|
-
const noCascadingSetState = defineRule({
|
|
28826
|
+
const noCascadingSetState = defineRetiredRule({
|
|
28546
28827
|
id: "no-cascading-set-state",
|
|
28547
28828
|
title: "Multiple setState calls in one effect",
|
|
28548
28829
|
severity: "warn",
|
|
28549
28830
|
tags: ["test-noise"],
|
|
28550
|
-
recommendation: "
|
|
28551
|
-
create: (context) => ({ CallExpression(node) {
|
|
28552
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
28553
|
-
if (isInitOnlyEffect(node)) return;
|
|
28554
|
-
const callback = getEffectCallback(node, context.scopes);
|
|
28555
|
-
if (!callback) return;
|
|
28556
|
-
if (isDevOnlyGuardedEffect(callback)) return;
|
|
28557
|
-
const setStateCallCount = countFunctionBodySetStateCalls(callback, {
|
|
28558
|
-
helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
|
|
28559
|
-
activeHelpers: /* @__PURE__ */ new Set(),
|
|
28560
|
-
effectCallback: callback
|
|
28561
|
-
});
|
|
28562
|
-
if (setStateCallCount >= 3) context.report({
|
|
28563
|
-
node,
|
|
28564
|
-
message: `${setStateCallCount} setState calls in one useEffect redraw your screen each time they run together.`
|
|
28565
|
-
});
|
|
28566
|
-
} })
|
|
28831
|
+
recommendation: "Retired: React batches synchronous state updates from one effect into the same follow-up commit, so setter count does not prove repeated redraws."
|
|
28567
28832
|
});
|
|
28568
28833
|
//#endregion
|
|
28569
28834
|
//#region src/plugin/rules/state-and-effects/no-chain-state-updates.ts
|
|
@@ -30437,6 +30702,9 @@ const noDirectMutationState = defineRule({
|
|
|
30437
30702
|
})
|
|
30438
30703
|
});
|
|
30439
30704
|
//#endregion
|
|
30705
|
+
//#region src/plugin/utils/is-setter-identifier.ts
|
|
30706
|
+
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
30707
|
+
//#endregion
|
|
30440
30708
|
//#region src/plugin/rules/state-and-effects/utils/collect-use-state-bindings.ts
|
|
30441
30709
|
const collectUseStateBindings = (componentBody) => {
|
|
30442
30710
|
const bindings = [];
|
|
@@ -31323,7 +31591,7 @@ const noEffectChain = defineRule({
|
|
|
31323
31591
|
const effectInfos = [];
|
|
31324
31592
|
for (const effectCall of findTopLevelEffectCalls(componentBody)) {
|
|
31325
31593
|
const callback = getEffectCallback(effectCall, context.scopes);
|
|
31326
|
-
if (!callback) continue;
|
|
31594
|
+
if (!callback || !isFunctionLike$2(callback) || callback.async) continue;
|
|
31327
31595
|
const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
|
|
31328
31596
|
const writtenStateNames = collectWrittenStateNamesInEffect(analysisFunctions, setterToStateName);
|
|
31329
31597
|
effectInfos.push({
|
|
@@ -32707,6 +32975,9 @@ const noEventTriggerState = defineRule({
|
|
|
32707
32975
|
}
|
|
32708
32976
|
});
|
|
32709
32977
|
//#endregion
|
|
32978
|
+
//#region src/plugin/utils/is-setter-call.ts
|
|
32979
|
+
const isSetterCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && isSetterIdentifier(node.callee.name);
|
|
32980
|
+
//#endregion
|
|
32710
32981
|
//#region src/plugin/rules/state-and-effects/no-fetch-in-effect.ts
|
|
32711
32982
|
const IMPORT_INITIALIZER_TYPES = new Set([
|
|
32712
32983
|
"ImportSpecifier",
|
|
@@ -33954,26 +34225,6 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
33954
34225
|
} })
|
|
33955
34226
|
});
|
|
33956
34227
|
//#endregion
|
|
33957
|
-
//#region src/plugin/utils/react-ref-origin.ts
|
|
33958
|
-
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
33959
|
-
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
33960
|
-
if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticPropertyName(memberExpression) !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
33961
|
-
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
33962
|
-
if (!symbol?.initializer) return null;
|
|
33963
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
33964
|
-
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
33965
|
-
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
33966
|
-
};
|
|
33967
|
-
const hasReactRefCurrentOrigin = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
33968
|
-
const expression = stripParenExpression(node);
|
|
33969
|
-
if (resolveReactRefSymbol(expression, scopes)) return true;
|
|
33970
|
-
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
33971
|
-
const symbol = resolveConstIdentifierAlias(expression, scopes);
|
|
33972
|
-
if (!symbol?.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
33973
|
-
visitedSymbolIds.add(symbol.id);
|
|
33974
|
-
return hasReactRefCurrentOrigin(symbol.initializer, scopes, visitedSymbolIds);
|
|
33975
|
-
};
|
|
33976
|
-
//#endregion
|
|
33977
34228
|
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
33978
34229
|
const TIMER_FUNCTION_NAMES = new Set([
|
|
33979
34230
|
"cancelAnimationFrame",
|
|
@@ -40937,10 +41188,10 @@ const noStaticElementInteractions = defineRule({
|
|
|
40937
41188
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
40938
41189
|
return { JSXOpeningElement(node) {
|
|
40939
41190
|
if (isTestlikeFile) return;
|
|
40940
|
-
let hasNonBlockerHandler = false;
|
|
40941
41191
|
let hasAnyHandler = false;
|
|
40942
41192
|
let isKeyboardTarget = null;
|
|
40943
41193
|
let seenHandlerNames = null;
|
|
41194
|
+
let nonBlockerHandlerNamesLower = null;
|
|
40944
41195
|
for (const attribute of node.attributes) {
|
|
40945
41196
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
40946
41197
|
const attributeName = getJsxAttributeName(attribute.name);
|
|
@@ -40955,13 +41206,12 @@ const noStaticElementInteractions = defineRule({
|
|
|
40955
41206
|
if (!isKeyboardTarget) continue;
|
|
40956
41207
|
}
|
|
40957
41208
|
hasAnyHandler = true;
|
|
40958
|
-
if (!isPureEventBlockerHandler(attribute))
|
|
40959
|
-
hasNonBlockerHandler = true;
|
|
40960
|
-
break;
|
|
40961
|
-
}
|
|
41209
|
+
if (!isPureEventBlockerHandler(attribute)) (nonBlockerHandlerNamesLower ??= /* @__PURE__ */ new Set()).add(handlerNameLower);
|
|
40962
41210
|
}
|
|
40963
41211
|
if (!hasAnyHandler) return;
|
|
40964
|
-
if (!
|
|
41212
|
+
if (!nonBlockerHandlerNamesLower) return;
|
|
41213
|
+
const onClick = nonBlockerHandlerNamesLower.size === 1 && nonBlockerHandlerNamesLower.has("onclick") ? hasJsxPropIgnoreCase(node.attributes, "onClick") : null;
|
|
41214
|
+
if (onClick && hasKeyboardActivatableDescendant(node.parent, onClick, context.scopes, context.settings)) return;
|
|
40965
41215
|
const elementType = getElementType(node, context.settings);
|
|
40966
41216
|
if (!HTML_TAGS.has(elementType)) return;
|
|
40967
41217
|
if (elementType === "svg") return;
|
|
@@ -45418,19 +45668,63 @@ const preferTagOverRole = defineRule({
|
|
|
45418
45668
|
});
|
|
45419
45669
|
//#endregion
|
|
45420
45670
|
//#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
|
|
45421
|
-
const
|
|
45422
|
-
|
|
45423
|
-
|
|
45671
|
+
const STABLE_REACT_HOOK_VALUE_NAMES = new Set([
|
|
45672
|
+
"useActionState",
|
|
45673
|
+
"useEffectEvent",
|
|
45674
|
+
"useReducer",
|
|
45675
|
+
"useRef",
|
|
45676
|
+
"useState",
|
|
45677
|
+
"useTransition"
|
|
45678
|
+
]);
|
|
45679
|
+
const isStableReactHookDependency = (dependency, context) => {
|
|
45680
|
+
const unwrappedDependency = stripParenExpression(dependency);
|
|
45681
|
+
if (!isNodeOfType(unwrappedDependency, "Identifier")) return false;
|
|
45682
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
45683
|
+
let dependencySymbol = context.scopes.symbolFor(unwrappedDependency);
|
|
45684
|
+
while (dependencySymbol) {
|
|
45685
|
+
if (visitedSymbolIds.has(dependencySymbol.id)) return false;
|
|
45686
|
+
visitedSymbolIds.add(dependencySymbol.id);
|
|
45687
|
+
if (symbolHasStableHookOrigin(dependencySymbol, context.scopes)) {
|
|
45688
|
+
let declarator = dependencySymbol.declarationNode;
|
|
45689
|
+
while (declarator && !isNodeOfType(declarator, "VariableDeclarator")) declarator = declarator.parent;
|
|
45690
|
+
if (!declarator?.init) return false;
|
|
45691
|
+
return isReactApiCall(stripParenExpression(declarator.init), STABLE_REACT_HOOK_VALUE_NAMES, context.scopes, {
|
|
45692
|
+
allowGlobalReactNamespace: true,
|
|
45693
|
+
resolveNamedAliases: true
|
|
45694
|
+
});
|
|
45695
|
+
}
|
|
45696
|
+
if (dependencySymbol.kind !== "const" || dependencySymbol.references.some((reference) => reference.flag !== "read") || !isNodeOfType(dependencySymbol.declarationNode, "VariableDeclarator") || dependencySymbol.declarationNode.id !== dependencySymbol.bindingIdentifier || !dependencySymbol.initializer) return false;
|
|
45697
|
+
const aliasInitializer = stripParenExpression(dependencySymbol.initializer);
|
|
45698
|
+
if (!isNodeOfType(aliasInitializer, "Identifier")) return false;
|
|
45699
|
+
dependencySymbol = context.scopes.symbolFor(aliasInitializer);
|
|
45700
|
+
}
|
|
45701
|
+
return false;
|
|
45702
|
+
};
|
|
45703
|
+
const isPotentiallyChangingReactUseCallback = (initializer, context) => {
|
|
45704
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
45705
|
+
if (!isNodeOfType(unwrappedInitializer, "CallExpression")) return false;
|
|
45706
|
+
if (!isReactApiCall(unwrappedInitializer, "useCallback", context.scopes, {
|
|
45707
|
+
allowGlobalReactNamespace: true,
|
|
45708
|
+
resolveNamedAliases: true
|
|
45709
|
+
})) return false;
|
|
45710
|
+
const dependencyList = unwrappedInitializer.arguments?.[1];
|
|
45711
|
+
if (!dependencyList) return true;
|
|
45712
|
+
const unwrappedDependencyList = stripParenExpression(dependencyList);
|
|
45713
|
+
if (!isNodeOfType(unwrappedDependencyList, "ArrayExpression")) return true;
|
|
45714
|
+
return (unwrappedDependencyList.elements ?? []).some((dependency) => dependency === null || !isStableReactHookDependency(dependency, context));
|
|
45715
|
+
};
|
|
45716
|
+
const collectPotentiallyChangingCallbackBindings = (componentBody, context) => {
|
|
45717
|
+
const potentiallyChangingCallbacks = /* @__PURE__ */ new Set();
|
|
45718
|
+
if (!isNodeOfType(componentBody, "BlockStatement")) return potentiallyChangingCallbacks;
|
|
45424
45719
|
for (const statement of componentBody.body ?? []) {
|
|
45425
45720
|
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
45426
45721
|
for (const declarator of statement.declarations ?? []) {
|
|
45427
45722
|
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
45428
|
-
if (!
|
|
45429
|
-
|
|
45430
|
-
functionTypedLocals.add(declarator.id.name);
|
|
45723
|
+
if (!declarator.init || !isPotentiallyChangingReactUseCallback(declarator.init, context)) continue;
|
|
45724
|
+
potentiallyChangingCallbacks.add(declarator.id.name);
|
|
45431
45725
|
}
|
|
45432
45726
|
}
|
|
45433
|
-
return
|
|
45727
|
+
return potentiallyChangingCallbacks;
|
|
45434
45728
|
};
|
|
45435
45729
|
const findEnclosingFunctionInsideEffect = (identifierNode, effectCallback) => {
|
|
45436
45730
|
let cursor = identifierNode.parent ?? null;
|
|
@@ -45504,7 +45798,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
45504
45798
|
create: (context) => {
|
|
45505
45799
|
const checkComponent = (componentBody) => {
|
|
45506
45800
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
45507
|
-
const
|
|
45801
|
+
const potentiallyChangingCallbackBindings = collectPotentiallyChangingCallbackBindings(componentBody, context);
|
|
45508
45802
|
for (const statement of componentBody.body ?? []) {
|
|
45509
45803
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
45510
45804
|
const effectCall = statement.expression;
|
|
@@ -45522,7 +45816,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
45522
45816
|
if (!isNodeOfType(depElement, "Identifier")) continue;
|
|
45523
45817
|
const depName = depElement.name;
|
|
45524
45818
|
const isFunctionTypedPropDep = propStackTracker.isPropName(depName) && REACT_HANDLER_PROP_PATTERN.test(depName);
|
|
45525
|
-
const isFunctionTypedLocalDep =
|
|
45819
|
+
const isFunctionTypedLocalDep = potentiallyChangingCallbackBindings.has(depName);
|
|
45526
45820
|
if (!isFunctionTypedPropDep && !isFunctionTypedLocalDep) continue;
|
|
45527
45821
|
const classification = classifyCallableReadsInsideEffect(depName, callback);
|
|
45528
45822
|
if (!classification.hasAnyRead) continue;
|
|
@@ -47068,6 +47362,37 @@ const containsLocaleEnvironmentRead = (expression) => {
|
|
|
47068
47362
|
return readsLocaleEnvironment;
|
|
47069
47363
|
};
|
|
47070
47364
|
//#endregion
|
|
47365
|
+
//#region src/plugin/utils/is-hook-binding-in-scope.ts
|
|
47366
|
+
const isHookBindingInScope = (node, query) => {
|
|
47367
|
+
const { bindingName, hookName, destructureIndex } = query;
|
|
47368
|
+
let cursor = node;
|
|
47369
|
+
while (cursor) {
|
|
47370
|
+
if (isNodeOfType(cursor, "BlockStatement") || isNodeOfType(cursor, "Program")) for (const statement of cursor.body ?? []) {
|
|
47371
|
+
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
47372
|
+
for (const declarator of statement.declarations ?? []) {
|
|
47373
|
+
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
47374
|
+
if (!isHookCall$2(declarator.init, hookName)) continue;
|
|
47375
|
+
if (destructureIndex !== void 0) {
|
|
47376
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
|
|
47377
|
+
const elements = declarator.id.elements ?? [];
|
|
47378
|
+
if (elements.length <= destructureIndex) continue;
|
|
47379
|
+
const element = elements[destructureIndex];
|
|
47380
|
+
if (isNodeOfType(element, "Identifier") && element.name === bindingName) return true;
|
|
47381
|
+
} else if (isNodeOfType(declarator.id, "Identifier") && declarator.id.name === bindingName) return true;
|
|
47382
|
+
}
|
|
47383
|
+
}
|
|
47384
|
+
cursor = cursor.parent ?? null;
|
|
47385
|
+
}
|
|
47386
|
+
return false;
|
|
47387
|
+
};
|
|
47388
|
+
//#endregion
|
|
47389
|
+
//#region src/plugin/utils/is-use-state-setter-in-scope.ts
|
|
47390
|
+
const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node, {
|
|
47391
|
+
bindingName: setterName,
|
|
47392
|
+
hookName: "useState",
|
|
47393
|
+
destructureIndex: 1
|
|
47394
|
+
});
|
|
47395
|
+
//#endregion
|
|
47071
47396
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
47072
47397
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
47073
47398
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -48757,14 +49082,6 @@ const rerenderTransitionsScroll = defineRule({
|
|
|
48757
49082
|
} })
|
|
48758
49083
|
});
|
|
48759
49084
|
//#endregion
|
|
48760
|
-
//#region src/plugin/utils/define-retired-rule.ts
|
|
48761
|
-
const defineRetiredRule = (rule) => defineRule({
|
|
48762
|
-
...rule,
|
|
48763
|
-
defaultEnabled: false,
|
|
48764
|
-
lifecycle: "retired",
|
|
48765
|
-
create: () => ({})
|
|
48766
|
-
});
|
|
48767
|
-
//#endregion
|
|
48768
49085
|
//#region src/plugin/rules/react-native/rn-animate-layout-property.ts
|
|
48769
49086
|
const rnAnimateLayoutProperty = defineRetiredRule({
|
|
48770
49087
|
id: "rn-animate-layout-property",
|
|
@@ -57932,8 +58249,7 @@ const webhookSignatureRisk = defineRule({
|
|
|
57932
58249
|
});
|
|
57933
58250
|
//#endregion
|
|
57934
58251
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
57935
|
-
const
|
|
57936
|
-
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
58252
|
+
const ZOD_MODULE_SOURCES = ["zod", "zod/v4"];
|
|
57937
58253
|
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
57938
58254
|
const getImportInfoForIdentifier = (identifier) => {
|
|
57939
58255
|
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|
|
@@ -57946,7 +58262,8 @@ const computeImportInfoForIdentifier = (identifier) => {
|
|
|
57946
58262
|
if (!specifier) return null;
|
|
57947
58263
|
const declaration = specifier.parent;
|
|
57948
58264
|
if (!declaration || !isNodeOfType(declaration, "ImportDeclaration")) return null;
|
|
57949
|
-
|
|
58265
|
+
const source = declaration.source?.value;
|
|
58266
|
+
if (typeof source !== "string" || !ZOD_MODULE_SOURCES.includes(source)) return null;
|
|
57950
58267
|
if (isNodeOfType(specifier, "ImportNamespaceSpecifier")) return {
|
|
57951
58268
|
imported: null,
|
|
57952
58269
|
isDefault: false,
|