oxlint-plugin-react-doctor 0.7.5-dev.3075e10 → 0.7.5-dev.3afd146
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 +235 -15
- 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;
|
|
@@ -6868,11 +6871,10 @@ const getTemplateInterpolations = (valueTail) => {
|
|
|
6868
6871
|
const interpolations = valueTail.slice(1, closingBacktickIndex).match(/\$\{[^}]*\}/g);
|
|
6869
6872
|
return interpolations === null ? "" : interpolations.join(" ");
|
|
6870
6873
|
};
|
|
6871
|
-
const getIdentifierDeclarationInitializer = (identifier, fileContent) => {
|
|
6872
|
-
const
|
|
6873
|
-
if (
|
|
6874
|
-
|
|
6875
|
-
return fileContent.slice(initializerStartIndex, initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
|
|
6874
|
+
const getIdentifierDeclarationInitializer = (identifier, sinkIndex, fileContent) => {
|
|
6875
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
6876
|
+
if (declaration === null) return null;
|
|
6877
|
+
return fileContent.slice(declaration.initializerStartIndex, declaration.initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
|
|
6876
6878
|
};
|
|
6877
6879
|
const isPureStringLiteralConcat = (initializerText) => {
|
|
6878
6880
|
if (!/^["']/.test(initializerText)) return false;
|
|
@@ -6942,6 +6944,153 @@ const isAllOperandsDomContentConcat = (valueExpression) => {
|
|
|
6942
6944
|
return !HTML_TAINT_PATTERN.test(withoutCast.replace(DOM_CONTENT_SOURCE_VALUE_PATTERN, ""));
|
|
6943
6945
|
});
|
|
6944
6946
|
};
|
|
6947
|
+
const findMatchingBraceIndex = (fileContent, openingBraceIndex) => {
|
|
6948
|
+
let depth = 0;
|
|
6949
|
+
let quote = null;
|
|
6950
|
+
for (let index = openingBraceIndex; index < fileContent.length; index += 1) {
|
|
6951
|
+
const character = fileContent[index];
|
|
6952
|
+
if (quote !== null) {
|
|
6953
|
+
if (character === quote && fileContent[index - 1] !== "\\") quote = null;
|
|
6954
|
+
continue;
|
|
6955
|
+
}
|
|
6956
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
6957
|
+
quote = character;
|
|
6958
|
+
continue;
|
|
6959
|
+
}
|
|
6960
|
+
if (character === "{") depth += 1;
|
|
6961
|
+
if (character === "}") {
|
|
6962
|
+
depth -= 1;
|
|
6963
|
+
if (depth === 0) return index;
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
return fileContent.length;
|
|
6967
|
+
};
|
|
6968
|
+
const findContainingBlockEndIndex = (fileContent, targetIndex) => {
|
|
6969
|
+
const openingBraceIndexes = [];
|
|
6970
|
+
let quote = null;
|
|
6971
|
+
let isLineComment = false;
|
|
6972
|
+
let isBlockComment = false;
|
|
6973
|
+
for (let index = 0; index < targetIndex; index += 1) {
|
|
6974
|
+
const character = fileContent[index];
|
|
6975
|
+
const nextCharacter = fileContent[index + 1];
|
|
6976
|
+
if (isLineComment) {
|
|
6977
|
+
if (character === "\n") isLineComment = false;
|
|
6978
|
+
continue;
|
|
6979
|
+
}
|
|
6980
|
+
if (isBlockComment) {
|
|
6981
|
+
if (character === "*" && nextCharacter === "/") {
|
|
6982
|
+
isBlockComment = false;
|
|
6983
|
+
index += 1;
|
|
6984
|
+
}
|
|
6985
|
+
continue;
|
|
6986
|
+
}
|
|
6987
|
+
if (quote !== null) {
|
|
6988
|
+
if (character === quote && fileContent[index - 1] !== "\\") quote = null;
|
|
6989
|
+
continue;
|
|
6990
|
+
}
|
|
6991
|
+
if (character === "/" && nextCharacter === "/") {
|
|
6992
|
+
isLineComment = true;
|
|
6993
|
+
index += 1;
|
|
6994
|
+
continue;
|
|
6995
|
+
}
|
|
6996
|
+
if (character === "/" && nextCharacter === "*") {
|
|
6997
|
+
isBlockComment = true;
|
|
6998
|
+
index += 1;
|
|
6999
|
+
continue;
|
|
7000
|
+
}
|
|
7001
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
7002
|
+
quote = character;
|
|
7003
|
+
continue;
|
|
7004
|
+
}
|
|
7005
|
+
if (character === "{") openingBraceIndexes.push(index);
|
|
7006
|
+
if (character === "}") openingBraceIndexes.pop();
|
|
7007
|
+
}
|
|
7008
|
+
const openingBraceIndex = openingBraceIndexes.at(-1);
|
|
7009
|
+
return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
|
|
7010
|
+
};
|
|
7011
|
+
const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
|
|
7012
|
+
const initializerPattern = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
|
|
7013
|
+
let nearestDeclaration = null;
|
|
7014
|
+
let nearestDeclarationIndex = -1;
|
|
7015
|
+
for (const match of fileContent.matchAll(initializerPattern)) {
|
|
7016
|
+
const declarationIndex = match.index;
|
|
7017
|
+
if (declarationIndex === void 0 || declarationIndex >= sinkIndex || declarationIndex <= nearestDeclarationIndex || findContainingBlockEndIndex(fileContent, declarationIndex) < sinkIndex) continue;
|
|
7018
|
+
nearestDeclarationIndex = declarationIndex;
|
|
7019
|
+
const initializer = match[1];
|
|
7020
|
+
if (initializer === void 0) continue;
|
|
7021
|
+
nearestDeclaration = {
|
|
7022
|
+
initializer,
|
|
7023
|
+
initializerStartIndex: declarationIndex + match[0].length - initializer.length
|
|
7024
|
+
};
|
|
7025
|
+
}
|
|
7026
|
+
return nearestDeclaration;
|
|
7027
|
+
};
|
|
7028
|
+
const findContainingFunctionParameterSource = (identifier, sinkIndex, fileContent) => {
|
|
7029
|
+
const patterns = [/function\s+([\w$]+)\s*\(([^)]*)\)\s*\{/g, /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/g];
|
|
7030
|
+
let closestSource = null;
|
|
7031
|
+
let closestStartIndex = -1;
|
|
7032
|
+
for (const pattern of patterns) for (const match of fileContent.matchAll(pattern)) {
|
|
7033
|
+
const matchIndex = match.index;
|
|
7034
|
+
if (matchIndex === void 0 || matchIndex >= sinkIndex || matchIndex < closestStartIndex) continue;
|
|
7035
|
+
if (findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{")) < sinkIndex) continue;
|
|
7036
|
+
const parameterIndex = (match[2] ?? "").split(",").findIndex((parameter) => parameter.trim().match(/^(?:\.\.\.)?([\w$]+)/)?.[1] === identifier);
|
|
7037
|
+
if (parameterIndex < 0) continue;
|
|
7038
|
+
closestStartIndex = matchIndex;
|
|
7039
|
+
closestSource = {
|
|
7040
|
+
functionName: match[1] ?? "",
|
|
7041
|
+
parameterIndex
|
|
7042
|
+
};
|
|
7043
|
+
}
|
|
7044
|
+
return closestSource;
|
|
7045
|
+
};
|
|
7046
|
+
const splitTopLevelArguments = (argumentText) => {
|
|
7047
|
+
const argumentsList = [];
|
|
7048
|
+
let startIndex = 0;
|
|
7049
|
+
let depth = 0;
|
|
7050
|
+
let quote = null;
|
|
7051
|
+
for (let index = 0; index < argumentText.length; index += 1) {
|
|
7052
|
+
const character = argumentText[index];
|
|
7053
|
+
if (quote !== null) {
|
|
7054
|
+
if (character === quote && argumentText[index - 1] !== "\\") quote = null;
|
|
7055
|
+
continue;
|
|
7056
|
+
}
|
|
7057
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
7058
|
+
quote = character;
|
|
7059
|
+
continue;
|
|
7060
|
+
}
|
|
7061
|
+
if (character === "(" || character === "[" || character === "{") depth += 1;
|
|
7062
|
+
if (character === ")" || character === "]" || character === "}") depth -= 1;
|
|
7063
|
+
if (character === "," && depth === 0) {
|
|
7064
|
+
argumentsList.push(argumentText.slice(startIndex, index).trim());
|
|
7065
|
+
startIndex = index + 1;
|
|
7066
|
+
}
|
|
7067
|
+
}
|
|
7068
|
+
argumentsList.push(argumentText.slice(startIndex).trim());
|
|
7069
|
+
return argumentsList;
|
|
7070
|
+
};
|
|
7071
|
+
const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers) => {
|
|
7072
|
+
const trimmedExpression = expression.trim();
|
|
7073
|
+
if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
|
|
7074
|
+
if (MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
|
|
7075
|
+
if (SANITIZER_PATTERN.test(trimmedExpression)) return false;
|
|
7076
|
+
if (ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression)) return false;
|
|
7077
|
+
if (I18N_VALUE_PATTERN.test(trimmedExpression)) return false;
|
|
7078
|
+
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression)) return false;
|
|
7079
|
+
if (HTML_TAINT_PATTERN.test(trimmedExpression)) return true;
|
|
7080
|
+
const identifier = trimmedExpression.match(/^([\w$]+)\s*(?:[;,})\n]|$)/)?.[1];
|
|
7081
|
+
if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
|
|
7082
|
+
visitedIdentifiers.add(identifier);
|
|
7083
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7084
|
+
if (declaration !== null && isHtmlTainted(declaration.initializer, fileContent, sinkIndex, visitedIdentifiers)) return true;
|
|
7085
|
+
const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
|
|
7086
|
+
if (parameterSource === null || parameterSource.functionName.length === 0) return false;
|
|
7087
|
+
const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(([^)]*)\\)`, "g");
|
|
7088
|
+
for (const callMatch of fileContent.matchAll(callPattern)) {
|
|
7089
|
+
const argument = splitTopLevelArguments(callMatch[1] ?? "")[parameterSource.parameterIndex];
|
|
7090
|
+
if (argument !== void 0 && isHtmlTainted(argument, fileContent, sinkIndex, new Set(visitedIdentifiers))) return true;
|
|
7091
|
+
}
|
|
7092
|
+
return false;
|
|
7093
|
+
};
|
|
6945
7094
|
const dangerousHtmlSink = defineRule({
|
|
6946
7095
|
id: "dangerous-html-sink",
|
|
6947
7096
|
title: "HTML injection sink with dynamic content",
|
|
@@ -6968,6 +7117,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
6968
7117
|
const valueTail = fullValueTail.slice(0, VALUE_EXPRESSION_MAX_CHARS);
|
|
6969
7118
|
const terminatorIndex = valueTail.search(/[;}]/);
|
|
6970
7119
|
const valueExpression = terminatorIndex >= 0 ? valueTail.slice(0, terminatorIndex + 1) : valueTail;
|
|
7120
|
+
const sinkIndex = lines.slice(0, lineIndex).join("\n").length + (lineIndex > 0 ? 1 : 0) + line.search(DANGEROUS_HTML_PATTERN);
|
|
6971
7121
|
if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
|
|
6972
7122
|
if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
|
|
6973
7123
|
if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
|
|
@@ -6979,7 +7129,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
6979
7129
|
let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
|
|
6980
7130
|
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
7131
|
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
|
|
6982
|
-
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, file.content);
|
|
7132
|
+
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
|
|
6983
7133
|
if (declarationInitializer !== null) {
|
|
6984
7134
|
if (isPureStringLiteralConcat(declarationInitializer)) continue;
|
|
6985
7135
|
templateInterpolations = getTemplateInterpolations(declarationInitializer);
|
|
@@ -6990,7 +7140,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
6990
7140
|
if (SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
6991
7141
|
if (ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
6992
7142
|
if (I18N_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
6993
|
-
if (!
|
|
7143
|
+
if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set())) continue;
|
|
6994
7144
|
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
|
|
6995
7145
|
if (/highlighted/i.test(valueExpression)) continue;
|
|
6996
7146
|
if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
@@ -7737,9 +7887,9 @@ const isImportedFromReact = (symbol) => {
|
|
|
7737
7887
|
const importDeclaration = symbol.declarationNode.parent;
|
|
7738
7888
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
|
|
7739
7889
|
};
|
|
7740
|
-
const isNamedReactApiImport = (identifier, apiNames, scopes) => {
|
|
7890
|
+
const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
|
|
7741
7891
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
7742
|
-
const symbol = scopes.symbolFor(identifier);
|
|
7892
|
+
const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
|
|
7743
7893
|
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
7744
7894
|
const importedName = getImportedName(symbol.declarationNode);
|
|
7745
7895
|
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
@@ -7753,7 +7903,7 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
|
|
|
7753
7903
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7754
7904
|
const callee = stripParenExpression(node.callee);
|
|
7755
7905
|
if (isNodeOfType(callee, "Identifier")) {
|
|
7756
|
-
if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
|
|
7906
|
+
if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
|
|
7757
7907
|
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
7758
7908
|
}
|
|
7759
7909
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
|
|
@@ -10962,6 +11112,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10962
11112
|
return { CallExpression(node) {
|
|
10963
11113
|
const hookName = getHookName(node.callee, context.scopes);
|
|
10964
11114
|
if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
|
|
11115
|
+
if (HOOKS_REQUIRING_DEPS_MATCH.has(hookName) && !isReactApiCall(node, HOOKS_REQUIRING_DEPS_MATCH, context.scopes, {
|
|
11116
|
+
allowGlobalReactNamespace: true,
|
|
11117
|
+
allowUnboundBareCalls: true,
|
|
11118
|
+
resolveNamedAliases: true
|
|
11119
|
+
})) return;
|
|
10965
11120
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
10966
11121
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
10967
11122
|
const callbackArgument = node.arguments[callbackArgumentIndex];
|
|
@@ -23998,6 +24153,26 @@ const STATEFUL_HTML_DESCENDANT_TAGS = new Set([
|
|
|
23998
24153
|
"dialog",
|
|
23999
24154
|
"canvas"
|
|
24000
24155
|
]);
|
|
24156
|
+
const STATEFUL_HTML_ATTRIBUTE_NAMES = new Set([
|
|
24157
|
+
"autofocus",
|
|
24158
|
+
"contenteditable",
|
|
24159
|
+
"draggable",
|
|
24160
|
+
"tabindex"
|
|
24161
|
+
]);
|
|
24162
|
+
const isStaticallyFalseAttributeValue = (attribute) => {
|
|
24163
|
+
if (!isNodeOfType(attribute, "JSXAttribute") || !attribute.value) return false;
|
|
24164
|
+
const value = isNodeOfType(attribute.value, "JSXExpressionContainer") ? attribute.value.expression : attribute.value;
|
|
24165
|
+
return isNodeOfType(value, "Literal") && (value.value === false || value.value === "false");
|
|
24166
|
+
};
|
|
24167
|
+
const hasStatefulHtmlAttribute = (openingElement) => {
|
|
24168
|
+
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
24169
|
+
return openingElement.attributes.some((attribute) => {
|
|
24170
|
+
if (!isNodeOfType(attribute, "JSXAttribute") || !isNodeOfType(attribute.name, "JSXIdentifier")) return false;
|
|
24171
|
+
const attributeName = attribute.name.name.toLowerCase();
|
|
24172
|
+
if (!STATEFUL_HTML_ATTRIBUTE_NAMES.has(attributeName)) return false;
|
|
24173
|
+
return attributeName === "tabindex" || !isStaticallyFalseAttributeValue(attribute);
|
|
24174
|
+
});
|
|
24175
|
+
};
|
|
24001
24176
|
const STATEFUL_DESCENDANT_SCAN_BUDGET = 200;
|
|
24002
24177
|
const rootIdentifierNameOf = (expression) => {
|
|
24003
24178
|
let object = expression;
|
|
@@ -24015,7 +24190,8 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
|
|
|
24015
24190
|
budget -= 1;
|
|
24016
24191
|
const node = stack.pop();
|
|
24017
24192
|
if (isNodeOfType(node, "JSXElement")) {
|
|
24018
|
-
const
|
|
24193
|
+
const opening = node.openingElement;
|
|
24194
|
+
const name = opening.name;
|
|
24019
24195
|
if (name && isNodeOfType(name, "JSXIdentifier")) {
|
|
24020
24196
|
const tagName = name.name;
|
|
24021
24197
|
const firstChar = tagName.charCodeAt(0);
|
|
@@ -24023,6 +24199,7 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
|
|
|
24023
24199
|
if (STATEFUL_HTML_DESCENDANT_TAGS.has(tagName)) return true;
|
|
24024
24200
|
}
|
|
24025
24201
|
if (name && isNodeOfType(name, "JSXMemberExpression")) return true;
|
|
24202
|
+
if (hasStatefulHtmlAttribute(opening)) return true;
|
|
24026
24203
|
const children = node.children ?? [];
|
|
24027
24204
|
for (const child of children) stack.push(child);
|
|
24028
24205
|
continue;
|
|
@@ -24582,6 +24759,14 @@ const templateHasOuterMemberIdentity = (template, bindingFunction) => {
|
|
|
24582
24759
|
}
|
|
24583
24760
|
return false;
|
|
24584
24761
|
};
|
|
24762
|
+
const findBareItemNamesReferencedByTemplate = (template, itemNames) => {
|
|
24763
|
+
const referencedItemNames = /* @__PURE__ */ new Set();
|
|
24764
|
+
for (const expression of template.expressions ?? []) {
|
|
24765
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
24766
|
+
if (isNodeOfType(unwrappedExpression, "Identifier") && itemNames.has(unwrappedExpression.name)) referencedItemNames.add(unwrappedExpression.name);
|
|
24767
|
+
}
|
|
24768
|
+
return referencedItemNames;
|
|
24769
|
+
};
|
|
24585
24770
|
const forLoopTestReadsDataLength = (test) => {
|
|
24586
24771
|
let didFindLengthRead = false;
|
|
24587
24772
|
walkAst(test, (child) => {
|
|
@@ -24718,9 +24903,11 @@ const noArrayIndexAsKey = defineRule({
|
|
|
24718
24903
|
} else if (STATELESS_HTML_LEAF_TAGS.has(elementName.name)) {
|
|
24719
24904
|
const jsxElement = openingElement.parent;
|
|
24720
24905
|
if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
|
|
24906
|
+
const isInlineTextRun = INLINE_TEXT_LEAF_TAGS.has(elementName.name);
|
|
24907
|
+
const primitiveItemNames = keyTemplate ? findBareItemNamesReferencedByTemplate(keyTemplate, itemNames) : EMPTY_NAME_SET;
|
|
24721
24908
|
if (!containsStatefulDescendant(jsxElement, {
|
|
24722
|
-
memberRootNames:
|
|
24723
|
-
bareIdentifierNames: derivedNames
|
|
24909
|
+
memberRootNames: isInlineTextRun ? itemNames : EMPTY_NAME_SET,
|
|
24910
|
+
bareIdentifierNames: primitiveItemNames.size > 0 ? new Set([...derivedNames, ...primitiveItemNames]) : derivedNames
|
|
24724
24911
|
})) return;
|
|
24725
24912
|
}
|
|
24726
24913
|
}
|
|
@@ -24888,7 +25075,11 @@ const noAsyncEffectCallback = defineRule({
|
|
|
24888
25075
|
severity: "warn",
|
|
24889
25076
|
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
25077
|
create: (context) => ({ CallExpression(node) {
|
|
24891
|
-
if (!
|
|
25078
|
+
if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
25079
|
+
allowGlobalReactNamespace: true,
|
|
25080
|
+
allowUnboundBareCalls: true,
|
|
25081
|
+
resolveNamedAliases: true
|
|
25082
|
+
})) return;
|
|
24892
25083
|
const callback = getEffectCallback(node);
|
|
24893
25084
|
if (!callback) return;
|
|
24894
25085
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
@@ -30169,7 +30360,11 @@ const noFetchInEffect = defineRule({
|
|
|
30169
30360
|
severity: "warn",
|
|
30170
30361
|
recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
|
|
30171
30362
|
create: (context) => ({ CallExpression(node) {
|
|
30172
|
-
if (!
|
|
30363
|
+
if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
30364
|
+
allowGlobalReactNamespace: true,
|
|
30365
|
+
allowUnboundBareCalls: true,
|
|
30366
|
+
resolveNamedAliases: true
|
|
30367
|
+
})) return;
|
|
30173
30368
|
const callback = getEffectCallback(node);
|
|
30174
30369
|
if (!callback) return;
|
|
30175
30370
|
const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
|
|
@@ -35017,6 +35212,30 @@ const guardsRenderShape = (comparison) => {
|
|
|
35017
35212
|
}
|
|
35018
35213
|
return false;
|
|
35019
35214
|
};
|
|
35215
|
+
const isPropsChildrenLength = (node, scopes) => {
|
|
35216
|
+
const unwrappedNode = stripParenExpression(node);
|
|
35217
|
+
return isNodeOfType(unwrappedNode, "MemberExpression") && !unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier") && unwrappedNode.property.name === "length" && resolvesToPropsChildren(stripParenExpression(unwrappedNode.object), scopes);
|
|
35218
|
+
};
|
|
35219
|
+
const isLargeTextLengthComparison = (node, scopes) => {
|
|
35220
|
+
const unwrappedNode = stripParenExpression(node);
|
|
35221
|
+
if (!isNodeOfType(unwrappedNode, "BinaryExpression")) return false;
|
|
35222
|
+
const leftIsLength = isPropsChildrenLength(unwrappedNode.left, scopes);
|
|
35223
|
+
const rightIsLength = isPropsChildrenLength(unwrappedNode.right, scopes);
|
|
35224
|
+
const thresholdNode = leftIsLength ? unwrappedNode.right : unwrappedNode.left;
|
|
35225
|
+
if (!leftIsLength && !rightIsLength || !isNodeOfType(thresholdNode, "Literal")) return false;
|
|
35226
|
+
if (typeof thresholdNode.value !== "number" || thresholdNode.value < 1e3) return false;
|
|
35227
|
+
return leftIsLength ? unwrappedNode.operator === ">" || unwrappedNode.operator === ">=" : unwrappedNode.operator === "<" || unwrappedNode.operator === "<=";
|
|
35228
|
+
};
|
|
35229
|
+
const isLargeStringOptimizationGuard = (comparison, scopes) => {
|
|
35230
|
+
let current = findTransparentExpressionRoot(comparison);
|
|
35231
|
+
while (current.parent) {
|
|
35232
|
+
const parent = current.parent;
|
|
35233
|
+
if (!isNodeOfType(parent, "LogicalExpression") || parent.operator !== "&&") return false;
|
|
35234
|
+
if (isLargeTextLengthComparison(parent.left === current ? parent.right : parent.left, scopes)) return true;
|
|
35235
|
+
current = findTransparentExpressionRoot(parent);
|
|
35236
|
+
}
|
|
35237
|
+
return false;
|
|
35238
|
+
};
|
|
35020
35239
|
const noPolymorphicChildren = defineRule({
|
|
35021
35240
|
id: "no-polymorphic-children",
|
|
35022
35241
|
title: "Children type checked at runtime",
|
|
@@ -35030,6 +35249,7 @@ const noPolymorphicChildren = defineRule({
|
|
|
35030
35249
|
const isStringLiteral = (operand) => isNodeOfType(operand, "Literal") && operand.value === "string";
|
|
35031
35250
|
if (!isStringLiteral(node.left) && !isStringLiteral(node.right)) return;
|
|
35032
35251
|
if (!guardsRenderShape(node)) return;
|
|
35252
|
+
if (isLargeStringOptimizationGuard(node, context.scopes)) return;
|
|
35033
35253
|
context.report({
|
|
35034
35254
|
node,
|
|
35035
35255
|
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."
|