oxlint-plugin-react-doctor 0.7.5-dev.3075e10 → 0.7.5-dev.3aef25a
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 +586 -141
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -397,6 +397,7 @@ const isProductionFilePath = (relativePath, sourceFilePattern) => {
|
|
|
397
397
|
//#endregion
|
|
398
398
|
//#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
|
|
399
399
|
const isProductionSourcePath = (relativePath) => {
|
|
400
|
+
if (/\.d\.[cm]?[jt]s$/i.test(relativePath)) return false;
|
|
400
401
|
return isProductionFilePath(relativePath, SOURCE_FILE_PATTERN);
|
|
401
402
|
};
|
|
402
403
|
//#endregion
|
|
@@ -5178,8 +5179,10 @@ const STORAGE_GLOBALS = new Set([
|
|
|
5178
5179
|
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
5179
5180
|
const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
|
|
5180
5181
|
const STRONG_AUTH_KEY_PATTERN = /jwt|secret|password|passwd|credential|private[-_]?key|api[-_]?key|bearer|access[-_]?token|refresh[-_]?token|auth[-_]?token|id[-_]?token|session/i;
|
|
5182
|
+
const PRODUCT_API_KEY_RECORDS_PATTERN = /(?:^|[._:-])(?:created|saved|integration|mailing)[-_]?api[-_]?keys$/i;
|
|
5181
5183
|
const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
|
|
5182
5184
|
const isAuthCredentialKey = (key) => {
|
|
5185
|
+
if (PRODUCT_API_KEY_RECORDS_PATTERN.test(key)) return false;
|
|
5183
5186
|
if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
|
|
5184
5187
|
if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
|
|
5185
5188
|
if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
|
|
@@ -5931,6 +5934,21 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5931
5934
|
}
|
|
5932
5935
|
});
|
|
5933
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
|
|
5934
5952
|
//#region src/plugin/utils/is-presentation-role.ts
|
|
5935
5953
|
const isPresentationRole = (openingElement) => {
|
|
5936
5954
|
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
@@ -5975,6 +5993,21 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
5975
5993
|
return false;
|
|
5976
5994
|
};
|
|
5977
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
|
|
5978
6011
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
5979
6012
|
const MESSAGE$57 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
5980
6013
|
const KEY_HANDLERS = [
|
|
@@ -5985,6 +6018,63 @@ const KEY_HANDLERS = [
|
|
|
5985
6018
|
"onKeyDownCapture",
|
|
5986
6019
|
"onKeyPressCapture"
|
|
5987
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
|
+
};
|
|
5988
6078
|
const FOCUSLESS_CONTAINER_TAGS = new Set([
|
|
5989
6079
|
"tr",
|
|
5990
6080
|
"td",
|
|
@@ -6032,7 +6122,10 @@ const isFocusForwardingFunctionBody = (body) => {
|
|
|
6032
6122
|
};
|
|
6033
6123
|
const resolveHandlerFunction = (attribute) => {
|
|
6034
6124
|
if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
|
|
6035
|
-
|
|
6125
|
+
return resolveHandlerFunctionExpression(attribute.value.expression);
|
|
6126
|
+
};
|
|
6127
|
+
const resolveHandlerFunctionExpression = (handlerExpression) => {
|
|
6128
|
+
let expression = handlerExpression;
|
|
6036
6129
|
if (isNodeOfType(expression, "Identifier")) {
|
|
6037
6130
|
const binding = findVariableInitializer(expression, expression.name);
|
|
6038
6131
|
if (!binding?.initializer) return null;
|
|
@@ -6131,18 +6224,22 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
6131
6224
|
if (!HTML_TAGS.has(tag)) return;
|
|
6132
6225
|
if (tag === "label") return;
|
|
6133
6226
|
if (!FOCUSLESS_CONTAINER_TAGS.has(tag) && isInteractiveElement(tag, node)) return;
|
|
6227
|
+
const spreadEventValues = getTransparentSpreadEventValues(node.attributes, context.scopes);
|
|
6228
|
+
if (!spreadEventValues) return;
|
|
6134
6229
|
const onClick = hasJsxPropIgnoreCase(node.attributes, "onClick") ?? hasJsxPropIgnoreCase(node.attributes, "onClickCapture");
|
|
6135
|
-
|
|
6136
|
-
if (
|
|
6137
|
-
if (
|
|
6138
|
-
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;
|
|
6139
6236
|
if (hasCompositeItemRole(node)) return;
|
|
6140
6237
|
if (isHoverSelectionListItem(tag, node)) return;
|
|
6141
|
-
if (isBackdropDismissHandler(onClick)) return;
|
|
6238
|
+
if (onClick && isBackdropDismissHandler(onClick)) return;
|
|
6142
6239
|
if (containsKeyboardActivatableDescendant(node.parent)) return;
|
|
6143
6240
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
6144
6241
|
if (isPresentationRole(node)) return;
|
|
6145
|
-
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;
|
|
6146
6243
|
context.report({
|
|
6147
6244
|
node: node.name,
|
|
6148
6245
|
message: MESSAGE$57
|
|
@@ -6868,11 +6965,10 @@ const getTemplateInterpolations = (valueTail) => {
|
|
|
6868
6965
|
const interpolations = valueTail.slice(1, closingBacktickIndex).match(/\$\{[^}]*\}/g);
|
|
6869
6966
|
return interpolations === null ? "" : interpolations.join(" ");
|
|
6870
6967
|
};
|
|
6871
|
-
const getIdentifierDeclarationInitializer = (identifier, fileContent) => {
|
|
6872
|
-
const
|
|
6873
|
-
if (
|
|
6874
|
-
|
|
6875
|
-
return fileContent.slice(initializerStartIndex, initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
|
|
6968
|
+
const getIdentifierDeclarationInitializer = (identifier, sinkIndex, fileContent) => {
|
|
6969
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
6970
|
+
if (declaration === null) return null;
|
|
6971
|
+
return fileContent.slice(declaration.initializerStartIndex, declaration.initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
|
|
6876
6972
|
};
|
|
6877
6973
|
const isPureStringLiteralConcat = (initializerText) => {
|
|
6878
6974
|
if (!/^["']/.test(initializerText)) return false;
|
|
@@ -6942,6 +7038,153 @@ const isAllOperandsDomContentConcat = (valueExpression) => {
|
|
|
6942
7038
|
return !HTML_TAINT_PATTERN.test(withoutCast.replace(DOM_CONTENT_SOURCE_VALUE_PATTERN, ""));
|
|
6943
7039
|
});
|
|
6944
7040
|
};
|
|
7041
|
+
const findMatchingBraceIndex = (fileContent, openingBraceIndex) => {
|
|
7042
|
+
let depth = 0;
|
|
7043
|
+
let quote = null;
|
|
7044
|
+
for (let index = openingBraceIndex; index < fileContent.length; index += 1) {
|
|
7045
|
+
const character = fileContent[index];
|
|
7046
|
+
if (quote !== null) {
|
|
7047
|
+
if (character === quote && fileContent[index - 1] !== "\\") quote = null;
|
|
7048
|
+
continue;
|
|
7049
|
+
}
|
|
7050
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
7051
|
+
quote = character;
|
|
7052
|
+
continue;
|
|
7053
|
+
}
|
|
7054
|
+
if (character === "{") depth += 1;
|
|
7055
|
+
if (character === "}") {
|
|
7056
|
+
depth -= 1;
|
|
7057
|
+
if (depth === 0) return index;
|
|
7058
|
+
}
|
|
7059
|
+
}
|
|
7060
|
+
return fileContent.length;
|
|
7061
|
+
};
|
|
7062
|
+
const findContainingBlockEndIndex = (fileContent, targetIndex) => {
|
|
7063
|
+
const openingBraceIndexes = [];
|
|
7064
|
+
let quote = null;
|
|
7065
|
+
let isLineComment = false;
|
|
7066
|
+
let isBlockComment = false;
|
|
7067
|
+
for (let index = 0; index < targetIndex; index += 1) {
|
|
7068
|
+
const character = fileContent[index];
|
|
7069
|
+
const nextCharacter = fileContent[index + 1];
|
|
7070
|
+
if (isLineComment) {
|
|
7071
|
+
if (character === "\n") isLineComment = false;
|
|
7072
|
+
continue;
|
|
7073
|
+
}
|
|
7074
|
+
if (isBlockComment) {
|
|
7075
|
+
if (character === "*" && nextCharacter === "/") {
|
|
7076
|
+
isBlockComment = false;
|
|
7077
|
+
index += 1;
|
|
7078
|
+
}
|
|
7079
|
+
continue;
|
|
7080
|
+
}
|
|
7081
|
+
if (quote !== null) {
|
|
7082
|
+
if (character === quote && fileContent[index - 1] !== "\\") quote = null;
|
|
7083
|
+
continue;
|
|
7084
|
+
}
|
|
7085
|
+
if (character === "/" && nextCharacter === "/") {
|
|
7086
|
+
isLineComment = true;
|
|
7087
|
+
index += 1;
|
|
7088
|
+
continue;
|
|
7089
|
+
}
|
|
7090
|
+
if (character === "/" && nextCharacter === "*") {
|
|
7091
|
+
isBlockComment = true;
|
|
7092
|
+
index += 1;
|
|
7093
|
+
continue;
|
|
7094
|
+
}
|
|
7095
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
7096
|
+
quote = character;
|
|
7097
|
+
continue;
|
|
7098
|
+
}
|
|
7099
|
+
if (character === "{") openingBraceIndexes.push(index);
|
|
7100
|
+
if (character === "}") openingBraceIndexes.pop();
|
|
7101
|
+
}
|
|
7102
|
+
const openingBraceIndex = openingBraceIndexes.at(-1);
|
|
7103
|
+
return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
|
|
7104
|
+
};
|
|
7105
|
+
const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
|
|
7106
|
+
const initializerPattern = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
|
|
7107
|
+
let nearestDeclaration = null;
|
|
7108
|
+
let nearestDeclarationIndex = -1;
|
|
7109
|
+
for (const match of fileContent.matchAll(initializerPattern)) {
|
|
7110
|
+
const declarationIndex = match.index;
|
|
7111
|
+
if (declarationIndex === void 0 || declarationIndex >= sinkIndex || declarationIndex <= nearestDeclarationIndex || findContainingBlockEndIndex(fileContent, declarationIndex) < sinkIndex) continue;
|
|
7112
|
+
nearestDeclarationIndex = declarationIndex;
|
|
7113
|
+
const initializer = match[1];
|
|
7114
|
+
if (initializer === void 0) continue;
|
|
7115
|
+
nearestDeclaration = {
|
|
7116
|
+
initializer,
|
|
7117
|
+
initializerStartIndex: declarationIndex + match[0].length - initializer.length
|
|
7118
|
+
};
|
|
7119
|
+
}
|
|
7120
|
+
return nearestDeclaration;
|
|
7121
|
+
};
|
|
7122
|
+
const findContainingFunctionParameterSource = (identifier, sinkIndex, fileContent) => {
|
|
7123
|
+
const patterns = [/function\s+([\w$]+)\s*\(([^)]*)\)\s*\{/g, /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/g];
|
|
7124
|
+
let closestSource = null;
|
|
7125
|
+
let closestStartIndex = -1;
|
|
7126
|
+
for (const pattern of patterns) for (const match of fileContent.matchAll(pattern)) {
|
|
7127
|
+
const matchIndex = match.index;
|
|
7128
|
+
if (matchIndex === void 0 || matchIndex >= sinkIndex || matchIndex < closestStartIndex) continue;
|
|
7129
|
+
if (findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{")) < sinkIndex) continue;
|
|
7130
|
+
const parameterIndex = (match[2] ?? "").split(",").findIndex((parameter) => parameter.trim().match(/^(?:\.\.\.)?([\w$]+)/)?.[1] === identifier);
|
|
7131
|
+
if (parameterIndex < 0) continue;
|
|
7132
|
+
closestStartIndex = matchIndex;
|
|
7133
|
+
closestSource = {
|
|
7134
|
+
functionName: match[1] ?? "",
|
|
7135
|
+
parameterIndex
|
|
7136
|
+
};
|
|
7137
|
+
}
|
|
7138
|
+
return closestSource;
|
|
7139
|
+
};
|
|
7140
|
+
const splitTopLevelArguments = (argumentText) => {
|
|
7141
|
+
const argumentsList = [];
|
|
7142
|
+
let startIndex = 0;
|
|
7143
|
+
let depth = 0;
|
|
7144
|
+
let quote = null;
|
|
7145
|
+
for (let index = 0; index < argumentText.length; index += 1) {
|
|
7146
|
+
const character = argumentText[index];
|
|
7147
|
+
if (quote !== null) {
|
|
7148
|
+
if (character === quote && argumentText[index - 1] !== "\\") quote = null;
|
|
7149
|
+
continue;
|
|
7150
|
+
}
|
|
7151
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
7152
|
+
quote = character;
|
|
7153
|
+
continue;
|
|
7154
|
+
}
|
|
7155
|
+
if (character === "(" || character === "[" || character === "{") depth += 1;
|
|
7156
|
+
if (character === ")" || character === "]" || character === "}") depth -= 1;
|
|
7157
|
+
if (character === "," && depth === 0) {
|
|
7158
|
+
argumentsList.push(argumentText.slice(startIndex, index).trim());
|
|
7159
|
+
startIndex = index + 1;
|
|
7160
|
+
}
|
|
7161
|
+
}
|
|
7162
|
+
argumentsList.push(argumentText.slice(startIndex).trim());
|
|
7163
|
+
return argumentsList;
|
|
7164
|
+
};
|
|
7165
|
+
const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers) => {
|
|
7166
|
+
const trimmedExpression = expression.trim();
|
|
7167
|
+
if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
|
|
7168
|
+
if (MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
|
|
7169
|
+
if (SANITIZER_PATTERN.test(trimmedExpression)) return false;
|
|
7170
|
+
if (ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression)) return false;
|
|
7171
|
+
if (I18N_VALUE_PATTERN.test(trimmedExpression)) return false;
|
|
7172
|
+
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression)) return false;
|
|
7173
|
+
if (HTML_TAINT_PATTERN.test(trimmedExpression)) return true;
|
|
7174
|
+
const identifier = trimmedExpression.match(/^([\w$]+)\s*(?:[;,})\n]|$)/)?.[1];
|
|
7175
|
+
if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
|
|
7176
|
+
visitedIdentifiers.add(identifier);
|
|
7177
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7178
|
+
if (declaration !== null && isHtmlTainted(declaration.initializer, fileContent, sinkIndex, visitedIdentifiers)) return true;
|
|
7179
|
+
const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
|
|
7180
|
+
if (parameterSource === null || parameterSource.functionName.length === 0) return false;
|
|
7181
|
+
const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(([^)]*)\\)`, "g");
|
|
7182
|
+
for (const callMatch of fileContent.matchAll(callPattern)) {
|
|
7183
|
+
const argument = splitTopLevelArguments(callMatch[1] ?? "")[parameterSource.parameterIndex];
|
|
7184
|
+
if (argument !== void 0 && isHtmlTainted(argument, fileContent, sinkIndex, new Set(visitedIdentifiers))) return true;
|
|
7185
|
+
}
|
|
7186
|
+
return false;
|
|
7187
|
+
};
|
|
6945
7188
|
const dangerousHtmlSink = defineRule({
|
|
6946
7189
|
id: "dangerous-html-sink",
|
|
6947
7190
|
title: "HTML injection sink with dynamic content",
|
|
@@ -6968,6 +7211,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
6968
7211
|
const valueTail = fullValueTail.slice(0, VALUE_EXPRESSION_MAX_CHARS);
|
|
6969
7212
|
const terminatorIndex = valueTail.search(/[;}]/);
|
|
6970
7213
|
const valueExpression = terminatorIndex >= 0 ? valueTail.slice(0, terminatorIndex + 1) : valueTail;
|
|
7214
|
+
const sinkIndex = lines.slice(0, lineIndex).join("\n").length + (lineIndex > 0 ? 1 : 0) + line.search(DANGEROUS_HTML_PATTERN);
|
|
6971
7215
|
if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
|
|
6972
7216
|
if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
|
|
6973
7217
|
if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
|
|
@@ -6979,7 +7223,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
6979
7223
|
let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
|
|
6980
7224
|
const valueIdentifier = BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) || MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN.test(valueExpression) || IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN.test(valueExpression) ? valueExpression.match(/^[\w$]+/)?.[0] : void 0;
|
|
6981
7225
|
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
|
|
6982
|
-
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, file.content);
|
|
7226
|
+
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
|
|
6983
7227
|
if (declarationInitializer !== null) {
|
|
6984
7228
|
if (isPureStringLiteralConcat(declarationInitializer)) continue;
|
|
6985
7229
|
templateInterpolations = getTemplateInterpolations(declarationInitializer);
|
|
@@ -6990,7 +7234,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
6990
7234
|
if (SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
6991
7235
|
if (ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
6992
7236
|
if (I18N_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
6993
|
-
if (!
|
|
7237
|
+
if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set())) continue;
|
|
6994
7238
|
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
|
|
6995
7239
|
if (/highlighted/i.test(valueExpression)) continue;
|
|
6996
7240
|
if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
@@ -7468,7 +7712,7 @@ const isCreateClassLikeCall = (node) => {
|
|
|
7468
7712
|
if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$2(callee) === "createClass";
|
|
7469
7713
|
return false;
|
|
7470
7714
|
};
|
|
7471
|
-
const isCreateContextCall = (node) => {
|
|
7715
|
+
const isCreateContextCall$1 = (node) => {
|
|
7472
7716
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7473
7717
|
const callee = node.callee;
|
|
7474
7718
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
|
|
@@ -7645,7 +7889,7 @@ const displayName = defineRule({
|
|
|
7645
7889
|
}
|
|
7646
7890
|
},
|
|
7647
7891
|
CallExpression(node) {
|
|
7648
|
-
if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall(node)) {
|
|
7892
|
+
if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall$1(node)) {
|
|
7649
7893
|
const assignedName = getAssignedName(node);
|
|
7650
7894
|
if (assignedName) {
|
|
7651
7895
|
const programRoot = findProgramRoot(node);
|
|
@@ -7692,21 +7936,6 @@ const displayName = defineRule({
|
|
|
7692
7936
|
}
|
|
7693
7937
|
});
|
|
7694
7938
|
//#endregion
|
|
7695
|
-
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
7696
|
-
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
7697
|
-
if (!isNodeOfType(node, "Property")) return null;
|
|
7698
|
-
if (node.computed) {
|
|
7699
|
-
if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
|
|
7700
|
-
return null;
|
|
7701
|
-
}
|
|
7702
|
-
if (isNodeOfType(node.key, "Identifier")) return node.key.name;
|
|
7703
|
-
if (isNodeOfType(node.key, "Literal")) {
|
|
7704
|
-
if (typeof node.key.value === "string") return node.key.value;
|
|
7705
|
-
if (options.stringifyNonStringLiterals) return String(node.key.value);
|
|
7706
|
-
}
|
|
7707
|
-
return null;
|
|
7708
|
-
};
|
|
7709
|
-
//#endregion
|
|
7710
7939
|
//#region src/plugin/utils/read-static-boolean.ts
|
|
7711
7940
|
const readStaticBoolean = (node) => {
|
|
7712
7941
|
if (!node) return null;
|
|
@@ -7715,21 +7944,6 @@ const readStaticBoolean = (node) => {
|
|
|
7715
7944
|
return unwrappedNode.value;
|
|
7716
7945
|
};
|
|
7717
7946
|
//#endregion
|
|
7718
|
-
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
7719
|
-
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
7720
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
7721
|
-
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
7722
|
-
let symbol = scopes.symbolFor(identifier);
|
|
7723
|
-
while (symbol?.kind === "const") {
|
|
7724
|
-
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
7725
|
-
visitedSymbolIds.add(symbol.id);
|
|
7726
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
7727
|
-
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
7728
|
-
symbol = scopes.symbolFor(initializer);
|
|
7729
|
-
}
|
|
7730
|
-
return symbol;
|
|
7731
|
-
};
|
|
7732
|
-
//#endregion
|
|
7733
7947
|
//#region src/plugin/utils/is-react-api-call.ts
|
|
7734
7948
|
const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
|
|
7735
7949
|
const isImportedFromReact = (symbol) => {
|
|
@@ -7737,9 +7951,9 @@ const isImportedFromReact = (symbol) => {
|
|
|
7737
7951
|
const importDeclaration = symbol.declarationNode.parent;
|
|
7738
7952
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
|
|
7739
7953
|
};
|
|
7740
|
-
const isNamedReactApiImport = (identifier, apiNames, scopes) => {
|
|
7954
|
+
const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
|
|
7741
7955
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
7742
|
-
const symbol = scopes.symbolFor(identifier);
|
|
7956
|
+
const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
|
|
7743
7957
|
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
7744
7958
|
const importedName = getImportedName(symbol.declarationNode);
|
|
7745
7959
|
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
@@ -7753,7 +7967,7 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
|
|
|
7753
7967
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7754
7968
|
const callee = stripParenExpression(node.callee);
|
|
7755
7969
|
if (isNodeOfType(callee, "Identifier")) {
|
|
7756
|
-
if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
|
|
7970
|
+
if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
|
|
7757
7971
|
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
7758
7972
|
}
|
|
7759
7973
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
|
|
@@ -8856,6 +9070,30 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
|
|
|
8856
9070
|
}
|
|
8857
9071
|
return matchingBlocks.size > 0;
|
|
8858
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
|
+
};
|
|
8859
9097
|
const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
|
|
8860
9098
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8861
9099
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
@@ -9056,6 +9294,83 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
|
|
|
9056
9294
|
});
|
|
9057
9295
|
return didCleanupFunctionMatch;
|
|
9058
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);
|
|
9059
9374
|
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
9060
9375
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
9061
9376
|
if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
@@ -9068,6 +9383,10 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
|
9068
9383
|
if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
|
|
9069
9384
|
const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
|
|
9070
9385
|
if (!returnedValue) return;
|
|
9386
|
+
if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) {
|
|
9387
|
+
matchingCleanupReturns.push(child);
|
|
9388
|
+
return;
|
|
9389
|
+
}
|
|
9071
9390
|
if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
|
|
9072
9391
|
matchingCleanupReturns.push(child);
|
|
9073
9392
|
return;
|
|
@@ -9083,13 +9402,17 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
|
9083
9402
|
if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
|
|
9084
9403
|
}
|
|
9085
9404
|
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
9405
|
+
if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) {
|
|
9406
|
+
matchingCleanupReturns.push(child);
|
|
9407
|
+
return;
|
|
9408
|
+
}
|
|
9086
9409
|
if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
|
|
9087
9410
|
if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
|
|
9088
9411
|
});
|
|
9089
9412
|
return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
|
|
9090
9413
|
};
|
|
9091
9414
|
const findFirstUsageWithoutCleanup = (callback, usages, context) => {
|
|
9092
|
-
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;
|
|
9093
9416
|
return null;
|
|
9094
9417
|
};
|
|
9095
9418
|
const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
@@ -10962,6 +11285,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10962
11285
|
return { CallExpression(node) {
|
|
10963
11286
|
const hookName = getHookName(node.callee, context.scopes);
|
|
10964
11287
|
if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
|
|
11288
|
+
if (HOOKS_REQUIRING_DEPS_MATCH.has(hookName) && !isReactApiCall(node, HOOKS_REQUIRING_DEPS_MATCH, context.scopes, {
|
|
11289
|
+
allowGlobalReactNamespace: true,
|
|
11290
|
+
allowUnboundBareCalls: true,
|
|
11291
|
+
resolveNamedAliases: true
|
|
11292
|
+
})) return;
|
|
10965
11293
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
10966
11294
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
10967
11295
|
const callbackArgument = node.arguments[callbackArgumentIndex];
|
|
@@ -22889,61 +23217,73 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
|
|
|
22889
23217
|
"Array",
|
|
22890
23218
|
"BigInt",
|
|
22891
23219
|
"Boolean",
|
|
23220
|
+
"encodeURIComponent",
|
|
22892
23221
|
"Number",
|
|
22893
23222
|
"Object",
|
|
22894
23223
|
"String",
|
|
22895
23224
|
"parseFloat",
|
|
22896
|
-
"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"])]
|
|
22897
23273
|
]);
|
|
22898
|
-
const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
|
|
22899
|
-
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
|
|
22900
|
-
"isRawJSON",
|
|
22901
|
-
"parse",
|
|
22902
|
-
"rawJSON",
|
|
22903
|
-
"stringify"
|
|
22904
|
-
])], ["Math", new Set([
|
|
22905
|
-
"abs",
|
|
22906
|
-
"acos",
|
|
22907
|
-
"acosh",
|
|
22908
|
-
"asin",
|
|
22909
|
-
"asinh",
|
|
22910
|
-
"atan",
|
|
22911
|
-
"atan2",
|
|
22912
|
-
"atanh",
|
|
22913
|
-
"cbrt",
|
|
22914
|
-
"ceil",
|
|
22915
|
-
"clz32",
|
|
22916
|
-
"cos",
|
|
22917
|
-
"cosh",
|
|
22918
|
-
"exp",
|
|
22919
|
-
"floor",
|
|
22920
|
-
"fround",
|
|
22921
|
-
"hypot",
|
|
22922
|
-
"imul",
|
|
22923
|
-
"log",
|
|
22924
|
-
"log10",
|
|
22925
|
-
"log1p",
|
|
22926
|
-
"log2",
|
|
22927
|
-
"max",
|
|
22928
|
-
"min",
|
|
22929
|
-
"pow",
|
|
22930
|
-
"round",
|
|
22931
|
-
"sign",
|
|
22932
|
-
"sin",
|
|
22933
|
-
"sinh",
|
|
22934
|
-
"sqrt",
|
|
22935
|
-
"tan",
|
|
22936
|
-
"tanh",
|
|
22937
|
-
"trunc"
|
|
22938
|
-
])]]);
|
|
22939
23274
|
const PURE_MEMBER_TRANSFORM_NAMES = new Set([
|
|
22940
23275
|
"concat",
|
|
22941
23276
|
"filter",
|
|
23277
|
+
"flatMap",
|
|
22942
23278
|
"join",
|
|
22943
23279
|
"map",
|
|
23280
|
+
"reduce",
|
|
23281
|
+
"replace",
|
|
23282
|
+
"slice",
|
|
22944
23283
|
"split",
|
|
22945
23284
|
"toLowerCase",
|
|
22946
23285
|
"toString",
|
|
23286
|
+
"toSorted",
|
|
22947
23287
|
"toUpperCase",
|
|
22948
23288
|
"trim"
|
|
22949
23289
|
]);
|
|
@@ -23325,6 +23665,11 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
|
|
|
23325
23665
|
const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
|
|
23326
23666
|
return callbackSummary.isValid && !callbackSummary.canContinue;
|
|
23327
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
|
+
}
|
|
23328
23673
|
if (isNodeOfType(node, "CallExpression")) {
|
|
23329
23674
|
const callee = stripParenExpression(node.callee);
|
|
23330
23675
|
const calleeRoot = getMemberRoot(callee);
|
|
@@ -23527,7 +23872,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23527
23872
|
}
|
|
23528
23873
|
const callee = stripParenExpression(node.callee);
|
|
23529
23874
|
const calleeRoot = getMemberRoot(callee);
|
|
23530
|
-
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;
|
|
23531
23876
|
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
|
|
23532
23877
|
if (isPureGlobalCall || isPureMemberTransform) {
|
|
23533
23878
|
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
@@ -23580,6 +23925,15 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23580
23925
|
}
|
|
23581
23926
|
return evidence;
|
|
23582
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
|
+
}
|
|
23583
23937
|
if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
|
|
23584
23938
|
evidence.hasUnknownSource = true;
|
|
23585
23939
|
return evidence;
|
|
@@ -23998,6 +24352,26 @@ const STATEFUL_HTML_DESCENDANT_TAGS = new Set([
|
|
|
23998
24352
|
"dialog",
|
|
23999
24353
|
"canvas"
|
|
24000
24354
|
]);
|
|
24355
|
+
const STATEFUL_HTML_ATTRIBUTE_NAMES = new Set([
|
|
24356
|
+
"autofocus",
|
|
24357
|
+
"contenteditable",
|
|
24358
|
+
"draggable",
|
|
24359
|
+
"tabindex"
|
|
24360
|
+
]);
|
|
24361
|
+
const isStaticallyFalseAttributeValue = (attribute) => {
|
|
24362
|
+
if (!isNodeOfType(attribute, "JSXAttribute") || !attribute.value) return false;
|
|
24363
|
+
const value = isNodeOfType(attribute.value, "JSXExpressionContainer") ? attribute.value.expression : attribute.value;
|
|
24364
|
+
return isNodeOfType(value, "Literal") && (value.value === false || value.value === "false");
|
|
24365
|
+
};
|
|
24366
|
+
const hasStatefulHtmlAttribute = (openingElement) => {
|
|
24367
|
+
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
24368
|
+
return openingElement.attributes.some((attribute) => {
|
|
24369
|
+
if (!isNodeOfType(attribute, "JSXAttribute") || !isNodeOfType(attribute.name, "JSXIdentifier")) return false;
|
|
24370
|
+
const attributeName = attribute.name.name.toLowerCase();
|
|
24371
|
+
if (!STATEFUL_HTML_ATTRIBUTE_NAMES.has(attributeName)) return false;
|
|
24372
|
+
return attributeName === "tabindex" || !isStaticallyFalseAttributeValue(attribute);
|
|
24373
|
+
});
|
|
24374
|
+
};
|
|
24001
24375
|
const STATEFUL_DESCENDANT_SCAN_BUDGET = 200;
|
|
24002
24376
|
const rootIdentifierNameOf = (expression) => {
|
|
24003
24377
|
let object = expression;
|
|
@@ -24015,7 +24389,8 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
|
|
|
24015
24389
|
budget -= 1;
|
|
24016
24390
|
const node = stack.pop();
|
|
24017
24391
|
if (isNodeOfType(node, "JSXElement")) {
|
|
24018
|
-
const
|
|
24392
|
+
const opening = node.openingElement;
|
|
24393
|
+
const name = opening.name;
|
|
24019
24394
|
if (name && isNodeOfType(name, "JSXIdentifier")) {
|
|
24020
24395
|
const tagName = name.name;
|
|
24021
24396
|
const firstChar = tagName.charCodeAt(0);
|
|
@@ -24023,6 +24398,7 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
|
|
|
24023
24398
|
if (STATEFUL_HTML_DESCENDANT_TAGS.has(tagName)) return true;
|
|
24024
24399
|
}
|
|
24025
24400
|
if (name && isNodeOfType(name, "JSXMemberExpression")) return true;
|
|
24401
|
+
if (hasStatefulHtmlAttribute(opening)) return true;
|
|
24026
24402
|
const children = node.children ?? [];
|
|
24027
24403
|
for (const child of children) stack.push(child);
|
|
24028
24404
|
continue;
|
|
@@ -24582,6 +24958,14 @@ const templateHasOuterMemberIdentity = (template, bindingFunction) => {
|
|
|
24582
24958
|
}
|
|
24583
24959
|
return false;
|
|
24584
24960
|
};
|
|
24961
|
+
const findBareItemNamesReferencedByTemplate = (template, itemNames) => {
|
|
24962
|
+
const referencedItemNames = /* @__PURE__ */ new Set();
|
|
24963
|
+
for (const expression of template.expressions ?? []) {
|
|
24964
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
24965
|
+
if (isNodeOfType(unwrappedExpression, "Identifier") && itemNames.has(unwrappedExpression.name)) referencedItemNames.add(unwrappedExpression.name);
|
|
24966
|
+
}
|
|
24967
|
+
return referencedItemNames;
|
|
24968
|
+
};
|
|
24585
24969
|
const forLoopTestReadsDataLength = (test) => {
|
|
24586
24970
|
let didFindLengthRead = false;
|
|
24587
24971
|
walkAst(test, (child) => {
|
|
@@ -24718,9 +25102,11 @@ const noArrayIndexAsKey = defineRule({
|
|
|
24718
25102
|
} else if (STATELESS_HTML_LEAF_TAGS.has(elementName.name)) {
|
|
24719
25103
|
const jsxElement = openingElement.parent;
|
|
24720
25104
|
if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
|
|
25105
|
+
const isInlineTextRun = INLINE_TEXT_LEAF_TAGS.has(elementName.name);
|
|
25106
|
+
const primitiveItemNames = keyTemplate ? findBareItemNamesReferencedByTemplate(keyTemplate, itemNames) : EMPTY_NAME_SET;
|
|
24721
25107
|
if (!containsStatefulDescendant(jsxElement, {
|
|
24722
|
-
memberRootNames:
|
|
24723
|
-
bareIdentifierNames: derivedNames
|
|
25108
|
+
memberRootNames: isInlineTextRun ? itemNames : EMPTY_NAME_SET,
|
|
25109
|
+
bareIdentifierNames: primitiveItemNames.size > 0 ? new Set([...derivedNames, ...primitiveItemNames]) : derivedNames
|
|
24724
25110
|
})) return;
|
|
24725
25111
|
}
|
|
24726
25112
|
}
|
|
@@ -24888,7 +25274,11 @@ const noAsyncEffectCallback = defineRule({
|
|
|
24888
25274
|
severity: "warn",
|
|
24889
25275
|
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.",
|
|
24890
25276
|
create: (context) => ({ CallExpression(node) {
|
|
24891
|
-
if (!
|
|
25277
|
+
if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
25278
|
+
allowGlobalReactNamespace: true,
|
|
25279
|
+
allowUnboundBareCalls: true,
|
|
25280
|
+
resolveNamedAliases: true
|
|
25281
|
+
})) return;
|
|
24892
25282
|
const callback = getEffectCallback(node);
|
|
24893
25283
|
if (!callback) return;
|
|
24894
25284
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
@@ -26086,6 +26476,19 @@ const noCloneElement = defineRule({
|
|
|
26086
26476
|
} })
|
|
26087
26477
|
});
|
|
26088
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
|
|
26089
26492
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
26090
26493
|
const MESSAGE$31 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
26091
26494
|
const CONTEXT_MODULES = [
|
|
@@ -26093,23 +26496,35 @@ const CONTEXT_MODULES = [
|
|
|
26093
26496
|
"use-context-selector",
|
|
26094
26497
|
"react-tracked"
|
|
26095
26498
|
];
|
|
26096
|
-
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);
|
|
26097
26517
|
if (isNodeOfType(callee, "Identifier")) {
|
|
26098
|
-
|
|
26099
|
-
|
|
26518
|
+
if (isDestructuredCreateContextBinding(callee, scopes)) return true;
|
|
26519
|
+
const symbol = resolveConstIdentifierAlias(callee, scopes);
|
|
26520
|
+
return getSupportedContextImportSource(symbol) !== null && Boolean(symbol && getImportedName(symbol.declarationNode) === "createContext");
|
|
26100
26521
|
}
|
|
26101
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
26102
|
-
|
|
26103
|
-
|
|
26104
|
-
|
|
26105
|
-
|
|
26106
|
-
|
|
26107
|
-
const namespaceName = namespaceIdentifier.name;
|
|
26108
|
-
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
26109
|
-
const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
|
|
26110
|
-
return importSource !== null && CONTEXT_MODULES.includes(importSource);
|
|
26111
|
-
}
|
|
26112
|
-
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));
|
|
26113
26528
|
};
|
|
26114
26529
|
const noCreateContextInRender = defineRule({
|
|
26115
26530
|
id: "no-create-context-in-render",
|
|
@@ -26118,7 +26533,7 @@ const noCreateContextInRender = defineRule({
|
|
|
26118
26533
|
category: "Correctness",
|
|
26119
26534
|
recommendation: "Move `createContext(...)` outside the component, to the top level of the file, so it stays the same on every render.",
|
|
26120
26535
|
create: (context) => ({ CallExpression(node) {
|
|
26121
|
-
if (!
|
|
26536
|
+
if (!isCreateContextCall(node, context.scopes)) return;
|
|
26122
26537
|
const componentOrHookName = enclosingComponentOrHookName(node);
|
|
26123
26538
|
if (!componentOrHookName) return;
|
|
26124
26539
|
context.report({
|
|
@@ -28068,20 +28483,31 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
28068
28483
|
} })
|
|
28069
28484
|
});
|
|
28070
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
|
|
28071
28495
|
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
28072
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.";
|
|
28073
28497
|
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
28498
|
+
const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
|
|
28074
28499
|
const noDocumentWrite = defineRule({
|
|
28075
28500
|
id: "no-document-write",
|
|
28076
28501
|
title: "document.write/writeln",
|
|
28077
28502
|
severity: "warn",
|
|
28078
28503
|
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
28079
28504
|
create: (context) => ({ CallExpression(node) {
|
|
28080
|
-
const callee = node.callee;
|
|
28081
|
-
if (!isNodeOfType(callee, "MemberExpression")
|
|
28505
|
+
const callee = stripParenExpression(node.callee);
|
|
28506
|
+
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
28082
28507
|
const receiver = stripParenExpression(callee.object);
|
|
28083
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver
|
|
28084
|
-
|
|
28508
|
+
if (!isNodeOfType(receiver, "Identifier") || !isGlobalDocumentReference(receiver, context)) return;
|
|
28509
|
+
const methodName = getStaticPropertyName(callee);
|
|
28510
|
+
if (methodName === null || !WRITE_METHODS.has(methodName)) return;
|
|
28085
28511
|
context.report({
|
|
28086
28512
|
node,
|
|
28087
28513
|
message: MESSAGE$24
|
|
@@ -28888,6 +29314,14 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
28888
29314
|
//#endregion
|
|
28889
29315
|
//#region src/plugin/rules/security/no-eval.ts
|
|
28890
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
|
+
};
|
|
28891
29325
|
const noEval = defineRule({
|
|
28892
29326
|
id: "no-eval",
|
|
28893
29327
|
title: "eval() runs untrusted code strings",
|
|
@@ -28897,20 +29331,21 @@ const noEval = defineRule({
|
|
|
28897
29331
|
if (SANDBOX_SURFACE_PATH_PATTERN.test(normalizeFilename(context.filename ?? ""))) return {};
|
|
28898
29332
|
return {
|
|
28899
29333
|
CallExpression(node) {
|
|
28900
|
-
|
|
29334
|
+
const executableGlobalName = getExecutableGlobalName(node.callee, context);
|
|
29335
|
+
if (executableGlobalName === "eval") {
|
|
28901
29336
|
context.report({
|
|
28902
29337
|
node,
|
|
28903
29338
|
message: "eval() is a code-injection vulnerability: it runs any string as code."
|
|
28904
29339
|
});
|
|
28905
29340
|
return;
|
|
28906
29341
|
}
|
|
28907
|
-
if (
|
|
29342
|
+
if ((executableGlobalName === "setTimeout" || executableGlobalName === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
|
|
28908
29343
|
node,
|
|
28909
|
-
message: `Passing a string to ${
|
|
29344
|
+
message: `Passing a string to ${executableGlobalName}() is a code-injection vulnerability, since it runs that string as code.`
|
|
28910
29345
|
});
|
|
28911
29346
|
},
|
|
28912
29347
|
NewExpression(node) {
|
|
28913
|
-
if (
|
|
29348
|
+
if (getExecutableGlobalName(node.callee, context) === "Function") {
|
|
28914
29349
|
const onlyArgument = node.arguments.length === 1 ? node.arguments[0] : void 0;
|
|
28915
29350
|
if (isNodeOfType(onlyArgument, "Literal") && typeof onlyArgument.value === "string" && onlyArgument.value.trim() === "return this") return;
|
|
28916
29351
|
context.report({
|
|
@@ -30169,7 +30604,11 @@ const noFetchInEffect = defineRule({
|
|
|
30169
30604
|
severity: "warn",
|
|
30170
30605
|
recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
|
|
30171
30606
|
create: (context) => ({ CallExpression(node) {
|
|
30172
|
-
if (!
|
|
30607
|
+
if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
30608
|
+
allowGlobalReactNamespace: true,
|
|
30609
|
+
allowUnboundBareCalls: true,
|
|
30610
|
+
resolveNamedAliases: true
|
|
30611
|
+
})) return;
|
|
30173
30612
|
const callback = getEffectCallback(node);
|
|
30174
30613
|
if (!callback) return;
|
|
30175
30614
|
const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
|
|
@@ -34379,19 +34818,6 @@ const DATA_SINK_METHOD_NAMES = new Set([
|
|
|
34379
34818
|
"deserialize"
|
|
34380
34819
|
]);
|
|
34381
34820
|
//#endregion
|
|
34382
|
-
//#region src/plugin/utils/get-call-method-name.ts
|
|
34383
|
-
/**
|
|
34384
|
-
* Returns the static method name of a call's callee when it's a
|
|
34385
|
-
* non-computed MemberExpression (`obj.method` → `"method"`), or
|
|
34386
|
-
* `null` otherwise. Used by `no-pass-data-to-parent` and
|
|
34387
|
-
* `no-pass-live-state-to-parent` to match the receiver-bound
|
|
34388
|
-
* method-call shape against an allow/block list.
|
|
34389
|
-
*/
|
|
34390
|
-
const getCallMethodName = (callee) => {
|
|
34391
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
34392
|
-
return null;
|
|
34393
|
-
};
|
|
34394
|
-
//#endregion
|
|
34395
34821
|
//#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
|
|
34396
34822
|
const isUseStateIdentifier = (identifier) => {
|
|
34397
34823
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
@@ -35017,6 +35443,30 @@ const guardsRenderShape = (comparison) => {
|
|
|
35017
35443
|
}
|
|
35018
35444
|
return false;
|
|
35019
35445
|
};
|
|
35446
|
+
const isPropsChildrenLength = (node, scopes) => {
|
|
35447
|
+
const unwrappedNode = stripParenExpression(node);
|
|
35448
|
+
return isNodeOfType(unwrappedNode, "MemberExpression") && !unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier") && unwrappedNode.property.name === "length" && resolvesToPropsChildren(stripParenExpression(unwrappedNode.object), scopes);
|
|
35449
|
+
};
|
|
35450
|
+
const isLargeTextLengthComparison = (node, scopes) => {
|
|
35451
|
+
const unwrappedNode = stripParenExpression(node);
|
|
35452
|
+
if (!isNodeOfType(unwrappedNode, "BinaryExpression")) return false;
|
|
35453
|
+
const leftIsLength = isPropsChildrenLength(unwrappedNode.left, scopes);
|
|
35454
|
+
const rightIsLength = isPropsChildrenLength(unwrappedNode.right, scopes);
|
|
35455
|
+
const thresholdNode = leftIsLength ? unwrappedNode.right : unwrappedNode.left;
|
|
35456
|
+
if (!leftIsLength && !rightIsLength || !isNodeOfType(thresholdNode, "Literal")) return false;
|
|
35457
|
+
if (typeof thresholdNode.value !== "number" || thresholdNode.value < 1e3) return false;
|
|
35458
|
+
return leftIsLength ? unwrappedNode.operator === ">" || unwrappedNode.operator === ">=" : unwrappedNode.operator === "<" || unwrappedNode.operator === "<=";
|
|
35459
|
+
};
|
|
35460
|
+
const isLargeStringOptimizationGuard = (comparison, scopes) => {
|
|
35461
|
+
let current = findTransparentExpressionRoot(comparison);
|
|
35462
|
+
while (current.parent) {
|
|
35463
|
+
const parent = current.parent;
|
|
35464
|
+
if (!isNodeOfType(parent, "LogicalExpression") || parent.operator !== "&&") return false;
|
|
35465
|
+
if (isLargeTextLengthComparison(parent.left === current ? parent.right : parent.left, scopes)) return true;
|
|
35466
|
+
current = findTransparentExpressionRoot(parent);
|
|
35467
|
+
}
|
|
35468
|
+
return false;
|
|
35469
|
+
};
|
|
35020
35470
|
const noPolymorphicChildren = defineRule({
|
|
35021
35471
|
id: "no-polymorphic-children",
|
|
35022
35472
|
title: "Children type checked at runtime",
|
|
@@ -35030,6 +35480,7 @@ const noPolymorphicChildren = defineRule({
|
|
|
35030
35480
|
const isStringLiteral = (operand) => isNodeOfType(operand, "Literal") && operand.value === "string";
|
|
35031
35481
|
if (!isStringLiteral(node.left) && !isStringLiteral(node.right)) return;
|
|
35032
35482
|
if (!guardsRenderShape(node)) return;
|
|
35483
|
+
if (isLargeStringOptimizationGuard(node, context.scopes)) return;
|
|
35033
35484
|
context.report({
|
|
35034
35485
|
node,
|
|
35035
35486
|
message: "Your users hit inconsistent behavior because `typeof children === \"string\"` makes this component switch on what callers pass, so add clear subcomponents like `<Button.Text>` instead."
|
|
@@ -54455,12 +54906,6 @@ const webhookSignatureRisk = defineRule({
|
|
|
54455
54906
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
54456
54907
|
const ZOD_MODULE = "zod";
|
|
54457
54908
|
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
54458
|
-
const getStaticPropertyName = (member) => {
|
|
54459
|
-
const property = member.property;
|
|
54460
|
-
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
54461
|
-
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
54462
|
-
return null;
|
|
54463
|
-
};
|
|
54464
54909
|
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
54465
54910
|
const getImportInfoForIdentifier = (identifier) => {
|
|
54466
54911
|
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|