oxlint-plugin-react-doctor 0.7.5-dev.3afd146 → 0.7.5-dev.70eff9a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +374 -127
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5934,6 +5934,21 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5934
5934
|
}
|
|
5935
5935
|
});
|
|
5936
5936
|
//#endregion
|
|
5937
|
+
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
5938
|
+
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
5939
|
+
if (!isNodeOfType(node, "Property")) return null;
|
|
5940
|
+
if (node.computed) {
|
|
5941
|
+
if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
|
|
5942
|
+
return null;
|
|
5943
|
+
}
|
|
5944
|
+
if (isNodeOfType(node.key, "Identifier")) return node.key.name;
|
|
5945
|
+
if (isNodeOfType(node.key, "Literal")) {
|
|
5946
|
+
if (typeof node.key.value === "string") return node.key.value;
|
|
5947
|
+
if (options.stringifyNonStringLiterals) return String(node.key.value);
|
|
5948
|
+
}
|
|
5949
|
+
return null;
|
|
5950
|
+
};
|
|
5951
|
+
//#endregion
|
|
5937
5952
|
//#region src/plugin/utils/is-presentation-role.ts
|
|
5938
5953
|
const isPresentationRole = (openingElement) => {
|
|
5939
5954
|
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
@@ -5978,6 +5993,21 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
5978
5993
|
return false;
|
|
5979
5994
|
};
|
|
5980
5995
|
//#endregion
|
|
5996
|
+
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
5997
|
+
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
5998
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
5999
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6000
|
+
let symbol = scopes.symbolFor(identifier);
|
|
6001
|
+
while (symbol?.kind === "const") {
|
|
6002
|
+
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
6003
|
+
visitedSymbolIds.add(symbol.id);
|
|
6004
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
6005
|
+
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
6006
|
+
symbol = scopes.symbolFor(initializer);
|
|
6007
|
+
}
|
|
6008
|
+
return symbol;
|
|
6009
|
+
};
|
|
6010
|
+
//#endregion
|
|
5981
6011
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
5982
6012
|
const MESSAGE$57 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
5983
6013
|
const KEY_HANDLERS = [
|
|
@@ -5988,6 +6018,63 @@ const KEY_HANDLERS = [
|
|
|
5988
6018
|
"onKeyDownCapture",
|
|
5989
6019
|
"onKeyPressCapture"
|
|
5990
6020
|
];
|
|
6021
|
+
const CLICK_HANDLERS = ["onClick", "onClickCapture"];
|
|
6022
|
+
const TRANSPARENT_SPREAD_EVENT_NAMES = new Set([...CLICK_HANDLERS, ...KEY_HANDLERS].map((eventName) => eventName.toLowerCase()));
|
|
6023
|
+
const CONSERVATIVE_SPREAD_PROP_NAMES = new Set([
|
|
6024
|
+
"aria-hidden",
|
|
6025
|
+
"onmouseenter",
|
|
6026
|
+
"onmouseover",
|
|
6027
|
+
"role"
|
|
6028
|
+
]);
|
|
6029
|
+
const resolveSpreadObjectExpression = (expression, scopes) => {
|
|
6030
|
+
const innerExpression = stripParenExpression(expression);
|
|
6031
|
+
if (isNodeOfType(innerExpression, "ObjectExpression")) return innerExpression;
|
|
6032
|
+
if (!isNodeOfType(innerExpression, "Identifier")) return null;
|
|
6033
|
+
const symbol = resolveConstIdentifierAlias(innerExpression, scopes);
|
|
6034
|
+
if (symbol?.kind !== "const" || !symbol.initializer) return null;
|
|
6035
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
6036
|
+
return isNodeOfType(initializer, "ObjectExpression") ? initializer : null;
|
|
6037
|
+
};
|
|
6038
|
+
const collectTransparentSpreadEventNames = (expression, scopes, eventValues, visitedObjectExpressions) => {
|
|
6039
|
+
const objectExpression = resolveSpreadObjectExpression(expression, scopes);
|
|
6040
|
+
if (!objectExpression || visitedObjectExpressions.has(objectExpression)) return false;
|
|
6041
|
+
visitedObjectExpressions.add(objectExpression);
|
|
6042
|
+
let isTransparent = true;
|
|
6043
|
+
for (const property of objectExpression.properties) {
|
|
6044
|
+
if (isNodeOfType(property, "SpreadElement")) {
|
|
6045
|
+
if (!collectTransparentSpreadEventNames(property.argument, scopes, eventValues, visitedObjectExpressions)) {
|
|
6046
|
+
isTransparent = false;
|
|
6047
|
+
break;
|
|
6048
|
+
}
|
|
6049
|
+
continue;
|
|
6050
|
+
}
|
|
6051
|
+
if (!isNodeOfType(property, "Property")) {
|
|
6052
|
+
isTransparent = false;
|
|
6053
|
+
break;
|
|
6054
|
+
}
|
|
6055
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
6056
|
+
if (!propertyName) {
|
|
6057
|
+
isTransparent = false;
|
|
6058
|
+
break;
|
|
6059
|
+
}
|
|
6060
|
+
const normalizedPropertyName = propertyName.toLowerCase();
|
|
6061
|
+
if (CONSERVATIVE_SPREAD_PROP_NAMES.has(normalizedPropertyName)) {
|
|
6062
|
+
isTransparent = false;
|
|
6063
|
+
break;
|
|
6064
|
+
}
|
|
6065
|
+
if (TRANSPARENT_SPREAD_EVENT_NAMES.has(normalizedPropertyName)) eventValues.set(normalizedPropertyName, property.value);
|
|
6066
|
+
}
|
|
6067
|
+
visitedObjectExpressions.delete(objectExpression);
|
|
6068
|
+
return isTransparent;
|
|
6069
|
+
};
|
|
6070
|
+
const getTransparentSpreadEventValues = (attributes, scopes) => {
|
|
6071
|
+
const eventValues = /* @__PURE__ */ new Map();
|
|
6072
|
+
for (const attribute of attributes) {
|
|
6073
|
+
if (!isNodeOfType(attribute, "JSXSpreadAttribute")) continue;
|
|
6074
|
+
if (!collectTransparentSpreadEventNames(attribute.argument, scopes, eventValues, /* @__PURE__ */ new Set())) return null;
|
|
6075
|
+
}
|
|
6076
|
+
return eventValues;
|
|
6077
|
+
};
|
|
5991
6078
|
const FOCUSLESS_CONTAINER_TAGS = new Set([
|
|
5992
6079
|
"tr",
|
|
5993
6080
|
"td",
|
|
@@ -6035,7 +6122,10 @@ const isFocusForwardingFunctionBody = (body) => {
|
|
|
6035
6122
|
};
|
|
6036
6123
|
const resolveHandlerFunction = (attribute) => {
|
|
6037
6124
|
if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
|
|
6038
|
-
|
|
6125
|
+
return resolveHandlerFunctionExpression(attribute.value.expression);
|
|
6126
|
+
};
|
|
6127
|
+
const resolveHandlerFunctionExpression = (handlerExpression) => {
|
|
6128
|
+
let expression = handlerExpression;
|
|
6039
6129
|
if (isNodeOfType(expression, "Identifier")) {
|
|
6040
6130
|
const binding = findVariableInitializer(expression, expression.name);
|
|
6041
6131
|
if (!binding?.initializer) return null;
|
|
@@ -6134,18 +6224,22 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
6134
6224
|
if (!HTML_TAGS.has(tag)) return;
|
|
6135
6225
|
if (tag === "label") return;
|
|
6136
6226
|
if (!FOCUSLESS_CONTAINER_TAGS.has(tag) && isInteractiveElement(tag, node)) return;
|
|
6227
|
+
const spreadEventValues = getTransparentSpreadEventValues(node.attributes, context.scopes);
|
|
6228
|
+
if (!spreadEventValues) return;
|
|
6137
6229
|
const onClick = hasJsxPropIgnoreCase(node.attributes, "onClick") ?? hasJsxPropIgnoreCase(node.attributes, "onClickCapture");
|
|
6138
|
-
|
|
6139
|
-
if (
|
|
6140
|
-
if (
|
|
6141
|
-
if (
|
|
6230
|
+
const spreadOnClickExpression = CLICK_HANDLERS.map((name) => spreadEventValues.get(name.toLowerCase())).find((expression) => expression !== void 0);
|
|
6231
|
+
if (!onClick && !spreadOnClickExpression) return;
|
|
6232
|
+
if (onClick && isPureEventBlockerHandler(onClick)) return;
|
|
6233
|
+
if (onClick && isFocusForwardingHandler(onClick)) return;
|
|
6234
|
+
const spreadHandlerFunction = spreadOnClickExpression ? resolveHandlerFunctionExpression(spreadOnClickExpression) : null;
|
|
6235
|
+
if (spreadHandlerFunction && (isFocusForwardingFunctionBody(spreadHandlerFunction.body ?? null) || containsBackdropDismissComparison(spreadHandlerFunction.body ?? null))) return;
|
|
6142
6236
|
if (hasCompositeItemRole(node)) return;
|
|
6143
6237
|
if (isHoverSelectionListItem(tag, node)) return;
|
|
6144
|
-
if (isBackdropDismissHandler(onClick)) return;
|
|
6238
|
+
if (onClick && isBackdropDismissHandler(onClick)) return;
|
|
6145
6239
|
if (containsKeyboardActivatableDescendant(node.parent)) return;
|
|
6146
6240
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
6147
6241
|
if (isPresentationRole(node)) return;
|
|
6148
|
-
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
6242
|
+
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler) || spreadEventValues.has(handler.toLowerCase()))) return;
|
|
6149
6243
|
context.report({
|
|
6150
6244
|
node: node.name,
|
|
6151
6245
|
message: MESSAGE$57
|
|
@@ -7618,7 +7712,7 @@ const isCreateClassLikeCall = (node) => {
|
|
|
7618
7712
|
if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$2(callee) === "createClass";
|
|
7619
7713
|
return false;
|
|
7620
7714
|
};
|
|
7621
|
-
const isCreateContextCall = (node) => {
|
|
7715
|
+
const isCreateContextCall$1 = (node) => {
|
|
7622
7716
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7623
7717
|
const callee = node.callee;
|
|
7624
7718
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
|
|
@@ -7795,7 +7889,7 @@ const displayName = defineRule({
|
|
|
7795
7889
|
}
|
|
7796
7890
|
},
|
|
7797
7891
|
CallExpression(node) {
|
|
7798
|
-
if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall(node)) {
|
|
7892
|
+
if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall$1(node)) {
|
|
7799
7893
|
const assignedName = getAssignedName(node);
|
|
7800
7894
|
if (assignedName) {
|
|
7801
7895
|
const programRoot = findProgramRoot(node);
|
|
@@ -7842,21 +7936,6 @@ const displayName = defineRule({
|
|
|
7842
7936
|
}
|
|
7843
7937
|
});
|
|
7844
7938
|
//#endregion
|
|
7845
|
-
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
7846
|
-
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
7847
|
-
if (!isNodeOfType(node, "Property")) return null;
|
|
7848
|
-
if (node.computed) {
|
|
7849
|
-
if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
|
|
7850
|
-
return null;
|
|
7851
|
-
}
|
|
7852
|
-
if (isNodeOfType(node.key, "Identifier")) return node.key.name;
|
|
7853
|
-
if (isNodeOfType(node.key, "Literal")) {
|
|
7854
|
-
if (typeof node.key.value === "string") return node.key.value;
|
|
7855
|
-
if (options.stringifyNonStringLiterals) return String(node.key.value);
|
|
7856
|
-
}
|
|
7857
|
-
return null;
|
|
7858
|
-
};
|
|
7859
|
-
//#endregion
|
|
7860
7939
|
//#region src/plugin/utils/read-static-boolean.ts
|
|
7861
7940
|
const readStaticBoolean = (node) => {
|
|
7862
7941
|
if (!node) return null;
|
|
@@ -7865,21 +7944,6 @@ const readStaticBoolean = (node) => {
|
|
|
7865
7944
|
return unwrappedNode.value;
|
|
7866
7945
|
};
|
|
7867
7946
|
//#endregion
|
|
7868
|
-
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
7869
|
-
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
7870
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
7871
|
-
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
7872
|
-
let symbol = scopes.symbolFor(identifier);
|
|
7873
|
-
while (symbol?.kind === "const") {
|
|
7874
|
-
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
7875
|
-
visitedSymbolIds.add(symbol.id);
|
|
7876
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
7877
|
-
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
7878
|
-
symbol = scopes.symbolFor(initializer);
|
|
7879
|
-
}
|
|
7880
|
-
return symbol;
|
|
7881
|
-
};
|
|
7882
|
-
//#endregion
|
|
7883
7947
|
//#region src/plugin/utils/is-react-api-call.ts
|
|
7884
7948
|
const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
|
|
7885
7949
|
const isImportedFromReact = (symbol) => {
|
|
@@ -9006,6 +9070,30 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
|
|
|
9006
9070
|
}
|
|
9007
9071
|
return matchingBlocks.size > 0;
|
|
9008
9072
|
};
|
|
9073
|
+
const doMatchingNodesCoverEveryPathFromFunctionEntry = (owner, matchingNodes, context) => {
|
|
9074
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
9075
|
+
if (!functionCfg) return false;
|
|
9076
|
+
const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
|
|
9077
|
+
if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
|
|
9078
|
+
const matchingBlock = functionCfg.blockOf(matchingNode);
|
|
9079
|
+
return matchingBlock ? [matchingBlock] : [];
|
|
9080
|
+
}));
|
|
9081
|
+
if (matchingBlocks.size === 0) return false;
|
|
9082
|
+
const visitedBlocks = new Set([functionCfg.entry]);
|
|
9083
|
+
const pendingBlocks = [functionCfg.entry];
|
|
9084
|
+
while (pendingBlocks.length > 0) {
|
|
9085
|
+
const currentBlock = pendingBlocks.pop();
|
|
9086
|
+
if (!currentBlock) break;
|
|
9087
|
+
if (matchingBlocks.has(currentBlock)) continue;
|
|
9088
|
+
for (const edge of currentBlock.successors) {
|
|
9089
|
+
if (edge.to === functionCfg.exit) return false;
|
|
9090
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
9091
|
+
visitedBlocks.add(edge.to);
|
|
9092
|
+
pendingBlocks.push(edge.to);
|
|
9093
|
+
}
|
|
9094
|
+
}
|
|
9095
|
+
return true;
|
|
9096
|
+
};
|
|
9009
9097
|
const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
|
|
9010
9098
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
9011
9099
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
@@ -9206,6 +9294,83 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
|
|
|
9206
9294
|
});
|
|
9207
9295
|
return didCleanupFunctionMatch;
|
|
9208
9296
|
};
|
|
9297
|
+
const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
|
|
9298
|
+
const callExpression = stripParenExpression(expression);
|
|
9299
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return false;
|
|
9300
|
+
const bindCallee = stripParenExpression(callExpression.callee);
|
|
9301
|
+
if (!isNodeOfType(bindCallee, "MemberExpression") || bindCallee.computed || !isNodeOfType(bindCallee.property, "Identifier") || bindCallee.property.name !== "bind") return false;
|
|
9302
|
+
const releaseMember = stripParenExpression(bindCallee.object);
|
|
9303
|
+
if (!isNodeOfType(releaseMember, "MemberExpression") || releaseMember.computed || !isNodeOfType(releaseMember.property, "Identifier")) return false;
|
|
9304
|
+
const releaseReceiverKey = resolveExpressionKey$1(releaseMember.object, context);
|
|
9305
|
+
if (releaseReceiverKey === null || releaseReceiverKey !== resolveExpressionKey$1(callExpression.arguments?.[0], context)) return false;
|
|
9306
|
+
const releaseVerbName = releaseMember.property.name;
|
|
9307
|
+
if (usage.kind === "socket") return usage.handleKey === releaseReceiverKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
9308
|
+
return usage.kind === "subscribe" && usage.handleKey === releaseReceiverKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName));
|
|
9309
|
+
};
|
|
9310
|
+
const callbackReturnsCleanupForUsage = (callback, usage, context) => {
|
|
9311
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
9312
|
+
const doesReturnedValueReleaseUsage = (returnedValue) => {
|
|
9313
|
+
if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) return true;
|
|
9314
|
+
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
9315
|
+
if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) return true;
|
|
9316
|
+
return Boolean(cleanupFunction && isFunctionLike$1(cleanupFunction) && doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context));
|
|
9317
|
+
};
|
|
9318
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return doesReturnedValueReleaseUsage(stripParenExpression(callback.body));
|
|
9319
|
+
const matchingCleanupReturns = [];
|
|
9320
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
9321
|
+
if (isNodeOfType(child, "ReturnStatement") && child.argument && doesReturnedValueReleaseUsage(stripParenExpression(child.argument))) matchingCleanupReturns.push(child);
|
|
9322
|
+
});
|
|
9323
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
|
|
9324
|
+
};
|
|
9325
|
+
const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
|
|
9326
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
9327
|
+
const functionCfg = context.cfg.cfgFor(callback);
|
|
9328
|
+
const usageBlock = functionCfg?.blockOf(usage.node);
|
|
9329
|
+
const usageStart = getRangeStart(usage.node);
|
|
9330
|
+
if (!functionCfg || !usageBlock || usageStart === null) return false;
|
|
9331
|
+
const findHandleGuard = (releaseCall) => {
|
|
9332
|
+
if (usage.handleKey === null) return null;
|
|
9333
|
+
let ancestor = releaseCall.parent;
|
|
9334
|
+
while (ancestor && ancestor !== callback.body) {
|
|
9335
|
+
if (isNodeOfType(ancestor, "IfStatement")) return ancestor.alternate === null && resolveExpressionKey$1(ancestor.test, context) === usage.handleKey && getRangeStart(ancestor) !== null && (getRangeStart(ancestor) ?? usageStart) < usageStart ? ancestor : null;
|
|
9336
|
+
ancestor = ancestor.parent;
|
|
9337
|
+
}
|
|
9338
|
+
return null;
|
|
9339
|
+
};
|
|
9340
|
+
const matchingReleaseAnchors = [];
|
|
9341
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
9342
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
9343
|
+
const releaseStart = getRangeStart(child);
|
|
9344
|
+
const handleGuard = findHandleGuard(child);
|
|
9345
|
+
if (releaseStart === null || releaseStart >= usageStart || functionCfg.blockOf(child) !== usageBlock && !handleGuard) return;
|
|
9346
|
+
if (doesReleaseCallMatchUsage(child, usage, context)) {
|
|
9347
|
+
matchingReleaseAnchors.push(handleGuard ?? child);
|
|
9348
|
+
return;
|
|
9349
|
+
}
|
|
9350
|
+
const helperFunction = resolveStableValue(child.callee, context);
|
|
9351
|
+
if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context)) matchingReleaseAnchors.push(handleGuard ?? child);
|
|
9352
|
+
});
|
|
9353
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingReleaseAnchors, context);
|
|
9354
|
+
};
|
|
9355
|
+
const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
9356
|
+
const componentFunction = findEnclosingFunction(callback);
|
|
9357
|
+
if (!componentFunction || !isNodeOfType(componentFunction, "ArrowFunctionExpression") && !isNodeOfType(componentFunction, "FunctionExpression") && !isNodeOfType(componentFunction, "FunctionDeclaration")) return false;
|
|
9358
|
+
let didFindUnmountCleanup = false;
|
|
9359
|
+
walkAst(componentFunction.body, (child) => {
|
|
9360
|
+
if (didFindUnmountCleanup) return false;
|
|
9361
|
+
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction(child) !== componentFunction) return;
|
|
9362
|
+
if (!isHookCall$2(child, CLEANUP_EFFECT_HOOK_NAMES)) return;
|
|
9363
|
+
const dependencyList = child.arguments?.[1];
|
|
9364
|
+
if (!isNodeOfType(dependencyList, "ArrayExpression") || dependencyList.elements.length > 0) return;
|
|
9365
|
+
const cleanupCallback = getEffectCallback(child);
|
|
9366
|
+
if (cleanupCallback && cleanupCallback !== callback && callbackReturnsCleanupForUsage(cleanupCallback, usage, context)) {
|
|
9367
|
+
didFindUnmountCleanup = true;
|
|
9368
|
+
return false;
|
|
9369
|
+
}
|
|
9370
|
+
});
|
|
9371
|
+
return didFindUnmountCleanup;
|
|
9372
|
+
};
|
|
9373
|
+
const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
|
|
9209
9374
|
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
9210
9375
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
9211
9376
|
if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
@@ -9218,6 +9383,10 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
|
9218
9383
|
if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
|
|
9219
9384
|
const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
|
|
9220
9385
|
if (!returnedValue) return;
|
|
9386
|
+
if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) {
|
|
9387
|
+
matchingCleanupReturns.push(child);
|
|
9388
|
+
return;
|
|
9389
|
+
}
|
|
9221
9390
|
if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
|
|
9222
9391
|
matchingCleanupReturns.push(child);
|
|
9223
9392
|
return;
|
|
@@ -9233,13 +9402,17 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
|
9233
9402
|
if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
|
|
9234
9403
|
}
|
|
9235
9404
|
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
9405
|
+
if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) {
|
|
9406
|
+
matchingCleanupReturns.push(child);
|
|
9407
|
+
return;
|
|
9408
|
+
}
|
|
9236
9409
|
if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
|
|
9237
9410
|
if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
|
|
9238
9411
|
});
|
|
9239
9412
|
return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
|
|
9240
9413
|
};
|
|
9241
9414
|
const findFirstUsageWithoutCleanup = (callback, usages, context) => {
|
|
9242
|
-
for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
|
|
9415
|
+
for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context) && !hasSplitLifecycleCleanup(callback, usage, context)) return usage;
|
|
9243
9416
|
return null;
|
|
9244
9417
|
};
|
|
9245
9418
|
const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
@@ -23044,61 +23217,73 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
|
|
|
23044
23217
|
"Array",
|
|
23045
23218
|
"BigInt",
|
|
23046
23219
|
"Boolean",
|
|
23220
|
+
"encodeURIComponent",
|
|
23047
23221
|
"Number",
|
|
23048
23222
|
"Object",
|
|
23049
23223
|
"String",
|
|
23050
23224
|
"parseFloat",
|
|
23051
|
-
"parseInt"
|
|
23225
|
+
"parseInt",
|
|
23226
|
+
"structuredClone"
|
|
23227
|
+
]);
|
|
23228
|
+
const PURE_GLOBAL_CONSTRUCTOR_NAMES = new Set(["Date", "Set"]);
|
|
23229
|
+
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([
|
|
23230
|
+
["Array", new Set(["from"])],
|
|
23231
|
+
["JSON", new Set([
|
|
23232
|
+
"isRawJSON",
|
|
23233
|
+
"parse",
|
|
23234
|
+
"rawJSON",
|
|
23235
|
+
"stringify"
|
|
23236
|
+
])],
|
|
23237
|
+
["Math", new Set([
|
|
23238
|
+
"abs",
|
|
23239
|
+
"acos",
|
|
23240
|
+
"acosh",
|
|
23241
|
+
"asin",
|
|
23242
|
+
"asinh",
|
|
23243
|
+
"atan",
|
|
23244
|
+
"atan2",
|
|
23245
|
+
"atanh",
|
|
23246
|
+
"cbrt",
|
|
23247
|
+
"ceil",
|
|
23248
|
+
"clz32",
|
|
23249
|
+
"cos",
|
|
23250
|
+
"cosh",
|
|
23251
|
+
"exp",
|
|
23252
|
+
"floor",
|
|
23253
|
+
"fround",
|
|
23254
|
+
"hypot",
|
|
23255
|
+
"imul",
|
|
23256
|
+
"log",
|
|
23257
|
+
"log10",
|
|
23258
|
+
"log1p",
|
|
23259
|
+
"log2",
|
|
23260
|
+
"max",
|
|
23261
|
+
"min",
|
|
23262
|
+
"pow",
|
|
23263
|
+
"round",
|
|
23264
|
+
"sign",
|
|
23265
|
+
"sin",
|
|
23266
|
+
"sinh",
|
|
23267
|
+
"sqrt",
|
|
23268
|
+
"tan",
|
|
23269
|
+
"tanh",
|
|
23270
|
+
"trunc"
|
|
23271
|
+
])],
|
|
23272
|
+
["Object", new Set(["assign"])]
|
|
23052
23273
|
]);
|
|
23053
|
-
const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
|
|
23054
|
-
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
|
|
23055
|
-
"isRawJSON",
|
|
23056
|
-
"parse",
|
|
23057
|
-
"rawJSON",
|
|
23058
|
-
"stringify"
|
|
23059
|
-
])], ["Math", new Set([
|
|
23060
|
-
"abs",
|
|
23061
|
-
"acos",
|
|
23062
|
-
"acosh",
|
|
23063
|
-
"asin",
|
|
23064
|
-
"asinh",
|
|
23065
|
-
"atan",
|
|
23066
|
-
"atan2",
|
|
23067
|
-
"atanh",
|
|
23068
|
-
"cbrt",
|
|
23069
|
-
"ceil",
|
|
23070
|
-
"clz32",
|
|
23071
|
-
"cos",
|
|
23072
|
-
"cosh",
|
|
23073
|
-
"exp",
|
|
23074
|
-
"floor",
|
|
23075
|
-
"fround",
|
|
23076
|
-
"hypot",
|
|
23077
|
-
"imul",
|
|
23078
|
-
"log",
|
|
23079
|
-
"log10",
|
|
23080
|
-
"log1p",
|
|
23081
|
-
"log2",
|
|
23082
|
-
"max",
|
|
23083
|
-
"min",
|
|
23084
|
-
"pow",
|
|
23085
|
-
"round",
|
|
23086
|
-
"sign",
|
|
23087
|
-
"sin",
|
|
23088
|
-
"sinh",
|
|
23089
|
-
"sqrt",
|
|
23090
|
-
"tan",
|
|
23091
|
-
"tanh",
|
|
23092
|
-
"trunc"
|
|
23093
|
-
])]]);
|
|
23094
23274
|
const PURE_MEMBER_TRANSFORM_NAMES = new Set([
|
|
23095
23275
|
"concat",
|
|
23096
23276
|
"filter",
|
|
23277
|
+
"flatMap",
|
|
23097
23278
|
"join",
|
|
23098
23279
|
"map",
|
|
23280
|
+
"reduce",
|
|
23281
|
+
"replace",
|
|
23282
|
+
"slice",
|
|
23099
23283
|
"split",
|
|
23100
23284
|
"toLowerCase",
|
|
23101
23285
|
"toString",
|
|
23286
|
+
"toSorted",
|
|
23102
23287
|
"toUpperCase",
|
|
23103
23288
|
"trim"
|
|
23104
23289
|
]);
|
|
@@ -23480,6 +23665,11 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
|
|
|
23480
23665
|
const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
|
|
23481
23666
|
return callbackSummary.isValid && !callbackSummary.canContinue;
|
|
23482
23667
|
}
|
|
23668
|
+
if (isNodeOfType(node, "NewExpression")) {
|
|
23669
|
+
const callee = stripParenExpression(node.callee);
|
|
23670
|
+
if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || environment.parameterIndices.has(callee.name) || environment.recursiveNames.has(callee.name) || environment.shadowedGlobalNames.has(callee.name)) return false;
|
|
23671
|
+
return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
|
|
23672
|
+
}
|
|
23483
23673
|
if (isNodeOfType(node, "CallExpression")) {
|
|
23484
23674
|
const callee = stripParenExpression(node.callee);
|
|
23485
23675
|
const calleeRoot = getMemberRoot(callee);
|
|
@@ -23682,7 +23872,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23682
23872
|
}
|
|
23683
23873
|
const callee = stripParenExpression(node.callee);
|
|
23684
23874
|
const calleeRoot = getMemberRoot(callee);
|
|
23685
|
-
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(
|
|
23875
|
+
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(callee, "MemberExpression") && isNodeOfType(calleeRoot, "Identifier") && getIdentifierBindingIdentity(analysis, calleeRoot) === null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(calleeRoot.name)?.has(getStaticMemberName$1(callee) ?? "") === true;
|
|
23686
23876
|
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
|
|
23687
23877
|
if (isPureGlobalCall || isPureMemberTransform) {
|
|
23688
23878
|
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
@@ -23735,6 +23925,15 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23735
23925
|
}
|
|
23736
23926
|
return evidence;
|
|
23737
23927
|
}
|
|
23928
|
+
if (isNodeOfType(node, "NewExpression")) {
|
|
23929
|
+
const callee = stripParenExpression(node.callee);
|
|
23930
|
+
if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || getIdentifierBindingIdentity(analysis, callee) !== null) {
|
|
23931
|
+
evidence.hasUnknownSource = true;
|
|
23932
|
+
return evidence;
|
|
23933
|
+
}
|
|
23934
|
+
for (const argument of node.arguments ?? []) mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
23935
|
+
return evidence;
|
|
23936
|
+
}
|
|
23738
23937
|
if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
|
|
23739
23938
|
evidence.hasUnknownSource = true;
|
|
23740
23939
|
return evidence;
|
|
@@ -26277,6 +26476,19 @@ const noCloneElement = defineRule({
|
|
|
26277
26476
|
} })
|
|
26278
26477
|
});
|
|
26279
26478
|
//#endregion
|
|
26479
|
+
//#region src/plugin/utils/get-call-method-name.ts
|
|
26480
|
+
/**
|
|
26481
|
+
* Returns the static method name of a call's callee when it's a
|
|
26482
|
+
* non-computed MemberExpression (`obj.method` → `"method"`), or
|
|
26483
|
+
* `null` otherwise. Used by `no-pass-data-to-parent` and
|
|
26484
|
+
* `no-pass-live-state-to-parent` to match the receiver-bound
|
|
26485
|
+
* method-call shape against an allow/block list.
|
|
26486
|
+
*/
|
|
26487
|
+
const getCallMethodName = (callee) => {
|
|
26488
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
26489
|
+
return null;
|
|
26490
|
+
};
|
|
26491
|
+
//#endregion
|
|
26280
26492
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
26281
26493
|
const MESSAGE$31 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
26282
26494
|
const CONTEXT_MODULES = [
|
|
@@ -26284,23 +26496,35 @@ const CONTEXT_MODULES = [
|
|
|
26284
26496
|
"use-context-selector",
|
|
26285
26497
|
"react-tracked"
|
|
26286
26498
|
];
|
|
26287
|
-
const
|
|
26499
|
+
const getSupportedContextImportSource = (symbol) => {
|
|
26500
|
+
if (symbol?.kind !== "import") return null;
|
|
26501
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
26502
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration") || typeof importDeclaration.source.value !== "string" || !CONTEXT_MODULES.includes(importDeclaration.source.value)) return null;
|
|
26503
|
+
return importDeclaration.source.value;
|
|
26504
|
+
};
|
|
26505
|
+
const isSupportedNamespaceSymbol = (symbol) => getSupportedContextImportSource(symbol) !== null && Boolean(symbol && (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")));
|
|
26506
|
+
const isDestructuredCreateContextBinding = (identifier, scopes) => {
|
|
26507
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
26508
|
+
const symbol = scopes.symbolFor(identifier);
|
|
26509
|
+
if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
|
|
26510
|
+
const property = symbol.bindingIdentifier.parent;
|
|
26511
|
+
if (!property || !isNodeOfType(property, "Property") || getStaticPropertyKeyName(property, { allowComputedString: true }) !== "createContext") return false;
|
|
26512
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
26513
|
+
return isNodeOfType(initializer, "Identifier") && isSupportedNamespaceSymbol(resolveConstIdentifierAlias(initializer, scopes));
|
|
26514
|
+
};
|
|
26515
|
+
const isCreateContextCall = (node, scopes) => {
|
|
26516
|
+
const callee = stripParenExpression(node.callee);
|
|
26288
26517
|
if (isNodeOfType(callee, "Identifier")) {
|
|
26289
|
-
|
|
26290
|
-
|
|
26518
|
+
if (isDestructuredCreateContextBinding(callee, scopes)) return true;
|
|
26519
|
+
const symbol = resolveConstIdentifierAlias(callee, scopes);
|
|
26520
|
+
return getSupportedContextImportSource(symbol) !== null && Boolean(symbol && getImportedName(symbol.declarationNode) === "createContext");
|
|
26291
26521
|
}
|
|
26292
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
26293
|
-
|
|
26294
|
-
|
|
26295
|
-
|
|
26296
|
-
|
|
26297
|
-
|
|
26298
|
-
const namespaceName = namespaceIdentifier.name;
|
|
26299
|
-
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
26300
|
-
const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
|
|
26301
|
-
return importSource !== null && CONTEXT_MODULES.includes(importSource);
|
|
26302
|
-
}
|
|
26303
|
-
return false;
|
|
26522
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
26523
|
+
if ((getCallMethodName(callee) ?? (callee.computed && isNodeOfType(callee.property, "Literal") && typeof callee.property.value === "string" ? callee.property.value : null)) !== "createContext") return false;
|
|
26524
|
+
const receiver = stripParenExpression(callee.object);
|
|
26525
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
26526
|
+
if (receiver.name === "React" && scopes.isGlobalReference(receiver)) return true;
|
|
26527
|
+
return isSupportedNamespaceSymbol(resolveConstIdentifierAlias(receiver, scopes));
|
|
26304
26528
|
};
|
|
26305
26529
|
const noCreateContextInRender = defineRule({
|
|
26306
26530
|
id: "no-create-context-in-render",
|
|
@@ -26309,7 +26533,7 @@ const noCreateContextInRender = defineRule({
|
|
|
26309
26533
|
category: "Correctness",
|
|
26310
26534
|
recommendation: "Move `createContext(...)` outside the component, to the top level of the file, so it stays the same on every render.",
|
|
26311
26535
|
create: (context) => ({ CallExpression(node) {
|
|
26312
|
-
if (!
|
|
26536
|
+
if (!isCreateContextCall(node, context.scopes)) return;
|
|
26313
26537
|
const componentOrHookName = enclosingComponentOrHookName(node);
|
|
26314
26538
|
if (!componentOrHookName) return;
|
|
26315
26539
|
context.report({
|
|
@@ -28259,20 +28483,31 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
28259
28483
|
} })
|
|
28260
28484
|
});
|
|
28261
28485
|
//#endregion
|
|
28486
|
+
//#region src/plugin/utils/get-static-property-name.ts
|
|
28487
|
+
const getStaticPropertyName = (memberExpression) => {
|
|
28488
|
+
const property = memberExpression.property;
|
|
28489
|
+
if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
28490
|
+
if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
|
|
28491
|
+
if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
|
|
28492
|
+
return null;
|
|
28493
|
+
};
|
|
28494
|
+
//#endregion
|
|
28262
28495
|
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
28263
28496
|
const MESSAGE$24 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
|
|
28264
28497
|
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
28498
|
+
const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
|
|
28265
28499
|
const noDocumentWrite = defineRule({
|
|
28266
28500
|
id: "no-document-write",
|
|
28267
28501
|
title: "document.write/writeln",
|
|
28268
28502
|
severity: "warn",
|
|
28269
28503
|
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
28270
28504
|
create: (context) => ({ CallExpression(node) {
|
|
28271
|
-
const callee = node.callee;
|
|
28272
|
-
if (!isNodeOfType(callee, "MemberExpression")
|
|
28505
|
+
const callee = stripParenExpression(node.callee);
|
|
28506
|
+
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
28273
28507
|
const receiver = stripParenExpression(callee.object);
|
|
28274
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver
|
|
28275
|
-
|
|
28508
|
+
if (!isNodeOfType(receiver, "Identifier") || !isGlobalDocumentReference(receiver, context)) return;
|
|
28509
|
+
const methodName = getStaticPropertyName(callee);
|
|
28510
|
+
if (methodName === null || !WRITE_METHODS.has(methodName)) return;
|
|
28276
28511
|
context.report({
|
|
28277
28512
|
node,
|
|
28278
28513
|
message: MESSAGE$24
|
|
@@ -29079,6 +29314,14 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
29079
29314
|
//#endregion
|
|
29080
29315
|
//#region src/plugin/rules/security/no-eval.ts
|
|
29081
29316
|
const SANDBOX_SURFACE_PATH_PATTERN = /(?:^|[/-])sandbox(?:$|[/-])|(?:^|\/)[\w.]+-sandbox(?:\/|\.[cm]?[jt]sx?$)/i;
|
|
29317
|
+
const getExecutableGlobalName = (node, context) => {
|
|
29318
|
+
const expression = stripParenExpression(node);
|
|
29319
|
+
if (isNodeOfType(expression, "Identifier")) return context.scopes.isGlobalReference(expression) ? expression.name : null;
|
|
29320
|
+
if (!isNodeOfType(expression, "MemberExpression")) return null;
|
|
29321
|
+
const receiver = stripParenExpression(expression.object);
|
|
29322
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "globalThis" || !context.scopes.isGlobalReference(receiver)) return null;
|
|
29323
|
+
return getStaticPropertyName(expression);
|
|
29324
|
+
};
|
|
29082
29325
|
const noEval = defineRule({
|
|
29083
29326
|
id: "no-eval",
|
|
29084
29327
|
title: "eval() runs untrusted code strings",
|
|
@@ -29088,20 +29331,21 @@ const noEval = defineRule({
|
|
|
29088
29331
|
if (SANDBOX_SURFACE_PATH_PATTERN.test(normalizeFilename(context.filename ?? ""))) return {};
|
|
29089
29332
|
return {
|
|
29090
29333
|
CallExpression(node) {
|
|
29091
|
-
|
|
29334
|
+
const executableGlobalName = getExecutableGlobalName(node.callee, context);
|
|
29335
|
+
if (executableGlobalName === "eval") {
|
|
29092
29336
|
context.report({
|
|
29093
29337
|
node,
|
|
29094
29338
|
message: "eval() is a code-injection vulnerability: it runs any string as code."
|
|
29095
29339
|
});
|
|
29096
29340
|
return;
|
|
29097
29341
|
}
|
|
29098
|
-
if (
|
|
29342
|
+
if ((executableGlobalName === "setTimeout" || executableGlobalName === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
|
|
29099
29343
|
node,
|
|
29100
|
-
message: `Passing a string to ${
|
|
29344
|
+
message: `Passing a string to ${executableGlobalName}() is a code-injection vulnerability, since it runs that string as code.`
|
|
29101
29345
|
});
|
|
29102
29346
|
},
|
|
29103
29347
|
NewExpression(node) {
|
|
29104
|
-
if (
|
|
29348
|
+
if (getExecutableGlobalName(node.callee, context) === "Function") {
|
|
29105
29349
|
const onlyArgument = node.arguments.length === 1 ? node.arguments[0] : void 0;
|
|
29106
29350
|
if (isNodeOfType(onlyArgument, "Literal") && typeof onlyArgument.value === "string" && onlyArgument.value.trim() === "return this") return;
|
|
29107
29351
|
context.report({
|
|
@@ -34574,19 +34818,6 @@ const DATA_SINK_METHOD_NAMES = new Set([
|
|
|
34574
34818
|
"deserialize"
|
|
34575
34819
|
]);
|
|
34576
34820
|
//#endregion
|
|
34577
|
-
//#region src/plugin/utils/get-call-method-name.ts
|
|
34578
|
-
/**
|
|
34579
|
-
* Returns the static method name of a call's callee when it's a
|
|
34580
|
-
* non-computed MemberExpression (`obj.method` → `"method"`), or
|
|
34581
|
-
* `null` otherwise. Used by `no-pass-data-to-parent` and
|
|
34582
|
-
* `no-pass-live-state-to-parent` to match the receiver-bound
|
|
34583
|
-
* method-call shape against an allow/block list.
|
|
34584
|
-
*/
|
|
34585
|
-
const getCallMethodName = (callee) => {
|
|
34586
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
34587
|
-
return null;
|
|
34588
|
-
};
|
|
34589
|
-
//#endregion
|
|
34590
34821
|
//#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
|
|
34591
34822
|
const isUseStateIdentifier = (identifier) => {
|
|
34592
34823
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
@@ -34831,6 +35062,14 @@ const isHandlerBagArgument = (analysis, argument) => {
|
|
|
34831
35062
|
};
|
|
34832
35063
|
const getFunctionalUpdaterDataRefs = (analysis, updater) => getDownstreamRefs(analysis, updater).filter((updaterRef) => !updaterRef.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updater));
|
|
34833
35064
|
const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
35065
|
+
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
|
|
35066
|
+
"useIntersectionObserver",
|
|
35067
|
+
"useMatchMedia",
|
|
35068
|
+
"useMediaQuery",
|
|
35069
|
+
"useResizeObserver",
|
|
35070
|
+
"useVisibility",
|
|
35071
|
+
"useWindowSize"
|
|
35072
|
+
]);
|
|
34834
35073
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
34835
35074
|
const node = def.node;
|
|
34836
35075
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
@@ -34853,6 +35092,19 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
34853
35092
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
34854
35093
|
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isProp(analysis, downstreamRef)));
|
|
34855
35094
|
};
|
|
35095
|
+
const isExternalSubscriptionHookRef = (ref) => {
|
|
35096
|
+
const identifier = ref.identifier;
|
|
35097
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
35098
|
+
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
35099
|
+
return Boolean(ref.resolved?.defs.some((def) => {
|
|
35100
|
+
const node = def.node;
|
|
35101
|
+
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
35102
|
+
const initializer = stripParenExpression(node.init);
|
|
35103
|
+
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
35104
|
+
const callee = stripParenExpression(initializer.callee);
|
|
35105
|
+
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(callee.name);
|
|
35106
|
+
}));
|
|
35107
|
+
};
|
|
34856
35108
|
const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
|
|
34857
35109
|
const isCalleePosition = (identifier) => {
|
|
34858
35110
|
const parent = identifier.parent;
|
|
@@ -34914,10 +35166,11 @@ const noPassDataToParent = defineRule({
|
|
|
34914
35166
|
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
34915
35167
|
}
|
|
34916
35168
|
return getDownstreamRefs(analysis, argument);
|
|
34917
|
-
}).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
35169
|
+
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
34918
35170
|
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
34919
35171
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
34920
35172
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
35173
|
+
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
34921
35174
|
if (isProp(analysis, argRef)) return false;
|
|
34922
35175
|
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
34923
35176
|
if (isRefCurrent(argRef)) return false;
|
|
@@ -54675,12 +54928,6 @@ const webhookSignatureRisk = defineRule({
|
|
|
54675
54928
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
54676
54929
|
const ZOD_MODULE = "zod";
|
|
54677
54930
|
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
54678
|
-
const getStaticPropertyName = (member) => {
|
|
54679
|
-
const property = member.property;
|
|
54680
|
-
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
54681
|
-
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
54682
|
-
return null;
|
|
54683
|
-
};
|
|
54684
54931
|
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
54685
54932
|
const getImportInfoForIdentifier = (identifier) => {
|
|
54686
54933
|
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|