oxlint-plugin-react-doctor 0.7.5-dev.d9676e2 → 0.7.5-dev.dbd4067
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 +259 -130
- 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) => {
|
|
@@ -7887,9 +7951,9 @@ const isImportedFromReact = (symbol) => {
|
|
|
7887
7951
|
const importDeclaration = symbol.declarationNode.parent;
|
|
7888
7952
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
|
|
7889
7953
|
};
|
|
7890
|
-
const isNamedReactApiImport = (identifier, apiNames, scopes) => {
|
|
7954
|
+
const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
|
|
7891
7955
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
7892
|
-
const symbol = scopes.symbolFor(identifier);
|
|
7956
|
+
const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
|
|
7893
7957
|
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
7894
7958
|
const importedName = getImportedName(symbol.declarationNode);
|
|
7895
7959
|
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
@@ -7903,7 +7967,7 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
|
|
|
7903
7967
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7904
7968
|
const callee = stripParenExpression(node.callee);
|
|
7905
7969
|
if (isNodeOfType(callee, "Identifier")) {
|
|
7906
|
-
if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
|
|
7970
|
+
if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
|
|
7907
7971
|
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
7908
7972
|
}
|
|
7909
7973
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
|
|
@@ -11112,6 +11176,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11112
11176
|
return { CallExpression(node) {
|
|
11113
11177
|
const hookName = getHookName(node.callee, context.scopes);
|
|
11114
11178
|
if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
|
|
11179
|
+
if (HOOKS_REQUIRING_DEPS_MATCH.has(hookName) && !isReactApiCall(node, HOOKS_REQUIRING_DEPS_MATCH, context.scopes, {
|
|
11180
|
+
allowGlobalReactNamespace: true,
|
|
11181
|
+
allowUnboundBareCalls: true,
|
|
11182
|
+
resolveNamedAliases: true
|
|
11183
|
+
})) return;
|
|
11115
11184
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
11116
11185
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
11117
11186
|
const callbackArgument = node.arguments[callbackArgumentIndex];
|
|
@@ -23039,61 +23108,73 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
|
|
|
23039
23108
|
"Array",
|
|
23040
23109
|
"BigInt",
|
|
23041
23110
|
"Boolean",
|
|
23111
|
+
"encodeURIComponent",
|
|
23042
23112
|
"Number",
|
|
23043
23113
|
"Object",
|
|
23044
23114
|
"String",
|
|
23045
23115
|
"parseFloat",
|
|
23046
|
-
"parseInt"
|
|
23116
|
+
"parseInt",
|
|
23117
|
+
"structuredClone"
|
|
23118
|
+
]);
|
|
23119
|
+
const PURE_GLOBAL_CONSTRUCTOR_NAMES = new Set(["Date", "Set"]);
|
|
23120
|
+
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([
|
|
23121
|
+
["Array", new Set(["from"])],
|
|
23122
|
+
["JSON", new Set([
|
|
23123
|
+
"isRawJSON",
|
|
23124
|
+
"parse",
|
|
23125
|
+
"rawJSON",
|
|
23126
|
+
"stringify"
|
|
23127
|
+
])],
|
|
23128
|
+
["Math", new Set([
|
|
23129
|
+
"abs",
|
|
23130
|
+
"acos",
|
|
23131
|
+
"acosh",
|
|
23132
|
+
"asin",
|
|
23133
|
+
"asinh",
|
|
23134
|
+
"atan",
|
|
23135
|
+
"atan2",
|
|
23136
|
+
"atanh",
|
|
23137
|
+
"cbrt",
|
|
23138
|
+
"ceil",
|
|
23139
|
+
"clz32",
|
|
23140
|
+
"cos",
|
|
23141
|
+
"cosh",
|
|
23142
|
+
"exp",
|
|
23143
|
+
"floor",
|
|
23144
|
+
"fround",
|
|
23145
|
+
"hypot",
|
|
23146
|
+
"imul",
|
|
23147
|
+
"log",
|
|
23148
|
+
"log10",
|
|
23149
|
+
"log1p",
|
|
23150
|
+
"log2",
|
|
23151
|
+
"max",
|
|
23152
|
+
"min",
|
|
23153
|
+
"pow",
|
|
23154
|
+
"round",
|
|
23155
|
+
"sign",
|
|
23156
|
+
"sin",
|
|
23157
|
+
"sinh",
|
|
23158
|
+
"sqrt",
|
|
23159
|
+
"tan",
|
|
23160
|
+
"tanh",
|
|
23161
|
+
"trunc"
|
|
23162
|
+
])],
|
|
23163
|
+
["Object", new Set(["assign"])]
|
|
23047
23164
|
]);
|
|
23048
|
-
const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
|
|
23049
|
-
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
|
|
23050
|
-
"isRawJSON",
|
|
23051
|
-
"parse",
|
|
23052
|
-
"rawJSON",
|
|
23053
|
-
"stringify"
|
|
23054
|
-
])], ["Math", new Set([
|
|
23055
|
-
"abs",
|
|
23056
|
-
"acos",
|
|
23057
|
-
"acosh",
|
|
23058
|
-
"asin",
|
|
23059
|
-
"asinh",
|
|
23060
|
-
"atan",
|
|
23061
|
-
"atan2",
|
|
23062
|
-
"atanh",
|
|
23063
|
-
"cbrt",
|
|
23064
|
-
"ceil",
|
|
23065
|
-
"clz32",
|
|
23066
|
-
"cos",
|
|
23067
|
-
"cosh",
|
|
23068
|
-
"exp",
|
|
23069
|
-
"floor",
|
|
23070
|
-
"fround",
|
|
23071
|
-
"hypot",
|
|
23072
|
-
"imul",
|
|
23073
|
-
"log",
|
|
23074
|
-
"log10",
|
|
23075
|
-
"log1p",
|
|
23076
|
-
"log2",
|
|
23077
|
-
"max",
|
|
23078
|
-
"min",
|
|
23079
|
-
"pow",
|
|
23080
|
-
"round",
|
|
23081
|
-
"sign",
|
|
23082
|
-
"sin",
|
|
23083
|
-
"sinh",
|
|
23084
|
-
"sqrt",
|
|
23085
|
-
"tan",
|
|
23086
|
-
"tanh",
|
|
23087
|
-
"trunc"
|
|
23088
|
-
])]]);
|
|
23089
23165
|
const PURE_MEMBER_TRANSFORM_NAMES = new Set([
|
|
23090
23166
|
"concat",
|
|
23091
23167
|
"filter",
|
|
23168
|
+
"flatMap",
|
|
23092
23169
|
"join",
|
|
23093
23170
|
"map",
|
|
23171
|
+
"reduce",
|
|
23172
|
+
"replace",
|
|
23173
|
+
"slice",
|
|
23094
23174
|
"split",
|
|
23095
23175
|
"toLowerCase",
|
|
23096
23176
|
"toString",
|
|
23177
|
+
"toSorted",
|
|
23097
23178
|
"toUpperCase",
|
|
23098
23179
|
"trim"
|
|
23099
23180
|
]);
|
|
@@ -23475,6 +23556,11 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
|
|
|
23475
23556
|
const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
|
|
23476
23557
|
return callbackSummary.isValid && !callbackSummary.canContinue;
|
|
23477
23558
|
}
|
|
23559
|
+
if (isNodeOfType(node, "NewExpression")) {
|
|
23560
|
+
const callee = stripParenExpression(node.callee);
|
|
23561
|
+
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;
|
|
23562
|
+
return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
|
|
23563
|
+
}
|
|
23478
23564
|
if (isNodeOfType(node, "CallExpression")) {
|
|
23479
23565
|
const callee = stripParenExpression(node.callee);
|
|
23480
23566
|
const calleeRoot = getMemberRoot(callee);
|
|
@@ -23677,7 +23763,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23677
23763
|
}
|
|
23678
23764
|
const callee = stripParenExpression(node.callee);
|
|
23679
23765
|
const calleeRoot = getMemberRoot(callee);
|
|
23680
|
-
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(
|
|
23766
|
+
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;
|
|
23681
23767
|
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
|
|
23682
23768
|
if (isPureGlobalCall || isPureMemberTransform) {
|
|
23683
23769
|
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
@@ -23730,6 +23816,15 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23730
23816
|
}
|
|
23731
23817
|
return evidence;
|
|
23732
23818
|
}
|
|
23819
|
+
if (isNodeOfType(node, "NewExpression")) {
|
|
23820
|
+
const callee = stripParenExpression(node.callee);
|
|
23821
|
+
if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || getIdentifierBindingIdentity(analysis, callee) !== null) {
|
|
23822
|
+
evidence.hasUnknownSource = true;
|
|
23823
|
+
return evidence;
|
|
23824
|
+
}
|
|
23825
|
+
for (const argument of node.arguments ?? []) mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
23826
|
+
return evidence;
|
|
23827
|
+
}
|
|
23733
23828
|
if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
|
|
23734
23829
|
evidence.hasUnknownSource = true;
|
|
23735
23830
|
return evidence;
|
|
@@ -25070,7 +25165,11 @@ const noAsyncEffectCallback = defineRule({
|
|
|
25070
25165
|
severity: "warn",
|
|
25071
25166
|
recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
|
|
25072
25167
|
create: (context) => ({ CallExpression(node) {
|
|
25073
|
-
if (!
|
|
25168
|
+
if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
25169
|
+
allowGlobalReactNamespace: true,
|
|
25170
|
+
allowUnboundBareCalls: true,
|
|
25171
|
+
resolveNamedAliases: true
|
|
25172
|
+
})) return;
|
|
25074
25173
|
const callback = getEffectCallback(node);
|
|
25075
25174
|
if (!callback) return;
|
|
25076
25175
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
@@ -26268,6 +26367,19 @@ const noCloneElement = defineRule({
|
|
|
26268
26367
|
} })
|
|
26269
26368
|
});
|
|
26270
26369
|
//#endregion
|
|
26370
|
+
//#region src/plugin/utils/get-call-method-name.ts
|
|
26371
|
+
/**
|
|
26372
|
+
* Returns the static method name of a call's callee when it's a
|
|
26373
|
+
* non-computed MemberExpression (`obj.method` → `"method"`), or
|
|
26374
|
+
* `null` otherwise. Used by `no-pass-data-to-parent` and
|
|
26375
|
+
* `no-pass-live-state-to-parent` to match the receiver-bound
|
|
26376
|
+
* method-call shape against an allow/block list.
|
|
26377
|
+
*/
|
|
26378
|
+
const getCallMethodName = (callee) => {
|
|
26379
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
26380
|
+
return null;
|
|
26381
|
+
};
|
|
26382
|
+
//#endregion
|
|
26271
26383
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
26272
26384
|
const MESSAGE$31 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
26273
26385
|
const CONTEXT_MODULES = [
|
|
@@ -26275,23 +26387,35 @@ const CONTEXT_MODULES = [
|
|
|
26275
26387
|
"use-context-selector",
|
|
26276
26388
|
"react-tracked"
|
|
26277
26389
|
];
|
|
26278
|
-
const
|
|
26390
|
+
const getSupportedContextImportSource = (symbol) => {
|
|
26391
|
+
if (symbol?.kind !== "import") return null;
|
|
26392
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
26393
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration") || typeof importDeclaration.source.value !== "string" || !CONTEXT_MODULES.includes(importDeclaration.source.value)) return null;
|
|
26394
|
+
return importDeclaration.source.value;
|
|
26395
|
+
};
|
|
26396
|
+
const isSupportedNamespaceSymbol = (symbol) => getSupportedContextImportSource(symbol) !== null && Boolean(symbol && (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")));
|
|
26397
|
+
const isDestructuredCreateContextBinding = (identifier, scopes) => {
|
|
26398
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
26399
|
+
const symbol = scopes.symbolFor(identifier);
|
|
26400
|
+
if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
|
|
26401
|
+
const property = symbol.bindingIdentifier.parent;
|
|
26402
|
+
if (!property || !isNodeOfType(property, "Property") || getStaticPropertyKeyName(property, { allowComputedString: true }) !== "createContext") return false;
|
|
26403
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
26404
|
+
return isNodeOfType(initializer, "Identifier") && isSupportedNamespaceSymbol(resolveConstIdentifierAlias(initializer, scopes));
|
|
26405
|
+
};
|
|
26406
|
+
const isCreateContextCall = (node, scopes) => {
|
|
26407
|
+
const callee = stripParenExpression(node.callee);
|
|
26279
26408
|
if (isNodeOfType(callee, "Identifier")) {
|
|
26280
|
-
|
|
26281
|
-
|
|
26409
|
+
if (isDestructuredCreateContextBinding(callee, scopes)) return true;
|
|
26410
|
+
const symbol = resolveConstIdentifierAlias(callee, scopes);
|
|
26411
|
+
return getSupportedContextImportSource(symbol) !== null && Boolean(symbol && getImportedName(symbol.declarationNode) === "createContext");
|
|
26282
26412
|
}
|
|
26283
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
26284
|
-
|
|
26285
|
-
|
|
26286
|
-
|
|
26287
|
-
|
|
26288
|
-
|
|
26289
|
-
const namespaceName = namespaceIdentifier.name;
|
|
26290
|
-
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
26291
|
-
const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
|
|
26292
|
-
return importSource !== null && CONTEXT_MODULES.includes(importSource);
|
|
26293
|
-
}
|
|
26294
|
-
return false;
|
|
26413
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
26414
|
+
if ((getCallMethodName(callee) ?? (callee.computed && isNodeOfType(callee.property, "Literal") && typeof callee.property.value === "string" ? callee.property.value : null)) !== "createContext") return false;
|
|
26415
|
+
const receiver = stripParenExpression(callee.object);
|
|
26416
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
26417
|
+
if (receiver.name === "React" && scopes.isGlobalReference(receiver)) return true;
|
|
26418
|
+
return isSupportedNamespaceSymbol(resolveConstIdentifierAlias(receiver, scopes));
|
|
26295
26419
|
};
|
|
26296
26420
|
const noCreateContextInRender = defineRule({
|
|
26297
26421
|
id: "no-create-context-in-render",
|
|
@@ -26300,7 +26424,7 @@ const noCreateContextInRender = defineRule({
|
|
|
26300
26424
|
category: "Correctness",
|
|
26301
26425
|
recommendation: "Move `createContext(...)` outside the component, to the top level of the file, so it stays the same on every render.",
|
|
26302
26426
|
create: (context) => ({ CallExpression(node) {
|
|
26303
|
-
if (!
|
|
26427
|
+
if (!isCreateContextCall(node, context.scopes)) return;
|
|
26304
26428
|
const componentOrHookName = enclosingComponentOrHookName(node);
|
|
26305
26429
|
if (!componentOrHookName) return;
|
|
26306
26430
|
context.report({
|
|
@@ -28250,20 +28374,31 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
28250
28374
|
} })
|
|
28251
28375
|
});
|
|
28252
28376
|
//#endregion
|
|
28377
|
+
//#region src/plugin/utils/get-static-property-name.ts
|
|
28378
|
+
const getStaticPropertyName = (memberExpression) => {
|
|
28379
|
+
const property = memberExpression.property;
|
|
28380
|
+
if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
28381
|
+
if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
|
|
28382
|
+
if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
|
|
28383
|
+
return null;
|
|
28384
|
+
};
|
|
28385
|
+
//#endregion
|
|
28253
28386
|
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
28254
28387
|
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.";
|
|
28255
28388
|
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
28389
|
+
const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
|
|
28256
28390
|
const noDocumentWrite = defineRule({
|
|
28257
28391
|
id: "no-document-write",
|
|
28258
28392
|
title: "document.write/writeln",
|
|
28259
28393
|
severity: "warn",
|
|
28260
28394
|
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
28261
28395
|
create: (context) => ({ CallExpression(node) {
|
|
28262
|
-
const callee = node.callee;
|
|
28263
|
-
if (!isNodeOfType(callee, "MemberExpression")
|
|
28396
|
+
const callee = stripParenExpression(node.callee);
|
|
28397
|
+
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
28264
28398
|
const receiver = stripParenExpression(callee.object);
|
|
28265
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver
|
|
28266
|
-
|
|
28399
|
+
if (!isNodeOfType(receiver, "Identifier") || !isGlobalDocumentReference(receiver, context)) return;
|
|
28400
|
+
const methodName = getStaticPropertyName(callee);
|
|
28401
|
+
if (methodName === null || !WRITE_METHODS.has(methodName)) return;
|
|
28267
28402
|
context.report({
|
|
28268
28403
|
node,
|
|
28269
28404
|
message: MESSAGE$24
|
|
@@ -29070,6 +29205,14 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
29070
29205
|
//#endregion
|
|
29071
29206
|
//#region src/plugin/rules/security/no-eval.ts
|
|
29072
29207
|
const SANDBOX_SURFACE_PATH_PATTERN = /(?:^|[/-])sandbox(?:$|[/-])|(?:^|\/)[\w.]+-sandbox(?:\/|\.[cm]?[jt]sx?$)/i;
|
|
29208
|
+
const getExecutableGlobalName = (node, context) => {
|
|
29209
|
+
const expression = stripParenExpression(node);
|
|
29210
|
+
if (isNodeOfType(expression, "Identifier")) return context.scopes.isGlobalReference(expression) ? expression.name : null;
|
|
29211
|
+
if (!isNodeOfType(expression, "MemberExpression")) return null;
|
|
29212
|
+
const receiver = stripParenExpression(expression.object);
|
|
29213
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "globalThis" || !context.scopes.isGlobalReference(receiver)) return null;
|
|
29214
|
+
return getStaticPropertyName(expression);
|
|
29215
|
+
};
|
|
29073
29216
|
const noEval = defineRule({
|
|
29074
29217
|
id: "no-eval",
|
|
29075
29218
|
title: "eval() runs untrusted code strings",
|
|
@@ -29079,20 +29222,21 @@ const noEval = defineRule({
|
|
|
29079
29222
|
if (SANDBOX_SURFACE_PATH_PATTERN.test(normalizeFilename(context.filename ?? ""))) return {};
|
|
29080
29223
|
return {
|
|
29081
29224
|
CallExpression(node) {
|
|
29082
|
-
|
|
29225
|
+
const executableGlobalName = getExecutableGlobalName(node.callee, context);
|
|
29226
|
+
if (executableGlobalName === "eval") {
|
|
29083
29227
|
context.report({
|
|
29084
29228
|
node,
|
|
29085
29229
|
message: "eval() is a code-injection vulnerability: it runs any string as code."
|
|
29086
29230
|
});
|
|
29087
29231
|
return;
|
|
29088
29232
|
}
|
|
29089
|
-
if (
|
|
29233
|
+
if ((executableGlobalName === "setTimeout" || executableGlobalName === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
|
|
29090
29234
|
node,
|
|
29091
|
-
message: `Passing a string to ${
|
|
29235
|
+
message: `Passing a string to ${executableGlobalName}() is a code-injection vulnerability, since it runs that string as code.`
|
|
29092
29236
|
});
|
|
29093
29237
|
},
|
|
29094
29238
|
NewExpression(node) {
|
|
29095
|
-
if (
|
|
29239
|
+
if (getExecutableGlobalName(node.callee, context) === "Function") {
|
|
29096
29240
|
const onlyArgument = node.arguments.length === 1 ? node.arguments[0] : void 0;
|
|
29097
29241
|
if (isNodeOfType(onlyArgument, "Literal") && typeof onlyArgument.value === "string" && onlyArgument.value.trim() === "return this") return;
|
|
29098
29242
|
context.report({
|
|
@@ -30351,7 +30495,11 @@ const noFetchInEffect = defineRule({
|
|
|
30351
30495
|
severity: "warn",
|
|
30352
30496
|
recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
|
|
30353
30497
|
create: (context) => ({ CallExpression(node) {
|
|
30354
|
-
if (!
|
|
30498
|
+
if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
30499
|
+
allowGlobalReactNamespace: true,
|
|
30500
|
+
allowUnboundBareCalls: true,
|
|
30501
|
+
resolveNamedAliases: true
|
|
30502
|
+
})) return;
|
|
30355
30503
|
const callback = getEffectCallback(node);
|
|
30356
30504
|
if (!callback) return;
|
|
30357
30505
|
const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
|
|
@@ -34561,19 +34709,6 @@ const DATA_SINK_METHOD_NAMES = new Set([
|
|
|
34561
34709
|
"deserialize"
|
|
34562
34710
|
]);
|
|
34563
34711
|
//#endregion
|
|
34564
|
-
//#region src/plugin/utils/get-call-method-name.ts
|
|
34565
|
-
/**
|
|
34566
|
-
* Returns the static method name of a call's callee when it's a
|
|
34567
|
-
* non-computed MemberExpression (`obj.method` → `"method"`), or
|
|
34568
|
-
* `null` otherwise. Used by `no-pass-data-to-parent` and
|
|
34569
|
-
* `no-pass-live-state-to-parent` to match the receiver-bound
|
|
34570
|
-
* method-call shape against an allow/block list.
|
|
34571
|
-
*/
|
|
34572
|
-
const getCallMethodName = (callee) => {
|
|
34573
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
34574
|
-
return null;
|
|
34575
|
-
};
|
|
34576
|
-
//#endregion
|
|
34577
34712
|
//#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
|
|
34578
34713
|
const isUseStateIdentifier = (identifier) => {
|
|
34579
34714
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
@@ -54662,12 +54797,6 @@ const webhookSignatureRisk = defineRule({
|
|
|
54662
54797
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
54663
54798
|
const ZOD_MODULE = "zod";
|
|
54664
54799
|
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
54665
|
-
const getStaticPropertyName = (member) => {
|
|
54666
|
-
const property = member.property;
|
|
54667
|
-
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
54668
|
-
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
54669
|
-
return null;
|
|
54670
|
-
};
|
|
54671
54800
|
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
54672
54801
|
const getImportInfoForIdentifier = (identifier) => {
|
|
54673
54802
|
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|