oxlint-plugin-react-doctor 0.7.6-dev.b36e439 → 0.7.6-dev.c1916d5
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 +1452 -510
- 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;
|
|
@@ -6890,6 +7080,27 @@ const createMethodMutationAnalysis = (context) => {
|
|
|
6890
7080
|
};
|
|
6891
7081
|
};
|
|
6892
7082
|
//#endregion
|
|
7083
|
+
//#region src/plugin/utils/collect-function-return-statements.ts
|
|
7084
|
+
const collectFunctionReturnStatements = (functionNode) => {
|
|
7085
|
+
if (!isFunctionLike$2(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return [];
|
|
7086
|
+
const returnStatements = [];
|
|
7087
|
+
walkAst(functionNode.body, (node) => {
|
|
7088
|
+
if (node !== functionNode.body && (isFunctionLike$2(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
7089
|
+
if (isNodeOfType(node, "ReturnStatement")) returnStatements.push(node);
|
|
7090
|
+
});
|
|
7091
|
+
return returnStatements;
|
|
7092
|
+
};
|
|
7093
|
+
//#endregion
|
|
7094
|
+
//#region src/plugin/utils/find-enclosing-function.ts
|
|
7095
|
+
const findEnclosingFunction = (node) => {
|
|
7096
|
+
let cursor = node.parent;
|
|
7097
|
+
while (cursor) {
|
|
7098
|
+
if (isFunctionLike$2(cursor)) return cursor;
|
|
7099
|
+
cursor = cursor.parent ?? null;
|
|
7100
|
+
}
|
|
7101
|
+
return null;
|
|
7102
|
+
};
|
|
7103
|
+
//#endregion
|
|
6893
7104
|
//#region src/plugin/utils/statement-always-exits.ts
|
|
6894
7105
|
const statementAlwaysExits = (statement) => {
|
|
6895
7106
|
if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
|
|
@@ -6899,17 +7110,109 @@ const statementAlwaysExits = (statement) => {
|
|
|
6899
7110
|
};
|
|
6900
7111
|
//#endregion
|
|
6901
7112
|
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
7113
|
+
const REASSIGNABLE_BINDING_KINDS = new Set(["let", "var"]);
|
|
7114
|
+
const CONDITIONAL_EXPRESSION_TYPES = new Set(["ConditionalExpression", "LogicalExpression"]);
|
|
6902
7115
|
const collectReturnedExpressions = (functionNode) => {
|
|
6903
|
-
if (!isFunctionLike$2(functionNode)) return [];
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
7116
|
+
if (!isFunctionLike$2(functionNode) || !functionNode.body) return [];
|
|
7117
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return [functionNode.body];
|
|
7118
|
+
return collectFunctionReturnStatements(functionNode).flatMap((returnStatement) => returnStatement.argument ? [returnStatement.argument] : []);
|
|
7119
|
+
};
|
|
7120
|
+
const getAssignedExpressionForWrite = (writeIdentifier) => {
|
|
7121
|
+
let assignmentTarget = writeIdentifier;
|
|
7122
|
+
let parent = assignmentTarget.parent;
|
|
7123
|
+
while (parent && stripParenExpression(parent) === writeIdentifier) {
|
|
7124
|
+
assignmentTarget = parent;
|
|
7125
|
+
parent = assignmentTarget.parent;
|
|
7126
|
+
}
|
|
7127
|
+
return parent && isNodeOfType(parent, "AssignmentExpression") && parent.operator === "=" && parent.left === assignmentTarget ? parent.right : null;
|
|
7128
|
+
};
|
|
7129
|
+
const isConditionalWithinBlock = (node, block, functionControlFlow) => {
|
|
7130
|
+
let current = node.parent ?? null;
|
|
7131
|
+
while (current && functionControlFlow.blockOf(current) === block) {
|
|
7132
|
+
if (CONDITIONAL_EXPRESSION_TYPES.has(current.type)) return true;
|
|
7133
|
+
current = current.parent ?? null;
|
|
7134
|
+
}
|
|
7135
|
+
return false;
|
|
7136
|
+
};
|
|
7137
|
+
const haveSameDefinitions = (left, right) => left.size === right.size && [...left].every((definition) => right.has(definition));
|
|
7138
|
+
const applyDefinitions = (incomingDefinitions, definitions) => {
|
|
7139
|
+
let currentDefinitions = new Set(incomingDefinitions);
|
|
7140
|
+
for (const definition of definitions) {
|
|
7141
|
+
if (!definition.isConditionalWithinBlock) currentDefinitions = /* @__PURE__ */ new Set();
|
|
7142
|
+
currentDefinitions.add(definition);
|
|
7143
|
+
}
|
|
7144
|
+
return currentDefinitions;
|
|
7145
|
+
};
|
|
7146
|
+
const collectPossibleAssignedExpressions = (symbol, referenceNode, controlFlow) => {
|
|
7147
|
+
if (!REASSIGNABLE_BINDING_KINDS.has(symbol.kind)) return symbol.initializer ? [symbol.initializer] : [];
|
|
7148
|
+
if (!controlFlow) return [];
|
|
7149
|
+
const referenceFunction = findEnclosingFunction(referenceNode);
|
|
7150
|
+
if (findEnclosingFunction(symbol.bindingIdentifier) !== referenceFunction) return [];
|
|
7151
|
+
if (!referenceFunction) return [];
|
|
7152
|
+
const functionControlFlow = controlFlow.cfgFor(referenceFunction);
|
|
7153
|
+
if (!functionControlFlow) return [];
|
|
7154
|
+
const referenceBlock = functionControlFlow.blockOf(referenceNode);
|
|
7155
|
+
if (!referenceBlock) return [];
|
|
7156
|
+
const referencePosition = getRangeStart(referenceNode);
|
|
7157
|
+
const bindingPosition = getRangeStart(symbol.bindingIdentifier);
|
|
7158
|
+
if (referencePosition === null || bindingPosition === null) return [];
|
|
7159
|
+
const definitionsByBlock = /* @__PURE__ */ new Map();
|
|
7160
|
+
const addDefinition = (expression, definitionNode, position) => {
|
|
7161
|
+
const block = functionControlFlow.blockOf(definitionNode);
|
|
7162
|
+
if (!block) return;
|
|
7163
|
+
const definitions = definitionsByBlock.get(block) ?? [];
|
|
7164
|
+
definitions.push({
|
|
7165
|
+
expression,
|
|
7166
|
+
position,
|
|
7167
|
+
isConditionalWithinBlock: isConditionalWithinBlock(definitionNode, block, functionControlFlow)
|
|
7168
|
+
});
|
|
7169
|
+
definitionsByBlock.set(block, definitions);
|
|
7170
|
+
};
|
|
7171
|
+
if (symbol.initializer) addDefinition(symbol.initializer, symbol.bindingIdentifier, bindingPosition);
|
|
7172
|
+
for (const reference of symbol.references) {
|
|
7173
|
+
const writePosition = getRangeStart(reference.identifier);
|
|
7174
|
+
if (reference.flag === "read" || findEnclosingFunction(reference.identifier) !== referenceFunction || writePosition === null || writePosition >= referencePosition) continue;
|
|
7175
|
+
const assignedExpression = getAssignedExpressionForWrite(reference.identifier);
|
|
7176
|
+
if (!assignedExpression) continue;
|
|
7177
|
+
addDefinition(assignedExpression, reference.identifier, writePosition);
|
|
7178
|
+
}
|
|
7179
|
+
for (const definitions of definitionsByBlock.values()) definitions.sort((left, right) => left.position - right.position);
|
|
7180
|
+
const incomingDefinitionsByBlock = /* @__PURE__ */ new Map();
|
|
7181
|
+
const outgoingDefinitionsByBlock = /* @__PURE__ */ new Map();
|
|
7182
|
+
const reachableBlocks = new Set([functionControlFlow.entry]);
|
|
7183
|
+
const pendingBlocks = [functionControlFlow.entry];
|
|
7184
|
+
while (pendingBlocks.length > 0) {
|
|
7185
|
+
const block = pendingBlocks.pop();
|
|
7186
|
+
if (!block) break;
|
|
7187
|
+
for (const edge of block.successors) {
|
|
7188
|
+
if (reachableBlocks.has(edge.to)) continue;
|
|
7189
|
+
reachableBlocks.add(edge.to);
|
|
7190
|
+
pendingBlocks.push(edge.to);
|
|
7191
|
+
}
|
|
7192
|
+
}
|
|
7193
|
+
if (!reachableBlocks.has(referenceBlock)) return [];
|
|
7194
|
+
let didDefinitionsChange = true;
|
|
7195
|
+
while (didDefinitionsChange) {
|
|
7196
|
+
didDefinitionsChange = false;
|
|
7197
|
+
for (const block of functionControlFlow.blocks) {
|
|
7198
|
+
if (!reachableBlocks.has(block)) continue;
|
|
7199
|
+
const incomingDefinitions = /* @__PURE__ */ new Set();
|
|
7200
|
+
for (const predecessor of block.predecessors) {
|
|
7201
|
+
if (!reachableBlocks.has(predecessor.from)) continue;
|
|
7202
|
+
for (const definition of outgoingDefinitionsByBlock.get(predecessor.from) ?? []) incomingDefinitions.add(definition);
|
|
7203
|
+
}
|
|
7204
|
+
const outgoingDefinitions = applyDefinitions(incomingDefinitions, definitionsByBlock.get(block) ?? []);
|
|
7205
|
+
const previousIncomingDefinitions = incomingDefinitionsByBlock.get(block) ?? /* @__PURE__ */ new Set();
|
|
7206
|
+
const previousOutgoingDefinitions = outgoingDefinitionsByBlock.get(block) ?? /* @__PURE__ */ new Set();
|
|
7207
|
+
if (!haveSameDefinitions(incomingDefinitions, previousIncomingDefinitions) || !haveSameDefinitions(outgoingDefinitions, previousOutgoingDefinitions)) {
|
|
7208
|
+
incomingDefinitionsByBlock.set(block, incomingDefinitions);
|
|
7209
|
+
outgoingDefinitionsByBlock.set(block, outgoingDefinitions);
|
|
7210
|
+
didDefinitionsChange = true;
|
|
7211
|
+
}
|
|
7212
|
+
}
|
|
7213
|
+
}
|
|
7214
|
+
const definitionsBeforeReference = (definitionsByBlock.get(referenceBlock) ?? []).filter((definition) => definition.position < referencePosition);
|
|
7215
|
+
return [...applyDefinitions(incomingDefinitionsByBlock.get(referenceBlock) ?? /* @__PURE__ */ new Set(), definitionsBeforeReference)].map((definition) => definition.expression);
|
|
6913
7216
|
};
|
|
6914
7217
|
const functionHasBareReturn = (functionNode) => {
|
|
6915
7218
|
if (!isFunctionLike$2(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return false;
|
|
@@ -6921,7 +7224,7 @@ const functionHasBareReturn = (functionNode) => {
|
|
|
6921
7224
|
});
|
|
6922
7225
|
return didFindBareReturn;
|
|
6923
7226
|
};
|
|
6924
|
-
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression, matchMode = "some") => {
|
|
7227
|
+
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression, controlFlow, matchMode = "some") => {
|
|
6925
7228
|
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
6926
7229
|
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
6927
7230
|
const functionMatches = (candidateFunction) => {
|
|
@@ -6938,10 +7241,11 @@ const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpressi
|
|
|
6938
7241
|
if (matchesExpression(unwrappedExpression)) return true;
|
|
6939
7242
|
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
6940
7243
|
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
6941
|
-
if (!symbol || symbol.kind !== "const"
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
7244
|
+
if (!symbol || symbol.kind !== "const" && !REASSIGNABLE_BINDING_KINDS.has(symbol.kind)) return false;
|
|
7245
|
+
return collectPossibleAssignedExpressions(symbol, unwrappedExpression, controlFlow).some((assignedExpression) => {
|
|
7246
|
+
const assignedValue = stripParenExpression(assignedExpression);
|
|
7247
|
+
return !isFunctionLike$2(assignedValue) && expressionMatches(assignedValue);
|
|
7248
|
+
});
|
|
6945
7249
|
}
|
|
6946
7250
|
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
6947
7251
|
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
@@ -7342,7 +7646,7 @@ const isProvenBrowserApiReceiver = (receiver, receiverKind, scopes, visitedSymbo
|
|
|
7342
7646
|
const callee = stripParenExpression(expression.callee);
|
|
7343
7647
|
if (isDomEventTarget && isNodeOfType(callee, "MemberExpression") && DOM_EVENT_TARGET_FACTORY_METHOD_NAMES.has(getStaticPropertyName(callee) ?? "") && isProvenBrowserApiReceiver(callee.object, receiverKind, scopes, visitedSymbolIds)) return true;
|
|
7344
7648
|
const calledFunction = getSameFileCalledFunction(expression, scopes, visitedSymbolIds);
|
|
7345
|
-
return Boolean(calledFunction && functionReturnsMatchingExpression(calledFunction, scopes, (returnedExpression) => isProvenBrowserApiReceiver(returnedExpression, receiverKind, scopes, new Set(visitedSymbolIds)), "every"));
|
|
7649
|
+
return Boolean(calledFunction && functionReturnsMatchingExpression(calledFunction, scopes, (returnedExpression) => isProvenBrowserApiReceiver(returnedExpression, receiverKind, scopes, new Set(visitedSymbolIds)), void 0, "every"));
|
|
7346
7650
|
}
|
|
7347
7651
|
if (!isNodeOfType(expression, "MemberExpression")) return false;
|
|
7348
7652
|
const classMember = getClassMemberDefinition(expression);
|
|
@@ -8045,50 +8349,6 @@ const assignedHandlerCallsPreventDefault = (scopeOwner, handlerName) => {
|
|
|
8045
8349
|
});
|
|
8046
8350
|
return didFindPreventDefault;
|
|
8047
8351
|
};
|
|
8048
|
-
const asHandlerFunction = (value) => {
|
|
8049
|
-
if (!value) return void 0;
|
|
8050
|
-
if (isNodeOfType(value, "FunctionExpression") || isNodeOfType(value, "ArrowFunctionExpression")) return value;
|
|
8051
|
-
};
|
|
8052
|
-
const memberKeyName = (keyNode) => {
|
|
8053
|
-
if (isNodeOfType(keyNode, "Identifier") || isNodeOfType(keyNode, "PrivateIdentifier")) return keyNode.name;
|
|
8054
|
-
};
|
|
8055
|
-
const resolveFromClassBody = (classBody, propertyName) => {
|
|
8056
|
-
for (const element of classBody.body ?? []) {
|
|
8057
|
-
if (!isNodeOfType(element, "MethodDefinition") && !isNodeOfType(element, "PropertyDefinition")) continue;
|
|
8058
|
-
if (memberKeyName(element.key) !== propertyName) continue;
|
|
8059
|
-
const resolved = asHandlerFunction(element.value);
|
|
8060
|
-
if (resolved) return resolved;
|
|
8061
|
-
}
|
|
8062
|
-
};
|
|
8063
|
-
const resolveFromObjectExpression = (objectExpression, propertyName) => {
|
|
8064
|
-
for (const objectProperty of objectExpression.properties ?? []) {
|
|
8065
|
-
if (!isNodeOfType(objectProperty, "Property")) continue;
|
|
8066
|
-
if (memberKeyName(objectProperty.key) !== propertyName) continue;
|
|
8067
|
-
const resolved = asHandlerFunction(objectProperty.value);
|
|
8068
|
-
if (resolved) return resolved;
|
|
8069
|
-
}
|
|
8070
|
-
};
|
|
8071
|
-
const resolveMemberHandlerFunction = (handler) => {
|
|
8072
|
-
const propertyName = memberKeyName(handler.property);
|
|
8073
|
-
if (propertyName === void 0) return void 0;
|
|
8074
|
-
const objectNode = handler.object;
|
|
8075
|
-
if (isNodeOfType(objectNode, "ThisExpression")) {
|
|
8076
|
-
let ancestor = handler.parent;
|
|
8077
|
-
while (ancestor) {
|
|
8078
|
-
if (isNodeOfType(ancestor, "ClassBody")) return resolveFromClassBody(ancestor, propertyName);
|
|
8079
|
-
if (isNodeOfType(ancestor, "ObjectExpression")) {
|
|
8080
|
-
const resolved = resolveFromObjectExpression(ancestor, propertyName);
|
|
8081
|
-
if (resolved) return resolved;
|
|
8082
|
-
}
|
|
8083
|
-
ancestor = ancestor.parent ?? null;
|
|
8084
|
-
}
|
|
8085
|
-
return;
|
|
8086
|
-
}
|
|
8087
|
-
if (isNodeOfType(objectNode, "Identifier")) {
|
|
8088
|
-
const initializer = findVariableInitializer(objectNode, objectNode.name)?.initializer;
|
|
8089
|
-
if (initializer && isNodeOfType(initializer, "ObjectExpression")) return resolveFromObjectExpression(initializer, propertyName);
|
|
8090
|
-
}
|
|
8091
|
-
};
|
|
8092
8352
|
const handlerArgumentCallsPreventDefault = (handler) => {
|
|
8093
8353
|
if (!handler) return false;
|
|
8094
8354
|
if (handlerCallsPreventDefault(handler)) return true;
|
|
@@ -8250,7 +8510,7 @@ const handlerMayExposeEvent = (handler, context) => {
|
|
|
8250
8510
|
}
|
|
8251
8511
|
const propertyName = getStaticPropertyName(expression);
|
|
8252
8512
|
if (isNodeOfType(memberObject, "ObjectExpression") && propertyName) {
|
|
8253
|
-
const property = memberObject.properties.find((candidate) => isNodeOfType(candidate, "Property") &&
|
|
8513
|
+
const property = memberObject.properties.find((candidate) => isNodeOfType(candidate, "Property") && getPropertyKeyName$2(candidate.key) === propertyName);
|
|
8254
8514
|
return Boolean(isNodeOfType(property, "Property") && expressionContainsParameterAlias(property.value));
|
|
8255
8515
|
}
|
|
8256
8516
|
if (isNodeOfType(memberObject, "ArrayExpression") && isNodeOfType(expression.property, "Literal") && typeof expression.property.value === "number") {
|
|
@@ -8291,9 +8551,9 @@ const handlerMayExposeEvent = (handler, context) => {
|
|
|
8291
8551
|
continue;
|
|
8292
8552
|
}
|
|
8293
8553
|
if (!isNodeOfType(patternProperty, "Property")) continue;
|
|
8294
|
-
const propertyName =
|
|
8554
|
+
const propertyName = getPropertyKeyName$2(patternProperty.key);
|
|
8295
8555
|
if (!propertyName) continue;
|
|
8296
|
-
const sourceProperty = source.properties.find((property) => isNodeOfType(property, "Property") &&
|
|
8556
|
+
const sourceProperty = source.properties.find((property) => isNodeOfType(property, "Property") && getPropertyKeyName$2(property.key) === propertyName);
|
|
8297
8557
|
if (isNodeOfType(sourceProperty, "Property")) addAliasesFromPattern(patternProperty.value, sourceProperty.value);
|
|
8298
8558
|
}
|
|
8299
8559
|
return;
|
|
@@ -10165,21 +10425,29 @@ const isListenerPathAmbiguous = (node, bodyNode, allowAmbiguousChild) => {
|
|
|
10165
10425
|
};
|
|
10166
10426
|
//#endregion
|
|
10167
10427
|
//#region src/plugin/rules/state-and-effects/utils/resolve-event-listener-capture.ts
|
|
10168
|
-
const resolveEventListenerCapture = (optionsNode) => {
|
|
10428
|
+
const resolveEventListenerCapture = (optionsNode, { allowComputedString = false, allowIndeterminateEntries = false } = {}) => {
|
|
10169
10429
|
if (!optionsNode) return false;
|
|
10170
10430
|
const unwrappedOptions = stripParenExpression(optionsNode);
|
|
10171
10431
|
if (isNodeOfType(unwrappedOptions, "Literal")) return typeof unwrappedOptions.value === "boolean" ? unwrappedOptions.value : null;
|
|
10172
10432
|
if (!isNodeOfType(unwrappedOptions, "ObjectExpression")) return null;
|
|
10173
10433
|
let capture = false;
|
|
10174
|
-
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
|
+
}
|
|
10175
10439
|
if (!isNodeOfType(property, "Property")) return null;
|
|
10176
|
-
const propertyName = getStaticPropertyKeyName(property, { allowComputedString
|
|
10440
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString });
|
|
10177
10441
|
if (propertyName === null) return null;
|
|
10178
10442
|
if (!property.computed && propertyName === "__proto__") return null;
|
|
10179
10443
|
if (propertyName !== "capture") continue;
|
|
10180
10444
|
const propertyValue = stripParenExpression(property.value);
|
|
10181
|
-
if (
|
|
10182
|
-
|
|
10445
|
+
if (isNodeOfType(propertyValue, "Literal") && typeof propertyValue.value === "boolean") {
|
|
10446
|
+
capture = propertyValue.value;
|
|
10447
|
+
continue;
|
|
10448
|
+
}
|
|
10449
|
+
if (!allowIndeterminateEntries) return null;
|
|
10450
|
+
capture = null;
|
|
10183
10451
|
}
|
|
10184
10452
|
return capture;
|
|
10185
10453
|
};
|
|
@@ -10422,7 +10690,7 @@ const readListenerCandidate = (node, methodName, context) => {
|
|
|
10422
10690
|
const targetKey = resolveTargetKey(targetNode, context);
|
|
10423
10691
|
const eventName = resolveStaticEventName(node.arguments?.[0], context);
|
|
10424
10692
|
const callbackIdentity = resolveCallbackIdentity(node.arguments?.[1], context);
|
|
10425
|
-
const capture = resolveEventListenerCapture(node.arguments?.[2]);
|
|
10693
|
+
const capture = resolveEventListenerCapture(node.arguments?.[2], { allowComputedString: true });
|
|
10426
10694
|
if (targetKey === null || eventName === null) return null;
|
|
10427
10695
|
return {
|
|
10428
10696
|
node,
|
|
@@ -10456,7 +10724,7 @@ const readDestructuredRemovalCandidate = (node, context) => {
|
|
|
10456
10724
|
targetKey,
|
|
10457
10725
|
eventName,
|
|
10458
10726
|
callbackIdentity: resolveCallbackIdentity(node.arguments?.[2], context),
|
|
10459
|
-
capture: resolveEventListenerCapture(node.arguments?.[3])
|
|
10727
|
+
capture: resolveEventListenerCapture(node.arguments?.[3], { allowComputedString: true })
|
|
10460
10728
|
};
|
|
10461
10729
|
};
|
|
10462
10730
|
const resolveReturnedCleanupBody = (returnedValue, context) => {
|
|
@@ -10816,16 +11084,6 @@ const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
|
10816
11084
|
return displayNameFromFunctionBinding(functionNode);
|
|
10817
11085
|
};
|
|
10818
11086
|
//#endregion
|
|
10819
|
-
//#region src/plugin/utils/find-enclosing-function.ts
|
|
10820
|
-
const findEnclosingFunction = (node) => {
|
|
10821
|
-
let cursor = node.parent;
|
|
10822
|
-
while (cursor) {
|
|
10823
|
-
if (isFunctionLike$2(cursor)) return cursor;
|
|
10824
|
-
cursor = cursor.parent ?? null;
|
|
10825
|
-
}
|
|
10826
|
-
return null;
|
|
10827
|
-
};
|
|
10828
|
-
//#endregion
|
|
10829
11087
|
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
10830
11088
|
const enclosingComponentOrHookName = (node) => {
|
|
10831
11089
|
const functionNode = findEnclosingFunction(node);
|
|
@@ -10844,7 +11102,7 @@ const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
|
10844
11102
|
};
|
|
10845
11103
|
//#endregion
|
|
10846
11104
|
//#region src/plugin/utils/executes-during-render.ts
|
|
10847
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES
|
|
11105
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
10848
11106
|
"map",
|
|
10849
11107
|
"filter",
|
|
10850
11108
|
"forEach",
|
|
@@ -10880,7 +11138,7 @@ const executesDuringRender = (functionNode, scopes) => {
|
|
|
10880
11138
|
if (parent.callee === functionNode) return true;
|
|
10881
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;
|
|
10882
11140
|
if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
|
|
10883
|
-
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;
|
|
10884
11142
|
};
|
|
10885
11143
|
//#endregion
|
|
10886
11144
|
//#region src/plugin/utils/find-render-phase-component-or-hook.ts
|
|
@@ -10911,6 +11169,26 @@ const getFunctionBindingName$1 = (functionNode) => getFunctionBindingIdentifier(
|
|
|
10911
11169
|
//#region src/plugin/utils/is-event-handler-attribute.ts
|
|
10912
11170
|
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && /^on[A-Z]/.test(node.name.name);
|
|
10913
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
|
|
10914
11192
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
10915
11193
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
10916
11194
|
if (!node || typeof node !== "object") return;
|
|
@@ -10947,6 +11225,7 @@ const isCleanupReturningSubscribeLikeCallExpression = (node) => {
|
|
|
10947
11225
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
10948
11226
|
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
10949
11227
|
const CLEANUP_EFFECT_HOOK_NAMES = new Set([...EFFECT_HOOK_NAMES$1, "useInsertionEffect"]);
|
|
11228
|
+
const REPLAYABLE_ITERATOR_COLLECTION_CACHE = /* @__PURE__ */ new WeakMap();
|
|
10950
11229
|
const RESOURCE_NOUN_BY_KIND = {
|
|
10951
11230
|
subscribe: "subscription",
|
|
10952
11231
|
timer: "timer",
|
|
@@ -11176,9 +11455,108 @@ const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
|
|
|
11176
11455
|
return !doMatchingNodesCoverEveryPathAfterUsage(usage.node, matchingReleaseCalls, context);
|
|
11177
11456
|
});
|
|
11178
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
|
+
};
|
|
11179
11553
|
const resolveIteratorCollectionKey = (expression, context) => {
|
|
11180
|
-
if (!expression
|
|
11181
|
-
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);
|
|
11182
11560
|
if (!symbol || symbol.kind !== "parameter") return null;
|
|
11183
11561
|
let callbackNode = symbol.bindingIdentifier.parent;
|
|
11184
11562
|
while (callbackNode && !isFunctionLike$2(callbackNode)) callbackNode = callbackNode.parent;
|
|
@@ -11192,6 +11570,34 @@ const resolveIteratorCollectionKey = (expression, context) => {
|
|
|
11192
11570
|
}
|
|
11193
11571
|
return null;
|
|
11194
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
|
+
};
|
|
11195
11601
|
const findCollectionMappingCall = (callbackNode) => {
|
|
11196
11602
|
if (!isNodeOfType(callbackNode, "ArrowFunctionExpression") && !isNodeOfType(callbackNode, "FunctionExpression") || callbackNode.async || callbackNode.generator) return null;
|
|
11197
11603
|
const callNode = callbackNode.parent;
|
|
@@ -11339,23 +11745,26 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
|
|
|
11339
11745
|
if (!isFunctionLike$2(cleanupFunction) || visitedFunctions.has(cleanupFunction)) return false;
|
|
11340
11746
|
visitedFunctions.add(cleanupFunction);
|
|
11341
11747
|
let didCleanupFunctionMatch = false;
|
|
11748
|
+
const matchingLoopOrHelperAnchors = [];
|
|
11342
11749
|
walkAst(cleanupFunction.body, (cleanupChild) => {
|
|
11343
11750
|
if (didCleanupFunctionMatch) return false;
|
|
11344
11751
|
if (cleanupChild !== cleanupFunction.body && isFunctionLike$2(cleanupChild) && !isSynchronousIteratorCallback(cleanupChild)) return false;
|
|
11752
|
+
const cleanupCall = isNodeOfType(cleanupChild, "ChainExpression") ? cleanupChild.expression : cleanupChild;
|
|
11345
11753
|
if (doesReleaseCallMatchUsage(cleanupChild, usage, context)) {
|
|
11346
|
-
|
|
11347
|
-
|
|
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;
|
|
11348
11761
|
}
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
const stableHelperFunction = resolveStableValue(helperCall.callee, context);
|
|
11762
|
+
if (!isNodeOfType(cleanupCall, "CallExpression")) return;
|
|
11763
|
+
const stableHelperFunction = resolveStableValue(cleanupCall.callee, context);
|
|
11352
11764
|
const helperFunction = isNodeOfType(stableHelperFunction, "Identifier") ? resolveSingleAssignedCleanupFunction(stableHelperFunction, usage, context) : stableHelperFunction;
|
|
11353
|
-
if (helperFunction && isFunctionLike$2(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, visitedFunctions))
|
|
11354
|
-
didCleanupFunctionMatch = true;
|
|
11355
|
-
return false;
|
|
11356
|
-
}
|
|
11765
|
+
if (helperFunction && isFunctionLike$2(helperFunction) && !helperFunction.async && !helperFunction.generator && doesCleanupFunctionReleaseUsage(helperFunction, usage, context, new Set(visitedFunctions))) matchingLoopOrHelperAnchors.push(cleanupCall);
|
|
11357
11766
|
});
|
|
11358
|
-
return didCleanupFunctionMatch;
|
|
11767
|
+
return didCleanupFunctionMatch || doMatchingNodesCoverEveryPathFromFunctionEntry(cleanupFunction, matchingLoopOrHelperAnchors, context);
|
|
11359
11768
|
};
|
|
11360
11769
|
const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
|
|
11361
11770
|
const callExpression = stripParenExpression(expression);
|
|
@@ -11372,6 +11781,7 @@ const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
|
|
|
11372
11781
|
};
|
|
11373
11782
|
const callbackReturnsCleanupForUsage = (callback, usage, context) => {
|
|
11374
11783
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
11784
|
+
if (callback.async) return false;
|
|
11375
11785
|
const doesReturnedValueReleaseUsage = (returnedValue) => {
|
|
11376
11786
|
if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) return true;
|
|
11377
11787
|
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
@@ -11436,6 +11846,7 @@ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
|
11436
11846
|
const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
|
|
11437
11847
|
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
11438
11848
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
11849
|
+
if (callback.async) return false;
|
|
11439
11850
|
if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
11440
11851
|
if (!isNodeOfType(callback.body, "BlockStatement")) return callback.body === usage.node && isCleanupReturningSubscribeLikeCallExpression(callback.body);
|
|
11441
11852
|
const matchingCleanupReturns = [];
|
|
@@ -11559,10 +11970,29 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
11559
11970
|
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
11560
11971
|
if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
|
|
11561
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;
|
|
11562
11976
|
if (usage.eventKey !== null && releaseEventKey !== null && usage.eventKey !== releaseEventKey) {
|
|
11563
|
-
|
|
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);
|
|
11564
11982
|
const releaseIteratorCollectionKey = resolveIteratorCollectionKey(callNode.arguments?.[0], context);
|
|
11565
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
|
+
}
|
|
11566
11996
|
}
|
|
11567
11997
|
if (releaseVerbName === "on") {
|
|
11568
11998
|
const handlerArgument = callNode.arguments?.[1];
|
|
@@ -11617,6 +12047,213 @@ const fileContainsReleaseForUsage = (usage, context) => {
|
|
|
11617
12047
|
});
|
|
11618
12048
|
return didFindRelease;
|
|
11619
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
|
+
};
|
|
11620
12257
|
const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
|
|
11621
12258
|
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
11622
12259
|
if (!bindingIdentifier) return false;
|
|
@@ -11657,7 +12294,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
11657
12294
|
let leak = null;
|
|
11658
12295
|
const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
|
|
11659
12296
|
const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
|
|
11660
|
-
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);
|
|
11661
12298
|
walkAst(body, (child) => {
|
|
11662
12299
|
if (leak !== null) return false;
|
|
11663
12300
|
if (isFunctionLike$2(child)) return false;
|
|
@@ -12866,7 +13503,7 @@ const stringifyMemberChain = (node) => {
|
|
|
12866
13503
|
}
|
|
12867
13504
|
return null;
|
|
12868
13505
|
};
|
|
12869
|
-
const collectCaptureDepKeys = (callback, scopes) => {
|
|
13506
|
+
const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys) => {
|
|
12870
13507
|
const keys = /* @__PURE__ */ new Set();
|
|
12871
13508
|
const stableCapturedNames = /* @__PURE__ */ new Set();
|
|
12872
13509
|
const moduleScopeCapturedNames = /* @__PURE__ */ new Set();
|
|
@@ -12896,6 +13533,10 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
12896
13533
|
continue;
|
|
12897
13534
|
}
|
|
12898
13535
|
if (depKey === symbol.name) {
|
|
13536
|
+
if (declaredExactBindingKeys?.has(depKey)) {
|
|
13537
|
+
keys.add(depKey);
|
|
13538
|
+
continue;
|
|
13539
|
+
}
|
|
12899
13540
|
const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
12900
13541
|
if (identitySourceKeys) {
|
|
12901
13542
|
if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
|
|
@@ -13004,13 +13645,13 @@ const isRegExpLiteral = (node) => {
|
|
|
13004
13645
|
if (!isNodeOfType(node, "Literal")) return false;
|
|
13005
13646
|
return Boolean(node.regex);
|
|
13006
13647
|
};
|
|
13007
|
-
const isUnstableInitializer = (node) => {
|
|
13648
|
+
const isUnstableInitializer = (node, isNestedInitializer = false) => {
|
|
13008
13649
|
if (!node) return false;
|
|
13009
13650
|
const stripped = unwrapExpression$3(node);
|
|
13010
13651
|
if (isRegExpLiteral(stripped)) return true;
|
|
13011
|
-
if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
|
|
13012
|
-
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
13013
|
-
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");
|
|
13014
13655
|
};
|
|
13015
13656
|
const isPotentiallyFreshComparedValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
13016
13657
|
const candidate = unwrapExpression$3(node);
|
|
@@ -13463,8 +14104,6 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
13463
14104
|
});
|
|
13464
14105
|
return;
|
|
13465
14106
|
}
|
|
13466
|
-
const { keys: captureKeys, stableCapturedNames, moduleScopeCapturedNames, outerFunctionCapturedNames } = collectCaptureDepKeys(callbackToAnalyze ?? callbackArgument, context.scopes);
|
|
13467
|
-
for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
|
|
13468
14107
|
const hasLiteralDepElement = depsArgument.elements.some((element) => {
|
|
13469
14108
|
if (!element) return false;
|
|
13470
14109
|
return isLiteralOrEmptyTemplate(unwrapExpression$3(element));
|
|
@@ -13478,6 +14117,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
13478
14117
|
message: buildLiteralDepMessage(hookName)
|
|
13479
14118
|
});
|
|
13480
14119
|
const declaredKeys = /* @__PURE__ */ new Set();
|
|
14120
|
+
const declaredExactBindingKeys = /* @__PURE__ */ new Set();
|
|
13481
14121
|
const declaredKeyToReportNode = /* @__PURE__ */ new Map();
|
|
13482
14122
|
const seenDeclaredKeys = /* @__PURE__ */ new Set();
|
|
13483
14123
|
let didReportRefCurrentDep = false;
|
|
@@ -13543,8 +14183,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
13543
14183
|
}
|
|
13544
14184
|
seenDeclaredKeys.add(key);
|
|
13545
14185
|
declaredKeys.add(key);
|
|
14186
|
+
if (isNodeOfType(stripped, "Identifier")) declaredExactBindingKeys.add(key);
|
|
13546
14187
|
declaredKeyToReportNode.set(key, elementNode);
|
|
13547
14188
|
}
|
|
14189
|
+
const { keys: captureKeys, stableCapturedNames, moduleScopeCapturedNames, outerFunctionCapturedNames } = collectCaptureDepKeys(callbackToAnalyze ?? callbackArgument, context.scopes, declaredExactBindingKeys);
|
|
14190
|
+
for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
|
|
13548
14191
|
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
|
|
13549
14192
|
const missingCaptureKeys = [];
|
|
13550
14193
|
for (const captureKey of captureKeys) {
|
|
@@ -15542,7 +16185,7 @@ const jotaiTqUseRawQueryAtom = defineRule({
|
|
|
15542
16185
|
});
|
|
15543
16186
|
//#endregion
|
|
15544
16187
|
//#region src/plugin/rules/js-performance/js-async-reduce-without-awaited-acc.ts
|
|
15545
|
-
const isAsyncFunctionLike
|
|
16188
|
+
const isAsyncFunctionLike = (node) => {
|
|
15546
16189
|
if (!node) return false;
|
|
15547
16190
|
if (!isNodeOfType(node, "ArrowFunctionExpression") && !isNodeOfType(node, "FunctionExpression")) return false;
|
|
15548
16191
|
return node.async === true;
|
|
@@ -15609,7 +16252,7 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
15609
16252
|
const args = node.arguments ?? [];
|
|
15610
16253
|
if (args.length === 0) return;
|
|
15611
16254
|
const reducerCandidate = stripParenExpression(args[0]);
|
|
15612
|
-
if (!isAsyncFunctionLike
|
|
16255
|
+
if (!isAsyncFunctionLike(reducerCandidate)) return;
|
|
15613
16256
|
const reducer = reducerCandidate;
|
|
15614
16257
|
if (!containsDirectAwait(reducer.body)) return;
|
|
15615
16258
|
const firstParameter = classifyFirstParameter(reducer);
|
|
@@ -16893,30 +17536,6 @@ const jsIndexMaps = defineRule({
|
|
|
16893
17536
|
} })
|
|
16894
17537
|
});
|
|
16895
17538
|
//#endregion
|
|
16896
|
-
//#region src/plugin/utils/are-expressions-structurally-equal.ts
|
|
16897
|
-
const areExpressionsStructurallyEqual = (a, b) => {
|
|
16898
|
-
if (!a || !b) return a === b;
|
|
16899
|
-
const unwrappedA = stripParenExpression(a);
|
|
16900
|
-
const unwrappedB = stripParenExpression(b);
|
|
16901
|
-
if (unwrappedA !== a || unwrappedB !== b) return areExpressionsStructurallyEqual(unwrappedA, unwrappedB);
|
|
16902
|
-
if (a.type !== b.type) return false;
|
|
16903
|
-
if (isNodeOfType(a, "ThisExpression")) return true;
|
|
16904
|
-
if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
|
|
16905
|
-
if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
|
|
16906
|
-
if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
|
|
16907
|
-
if (a.computed !== b.computed) return false;
|
|
16908
|
-
return areExpressionsStructurallyEqual(a.object, b.object) && areExpressionsStructurallyEqual(a.property, b.property);
|
|
16909
|
-
}
|
|
16910
|
-
if (isNodeOfType(a, "CallExpression") && isNodeOfType(b, "CallExpression")) {
|
|
16911
|
-
if (!areExpressionsStructurallyEqual(a.callee, b.callee)) return false;
|
|
16912
|
-
const argumentsA = a.arguments ?? [];
|
|
16913
|
-
const argumentsB = b.arguments ?? [];
|
|
16914
|
-
if (argumentsA.length !== argumentsB.length) return false;
|
|
16915
|
-
return argumentsA.every((argument, index) => areExpressionsStructurallyEqual(argument, argumentsB[index]));
|
|
16916
|
-
}
|
|
16917
|
-
return false;
|
|
16918
|
-
};
|
|
16919
|
-
//#endregion
|
|
16920
17539
|
//#region src/plugin/utils/flatten-logical-and-chain.ts
|
|
16921
17540
|
const flattenLogicalAndChain = (node) => {
|
|
16922
17541
|
if (isNodeOfType(node, "LogicalExpression") && node.operator === "&&") return [...flattenLogicalAndChain(node.left), ...flattenLogicalAndChain(node.right)];
|
|
@@ -17173,12 +17792,23 @@ const jsLengthCheckFirst = defineRule({
|
|
|
17173
17792
|
});
|
|
17174
17793
|
//#endregion
|
|
17175
17794
|
//#region src/plugin/rules/js-performance/js-min-max-loop.ts
|
|
17795
|
+
const builtinMutationByProgram = /* @__PURE__ */ new WeakMap();
|
|
17796
|
+
const RUNTIMELESS_SYMBOL_KINDS = new Set(["ts-interface", "ts-type-alias"]);
|
|
17797
|
+
const PROTOTYPE_METHOD_NAMES = new Set(["getPrototypeOf"]);
|
|
17798
|
+
const OBJECT_ASSIGN_METHOD_NAMES = new Set(["assign"]);
|
|
17799
|
+
const OBJECT_DEFINE_PROPERTIES_METHOD_NAMES = new Set(["defineProperties"]);
|
|
17800
|
+
const KEYED_MUTATION_METHOD_NAMES = new Set([
|
|
17801
|
+
"defineProperty",
|
|
17802
|
+
"deleteProperty",
|
|
17803
|
+
"set"
|
|
17804
|
+
]);
|
|
17176
17805
|
const numericComparatorDirection = (comparator) => {
|
|
17177
|
-
if (!isInlineFunctionExpression(comparator)) return null;
|
|
17806
|
+
if (!isInlineFunctionExpression(comparator) || comparator.async || comparator.generator) return null;
|
|
17178
17807
|
const parameters = comparator.params ?? [];
|
|
17179
17808
|
if (parameters.length !== 2) return null;
|
|
17180
17809
|
const [firstParameter, secondParameter] = parameters;
|
|
17181
17810
|
if (!isNodeOfType(firstParameter, "Identifier") || !isNodeOfType(secondParameter, "Identifier")) return null;
|
|
17811
|
+
if (firstParameter.name === secondParameter.name) return null;
|
|
17182
17812
|
let comparisonExpression = null;
|
|
17183
17813
|
const comparatorBody = stripParenExpression(comparator.body);
|
|
17184
17814
|
if (isNodeOfType(comparatorBody, "BinaryExpression")) comparisonExpression = comparatorBody;
|
|
@@ -17196,6 +17826,150 @@ const numericComparatorDirection = (comparator) => {
|
|
|
17196
17826
|
if (leftName === secondParameter.name && rightName === firstParameter.name) return "descending";
|
|
17197
17827
|
return null;
|
|
17198
17828
|
};
|
|
17829
|
+
const getStaticFiniteNumericValue = (expression) => {
|
|
17830
|
+
const strippedExpression = stripParenExpression(expression);
|
|
17831
|
+
if (isNodeOfType(strippedExpression, "Literal")) return typeof strippedExpression.value === "number" && Number.isFinite(strippedExpression.value) ? strippedExpression.value : null;
|
|
17832
|
+
if (!isNodeOfType(strippedExpression, "UnaryExpression") || strippedExpression.operator !== "+" && strippedExpression.operator !== "-") return null;
|
|
17833
|
+
const argumentValue = getStaticFiniteNumericValue(strippedExpression.argument);
|
|
17834
|
+
if (argumentValue === null) return null;
|
|
17835
|
+
return strippedExpression.operator === "-" ? -argumentValue : argumentValue;
|
|
17836
|
+
};
|
|
17837
|
+
const isSafeFreshNumericArray = (arrayExpression) => {
|
|
17838
|
+
if (arrayExpression.elements.length === 0 || arrayExpression.elements.length > 1024) return false;
|
|
17839
|
+
let didFindPositiveZero = false;
|
|
17840
|
+
let didFindNegativeZero = false;
|
|
17841
|
+
for (const element of arrayExpression.elements) {
|
|
17842
|
+
if (!element || isNodeOfType(element, "SpreadElement")) return false;
|
|
17843
|
+
const numericValue = getStaticFiniteNumericValue(element);
|
|
17844
|
+
if (numericValue === null) return false;
|
|
17845
|
+
if (Object.is(numericValue, 0)) didFindPositiveZero = true;
|
|
17846
|
+
if (Object.is(numericValue, -0)) didFindNegativeZero = true;
|
|
17847
|
+
}
|
|
17848
|
+
return !(didFindPositiveZero && didFindNegativeZero);
|
|
17849
|
+
};
|
|
17850
|
+
const isGlobalObjectReference = (expression, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
17851
|
+
const strippedExpression = stripParenExpression(expression);
|
|
17852
|
+
if (!isNodeOfType(strippedExpression, "Identifier")) return false;
|
|
17853
|
+
if ((strippedExpression.name === "globalThis" || strippedExpression.name === "window" || strippedExpression.name === "self" || strippedExpression.name === "global") && scopes.isGlobalReference(strippedExpression)) return true;
|
|
17854
|
+
const symbol = scopes.symbolFor(strippedExpression);
|
|
17855
|
+
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
|
|
17856
|
+
visitedSymbols.add(symbol.id);
|
|
17857
|
+
return isGlobalObjectReference(symbol.initializer, scopes, visitedSymbols);
|
|
17858
|
+
};
|
|
17859
|
+
const resolvesToGlobalNamespace = (expression, namespaceName, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
17860
|
+
const strippedExpression = stripParenExpression(expression);
|
|
17861
|
+
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
17862
|
+
if (strippedExpression.name === namespaceName && scopes.isGlobalReference(strippedExpression)) return true;
|
|
17863
|
+
const symbol = scopes.symbolFor(strippedExpression);
|
|
17864
|
+
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
|
|
17865
|
+
visitedSymbols.add(symbol.id);
|
|
17866
|
+
return resolvesToGlobalNamespace(symbol.initializer, namespaceName, scopes, visitedSymbols);
|
|
17867
|
+
}
|
|
17868
|
+
return isNodeOfType(strippedExpression, "MemberExpression") && getStaticPropertyName(strippedExpression) === namespaceName && isGlobalObjectReference(strippedExpression.object, scopes);
|
|
17869
|
+
};
|
|
17870
|
+
const resolvesToGlobalMethod = (expression, namespaceName, methodNames, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
17871
|
+
const strippedExpression = stripParenExpression(expression);
|
|
17872
|
+
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
17873
|
+
const symbol = scopes.symbolFor(strippedExpression);
|
|
17874
|
+
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
|
|
17875
|
+
visitedSymbols.add(symbol.id);
|
|
17876
|
+
return resolvesToGlobalMethod(symbol.initializer, namespaceName, methodNames, scopes, visitedSymbols);
|
|
17877
|
+
}
|
|
17878
|
+
return isNodeOfType(strippedExpression, "MemberExpression") && methodNames.has(getStaticPropertyName(strippedExpression) ?? "") && resolvesToGlobalNamespace(strippedExpression.object, namespaceName, scopes);
|
|
17879
|
+
};
|
|
17880
|
+
const resolvesToNativeArrayPrototype = (expression, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
17881
|
+
const strippedExpression = stripParenExpression(expression);
|
|
17882
|
+
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
17883
|
+
const symbol = scopes.symbolFor(strippedExpression);
|
|
17884
|
+
if (!symbol?.initializer || symbol.kind !== "const" || visitedSymbols.has(symbol.id)) return false;
|
|
17885
|
+
visitedSymbols.add(symbol.id);
|
|
17886
|
+
return resolvesToNativeArrayPrototype(symbol.initializer, scopes, visitedSymbols);
|
|
17887
|
+
}
|
|
17888
|
+
if (isNodeOfType(strippedExpression, "MemberExpression")) {
|
|
17889
|
+
const propertyName = getStaticPropertyName(strippedExpression);
|
|
17890
|
+
if (propertyName === "prototype") return resolvesToGlobalNamespace(strippedExpression.object, "Array", scopes);
|
|
17891
|
+
return propertyName === "__proto__" && isNodeOfType(stripParenExpression(strippedExpression.object), "ArrayExpression");
|
|
17892
|
+
}
|
|
17893
|
+
if (!isNodeOfType(strippedExpression, "CallExpression")) return false;
|
|
17894
|
+
if (!resolvesToGlobalMethod(strippedExpression.callee, "Object", PROTOTYPE_METHOD_NAMES, scopes) && !resolvesToGlobalMethod(strippedExpression.callee, "Reflect", PROTOTYPE_METHOD_NAMES, scopes)) return false;
|
|
17895
|
+
const argument = strippedExpression.arguments[0];
|
|
17896
|
+
return Boolean(argument && isNodeOfType(stripParenExpression(argument), "ArrayExpression"));
|
|
17897
|
+
};
|
|
17898
|
+
const isGlobalNamespaceReplacementTarget = (target, namespaceName, scopes) => {
|
|
17899
|
+
const strippedTarget = stripParenExpression(target);
|
|
17900
|
+
if (isNodeOfType(strippedTarget, "Identifier")) return strippedTarget.name === namespaceName && scopes.isGlobalReference(strippedTarget);
|
|
17901
|
+
return isNodeOfType(strippedTarget, "MemberExpression") && getStaticPropertyName(strippedTarget) === namespaceName && isGlobalObjectReference(strippedTarget.object, scopes);
|
|
17902
|
+
};
|
|
17903
|
+
const isUnsafeBuiltinMemberTarget = (target, targetFunction, scopes) => {
|
|
17904
|
+
const strippedTarget = stripParenExpression(target);
|
|
17905
|
+
if (!isNodeOfType(strippedTarget, "MemberExpression")) return false;
|
|
17906
|
+
const propertyName = getStaticPropertyName(strippedTarget);
|
|
17907
|
+
if (resolvesToNativeArrayPrototype(strippedTarget.object, scopes)) return propertyName === null || propertyName === "sort";
|
|
17908
|
+
if (resolvesToGlobalNamespace(strippedTarget.object, "Math", scopes)) return propertyName === null || propertyName === targetFunction;
|
|
17909
|
+
return isGlobalObjectReference(strippedTarget.object, scopes) && (propertyName === null || propertyName === "Math");
|
|
17910
|
+
};
|
|
17911
|
+
const isUnsafeBuiltinMutationApiCall = (callExpression, targetFunction, scopes) => {
|
|
17912
|
+
const target = callExpression.arguments[0];
|
|
17913
|
+
if (!target) return false;
|
|
17914
|
+
let propertyName = null;
|
|
17915
|
+
if (resolvesToNativeArrayPrototype(target, scopes)) propertyName = "sort";
|
|
17916
|
+
else if (resolvesToGlobalNamespace(target, "Math", scopes)) propertyName = targetFunction;
|
|
17917
|
+
else if (isGlobalObjectReference(target, scopes)) propertyName = "Math";
|
|
17918
|
+
if (!propertyName) return false;
|
|
17919
|
+
const canObjectExpressionSetProperty = (properties) => {
|
|
17920
|
+
if (!isNodeOfType(properties, "ObjectExpression")) return true;
|
|
17921
|
+
return properties.properties.some((property) => {
|
|
17922
|
+
if (isNodeOfType(property, "SpreadElement")) return true;
|
|
17923
|
+
const keyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
17924
|
+
return keyName === propertyName || keyName === null;
|
|
17925
|
+
});
|
|
17926
|
+
};
|
|
17927
|
+
if (resolvesToGlobalMethod(callExpression.callee, "Object", OBJECT_ASSIGN_METHOD_NAMES, scopes)) return callExpression.arguments.slice(1).some((properties) => canObjectExpressionSetProperty(properties));
|
|
17928
|
+
if (resolvesToGlobalMethod(callExpression.callee, "Object", OBJECT_DEFINE_PROPERTIES_METHOD_NAMES, scopes)) {
|
|
17929
|
+
const properties = callExpression.arguments[1];
|
|
17930
|
+
return !properties || canObjectExpressionSetProperty(properties);
|
|
17931
|
+
}
|
|
17932
|
+
if (!resolvesToGlobalMethod(callExpression.callee, "Object", KEYED_MUTATION_METHOD_NAMES, scopes) && !resolvesToGlobalMethod(callExpression.callee, "Reflect", KEYED_MUTATION_METHOD_NAMES, scopes)) return false;
|
|
17933
|
+
const propertyArgument = callExpression.arguments[1];
|
|
17934
|
+
if (!propertyArgument) return true;
|
|
17935
|
+
return isNodeOfType(propertyArgument, "Literal") ? propertyArgument.value === propertyName : true;
|
|
17936
|
+
};
|
|
17937
|
+
const hasUnsafeBuiltinMutation = (node, targetFunction, scopes) => {
|
|
17938
|
+
const programRoot = findProgramRoot(node);
|
|
17939
|
+
if (!programRoot) return true;
|
|
17940
|
+
let mutationByTargetFunction = builtinMutationByProgram.get(programRoot);
|
|
17941
|
+
if (!mutationByTargetFunction) {
|
|
17942
|
+
mutationByTargetFunction = /* @__PURE__ */ new Map();
|
|
17943
|
+
builtinMutationByProgram.set(programRoot, mutationByTargetFunction);
|
|
17944
|
+
}
|
|
17945
|
+
const cachedResult = mutationByTargetFunction.get(targetFunction);
|
|
17946
|
+
if (cachedResult !== void 0) return cachedResult;
|
|
17947
|
+
let didFindUnsafeMutation = false;
|
|
17948
|
+
walkAst(programRoot, (candidate) => {
|
|
17949
|
+
if (didFindUnsafeMutation) return false;
|
|
17950
|
+
if (isNodeOfType(candidate, "CallExpression")) {
|
|
17951
|
+
didFindUnsafeMutation = isUnsafeBuiltinMutationApiCall(candidate, targetFunction, scopes);
|
|
17952
|
+
return;
|
|
17953
|
+
}
|
|
17954
|
+
let mutationTarget = null;
|
|
17955
|
+
if (isNodeOfType(candidate, "AssignmentExpression")) mutationTarget = candidate.left;
|
|
17956
|
+
if (isNodeOfType(candidate, "UpdateExpression")) mutationTarget = candidate.argument;
|
|
17957
|
+
if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator === "delete") mutationTarget = candidate.argument;
|
|
17958
|
+
if (!mutationTarget) return;
|
|
17959
|
+
didFindUnsafeMutation = isUnsafeBuiltinMemberTarget(mutationTarget, targetFunction, scopes) || isGlobalNamespaceReplacementTarget(mutationTarget, "Math", scopes);
|
|
17960
|
+
});
|
|
17961
|
+
mutationByTargetFunction.set(targetFunction, didFindUnsafeMutation);
|
|
17962
|
+
return didFindUnsafeMutation;
|
|
17963
|
+
};
|
|
17964
|
+
const hasUnsafeMathBinding = (node, scopes) => {
|
|
17965
|
+
let scope = scopes.scopeFor(node);
|
|
17966
|
+
while (scope) {
|
|
17967
|
+
const symbol = scope.symbolsByName.get("Math");
|
|
17968
|
+
if (symbol && !RUNTIMELESS_SYMBOL_KINDS.has(symbol.kind)) return !(symbol.kind === "const" && symbol.initializer && resolvesToGlobalNamespace(symbol.initializer, "Math", scopes));
|
|
17969
|
+
scope = scope.parent;
|
|
17970
|
+
}
|
|
17971
|
+
return false;
|
|
17972
|
+
};
|
|
17199
17973
|
const jsMinMaxLoop = defineRule({
|
|
17200
17974
|
id: "js-min-max-loop",
|
|
17201
17975
|
title: "sort() to find min or max",
|
|
@@ -17206,18 +17980,18 @@ const jsMinMaxLoop = defineRule({
|
|
|
17206
17980
|
if (!node.computed) return;
|
|
17207
17981
|
const object = node.object;
|
|
17208
17982
|
if (!isNodeOfType(object, "CallExpression") || !isMemberProperty(object.callee, "sort")) return;
|
|
17983
|
+
const sortReceiver = stripParenExpression(object.callee.object);
|
|
17984
|
+
if (!isNodeOfType(sortReceiver, "ArrayExpression") || !isSafeFreshNumericArray(sortReceiver)) return;
|
|
17209
17985
|
const comparator = object.arguments?.[0];
|
|
17210
17986
|
const direction = numericComparatorDirection(comparator);
|
|
17211
17987
|
if (!direction) return;
|
|
17212
|
-
|
|
17213
|
-
const
|
|
17214
|
-
if (
|
|
17215
|
-
|
|
17216
|
-
|
|
17217
|
-
|
|
17218
|
-
|
|
17219
|
-
});
|
|
17220
|
-
}
|
|
17988
|
+
if (!(isNodeOfType(node.property, "Literal") && node.property.value === 0)) return;
|
|
17989
|
+
const targetFunction = direction === "ascending" ? "min" : "max";
|
|
17990
|
+
if (hasUnsafeMathBinding(node, context.scopes) || hasUnsafeBuiltinMutation(node, targetFunction, context.scopes)) return;
|
|
17991
|
+
context.report({
|
|
17992
|
+
node,
|
|
17993
|
+
message: `This is slow because array.sort()[0] sorts the whole list just to grab the smallest or largest, so use Math.${targetFunction}(...array) instead`
|
|
17994
|
+
});
|
|
17221
17995
|
} })
|
|
17222
17996
|
});
|
|
17223
17997
|
//#endregion
|
|
@@ -27691,6 +28465,152 @@ const noBarrelImport = defineRule({
|
|
|
27691
28465
|
}
|
|
27692
28466
|
});
|
|
27693
28467
|
//#endregion
|
|
28468
|
+
//#region src/plugin/utils/has-static-property-write-before.ts
|
|
28469
|
+
const equivalentSymbolsByAnalysis = /* @__PURE__ */ new WeakMap();
|
|
28470
|
+
const CONDITIONAL_EXECUTION_NODE_TYPES = new Set([
|
|
28471
|
+
"CatchClause",
|
|
28472
|
+
"ConditionalExpression",
|
|
28473
|
+
"DoWhileStatement",
|
|
28474
|
+
"ForInStatement",
|
|
28475
|
+
"ForOfStatement",
|
|
28476
|
+
"ForStatement",
|
|
28477
|
+
"IfStatement",
|
|
28478
|
+
"LogicalExpression",
|
|
28479
|
+
"SwitchCase",
|
|
28480
|
+
"SwitchStatement",
|
|
28481
|
+
"TryStatement",
|
|
28482
|
+
"WhileStatement"
|
|
28483
|
+
]);
|
|
28484
|
+
const getResolvedStaticPropertyName = (memberExpression, scopes) => {
|
|
28485
|
+
if (!isNodeOfType(memberExpression, "MemberExpression")) return null;
|
|
28486
|
+
const directPropertyName = getStaticPropertyName(memberExpression);
|
|
28487
|
+
if (directPropertyName || !memberExpression.computed) return directPropertyName;
|
|
28488
|
+
const property = stripParenExpression(memberExpression.property);
|
|
28489
|
+
if (!isNodeOfType(property, "Identifier")) return null;
|
|
28490
|
+
const propertySymbol = resolveConstIdentifierAlias(property, scopes);
|
|
28491
|
+
const initializer = propertySymbol?.initializer ? stripParenExpression(propertySymbol.initializer) : null;
|
|
28492
|
+
return initializer && isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
|
|
28493
|
+
};
|
|
28494
|
+
const collectScopeSymbols = (scope, symbols) => {
|
|
28495
|
+
symbols.push(...scope.symbols);
|
|
28496
|
+
for (const childScope of scope.children) collectScopeSymbols(childScope, symbols);
|
|
28497
|
+
};
|
|
28498
|
+
const getEquivalentSymbols = (identifier, scopes) => {
|
|
28499
|
+
const rootSymbol = resolveConstIdentifierAlias(identifier, scopes);
|
|
28500
|
+
if (!rootSymbol) return [];
|
|
28501
|
+
let symbolsByRootId = equivalentSymbolsByAnalysis.get(scopes);
|
|
28502
|
+
if (!symbolsByRootId) {
|
|
28503
|
+
symbolsByRootId = /* @__PURE__ */ new Map();
|
|
28504
|
+
equivalentSymbolsByAnalysis.set(scopes, symbolsByRootId);
|
|
28505
|
+
}
|
|
28506
|
+
const cachedSymbols = symbolsByRootId.get(rootSymbol.id);
|
|
28507
|
+
if (cachedSymbols) return cachedSymbols;
|
|
28508
|
+
const allSymbols = [];
|
|
28509
|
+
collectScopeSymbols(scopes.rootScope, allSymbols);
|
|
28510
|
+
const equivalentSymbols = allSymbols.filter((symbol) => resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes)?.id === rootSymbol.id);
|
|
28511
|
+
symbolsByRootId.set(rootSymbol.id, equivalentSymbols);
|
|
28512
|
+
return equivalentSymbols;
|
|
28513
|
+
};
|
|
28514
|
+
const findExecutionBoundary = (node) => {
|
|
28515
|
+
let current = node;
|
|
28516
|
+
while (current) {
|
|
28517
|
+
if (isFunctionLike$2(current) || isNodeOfType(current, "Program")) return current;
|
|
28518
|
+
current = current.parent ?? null;
|
|
28519
|
+
}
|
|
28520
|
+
return null;
|
|
28521
|
+
};
|
|
28522
|
+
const isOnUnconditionalPath = (node, boundary) => {
|
|
28523
|
+
let current = node.parent ?? null;
|
|
28524
|
+
while (current && current !== boundary) {
|
|
28525
|
+
if (CONDITIONAL_EXECUTION_NODE_TYPES.has(current.type)) return false;
|
|
28526
|
+
current = current.parent ?? null;
|
|
28527
|
+
}
|
|
28528
|
+
return current === boundary;
|
|
28529
|
+
};
|
|
28530
|
+
const findFunctionBindingIdentifier = (functionNode) => {
|
|
28531
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration")) return functionNode.id ?? null;
|
|
28532
|
+
const parent = functionNode.parent;
|
|
28533
|
+
if (parent && isNodeOfType(parent, "VariableDeclarator") && parent.init === functionNode && isNodeOfType(parent.id, "Identifier")) return parent.id;
|
|
28534
|
+
return null;
|
|
28535
|
+
};
|
|
28536
|
+
const findDirectCall = (identifier) => {
|
|
28537
|
+
let callee = identifier;
|
|
28538
|
+
let parent = callee.parent;
|
|
28539
|
+
while (parent && stripParenExpression(parent) === identifier) {
|
|
28540
|
+
callee = parent;
|
|
28541
|
+
parent = callee.parent;
|
|
28542
|
+
}
|
|
28543
|
+
return parent && isNodeOfType(parent, "CallExpression") && parent.callee === callee ? parent : null;
|
|
28544
|
+
};
|
|
28545
|
+
const isFunctionSynchronouslyInvokedBefore = (functionNode, referenceNode, scopes, visitedFunctionNodes = /* @__PURE__ */ new Set()) => {
|
|
28546
|
+
if (visitedFunctionNodes.has(functionNode) || !isFunctionLike$2(functionNode) || functionNode.generator) return false;
|
|
28547
|
+
visitedFunctionNodes.add(functionNode);
|
|
28548
|
+
const referenceBoundary = findExecutionBoundary(referenceNode);
|
|
28549
|
+
if (!referenceBoundary) return false;
|
|
28550
|
+
const invocationCalls = [];
|
|
28551
|
+
const bindingIdentifier = findFunctionBindingIdentifier(functionNode);
|
|
28552
|
+
if (bindingIdentifier) for (const symbol of getEquivalentSymbols(bindingIdentifier, scopes)) for (const reference of symbol.references) {
|
|
28553
|
+
const call = findDirectCall(reference.identifier);
|
|
28554
|
+
if (call) invocationCalls.push(call);
|
|
28555
|
+
}
|
|
28556
|
+
else {
|
|
28557
|
+
const call = findDirectCall(functionNode);
|
|
28558
|
+
if (call) invocationCalls.push(call);
|
|
28559
|
+
}
|
|
28560
|
+
return invocationCalls.some((call) => {
|
|
28561
|
+
if (call.range[0] >= referenceNode.range[0]) return false;
|
|
28562
|
+
const callBoundary = findExecutionBoundary(call);
|
|
28563
|
+
if (!callBoundary) return false;
|
|
28564
|
+
if (callBoundary === referenceBoundary) return true;
|
|
28565
|
+
if (!isFunctionLike$2(callBoundary)) return false;
|
|
28566
|
+
return isFunctionSynchronouslyInvokedBefore(callBoundary, referenceNode, scopes, new Set(visitedFunctionNodes));
|
|
28567
|
+
});
|
|
28568
|
+
};
|
|
28569
|
+
const isMemberWriteTarget = (memberExpression) => {
|
|
28570
|
+
const parent = memberExpression.parent;
|
|
28571
|
+
if (!parent) return false;
|
|
28572
|
+
if (isNodeOfType(parent, "AssignmentExpression")) return parent.left === memberExpression;
|
|
28573
|
+
if (isNodeOfType(parent, "UpdateExpression")) return parent.argument === memberExpression;
|
|
28574
|
+
return isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === memberExpression;
|
|
28575
|
+
};
|
|
28576
|
+
const symbolHasStaticPropertyWriteBefore = (symbol, propertyName, referenceNode, scopes) => symbol.references.some((reference) => {
|
|
28577
|
+
let parent = reference.identifier.parent;
|
|
28578
|
+
while (parent && stripParenExpression(parent) === reference.identifier) parent = parent.parent;
|
|
28579
|
+
if (!parent || !isNodeOfType(parent, "MemberExpression") || stripParenExpression(parent.object) !== reference.identifier || getResolvedStaticPropertyName(parent, scopes) !== propertyName || !isMemberWriteTarget(parent)) return false;
|
|
28580
|
+
const writeBoundary = findExecutionBoundary(parent);
|
|
28581
|
+
const referenceBoundary = findExecutionBoundary(referenceNode);
|
|
28582
|
+
if (!writeBoundary || !referenceBoundary || !isOnUnconditionalPath(parent, writeBoundary)) return false;
|
|
28583
|
+
if (writeBoundary === referenceBoundary) return parent.range[0] < referenceNode.range[0];
|
|
28584
|
+
return isFunctionLike$2(writeBoundary) && isFunctionSynchronouslyInvokedBefore(writeBoundary, referenceNode, scopes);
|
|
28585
|
+
});
|
|
28586
|
+
const hasStaticPropertyWriteBefore = (identifier, propertyName, referenceNode, scopes) => {
|
|
28587
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
28588
|
+
return getEquivalentSymbols(identifier, scopes).some((symbol) => symbolHasStaticPropertyWriteBefore(symbol, propertyName, referenceNode, scopes));
|
|
28589
|
+
};
|
|
28590
|
+
//#endregion
|
|
28591
|
+
//#region src/plugin/utils/has-symbol-write-before.ts
|
|
28592
|
+
const hasSymbolWriteBefore = (symbol, referenceNode, scopes) => symbol.references.some((reference) => {
|
|
28593
|
+
if (reference.flag === "read") return false;
|
|
28594
|
+
const writeFunction = findEnclosingFunction(reference.identifier);
|
|
28595
|
+
if (writeFunction === findEnclosingFunction(referenceNode)) return reference.identifier.range[0] < referenceNode.range[0];
|
|
28596
|
+
if (!writeFunction) return true;
|
|
28597
|
+
return isFunctionSynchronouslyInvokedBefore(writeFunction, referenceNode, scopes);
|
|
28598
|
+
});
|
|
28599
|
+
//#endregion
|
|
28600
|
+
//#region src/plugin/utils/has-stable-call-target.ts
|
|
28601
|
+
const hasStableCallTarget = (callExpression, scopes) => {
|
|
28602
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return false;
|
|
28603
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
28604
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
28605
|
+
const symbol = scopes.symbolFor(callee);
|
|
28606
|
+
return Boolean(symbol && !hasSymbolWriteBefore(symbol, callee, scopes));
|
|
28607
|
+
}
|
|
28608
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
28609
|
+
const propertyName = getStaticPropertyName(callee);
|
|
28610
|
+
const receiver = stripParenExpression(callee.object);
|
|
28611
|
+
return Boolean(propertyName && isNodeOfType(receiver, "Identifier") && !hasStaticPropertyWriteBefore(receiver, propertyName, callee, scopes));
|
|
28612
|
+
};
|
|
28613
|
+
//#endregion
|
|
27694
28614
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
27695
28615
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
27696
28616
|
"FunctionDeclaration",
|
|
@@ -27703,19 +28623,68 @@ const REACT_CREATE_ELEMENT_OPTIONS = {
|
|
|
27703
28623
|
allowGlobalReactNamespace: false,
|
|
27704
28624
|
allowUnboundBareCalls: false
|
|
27705
28625
|
};
|
|
27706
|
-
const
|
|
28626
|
+
const LODASH_SORT_BY_MODULE_SOURCES = new Set([
|
|
28627
|
+
"lodash/sortBy",
|
|
28628
|
+
"lodash/sortBy.js",
|
|
28629
|
+
"lodash-es/sortBy",
|
|
28630
|
+
"lodash-es/sortBy.js"
|
|
28631
|
+
]);
|
|
28632
|
+
const isArrayTypeAnnotation = (node) => {
|
|
28633
|
+
if (isNodeOfType(node, "TSArrayType") || isNodeOfType(node, "TSTupleType")) return true;
|
|
28634
|
+
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
28635
|
+
return isNodeOfType(node.typeName, "Identifier") && (node.typeName.name === "Array" || node.typeName.name === "ReadonlyArray");
|
|
28636
|
+
};
|
|
28637
|
+
const hasArrayTypeAnnotation = (identifier) => {
|
|
28638
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
28639
|
+
const typeAnnotation = identifier.typeAnnotation;
|
|
28640
|
+
return Boolean(typeAnnotation && isNodeOfType(typeAnnotation, "TSTypeAnnotation") && isArrayTypeAnnotation(typeAnnotation.typeAnnotation));
|
|
28641
|
+
};
|
|
28642
|
+
const isProvenArrayProducerCall = (node, scopes) => {
|
|
28643
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
28644
|
+
const callee = stripParenExpression(node.callee);
|
|
28645
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
28646
|
+
const symbol = scopes.symbolFor(callee);
|
|
28647
|
+
if (!symbol || hasSymbolWriteBefore(symbol, callee, scopes)) return false;
|
|
28648
|
+
if (!isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") && !isNodeOfType(symbol.declarationNode, "ImportSpecifier")) return false;
|
|
28649
|
+
const importBinding = getImportBindingForName(callee, callee.name);
|
|
28650
|
+
if (!importBinding || importBinding.isNamespace) return false;
|
|
28651
|
+
if (LODASH_SORT_BY_MODULE_SOURCES.has(importBinding.source) && importBinding.exportedName === "default") return true;
|
|
28652
|
+
return (importBinding.source === "lodash" || importBinding.source === "lodash-es") && importBinding.exportedName === "sortBy";
|
|
28653
|
+
};
|
|
28654
|
+
const isProvenArrayExpression = (expression, scopes, referenceNode, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
28655
|
+
const candidate = stripParenExpression(expression);
|
|
28656
|
+
if (isNodeOfType(candidate, "ArrayExpression")) return true;
|
|
28657
|
+
if (isProvenArrayProducerCall(candidate, scopes)) return true;
|
|
28658
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
28659
|
+
const symbol = scopes.symbolFor(candidate);
|
|
28660
|
+
if (!symbol || visitedSymbolIds.has(symbol.id) || hasSymbolWriteBefore(symbol, referenceNode, scopes)) return false;
|
|
28661
|
+
if (hasArrayTypeAnnotation(symbol.bindingIdentifier)) return true;
|
|
28662
|
+
if (!symbol.initializer) return false;
|
|
28663
|
+
visitedSymbolIds.add(symbol.id);
|
|
28664
|
+
return isProvenArrayExpression(symbol.initializer, scopes, referenceNode, visitedSymbolIds);
|
|
28665
|
+
};
|
|
28666
|
+
const isProvenArrayMapCall = (node, scopes) => {
|
|
28667
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
28668
|
+
const callee = stripParenExpression(node.callee);
|
|
28669
|
+
if (!isNodeOfType(callee, "MemberExpression") || getStaticPropertyName(callee) !== "map") return false;
|
|
28670
|
+
const receiver = stripParenExpression(callee.object);
|
|
28671
|
+
if (!isProvenArrayExpression(receiver, scopes, node)) return false;
|
|
28672
|
+
return !(isNodeOfType(receiver, "Identifier") && hasStaticPropertyWriteBefore(receiver, "map", node, scopes));
|
|
28673
|
+
};
|
|
28674
|
+
const isRenderPreservingCallArgumentFunction = (node, scopes) => {
|
|
27707
28675
|
if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") return false;
|
|
27708
28676
|
const parent = node.parent;
|
|
27709
28677
|
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
27710
|
-
|
|
28678
|
+
if (isReactApiCall(parent, "useMemo", scopes, { resolveNamedAliases: true }) && hasStableCallTarget(parent, scopes)) return parent.arguments[0] === node;
|
|
28679
|
+
return parent.arguments.some((argumentNode) => argumentNode === node) && isProvenArrayMapCall(parent, scopes);
|
|
27711
28680
|
};
|
|
27712
|
-
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !
|
|
28681
|
+
const isNestedRenderEvidenceBoundary = (node, scopes) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isRenderPreservingCallArgumentFunction(node, scopes);
|
|
27713
28682
|
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
27714
28683
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
27715
28684
|
let hasRenderOutput = false;
|
|
27716
28685
|
walkAst(rootNode, (node) => {
|
|
27717
28686
|
if (hasRenderOutput) return false;
|
|
27718
|
-
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
28687
|
+
if (node !== rootNode && isNestedRenderEvidenceBoundary(node, scopes)) return false;
|
|
27719
28688
|
if (isRenderOutputExpression(node, scopes)) {
|
|
27720
28689
|
hasRenderOutput = true;
|
|
27721
28690
|
return false;
|
|
@@ -27724,12 +28693,13 @@ const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
|
27724
28693
|
return hasRenderOutput;
|
|
27725
28694
|
};
|
|
27726
28695
|
const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
27727
|
-
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
28696
|
+
const functionContainsReactRenderOutput = (functionNode, scopes, controlFlow) => {
|
|
27728
28697
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
27729
|
-
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
27730
|
-
const hasRenderOutput =
|
|
28698
|
+
if (cachedEntry && cachedEntry.scopes === scopes && cachedEntry.controlFlow === controlFlow) return cachedEntry.hasRenderOutput;
|
|
28699
|
+
const hasRenderOutput = functionReturnsMatchingExpression(functionNode, scopes, (expression) => containsRenderOutput$1(expression, scopes), controlFlow);
|
|
27731
28700
|
renderOutputCache.set(functionNode, {
|
|
27732
28701
|
scopes,
|
|
28702
|
+
controlFlow,
|
|
27733
28703
|
hasRenderOutput
|
|
27734
28704
|
});
|
|
27735
28705
|
return hasRenderOutput;
|
|
@@ -27745,8 +28715,8 @@ const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration
|
|
|
27745
28715
|
const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
|
|
27746
28716
|
const symbolIsLocalComponent = (symbol, context) => {
|
|
27747
28717
|
const declaration = symbol.declarationNode;
|
|
27748
|
-
if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
|
|
27749
|
-
if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
|
|
28718
|
+
if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes, context.cfg);
|
|
28719
|
+
if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes, context.cfg);
|
|
27750
28720
|
return false;
|
|
27751
28721
|
};
|
|
27752
28722
|
const isModuleScopeDeclaration = (declaration) => {
|
|
@@ -27844,271 +28814,21 @@ const noCallComponentAsFunction = defineRule({
|
|
|
27844
28814
|
}
|
|
27845
28815
|
});
|
|
27846
28816
|
//#endregion
|
|
27847
|
-
//#region src/plugin/utils/
|
|
27848
|
-
const
|
|
27849
|
-
|
|
27850
|
-
|
|
27851
|
-
|
|
27852
|
-
|
|
27853
|
-
//#region src/plugin/utils/is-hook-binding-in-scope.ts
|
|
27854
|
-
const isHookBindingInScope = (node, query) => {
|
|
27855
|
-
const { bindingName, hookName, destructureIndex } = query;
|
|
27856
|
-
let cursor = node;
|
|
27857
|
-
while (cursor) {
|
|
27858
|
-
if (isNodeOfType(cursor, "BlockStatement") || isNodeOfType(cursor, "Program")) for (const statement of cursor.body ?? []) {
|
|
27859
|
-
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
27860
|
-
for (const declarator of statement.declarations ?? []) {
|
|
27861
|
-
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
27862
|
-
if (!isHookCall$2(declarator.init, hookName)) continue;
|
|
27863
|
-
if (destructureIndex !== void 0) {
|
|
27864
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
|
|
27865
|
-
const elements = declarator.id.elements ?? [];
|
|
27866
|
-
if (elements.length <= destructureIndex) continue;
|
|
27867
|
-
const element = elements[destructureIndex];
|
|
27868
|
-
if (isNodeOfType(element, "Identifier") && element.name === bindingName) return true;
|
|
27869
|
-
} else if (isNodeOfType(declarator.id, "Identifier") && declarator.id.name === bindingName) return true;
|
|
27870
|
-
}
|
|
27871
|
-
}
|
|
27872
|
-
cursor = cursor.parent ?? null;
|
|
27873
|
-
}
|
|
27874
|
-
return false;
|
|
27875
|
-
};
|
|
27876
|
-
//#endregion
|
|
27877
|
-
//#region src/plugin/utils/is-use-state-setter-in-scope.ts
|
|
27878
|
-
const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node, {
|
|
27879
|
-
bindingName: setterName,
|
|
27880
|
-
hookName: "useState",
|
|
27881
|
-
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: () => ({})
|
|
27882
28823
|
});
|
|
27883
28824
|
//#endregion
|
|
27884
28825
|
//#region src/plugin/rules/state-and-effects/no-cascading-set-state.ts
|
|
27885
|
-
const
|
|
27886
|
-
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
27887
|
-
return false;
|
|
27888
|
-
};
|
|
27889
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
27890
|
-
"forEach",
|
|
27891
|
-
"map",
|
|
27892
|
-
"filter",
|
|
27893
|
-
"reduce",
|
|
27894
|
-
"reduceRight",
|
|
27895
|
-
"flatMap",
|
|
27896
|
-
"some",
|
|
27897
|
-
"every",
|
|
27898
|
-
"find",
|
|
27899
|
-
"findIndex",
|
|
27900
|
-
"findLast",
|
|
27901
|
-
"findLastIndex",
|
|
27902
|
-
"sort"
|
|
27903
|
-
]);
|
|
27904
|
-
const runsOnEffectDispatch = (functionNode) => {
|
|
27905
|
-
const parent = functionNode.parent;
|
|
27906
|
-
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
27907
|
-
if (parent.callee === functionNode) return true;
|
|
27908
|
-
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
27909
|
-
const callee = parent.callee;
|
|
27910
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
27911
|
-
};
|
|
27912
|
-
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
27913
|
-
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
27914
|
-
const analyzeStatementSequence = (statements, context) => {
|
|
27915
|
-
let fallThroughCount = 0;
|
|
27916
|
-
let maxTerminatedCount = 0;
|
|
27917
|
-
for (const statement of statements) {
|
|
27918
|
-
if (isNodeOfType(statement, "IfStatement")) {
|
|
27919
|
-
fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
|
|
27920
|
-
const thenSummary = analyzeBranchStatements(statement.consequent, context);
|
|
27921
|
-
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
27922
|
-
fallThroughCount: 0,
|
|
27923
|
-
maxTerminatedCount: 0,
|
|
27924
|
-
doAllPathsTerminate: false
|
|
27925
|
-
};
|
|
27926
|
-
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
27927
|
-
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
27928
|
-
fallThroughCount: 0,
|
|
27929
|
-
maxTerminatedCount,
|
|
27930
|
-
doAllPathsTerminate: true
|
|
27931
|
-
};
|
|
27932
|
-
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
27933
|
-
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
27934
|
-
continue;
|
|
27935
|
-
}
|
|
27936
|
-
if (isTerminatingStatement(statement)) {
|
|
27937
|
-
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
27938
|
-
return {
|
|
27939
|
-
fallThroughCount: 0,
|
|
27940
|
-
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
27941
|
-
doAllPathsTerminate: true
|
|
27942
|
-
};
|
|
27943
|
-
}
|
|
27944
|
-
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
27945
|
-
}
|
|
27946
|
-
return {
|
|
27947
|
-
fallThroughCount,
|
|
27948
|
-
maxTerminatedCount,
|
|
27949
|
-
doAllPathsTerminate: false
|
|
27950
|
-
};
|
|
27951
|
-
};
|
|
27952
|
-
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
27953
|
-
const summary = analyzeStatementSequence(statements, context);
|
|
27954
|
-
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
27955
|
-
};
|
|
27956
|
-
const collectLocalHelperFunctions = (root) => {
|
|
27957
|
-
const helpersByName = /* @__PURE__ */ new Map();
|
|
27958
|
-
const visit = (node) => {
|
|
27959
|
-
if (isNodeOfType(node, "FunctionDeclaration") && node.id) helpersByName.set(node.id.name, node);
|
|
27960
|
-
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.init && isFunctionLike$2(node.init)) helpersByName.set(node.id.name, node.init);
|
|
27961
|
-
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]);
|
|
27962
|
-
const record = node;
|
|
27963
|
-
for (const key of Object.keys(record)) {
|
|
27964
|
-
if (key === "parent") continue;
|
|
27965
|
-
const child = record[key];
|
|
27966
|
-
if (Array.isArray(child)) {
|
|
27967
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item) visit(item);
|
|
27968
|
-
} else if (child && typeof child === "object" && "type" in child) visit(child);
|
|
27969
|
-
}
|
|
27970
|
-
};
|
|
27971
|
-
visit(root);
|
|
27972
|
-
return helpersByName;
|
|
27973
|
-
};
|
|
27974
|
-
const isWholesaleDelegationCall = (callNode, effectCallback) => {
|
|
27975
|
-
const parent = callNode.parent;
|
|
27976
|
-
if (!parent) return false;
|
|
27977
|
-
if (parent === effectCallback) return true;
|
|
27978
|
-
if (!isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
27979
|
-
return parent.parent === effectCallback.body;
|
|
27980
|
-
};
|
|
27981
|
-
const countFunctionBodySetStateCalls = (functionNode, context) => {
|
|
27982
|
-
if (isAsyncFunctionLike(functionNode)) return 0;
|
|
27983
|
-
const body = functionNode.body;
|
|
27984
|
-
if (!body) return 0;
|
|
27985
|
-
return countMaxPathSetStateCalls(body, context);
|
|
27986
|
-
};
|
|
27987
|
-
const countMaxPathSetStateCalls = (node, context) => {
|
|
27988
|
-
if (!node || typeof node !== "object") return 0;
|
|
27989
|
-
if (isFunctionLike$2(node)) return 0;
|
|
27990
|
-
if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
|
|
27991
|
-
if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
|
|
27992
|
-
const consequent = node.consequent;
|
|
27993
|
-
const alternate = node.alternate;
|
|
27994
|
-
const testCount = countMaxPathSetStateCalls(node.test, context);
|
|
27995
|
-
const thenCount = countMaxPathSetStateCalls(consequent, context);
|
|
27996
|
-
const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
|
|
27997
|
-
return testCount + Math.max(thenCount, elseCount);
|
|
27998
|
-
}
|
|
27999
|
-
if (isNodeOfType(node, "SwitchStatement")) {
|
|
28000
|
-
let maxRunSetters = 0;
|
|
28001
|
-
let currentRunSetters = 0;
|
|
28002
|
-
for (const switchCase of node.cases ?? []) {
|
|
28003
|
-
const consequent = switchCase.consequent ?? [];
|
|
28004
|
-
let caseSetters = 0;
|
|
28005
|
-
let runEnds = false;
|
|
28006
|
-
for (const statement of consequent) {
|
|
28007
|
-
caseSetters += countMaxPathSetStateCalls(statement, context);
|
|
28008
|
-
if (isTerminatingStatement(statement)) runEnds = true;
|
|
28009
|
-
}
|
|
28010
|
-
currentRunSetters += caseSetters;
|
|
28011
|
-
if (runEnds) {
|
|
28012
|
-
if (currentRunSetters > maxRunSetters) maxRunSetters = currentRunSetters;
|
|
28013
|
-
currentRunSetters = 0;
|
|
28014
|
-
}
|
|
28015
|
-
}
|
|
28016
|
-
if (currentRunSetters > maxRunSetters) maxRunSetters = currentRunSetters;
|
|
28017
|
-
return maxRunSetters;
|
|
28018
|
-
}
|
|
28019
|
-
if (isNodeOfType(node, "TryStatement")) {
|
|
28020
|
-
const tryCount = countMaxPathSetStateCalls(node.block, context);
|
|
28021
|
-
const catchCount = node.handler ? countMaxPathSetStateCalls(node.handler.body, context) : 0;
|
|
28022
|
-
const finallyCount = node.finalizer ? countMaxPathSetStateCalls(node.finalizer, context) : 0;
|
|
28023
|
-
return Math.max(tryCount, catchCount) + finallyCount;
|
|
28024
|
-
}
|
|
28025
|
-
if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
|
|
28026
|
-
let nestedSettersInArgs = 0;
|
|
28027
|
-
for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$2(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
|
|
28028
|
-
return 1 + nestedSettersInArgs;
|
|
28029
|
-
}
|
|
28030
|
-
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
|
|
28031
|
-
const helperFunction = context.helpersByName.get(node.callee.name);
|
|
28032
|
-
if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
|
|
28033
|
-
context.activeHelpers.add(helperFunction);
|
|
28034
|
-
let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
|
|
28035
|
-
context.activeHelpers.delete(helperFunction);
|
|
28036
|
-
for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
|
|
28037
|
-
return helperCount;
|
|
28038
|
-
}
|
|
28039
|
-
}
|
|
28040
|
-
const countChild = (child) => {
|
|
28041
|
-
if (isFunctionLike$2(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
|
|
28042
|
-
return countMaxPathSetStateCalls(child, context);
|
|
28043
|
-
};
|
|
28044
|
-
let total = 0;
|
|
28045
|
-
const nodeRecord = node;
|
|
28046
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
28047
|
-
if (key === "parent") continue;
|
|
28048
|
-
const child = nodeRecord[key];
|
|
28049
|
-
if (Array.isArray(child)) {
|
|
28050
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
|
|
28051
|
-
} else if (child && typeof child === "object" && "type" in child) total += countChild(child);
|
|
28052
|
-
}
|
|
28053
|
-
return total;
|
|
28054
|
-
};
|
|
28055
|
-
const isInitOnlyEffect = (node) => {
|
|
28056
|
-
const depsArg = node.arguments?.[1];
|
|
28057
|
-
if (!depsArg) return false;
|
|
28058
|
-
if (!isNodeOfType(depsArg, "ArrayExpression")) return false;
|
|
28059
|
-
return (depsArg.elements ?? []).length === 0;
|
|
28060
|
-
};
|
|
28061
|
-
const DEV_ENV_FLAG_NAMES = new Set([
|
|
28062
|
-
"DEV",
|
|
28063
|
-
"PROD",
|
|
28064
|
-
"MODE",
|
|
28065
|
-
"NODE_ENV"
|
|
28066
|
-
]);
|
|
28067
|
-
const mentionsDevEnvFlag = (node) => {
|
|
28068
|
-
if (!node || typeof node !== "object") return false;
|
|
28069
|
-
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;
|
|
28070
|
-
const record = node;
|
|
28071
|
-
for (const key of Object.keys(record)) {
|
|
28072
|
-
if (key === "parent") continue;
|
|
28073
|
-
const child = record[key];
|
|
28074
|
-
if (Array.isArray(child)) {
|
|
28075
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item && mentionsDevEnvFlag(item)) return true;
|
|
28076
|
-
} else if (child && typeof child === "object" && "type" in child && mentionsDevEnvFlag(child)) return true;
|
|
28077
|
-
}
|
|
28078
|
-
return false;
|
|
28079
|
-
};
|
|
28080
|
-
const isDevOnlyGuardedEffect = (callback) => {
|
|
28081
|
-
const body = callback.body;
|
|
28082
|
-
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
28083
|
-
const firstStatement = (body.body ?? [])[0];
|
|
28084
|
-
if (!firstStatement) return false;
|
|
28085
|
-
if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
|
|
28086
|
-
const consequent = firstStatement.consequent;
|
|
28087
|
-
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
28088
|
-
return mentionsDevEnvFlag(firstStatement.test);
|
|
28089
|
-
};
|
|
28090
|
-
const noCascadingSetState = defineRule({
|
|
28826
|
+
const noCascadingSetState = defineRetiredRule({
|
|
28091
28827
|
id: "no-cascading-set-state",
|
|
28092
28828
|
title: "Multiple setState calls in one effect",
|
|
28093
28829
|
severity: "warn",
|
|
28094
28830
|
tags: ["test-noise"],
|
|
28095
|
-
recommendation: "
|
|
28096
|
-
create: (context) => ({ CallExpression(node) {
|
|
28097
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
28098
|
-
if (isInitOnlyEffect(node)) return;
|
|
28099
|
-
const callback = getEffectCallback(node, context.scopes);
|
|
28100
|
-
if (!callback) return;
|
|
28101
|
-
if (isDevOnlyGuardedEffect(callback)) return;
|
|
28102
|
-
const setStateCallCount = countFunctionBodySetStateCalls(callback, {
|
|
28103
|
-
helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
|
|
28104
|
-
activeHelpers: /* @__PURE__ */ new Set(),
|
|
28105
|
-
effectCallback: callback
|
|
28106
|
-
});
|
|
28107
|
-
if (setStateCallCount >= 3) context.report({
|
|
28108
|
-
node,
|
|
28109
|
-
message: `${setStateCallCount} setState calls in one useEffect redraw your screen each time they run together.`
|
|
28110
|
-
});
|
|
28111
|
-
} })
|
|
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."
|
|
28112
28832
|
});
|
|
28113
28833
|
//#endregion
|
|
28114
28834
|
//#region src/plugin/rules/state-and-effects/no-chain-state-updates.ts
|
|
@@ -28364,7 +29084,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
28364
29084
|
if (!enclosingFunction) return;
|
|
28365
29085
|
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
28366
29086
|
if (!displayName) return;
|
|
28367
|
-
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
29087
|
+
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes, context.cfg))) return;
|
|
28368
29088
|
context.report({
|
|
28369
29089
|
node,
|
|
28370
29090
|
message: MESSAGE$30
|
|
@@ -29982,6 +30702,9 @@ const noDirectMutationState = defineRule({
|
|
|
29982
30702
|
})
|
|
29983
30703
|
});
|
|
29984
30704
|
//#endregion
|
|
30705
|
+
//#region src/plugin/utils/is-setter-identifier.ts
|
|
30706
|
+
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
30707
|
+
//#endregion
|
|
29985
30708
|
//#region src/plugin/rules/state-and-effects/utils/collect-use-state-bindings.ts
|
|
29986
30709
|
const collectUseStateBindings = (componentBody) => {
|
|
29987
30710
|
const bindings = [];
|
|
@@ -30868,7 +31591,7 @@ const noEffectChain = defineRule({
|
|
|
30868
31591
|
const effectInfos = [];
|
|
30869
31592
|
for (const effectCall of findTopLevelEffectCalls(componentBody)) {
|
|
30870
31593
|
const callback = getEffectCallback(effectCall, context.scopes);
|
|
30871
|
-
if (!callback) continue;
|
|
31594
|
+
if (!callback || !isFunctionLike$2(callback) || callback.async) continue;
|
|
30872
31595
|
const analysisFunctions = collectSynchronouslyInvokedFunctions(callback, context.scopes);
|
|
30873
31596
|
const writtenStateNames = collectWrittenStateNamesInEffect(analysisFunctions, setterToStateName);
|
|
30874
31597
|
effectInfos.push({
|
|
@@ -32252,6 +32975,9 @@ const noEventTriggerState = defineRule({
|
|
|
32252
32975
|
}
|
|
32253
32976
|
});
|
|
32254
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
|
|
32255
32981
|
//#region src/plugin/rules/state-and-effects/no-fetch-in-effect.ts
|
|
32256
32982
|
const IMPORT_INITIALIZER_TYPES = new Set([
|
|
32257
32983
|
"ImportSpecifier",
|
|
@@ -32853,7 +33579,7 @@ const noGiantComponent = defineRule({
|
|
|
32853
33579
|
FunctionDeclaration(node) {
|
|
32854
33580
|
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
32855
33581
|
if (getOversizedComponentLineCount(node) === null) return;
|
|
32856
|
-
if (!functionContainsReactRenderOutput(node, context.scopes)) return;
|
|
33582
|
+
if (!functionContainsReactRenderOutput(node, context.scopes, context.cfg)) return;
|
|
32857
33583
|
reportOversizedComponent(node.id, node.id.name);
|
|
32858
33584
|
},
|
|
32859
33585
|
VariableDeclarator(node) {
|
|
@@ -32861,7 +33587,7 @@ const noGiantComponent = defineRule({
|
|
|
32861
33587
|
const functionNode = unwrapReactHocFunction(node.init);
|
|
32862
33588
|
if (!functionNode) return;
|
|
32863
33589
|
if (getOversizedComponentLineCount(functionNode) === null) return;
|
|
32864
|
-
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
33590
|
+
if (!functionContainsReactRenderOutput(functionNode, context.scopes, context.cfg)) return;
|
|
32865
33591
|
reportOversizedComponent(node.id, node.id.name);
|
|
32866
33592
|
}
|
|
32867
33593
|
};
|
|
@@ -33499,26 +34225,6 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
33499
34225
|
} })
|
|
33500
34226
|
});
|
|
33501
34227
|
//#endregion
|
|
33502
|
-
//#region src/plugin/utils/react-ref-origin.ts
|
|
33503
|
-
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
33504
|
-
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
33505
|
-
if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticPropertyName(memberExpression) !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
33506
|
-
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
33507
|
-
if (!symbol?.initializer) return null;
|
|
33508
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
33509
|
-
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
33510
|
-
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
33511
|
-
};
|
|
33512
|
-
const hasReactRefCurrentOrigin = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
33513
|
-
const expression = stripParenExpression(node);
|
|
33514
|
-
if (resolveReactRefSymbol(expression, scopes)) return true;
|
|
33515
|
-
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
33516
|
-
const symbol = resolveConstIdentifierAlias(expression, scopes);
|
|
33517
|
-
if (!symbol?.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
33518
|
-
visitedSymbolIds.add(symbol.id);
|
|
33519
|
-
return hasReactRefCurrentOrigin(symbol.initializer, scopes, visitedSymbolIds);
|
|
33520
|
-
};
|
|
33521
|
-
//#endregion
|
|
33522
34228
|
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
33523
34229
|
const TIMER_FUNCTION_NAMES = new Set([
|
|
33524
34230
|
"cancelAnimationFrame",
|
|
@@ -35141,7 +35847,7 @@ const noManyBooleanProps = defineRule({
|
|
|
35141
35847
|
if (!param) return;
|
|
35142
35848
|
const propsBinding = resolveFirstArgumentBinding(param);
|
|
35143
35849
|
if (!propsBinding) return;
|
|
35144
|
-
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
35850
|
+
if (!functionContainsReactRenderOutput(functionNode, context.scopes, context.cfg)) return;
|
|
35145
35851
|
if (isNodeOfType(propsBinding, "ObjectPattern")) {
|
|
35146
35852
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
35147
35853
|
const booleanLikePropNames = [];
|
|
@@ -38103,6 +38809,174 @@ const noPropCallbackInRender = defineRule({
|
|
|
38103
38809
|
} })
|
|
38104
38810
|
});
|
|
38105
38811
|
//#endregion
|
|
38812
|
+
//#region src/plugin/utils/is-proven-react-class-component.ts
|
|
38813
|
+
const REACT_COMPONENT_CLASS_NAMES = new Set(["Component", "PureComponent"]);
|
|
38814
|
+
const isReactComponentClassValue = (node, scopes, visitedClassNodes, visitedSymbolIds) => {
|
|
38815
|
+
const expression = stripParenExpression(node);
|
|
38816
|
+
if (isNodeOfType(expression, "MemberExpression")) {
|
|
38817
|
+
const propertyName = getStaticPropertyName(expression);
|
|
38818
|
+
const receiver = stripParenExpression(expression.object);
|
|
38819
|
+
return Boolean(propertyName && REACT_COMPONENT_CLASS_NAMES.has(propertyName) && isNodeOfType(receiver, "Identifier") && !hasStaticPropertyWriteBefore(receiver, propertyName, expression, scopes) && isReactNamespaceImport(receiver, scopes));
|
|
38820
|
+
}
|
|
38821
|
+
if (isNodeOfType(expression, "ClassExpression")) return isProvenReactClassComponent(expression, scopes, visitedClassNodes, visitedSymbolIds);
|
|
38822
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
38823
|
+
const symbol = scopes.symbolFor(expression);
|
|
38824
|
+
if (!symbol || visitedSymbolIds.has(symbol.id) || hasSymbolWriteBefore(symbol, expression, scopes)) return false;
|
|
38825
|
+
visitedSymbolIds.add(symbol.id);
|
|
38826
|
+
if (isImportedFromReact(symbol)) {
|
|
38827
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
38828
|
+
return Boolean(importedName && REACT_COMPONENT_CLASS_NAMES.has(importedName));
|
|
38829
|
+
}
|
|
38830
|
+
if (isNodeOfType(symbol.declarationNode, "ClassDeclaration") || isNodeOfType(symbol.declarationNode, "ClassExpression")) return isProvenReactClassComponent(symbol.declarationNode, scopes, visitedClassNodes, visitedSymbolIds);
|
|
38831
|
+
return Boolean(symbol.kind === "const" && symbol.initializer && isReactComponentClassValue(symbol.initializer, scopes, visitedClassNodes, visitedSymbolIds));
|
|
38832
|
+
};
|
|
38833
|
+
const isProvenReactClassComponent = (classNode, scopes, visitedClassNodes = /* @__PURE__ */ new Set(), visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
38834
|
+
if (!isNodeOfType(classNode, "ClassDeclaration") && !isNodeOfType(classNode, "ClassExpression") || visitedClassNodes.has(classNode) || !classNode.superClass) return false;
|
|
38835
|
+
visitedClassNodes.add(classNode);
|
|
38836
|
+
return isReactComponentClassValue(classNode.superClass, scopes, visitedClassNodes, visitedSymbolIds);
|
|
38837
|
+
};
|
|
38838
|
+
//#endregion
|
|
38839
|
+
//#region src/plugin/utils/function-contains-proven-react-hook-call.ts
|
|
38840
|
+
const functionContainsProvenReactHookCall = (functionNode, scopes) => {
|
|
38841
|
+
if (!isFunctionLike$2(functionNode)) return false;
|
|
38842
|
+
let containsReactHookCall = false;
|
|
38843
|
+
walkAst(functionNode.body, (node) => {
|
|
38844
|
+
if (containsReactHookCall) return false;
|
|
38845
|
+
if (node !== functionNode.body && (isFunctionLike$2(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
38846
|
+
if (isNodeOfType(node, "CallExpression") && isReactApiCall(node, BUILTIN_HOOK_NAMES, scopes, { resolveNamedAliases: true })) {
|
|
38847
|
+
containsReactHookCall = true;
|
|
38848
|
+
return false;
|
|
38849
|
+
}
|
|
38850
|
+
});
|
|
38851
|
+
return containsReactHookCall;
|
|
38852
|
+
};
|
|
38853
|
+
//#endregion
|
|
38854
|
+
//#region src/plugin/utils/function-returns-props-children.ts
|
|
38855
|
+
const functionReturnsPropsChildren = (functionNode, scopes, controlFlow) => {
|
|
38856
|
+
if (!isFunctionLike$2(functionNode) || functionNode.params.length === 0) return false;
|
|
38857
|
+
const firstParameter = stripParenExpression(functionNode.params[0]);
|
|
38858
|
+
const firstParameterPattern = isNodeOfType(firstParameter, "AssignmentPattern") ? stripParenExpression(firstParameter.left) : firstParameter;
|
|
38859
|
+
const propsParameterSymbol = isNodeOfType(firstParameterPattern, "Identifier") ? scopes.symbolFor(firstParameterPattern) : null;
|
|
38860
|
+
const childrenBindingSymbolIds = /* @__PURE__ */ new Set();
|
|
38861
|
+
if (isNodeOfType(firstParameterPattern, "ObjectPattern")) {
|
|
38862
|
+
for (const property of firstParameterPattern.properties) if (isNodeOfType(property, "Property") && getStaticPropertyKeyName(property, { allowComputedString: true }) === "children") {
|
|
38863
|
+
const propertyValue = stripParenExpression(property.value);
|
|
38864
|
+
const childrenBinding = isNodeOfType(propertyValue, "AssignmentPattern") ? stripParenExpression(propertyValue.left) : propertyValue;
|
|
38865
|
+
if (!isNodeOfType(childrenBinding, "Identifier")) continue;
|
|
38866
|
+
const childrenBindingSymbol = scopes.symbolFor(childrenBinding);
|
|
38867
|
+
if (childrenBindingSymbol) childrenBindingSymbolIds.add(childrenBindingSymbol.id);
|
|
38868
|
+
}
|
|
38869
|
+
}
|
|
38870
|
+
return functionReturnsMatchingExpression(functionNode, scopes, (expression) => {
|
|
38871
|
+
const candidate = stripParenExpression(expression);
|
|
38872
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
38873
|
+
const symbol = scopes.symbolFor(candidate);
|
|
38874
|
+
return Boolean(symbol && childrenBindingSymbolIds.has(symbol.id) && !hasSymbolWriteBefore(symbol, candidate, scopes));
|
|
38875
|
+
}
|
|
38876
|
+
if (!isNodeOfType(candidate, "MemberExpression")) return false;
|
|
38877
|
+
if (getStaticPropertyName(candidate) !== "children") return false;
|
|
38878
|
+
const receiver = stripParenExpression(candidate.object);
|
|
38879
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
38880
|
+
const receiverSymbol = scopes.symbolFor(receiver);
|
|
38881
|
+
if (!receiverSymbol || !propsParameterSymbol) return false;
|
|
38882
|
+
return receiverSymbol.id === propsParameterSymbol.id && !hasSymbolWriteBefore(receiverSymbol, candidate, scopes) && !hasStaticPropertyWriteBefore(receiver, "children", candidate, scopes);
|
|
38883
|
+
}, controlFlow);
|
|
38884
|
+
};
|
|
38885
|
+
//#endregion
|
|
38886
|
+
//#region src/plugin/utils/function-returns-only-null.ts
|
|
38887
|
+
const isNullExpression = (expression) => {
|
|
38888
|
+
const candidate = stripParenExpression(expression);
|
|
38889
|
+
return isNodeOfType(candidate, "Literal") && candidate.value === null;
|
|
38890
|
+
};
|
|
38891
|
+
const functionReturnsOnlyNull = (functionNode) => {
|
|
38892
|
+
if (!isFunctionLike$2(functionNode)) return false;
|
|
38893
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) return isNullExpression(functionNode.body);
|
|
38894
|
+
const returnStatements = collectFunctionReturnStatements(functionNode);
|
|
38895
|
+
return returnStatements.length > 0 && returnStatements.every((returnStatement) => Boolean(returnStatement.argument && isNullExpression(returnStatement.argument)));
|
|
38896
|
+
};
|
|
38897
|
+
//#endregion
|
|
38898
|
+
//#region src/plugin/utils/is-proven-styled-component-expression.ts
|
|
38899
|
+
const findFactoryRoot = (node) => {
|
|
38900
|
+
const candidate = stripParenExpression(node);
|
|
38901
|
+
if (isNodeOfType(candidate, "Identifier")) return candidate;
|
|
38902
|
+
if (isNodeOfType(candidate, "MemberExpression")) return findFactoryRoot(candidate.object);
|
|
38903
|
+
if (isNodeOfType(candidate, "CallExpression")) return findFactoryRoot(candidate.callee);
|
|
38904
|
+
return null;
|
|
38905
|
+
};
|
|
38906
|
+
const findFactoryPropertyName = (node) => {
|
|
38907
|
+
const candidate = stripParenExpression(node);
|
|
38908
|
+
if (isNodeOfType(candidate, "MemberExpression")) return findFactoryPropertyName(candidate.object) ?? getStaticPropertyName(candidate);
|
|
38909
|
+
if (isNodeOfType(candidate, "CallExpression")) return findFactoryPropertyName(candidate.callee);
|
|
38910
|
+
return null;
|
|
38911
|
+
};
|
|
38912
|
+
const isProvenStyledComponentExpression = (expression, scopes) => {
|
|
38913
|
+
const candidate = stripParenExpression(expression);
|
|
38914
|
+
if (!isNodeOfType(candidate, "TaggedTemplateExpression")) return false;
|
|
38915
|
+
const factoryRoot = findFactoryRoot(candidate.tag);
|
|
38916
|
+
if (!factoryRoot) return false;
|
|
38917
|
+
const factoryPropertyName = findFactoryPropertyName(candidate.tag);
|
|
38918
|
+
if (factoryPropertyName && hasStaticPropertyWriteBefore(factoryRoot, factoryPropertyName, candidate, scopes)) return false;
|
|
38919
|
+
const symbol = resolveConstIdentifierAlias(factoryRoot, scopes);
|
|
38920
|
+
if (!symbol || symbol.kind !== "import") return false;
|
|
38921
|
+
if (!(isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || getImportedName(symbol.declarationNode) === "styled")) return false;
|
|
38922
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
38923
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "styled-components");
|
|
38924
|
+
};
|
|
38925
|
+
//#endregion
|
|
38926
|
+
//#region src/plugin/utils/is-proven-react-component-symbol.ts
|
|
38927
|
+
const REACT_COMPONENT_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
38928
|
+
const functionHasComponentEvidence = (functionNode, scopes, controlFlow) => functionContainsReactRenderOutput(functionNode, scopes, controlFlow) || functionReturnsPropsChildren(functionNode, scopes, controlFlow) || functionContainsProvenReactHookCall(functionNode, scopes) && functionReturnsOnlyNull(functionNode);
|
|
38929
|
+
const isProvenReactComponentExpression = (expression, scopes, controlFlow, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
38930
|
+
const candidate = stripParenExpression(expression);
|
|
38931
|
+
if (isInlineFunctionExpression(candidate)) return functionHasComponentEvidence(candidate, scopes, controlFlow);
|
|
38932
|
+
if (isNodeOfType(candidate, "ClassExpression")) return isProvenReactClassComponent(candidate, scopes);
|
|
38933
|
+
if (isProvenStyledComponentExpression(candidate, scopes)) return true;
|
|
38934
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
38935
|
+
const symbol = scopes.symbolFor(candidate);
|
|
38936
|
+
if (!symbol || visitedSymbolIds.has(symbol.id) || hasSymbolWriteBefore(symbol, candidate, scopes)) return false;
|
|
38937
|
+
visitedSymbolIds.add(symbol.id);
|
|
38938
|
+
if (isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return functionHasComponentEvidence(symbol.declarationNode, scopes, controlFlow);
|
|
38939
|
+
if (isNodeOfType(symbol.declarationNode, "ClassDeclaration") || isNodeOfType(symbol.declarationNode, "ClassExpression")) return isProvenReactClassComponent(symbol.declarationNode, scopes);
|
|
38940
|
+
return Boolean(symbol.initializer && isProvenReactComponentExpression(symbol.initializer, scopes, controlFlow, visitedSymbolIds));
|
|
38941
|
+
}
|
|
38942
|
+
if (!isNodeOfType(candidate, "CallExpression")) return false;
|
|
38943
|
+
if (!hasStableCallTarget(candidate, scopes)) return false;
|
|
38944
|
+
if (isReactApiCall(candidate, REACT_COMPONENT_HOC_NAMES, scopes, { resolveNamedAliases: true })) {
|
|
38945
|
+
const wrappedComponent = candidate.arguments[0];
|
|
38946
|
+
return Boolean(wrappedComponent && !isNodeOfType(wrappedComponent, "SpreadElement") && isProvenReactComponentExpression(wrappedComponent, scopes, controlFlow, visitedSymbolIds));
|
|
38947
|
+
}
|
|
38948
|
+
if (!isReactApiCall(candidate, "useMemo", scopes, { resolveNamedAliases: true })) return false;
|
|
38949
|
+
const factory = candidate.arguments[0];
|
|
38950
|
+
if (!factory || isNodeOfType(factory, "SpreadElement")) return false;
|
|
38951
|
+
const unwrappedFactory = stripParenExpression(factory);
|
|
38952
|
+
if (!isInlineFunctionExpression(unwrappedFactory)) return false;
|
|
38953
|
+
if (!isNodeOfType(unwrappedFactory.body, "BlockStatement")) return isProvenReactComponentExpression(unwrappedFactory.body, scopes, controlFlow, visitedSymbolIds);
|
|
38954
|
+
const returnStatements = collectFunctionReturnStatements(unwrappedFactory);
|
|
38955
|
+
const returnedExpression = returnStatements[0]?.argument;
|
|
38956
|
+
return Boolean(returnStatements.length === 1 && returnedExpression && isProvenReactComponentExpression(returnedExpression, scopes, controlFlow, visitedSymbolIds));
|
|
38957
|
+
};
|
|
38958
|
+
const isProvenReactComponentSymbol = (symbol, scopes, controlFlow, componentReference) => {
|
|
38959
|
+
const candidateSymbols = symbol.kind === "ts-module" ? symbol.scope.symbols.filter((candidateSymbol) => candidateSymbol.name === symbol.name && candidateSymbol.kind !== "ts-module") : [symbol];
|
|
38960
|
+
for (const candidateSymbol of candidateSymbols) {
|
|
38961
|
+
if (hasSymbolWriteBefore(candidateSymbol, componentReference, scopes)) continue;
|
|
38962
|
+
if (isComponentDeclaration(candidateSymbol.declarationNode)) {
|
|
38963
|
+
if (functionHasComponentEvidence(candidateSymbol.declarationNode, scopes, controlFlow)) return true;
|
|
38964
|
+
continue;
|
|
38965
|
+
}
|
|
38966
|
+
const initializer = candidateSymbol.initializer ? stripParenExpression(candidateSymbol.initializer) : null;
|
|
38967
|
+
if (isNodeOfType(candidateSymbol.declarationNode, "VariableDeclarator") && isNodeOfType(candidateSymbol.declarationNode.id, "Identifier") && isUppercaseName(candidateSymbol.declarationNode.id.name) && initializer) {
|
|
38968
|
+
if (isProvenReactComponentExpression(initializer, scopes, controlFlow)) return true;
|
|
38969
|
+
continue;
|
|
38970
|
+
}
|
|
38971
|
+
if (isNodeOfType(candidateSymbol.declarationNode, "ClassDeclaration") || isNodeOfType(candidateSymbol.declarationNode, "ClassExpression")) {
|
|
38972
|
+
if (isProvenReactClassComponent(candidateSymbol.declarationNode, scopes)) return true;
|
|
38973
|
+
continue;
|
|
38974
|
+
}
|
|
38975
|
+
if (initializer && isNodeOfType(initializer, "ClassExpression") && isProvenReactClassComponent(initializer, scopes)) return true;
|
|
38976
|
+
}
|
|
38977
|
+
return false;
|
|
38978
|
+
};
|
|
38979
|
+
//#endregion
|
|
38106
38980
|
//#region src/plugin/rules/architecture/no-prop-types.ts
|
|
38107
38981
|
const PROP_TYPES_PROPERTY = "propTypes";
|
|
38108
38982
|
const isPropTypesKey = (key, computed) => {
|
|
@@ -38110,20 +38984,19 @@ const isPropTypesKey = (key, computed) => {
|
|
|
38110
38984
|
if (computed) return isNodeOfType(key, "Literal") && key.value === PROP_TYPES_PROPERTY;
|
|
38111
38985
|
return isNodeOfType(key, "Identifier") && key.name === PROP_TYPES_PROPERTY;
|
|
38112
38986
|
};
|
|
38113
|
-
const
|
|
38987
|
+
const getComponentFromPropTypesAssignment = (left) => {
|
|
38114
38988
|
if (!isNodeOfType(left, "MemberExpression")) return null;
|
|
38115
38989
|
if (!isPropTypesKey(left.property, Boolean(left.computed))) return null;
|
|
38116
|
-
|
|
38117
|
-
if (!
|
|
38118
|
-
|
|
38990
|
+
const receiver = stripParenExpression(left.object);
|
|
38991
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
38992
|
+
if (!isUppercaseName(receiver.name)) return null;
|
|
38993
|
+
return receiver;
|
|
38119
38994
|
};
|
|
38120
|
-
const getComponentNameFromClassProperty = (node) => {
|
|
38995
|
+
const getComponentNameFromClassProperty = (node, scopes) => {
|
|
38121
38996
|
if (!node.static) return null;
|
|
38122
38997
|
if (!isPropTypesKey(node.key, Boolean(node.computed))) return null;
|
|
38123
|
-
const
|
|
38124
|
-
if (!
|
|
38125
|
-
const classNode = classBody.parent;
|
|
38126
|
-
if (!classNode) return null;
|
|
38998
|
+
const classNode = isNodeOfType(node.parent, "ClassBody") ? node.parent.parent : null;
|
|
38999
|
+
if (!classNode || !isProvenReactClassComponent(classNode, scopes)) return null;
|
|
38127
39000
|
if ((isNodeOfType(classNode, "ClassDeclaration") || isNodeOfType(classNode, "ClassExpression")) && classNode.id?.name && isUppercaseName(classNode.id.name)) return classNode.id.name;
|
|
38128
39001
|
if (!isNodeOfType(classNode, "ClassExpression")) return null;
|
|
38129
39002
|
const declarator = classNode.parent;
|
|
@@ -38144,15 +39017,17 @@ const noPropTypes = defineRule({
|
|
|
38144
39017
|
create: (context) => ({
|
|
38145
39018
|
AssignmentExpression(node) {
|
|
38146
39019
|
if (node.operator !== "=") return;
|
|
38147
|
-
const
|
|
38148
|
-
if (!
|
|
39020
|
+
const component = getComponentFromPropTypesAssignment(node.left);
|
|
39021
|
+
if (!component) return;
|
|
39022
|
+
const symbol = context.scopes.symbolFor(component);
|
|
39023
|
+
if (!symbol || !isProvenReactComponentSymbol(symbol, context.scopes, context.cfg, component)) return;
|
|
38149
39024
|
context.report({
|
|
38150
39025
|
node: node.left,
|
|
38151
|
-
message: buildMessage$11(
|
|
39026
|
+
message: buildMessage$11(component.name)
|
|
38152
39027
|
});
|
|
38153
39028
|
},
|
|
38154
39029
|
PropertyDefinition(node) {
|
|
38155
|
-
const componentName = getComponentNameFromClassProperty(node);
|
|
39030
|
+
const componentName = getComponentNameFromClassProperty(node, context.scopes);
|
|
38156
39031
|
if (!componentName) return;
|
|
38157
39032
|
context.report({
|
|
38158
39033
|
node: node.key,
|
|
@@ -40313,10 +41188,10 @@ const noStaticElementInteractions = defineRule({
|
|
|
40313
41188
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
40314
41189
|
return { JSXOpeningElement(node) {
|
|
40315
41190
|
if (isTestlikeFile) return;
|
|
40316
|
-
let hasNonBlockerHandler = false;
|
|
40317
41191
|
let hasAnyHandler = false;
|
|
40318
41192
|
let isKeyboardTarget = null;
|
|
40319
41193
|
let seenHandlerNames = null;
|
|
41194
|
+
let nonBlockerHandlerNamesLower = null;
|
|
40320
41195
|
for (const attribute of node.attributes) {
|
|
40321
41196
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
40322
41197
|
const attributeName = getJsxAttributeName(attribute.name);
|
|
@@ -40331,13 +41206,12 @@ const noStaticElementInteractions = defineRule({
|
|
|
40331
41206
|
if (!isKeyboardTarget) continue;
|
|
40332
41207
|
}
|
|
40333
41208
|
hasAnyHandler = true;
|
|
40334
|
-
if (!isPureEventBlockerHandler(attribute))
|
|
40335
|
-
hasNonBlockerHandler = true;
|
|
40336
|
-
break;
|
|
40337
|
-
}
|
|
41209
|
+
if (!isPureEventBlockerHandler(attribute)) (nonBlockerHandlerNamesLower ??= /* @__PURE__ */ new Set()).add(handlerNameLower);
|
|
40338
41210
|
}
|
|
40339
41211
|
if (!hasAnyHandler) return;
|
|
40340
|
-
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;
|
|
40341
41215
|
const elementType = getElementType(node, context.settings);
|
|
40342
41216
|
if (!HTML_TAGS.has(elementType)) return;
|
|
40343
41217
|
if (elementType === "svg") return;
|
|
@@ -40630,7 +41504,7 @@ const noThisInSfc = defineRule({
|
|
|
40630
41504
|
const enclosingFunction = findEnclosingFunctionComponent(node);
|
|
40631
41505
|
if (!enclosingFunction) return;
|
|
40632
41506
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
40633
|
-
if (!functionContainsReactRenderOutput(enclosingFunction, context.scopes)) return;
|
|
41507
|
+
if (!functionContainsReactRenderOutput(enclosingFunction, context.scopes, context.cfg)) return;
|
|
40634
41508
|
if (functionHasOwnThisMemberWrite(enclosingFunction)) return;
|
|
40635
41509
|
context.report({
|
|
40636
41510
|
node,
|
|
@@ -42295,21 +43169,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
42295
43169
|
});
|
|
42296
43170
|
return found;
|
|
42297
43171
|
};
|
|
42298
|
-
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
43172
|
+
const functionContainsJsxOrCreateElement = (functionNode, scopes, controlFlow) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement, controlFlow);
|
|
42299
43173
|
const isReactClassComponent = (classNode) => {
|
|
42300
43174
|
if (isEs6Component(classNode)) return true;
|
|
42301
43175
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
42302
43176
|
};
|
|
42303
|
-
const findEnclosingComponent = (node, scopes) => {
|
|
43177
|
+
const findEnclosingComponent = (node, scopes, controlFlow) => {
|
|
42304
43178
|
let walker = node.parent;
|
|
42305
43179
|
while (walker) {
|
|
42306
43180
|
if (isFunctionLike$2(walker)) {
|
|
42307
43181
|
const componentName = inferFunctionLikeName(walker);
|
|
42308
|
-
if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
|
|
43182
|
+
if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes, controlFlow)) return {
|
|
42309
43183
|
component: walker,
|
|
42310
43184
|
name: componentName
|
|
42311
43185
|
};
|
|
42312
|
-
if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
43186
|
+
if (!componentName && functionContainsJsxOrCreateElement(walker, scopes, controlFlow) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
42313
43187
|
component: walker,
|
|
42314
43188
|
name: null
|
|
42315
43189
|
};
|
|
@@ -42548,7 +43422,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
42548
43422
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
42549
43423
|
if (settings.allowAsProps) return;
|
|
42550
43424
|
}
|
|
42551
|
-
const enclosing = findEnclosingComponent(candidateNode, context.scopes);
|
|
43425
|
+
const enclosing = findEnclosingComponent(candidateNode, context.scopes, context.cfg);
|
|
42552
43426
|
if (!enclosing) return;
|
|
42553
43427
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
42554
43428
|
queuedReports.push({
|
|
@@ -42559,7 +43433,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
42559
43433
|
});
|
|
42560
43434
|
};
|
|
42561
43435
|
const checkFunctionLike = (node) => {
|
|
42562
|
-
if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
|
|
43436
|
+
if (!functionContainsJsxOrCreateElement(node, context.scopes, context.cfg)) return;
|
|
42563
43437
|
const inferredName = inferFunctionLikeName(node);
|
|
42564
43438
|
const propInfo = isComponentDeclaredInProp(node);
|
|
42565
43439
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -43170,7 +44044,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
|
43170
44044
|
continue;
|
|
43171
44045
|
}
|
|
43172
44046
|
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
43173
|
-
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
|
|
44047
|
+
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes, state.controlFlow)) return true;
|
|
43174
44048
|
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
43175
44049
|
}
|
|
43176
44050
|
return false;
|
|
@@ -43280,11 +44154,12 @@ const onlyExportComponents = defineRule({
|
|
|
43280
44154
|
allowExportNames: new Set(settings.allowExportNames),
|
|
43281
44155
|
allowConstantExport: settings.allowConstantExport,
|
|
43282
44156
|
localComponentNames,
|
|
43283
|
-
scopes: context.scopes
|
|
44157
|
+
scopes: context.scopes,
|
|
44158
|
+
controlFlow: context.cfg
|
|
43284
44159
|
};
|
|
43285
44160
|
for (const child of componentCandidates) {
|
|
43286
44161
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
43287
|
-
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
|
|
44162
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes, context.cfg)) localComponentNames.add(child.id.name);
|
|
43288
44163
|
}
|
|
43289
44164
|
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
43290
44165
|
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
@@ -43293,7 +44168,7 @@ const onlyExportComponents = defineRule({
|
|
|
43293
44168
|
const initializer = child.init;
|
|
43294
44169
|
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
|
|
43295
44170
|
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
43296
|
-
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
44171
|
+
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes, context.cfg)) localComponentNames.add(child.id.name);
|
|
43297
44172
|
}
|
|
43298
44173
|
}
|
|
43299
44174
|
}
|
|
@@ -44793,19 +45668,63 @@ const preferTagOverRole = defineRule({
|
|
|
44793
45668
|
});
|
|
44794
45669
|
//#endregion
|
|
44795
45670
|
//#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
|
|
44796
|
-
const
|
|
44797
|
-
|
|
44798
|
-
|
|
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;
|
|
44799
45719
|
for (const statement of componentBody.body ?? []) {
|
|
44800
45720
|
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
44801
45721
|
for (const declarator of statement.declarations ?? []) {
|
|
44802
45722
|
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
44803
|
-
if (!
|
|
44804
|
-
|
|
44805
|
-
functionTypedLocals.add(declarator.id.name);
|
|
45723
|
+
if (!declarator.init || !isPotentiallyChangingReactUseCallback(declarator.init, context)) continue;
|
|
45724
|
+
potentiallyChangingCallbacks.add(declarator.id.name);
|
|
44806
45725
|
}
|
|
44807
45726
|
}
|
|
44808
|
-
return
|
|
45727
|
+
return potentiallyChangingCallbacks;
|
|
44809
45728
|
};
|
|
44810
45729
|
const findEnclosingFunctionInsideEffect = (identifierNode, effectCallback) => {
|
|
44811
45730
|
let cursor = identifierNode.parent ?? null;
|
|
@@ -44879,7 +45798,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
44879
45798
|
create: (context) => {
|
|
44880
45799
|
const checkComponent = (componentBody) => {
|
|
44881
45800
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
44882
|
-
const
|
|
45801
|
+
const potentiallyChangingCallbackBindings = collectPotentiallyChangingCallbackBindings(componentBody, context);
|
|
44883
45802
|
for (const statement of componentBody.body ?? []) {
|
|
44884
45803
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
44885
45804
|
const effectCall = statement.expression;
|
|
@@ -44897,7 +45816,7 @@ const preferUseEffectEvent = defineRule({
|
|
|
44897
45816
|
if (!isNodeOfType(depElement, "Identifier")) continue;
|
|
44898
45817
|
const depName = depElement.name;
|
|
44899
45818
|
const isFunctionTypedPropDep = propStackTracker.isPropName(depName) && REACT_HANDLER_PROP_PATTERN.test(depName);
|
|
44900
|
-
const isFunctionTypedLocalDep =
|
|
45819
|
+
const isFunctionTypedLocalDep = potentiallyChangingCallbackBindings.has(depName);
|
|
44901
45820
|
if (!isFunctionTypedPropDep && !isFunctionTypedLocalDep) continue;
|
|
44902
45821
|
const classification = classifyCallableReadsInsideEffect(depName, callback);
|
|
44903
45822
|
if (!classification.hasAnyRead) continue;
|
|
@@ -46443,6 +47362,37 @@ const containsLocaleEnvironmentRead = (expression) => {
|
|
|
46443
47362
|
return readsLocaleEnvironment;
|
|
46444
47363
|
};
|
|
46445
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
|
|
46446
47396
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
46447
47397
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
46448
47398
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -47515,10 +48465,10 @@ const rerenderLazyStateInit = defineRule({
|
|
|
47515
48465
|
//#endregion
|
|
47516
48466
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
47517
48467
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
47518
|
-
const callbackReturnsJsx = (callback, scopes) => {
|
|
48468
|
+
const callbackReturnsJsx = (callback, scopes, controlFlow) => {
|
|
47519
48469
|
if (!callback) return false;
|
|
47520
48470
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
47521
|
-
return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
|
|
48471
|
+
return functionReturnsMatchingExpression(callback, scopes, isJsxExpression, controlFlow);
|
|
47522
48472
|
};
|
|
47523
48473
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
47524
48474
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -47596,7 +48546,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
47596
48546
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
47597
48547
|
for (const declarator of stmt.declarations ?? []) {
|
|
47598
48548
|
const init = declarator.init;
|
|
47599
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
|
|
48549
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes, context.cfg)) {
|
|
47600
48550
|
memoNode = declarator;
|
|
47601
48551
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
47602
48552
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -48132,14 +49082,6 @@ const rerenderTransitionsScroll = defineRule({
|
|
|
48132
49082
|
} })
|
|
48133
49083
|
});
|
|
48134
49084
|
//#endregion
|
|
48135
|
-
//#region src/plugin/utils/define-retired-rule.ts
|
|
48136
|
-
const defineRetiredRule = (rule) => defineRule({
|
|
48137
|
-
...rule,
|
|
48138
|
-
defaultEnabled: false,
|
|
48139
|
-
lifecycle: "retired",
|
|
48140
|
-
create: () => ({})
|
|
48141
|
-
});
|
|
48142
|
-
//#endregion
|
|
48143
49085
|
//#region src/plugin/rules/react-native/rn-animate-layout-property.ts
|
|
48144
49086
|
const rnAnimateLayoutProperty = defineRetiredRule({
|
|
48145
49087
|
id: "rn-animate-layout-property",
|
|
@@ -57307,8 +58249,7 @@ const webhookSignatureRisk = defineRule({
|
|
|
57307
58249
|
});
|
|
57308
58250
|
//#endregion
|
|
57309
58251
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
57310
|
-
const
|
|
57311
|
-
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
58252
|
+
const ZOD_MODULE_SOURCES = ["zod", "zod/v4"];
|
|
57312
58253
|
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
57313
58254
|
const getImportInfoForIdentifier = (identifier) => {
|
|
57314
58255
|
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|
|
@@ -57321,7 +58262,8 @@ const computeImportInfoForIdentifier = (identifier) => {
|
|
|
57321
58262
|
if (!specifier) return null;
|
|
57322
58263
|
const declaration = specifier.parent;
|
|
57323
58264
|
if (!declaration || !isNodeOfType(declaration, "ImportDeclaration")) return null;
|
|
57324
|
-
|
|
58265
|
+
const source = declaration.source?.value;
|
|
58266
|
+
if (typeof source !== "string" || !ZOD_MODULE_SOURCES.includes(source)) return null;
|
|
57325
58267
|
if (isNodeOfType(specifier, "ImportNamespaceSpecifier")) return {
|
|
57326
58268
|
imported: null,
|
|
57327
58269
|
isDefault: false,
|