oxlint-plugin-react-doctor 0.7.5-dev.037bd56 → 0.7.5-dev.18e8717
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 +146 -612
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -397,7 +397,6 @@ 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;
|
|
401
400
|
return isProductionFilePath(relativePath, SOURCE_FILE_PATTERN);
|
|
402
401
|
};
|
|
403
402
|
//#endregion
|
|
@@ -5179,10 +5178,8 @@ const STORAGE_GLOBALS = new Set([
|
|
|
5179
5178
|
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
5180
5179
|
const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
|
|
5181
5180
|
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;
|
|
5183
5181
|
const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
|
|
5184
5182
|
const isAuthCredentialKey = (key) => {
|
|
5185
|
-
if (PRODUCT_API_KEY_RECORDS_PATTERN.test(key)) return false;
|
|
5186
5183
|
if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
|
|
5187
5184
|
if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
|
|
5188
5185
|
if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
|
|
@@ -5934,21 +5931,6 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5934
5931
|
}
|
|
5935
5932
|
});
|
|
5936
5933
|
//#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
|
|
5952
5934
|
//#region src/plugin/utils/is-presentation-role.ts
|
|
5953
5935
|
const isPresentationRole = (openingElement) => {
|
|
5954
5936
|
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
@@ -5993,21 +5975,6 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
5993
5975
|
return false;
|
|
5994
5976
|
};
|
|
5995
5977
|
//#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
|
|
6011
5978
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
6012
5979
|
const MESSAGE$57 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
6013
5980
|
const KEY_HANDLERS = [
|
|
@@ -6018,63 +5985,6 @@ const KEY_HANDLERS = [
|
|
|
6018
5985
|
"onKeyDownCapture",
|
|
6019
5986
|
"onKeyPressCapture"
|
|
6020
5987
|
];
|
|
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
|
-
};
|
|
6078
5988
|
const FOCUSLESS_CONTAINER_TAGS = new Set([
|
|
6079
5989
|
"tr",
|
|
6080
5990
|
"td",
|
|
@@ -6122,10 +6032,7 @@ const isFocusForwardingFunctionBody = (body) => {
|
|
|
6122
6032
|
};
|
|
6123
6033
|
const resolveHandlerFunction = (attribute) => {
|
|
6124
6034
|
if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
|
|
6125
|
-
|
|
6126
|
-
};
|
|
6127
|
-
const resolveHandlerFunctionExpression = (handlerExpression) => {
|
|
6128
|
-
let expression = handlerExpression;
|
|
6035
|
+
let expression = attribute.value.expression;
|
|
6129
6036
|
if (isNodeOfType(expression, "Identifier")) {
|
|
6130
6037
|
const binding = findVariableInitializer(expression, expression.name);
|
|
6131
6038
|
if (!binding?.initializer) return null;
|
|
@@ -6224,22 +6131,18 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
6224
6131
|
if (!HTML_TAGS.has(tag)) return;
|
|
6225
6132
|
if (tag === "label") return;
|
|
6226
6133
|
if (!FOCUSLESS_CONTAINER_TAGS.has(tag) && isInteractiveElement(tag, node)) return;
|
|
6227
|
-
const spreadEventValues = getTransparentSpreadEventValues(node.attributes, context.scopes);
|
|
6228
|
-
if (!spreadEventValues) return;
|
|
6229
6134
|
const onClick = hasJsxPropIgnoreCase(node.attributes, "onClick") ?? hasJsxPropIgnoreCase(node.attributes, "onClickCapture");
|
|
6230
|
-
|
|
6231
|
-
if (
|
|
6232
|
-
if (
|
|
6233
|
-
if (
|
|
6234
|
-
const spreadHandlerFunction = spreadOnClickExpression ? resolveHandlerFunctionExpression(spreadOnClickExpression) : null;
|
|
6235
|
-
if (spreadHandlerFunction && (isFocusForwardingFunctionBody(spreadHandlerFunction.body ?? null) || containsBackdropDismissComparison(spreadHandlerFunction.body ?? null))) return;
|
|
6135
|
+
if (!onClick) return;
|
|
6136
|
+
if (isPureEventBlockerHandler(onClick)) return;
|
|
6137
|
+
if (isFocusForwardingHandler(onClick)) return;
|
|
6138
|
+
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
6236
6139
|
if (hasCompositeItemRole(node)) return;
|
|
6237
6140
|
if (isHoverSelectionListItem(tag, node)) return;
|
|
6238
|
-
if (
|
|
6141
|
+
if (isBackdropDismissHandler(onClick)) return;
|
|
6239
6142
|
if (containsKeyboardActivatableDescendant(node.parent)) return;
|
|
6240
6143
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
6241
6144
|
if (isPresentationRole(node)) return;
|
|
6242
|
-
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler)
|
|
6145
|
+
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
6243
6146
|
context.report({
|
|
6244
6147
|
node: node.name,
|
|
6245
6148
|
message: MESSAGE$57
|
|
@@ -6965,10 +6868,11 @@ const getTemplateInterpolations = (valueTail) => {
|
|
|
6965
6868
|
const interpolations = valueTail.slice(1, closingBacktickIndex).match(/\$\{[^}]*\}/g);
|
|
6966
6869
|
return interpolations === null ? "" : interpolations.join(" ");
|
|
6967
6870
|
};
|
|
6968
|
-
const getIdentifierDeclarationInitializer = (identifier,
|
|
6969
|
-
const
|
|
6970
|
-
if (
|
|
6971
|
-
|
|
6871
|
+
const getIdentifierDeclarationInitializer = (identifier, fileContent) => {
|
|
6872
|
+
const declarationMatch = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]{0,120})?=\\s*`).exec(fileContent);
|
|
6873
|
+
if (declarationMatch === null) return null;
|
|
6874
|
+
const initializerStartIndex = declarationMatch.index + declarationMatch[0].length;
|
|
6875
|
+
return fileContent.slice(initializerStartIndex, initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
|
|
6972
6876
|
};
|
|
6973
6877
|
const isPureStringLiteralConcat = (initializerText) => {
|
|
6974
6878
|
if (!/^["']/.test(initializerText)) return false;
|
|
@@ -7038,153 +6942,6 @@ const isAllOperandsDomContentConcat = (valueExpression) => {
|
|
|
7038
6942
|
return !HTML_TAINT_PATTERN.test(withoutCast.replace(DOM_CONTENT_SOURCE_VALUE_PATTERN, ""));
|
|
7039
6943
|
});
|
|
7040
6944
|
};
|
|
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
|
-
};
|
|
7188
6945
|
const dangerousHtmlSink = defineRule({
|
|
7189
6946
|
id: "dangerous-html-sink",
|
|
7190
6947
|
title: "HTML injection sink with dynamic content",
|
|
@@ -7211,7 +6968,6 @@ const dangerousHtmlSink = defineRule({
|
|
|
7211
6968
|
const valueTail = fullValueTail.slice(0, VALUE_EXPRESSION_MAX_CHARS);
|
|
7212
6969
|
const terminatorIndex = valueTail.search(/[;}]/);
|
|
7213
6970
|
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);
|
|
7215
6971
|
if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
|
|
7216
6972
|
if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
|
|
7217
6973
|
if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
|
|
@@ -7223,7 +6979,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
7223
6979
|
let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
|
|
7224
6980
|
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;
|
|
7225
6981
|
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
|
|
7226
|
-
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier,
|
|
6982
|
+
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, file.content);
|
|
7227
6983
|
if (declarationInitializer !== null) {
|
|
7228
6984
|
if (isPureStringLiteralConcat(declarationInitializer)) continue;
|
|
7229
6985
|
templateInterpolations = getTemplateInterpolations(declarationInitializer);
|
|
@@ -7234,7 +6990,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
7234
6990
|
if (SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
7235
6991
|
if (ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7236
6992
|
if (I18N_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7237
|
-
if (!
|
|
6993
|
+
if (!HTML_TAINT_PATTERN.test(judgedExpression)) continue;
|
|
7238
6994
|
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
|
|
7239
6995
|
if (/highlighted/i.test(valueExpression)) continue;
|
|
7240
6996
|
if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
@@ -7712,7 +7468,7 @@ const isCreateClassLikeCall = (node) => {
|
|
|
7712
7468
|
if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$2(callee) === "createClass";
|
|
7713
7469
|
return false;
|
|
7714
7470
|
};
|
|
7715
|
-
const isCreateContextCall
|
|
7471
|
+
const isCreateContextCall = (node) => {
|
|
7716
7472
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7717
7473
|
const callee = node.callee;
|
|
7718
7474
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
|
|
@@ -7889,7 +7645,7 @@ const displayName = defineRule({
|
|
|
7889
7645
|
}
|
|
7890
7646
|
},
|
|
7891
7647
|
CallExpression(node) {
|
|
7892
|
-
if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall
|
|
7648
|
+
if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall(node)) {
|
|
7893
7649
|
const assignedName = getAssignedName(node);
|
|
7894
7650
|
if (assignedName) {
|
|
7895
7651
|
const programRoot = findProgramRoot(node);
|
|
@@ -7936,6 +7692,21 @@ const displayName = defineRule({
|
|
|
7936
7692
|
}
|
|
7937
7693
|
});
|
|
7938
7694
|
//#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
|
|
7939
7710
|
//#region src/plugin/utils/read-static-boolean.ts
|
|
7940
7711
|
const readStaticBoolean = (node) => {
|
|
7941
7712
|
if (!node) return null;
|
|
@@ -7944,6 +7715,21 @@ const readStaticBoolean = (node) => {
|
|
|
7944
7715
|
return unwrappedNode.value;
|
|
7945
7716
|
};
|
|
7946
7717
|
//#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
|
|
7947
7733
|
//#region src/plugin/utils/is-react-api-call.ts
|
|
7948
7734
|
const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
|
|
7949
7735
|
const isImportedFromReact = (symbol) => {
|
|
@@ -7951,9 +7737,9 @@ const isImportedFromReact = (symbol) => {
|
|
|
7951
7737
|
const importDeclaration = symbol.declarationNode.parent;
|
|
7952
7738
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
|
|
7953
7739
|
};
|
|
7954
|
-
const isNamedReactApiImport = (identifier, apiNames, scopes
|
|
7740
|
+
const isNamedReactApiImport = (identifier, apiNames, scopes) => {
|
|
7955
7741
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
7956
|
-
const symbol =
|
|
7742
|
+
const symbol = scopes.symbolFor(identifier);
|
|
7957
7743
|
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
7958
7744
|
const importedName = getImportedName(symbol.declarationNode);
|
|
7959
7745
|
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
@@ -7967,7 +7753,7 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
|
|
|
7967
7753
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7968
7754
|
const callee = stripParenExpression(node.callee);
|
|
7969
7755
|
if (isNodeOfType(callee, "Identifier")) {
|
|
7970
|
-
if (isNamedReactApiImport(callee, apiNames, scopes
|
|
7756
|
+
if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
|
|
7971
7757
|
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
7972
7758
|
}
|
|
7973
7759
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
|
|
@@ -9070,30 +8856,6 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
|
|
|
9070
8856
|
}
|
|
9071
8857
|
return matchingBlocks.size > 0;
|
|
9072
8858
|
};
|
|
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
|
-
};
|
|
9097
8859
|
const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
|
|
9098
8860
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
9099
8861
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
@@ -9294,83 +9056,6 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
|
|
|
9294
9056
|
});
|
|
9295
9057
|
return didCleanupFunctionMatch;
|
|
9296
9058
|
};
|
|
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);
|
|
9374
9059
|
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
9375
9060
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
9376
9061
|
if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
@@ -9383,10 +9068,6 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
|
9383
9068
|
if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
|
|
9384
9069
|
const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
|
|
9385
9070
|
if (!returnedValue) return;
|
|
9386
|
-
if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) {
|
|
9387
|
-
matchingCleanupReturns.push(child);
|
|
9388
|
-
return;
|
|
9389
|
-
}
|
|
9390
9071
|
if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
|
|
9391
9072
|
matchingCleanupReturns.push(child);
|
|
9392
9073
|
return;
|
|
@@ -9402,17 +9083,13 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
|
9402
9083
|
if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
|
|
9403
9084
|
}
|
|
9404
9085
|
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
9405
|
-
if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) {
|
|
9406
|
-
matchingCleanupReturns.push(child);
|
|
9407
|
-
return;
|
|
9408
|
-
}
|
|
9409
9086
|
if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
|
|
9410
9087
|
if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
|
|
9411
9088
|
});
|
|
9412
9089
|
return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
|
|
9413
9090
|
};
|
|
9414
9091
|
const findFirstUsageWithoutCleanup = (callback, usages, context) => {
|
|
9415
|
-
for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)
|
|
9092
|
+
for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
|
|
9416
9093
|
return null;
|
|
9417
9094
|
};
|
|
9418
9095
|
const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
@@ -11285,11 +10962,6 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11285
10962
|
return { CallExpression(node) {
|
|
11286
10963
|
const hookName = getHookName(node.callee, context.scopes);
|
|
11287
10964
|
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;
|
|
11293
10965
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
11294
10966
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
11295
10967
|
const callbackArgument = node.arguments[callbackArgumentIndex];
|
|
@@ -23217,73 +22889,61 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
|
|
|
23217
22889
|
"Array",
|
|
23218
22890
|
"BigInt",
|
|
23219
22891
|
"Boolean",
|
|
23220
|
-
"encodeURIComponent",
|
|
23221
22892
|
"Number",
|
|
23222
22893
|
"Object",
|
|
23223
22894
|
"String",
|
|
23224
22895
|
"parseFloat",
|
|
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"])]
|
|
22896
|
+
"parseInt"
|
|
23273
22897
|
]);
|
|
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
|
+
])]]);
|
|
23274
22939
|
const PURE_MEMBER_TRANSFORM_NAMES = new Set([
|
|
23275
22940
|
"concat",
|
|
23276
22941
|
"filter",
|
|
23277
|
-
"flatMap",
|
|
23278
22942
|
"join",
|
|
23279
22943
|
"map",
|
|
23280
|
-
"reduce",
|
|
23281
|
-
"replace",
|
|
23282
|
-
"slice",
|
|
23283
22944
|
"split",
|
|
23284
22945
|
"toLowerCase",
|
|
23285
22946
|
"toString",
|
|
23286
|
-
"toSorted",
|
|
23287
22947
|
"toUpperCase",
|
|
23288
22948
|
"trim"
|
|
23289
22949
|
]);
|
|
@@ -23665,11 +23325,6 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
|
|
|
23665
23325
|
const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
|
|
23666
23326
|
return callbackSummary.isValid && !callbackSummary.canContinue;
|
|
23667
23327
|
}
|
|
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
|
-
}
|
|
23673
23328
|
if (isNodeOfType(node, "CallExpression")) {
|
|
23674
23329
|
const callee = stripParenExpression(node.callee);
|
|
23675
23330
|
const calleeRoot = getMemberRoot(callee);
|
|
@@ -23872,7 +23527,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23872
23527
|
}
|
|
23873
23528
|
const callee = stripParenExpression(node.callee);
|
|
23874
23529
|
const calleeRoot = getMemberRoot(callee);
|
|
23875
|
-
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(
|
|
23530
|
+
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(calleeRoot, "Identifier") && PURE_GLOBAL_NAMESPACE_NAMES.has(calleeRoot.name) && getIdentifierBindingIdentity(analysis, calleeRoot) === null;
|
|
23876
23531
|
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
|
|
23877
23532
|
if (isPureGlobalCall || isPureMemberTransform) {
|
|
23878
23533
|
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
@@ -23925,15 +23580,6 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23925
23580
|
}
|
|
23926
23581
|
return evidence;
|
|
23927
23582
|
}
|
|
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
|
-
}
|
|
23937
23583
|
if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
|
|
23938
23584
|
evidence.hasUnknownSource = true;
|
|
23939
23585
|
return evidence;
|
|
@@ -24352,26 +23998,6 @@ const STATEFUL_HTML_DESCENDANT_TAGS = new Set([
|
|
|
24352
23998
|
"dialog",
|
|
24353
23999
|
"canvas"
|
|
24354
24000
|
]);
|
|
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
|
-
};
|
|
24375
24001
|
const STATEFUL_DESCENDANT_SCAN_BUDGET = 200;
|
|
24376
24002
|
const rootIdentifierNameOf = (expression) => {
|
|
24377
24003
|
let object = expression;
|
|
@@ -24389,8 +24015,7 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
|
|
|
24389
24015
|
budget -= 1;
|
|
24390
24016
|
const node = stack.pop();
|
|
24391
24017
|
if (isNodeOfType(node, "JSXElement")) {
|
|
24392
|
-
const
|
|
24393
|
-
const name = opening.name;
|
|
24018
|
+
const name = node.openingElement.name;
|
|
24394
24019
|
if (name && isNodeOfType(name, "JSXIdentifier")) {
|
|
24395
24020
|
const tagName = name.name;
|
|
24396
24021
|
const firstChar = tagName.charCodeAt(0);
|
|
@@ -24398,7 +24023,6 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
|
|
|
24398
24023
|
if (STATEFUL_HTML_DESCENDANT_TAGS.has(tagName)) return true;
|
|
24399
24024
|
}
|
|
24400
24025
|
if (name && isNodeOfType(name, "JSXMemberExpression")) return true;
|
|
24401
|
-
if (hasStatefulHtmlAttribute(opening)) return true;
|
|
24402
24026
|
const children = node.children ?? [];
|
|
24403
24027
|
for (const child of children) stack.push(child);
|
|
24404
24028
|
continue;
|
|
@@ -24958,14 +24582,6 @@ const templateHasOuterMemberIdentity = (template, bindingFunction) => {
|
|
|
24958
24582
|
}
|
|
24959
24583
|
return false;
|
|
24960
24584
|
};
|
|
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
|
-
};
|
|
24969
24585
|
const forLoopTestReadsDataLength = (test) => {
|
|
24970
24586
|
let didFindLengthRead = false;
|
|
24971
24587
|
walkAst(test, (child) => {
|
|
@@ -25102,11 +24718,9 @@ const noArrayIndexAsKey = defineRule({
|
|
|
25102
24718
|
} else if (STATELESS_HTML_LEAF_TAGS.has(elementName.name)) {
|
|
25103
24719
|
const jsxElement = openingElement.parent;
|
|
25104
24720
|
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;
|
|
25107
24721
|
if (!containsStatefulDescendant(jsxElement, {
|
|
25108
|
-
memberRootNames:
|
|
25109
|
-
bareIdentifierNames:
|
|
24722
|
+
memberRootNames: INLINE_TEXT_LEAF_TAGS.has(elementName.name) ? itemNames : EMPTY_NAME_SET,
|
|
24723
|
+
bareIdentifierNames: derivedNames
|
|
25110
24724
|
})) return;
|
|
25111
24725
|
}
|
|
25112
24726
|
}
|
|
@@ -25274,11 +24888,7 @@ const noAsyncEffectCallback = defineRule({
|
|
|
25274
24888
|
severity: "warn",
|
|
25275
24889
|
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.",
|
|
25276
24890
|
create: (context) => ({ CallExpression(node) {
|
|
25277
|
-
if (!
|
|
25278
|
-
allowGlobalReactNamespace: true,
|
|
25279
|
-
allowUnboundBareCalls: true,
|
|
25280
|
-
resolveNamedAliases: true
|
|
25281
|
-
})) return;
|
|
24891
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
25282
24892
|
const callback = getEffectCallback(node);
|
|
25283
24893
|
if (!callback) return;
|
|
25284
24894
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
@@ -26476,19 +26086,6 @@ const noCloneElement = defineRule({
|
|
|
26476
26086
|
} })
|
|
26477
26087
|
});
|
|
26478
26088
|
//#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
|
|
26492
26089
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
26493
26090
|
const MESSAGE$31 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
26494
26091
|
const CONTEXT_MODULES = [
|
|
@@ -26496,35 +26093,23 @@ const CONTEXT_MODULES = [
|
|
|
26496
26093
|
"use-context-selector",
|
|
26497
26094
|
"react-tracked"
|
|
26498
26095
|
];
|
|
26499
|
-
const
|
|
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);
|
|
26096
|
+
const isCreateContextCallee = (callee) => {
|
|
26517
26097
|
if (isNodeOfType(callee, "Identifier")) {
|
|
26518
|
-
|
|
26519
|
-
|
|
26520
|
-
return getSupportedContextImportSource(symbol) !== null && Boolean(symbol && getImportedName(symbol.declarationNode) === "createContext");
|
|
26098
|
+
const binding = getImportBindingForName(callee, callee.name);
|
|
26099
|
+
return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
|
|
26521
26100
|
}
|
|
26522
|
-
if (
|
|
26523
|
-
|
|
26524
|
-
|
|
26525
|
-
|
|
26526
|
-
|
|
26527
|
-
|
|
26101
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
26102
|
+
const namespaceIdentifier = callee.object;
|
|
26103
|
+
const propertyIdentifier = callee.property;
|
|
26104
|
+
if (!isNodeOfType(namespaceIdentifier, "Identifier")) return false;
|
|
26105
|
+
if (!isNodeOfType(propertyIdentifier, "Identifier")) return false;
|
|
26106
|
+
if (propertyIdentifier.name !== "createContext") return false;
|
|
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;
|
|
26528
26113
|
};
|
|
26529
26114
|
const noCreateContextInRender = defineRule({
|
|
26530
26115
|
id: "no-create-context-in-render",
|
|
@@ -26533,7 +26118,7 @@ const noCreateContextInRender = defineRule({
|
|
|
26533
26118
|
category: "Correctness",
|
|
26534
26119
|
recommendation: "Move `createContext(...)` outside the component, to the top level of the file, so it stays the same on every render.",
|
|
26535
26120
|
create: (context) => ({ CallExpression(node) {
|
|
26536
|
-
if (!
|
|
26121
|
+
if (!isCreateContextCallee(node.callee)) return;
|
|
26537
26122
|
const componentOrHookName = enclosingComponentOrHookName(node);
|
|
26538
26123
|
if (!componentOrHookName) return;
|
|
26539
26124
|
context.report({
|
|
@@ -28483,31 +28068,20 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
28483
28068
|
} })
|
|
28484
28069
|
});
|
|
28485
28070
|
//#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
|
|
28495
28071
|
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
28496
28072
|
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.";
|
|
28497
28073
|
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
28498
|
-
const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
|
|
28499
28074
|
const noDocumentWrite = defineRule({
|
|
28500
28075
|
id: "no-document-write",
|
|
28501
28076
|
title: "document.write/writeln",
|
|
28502
28077
|
severity: "warn",
|
|
28503
28078
|
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
28504
28079
|
create: (context) => ({ CallExpression(node) {
|
|
28505
|
-
const callee =
|
|
28506
|
-
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
28080
|
+
const callee = node.callee;
|
|
28081
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
28507
28082
|
const receiver = stripParenExpression(callee.object);
|
|
28508
|
-
if (!isNodeOfType(receiver, "Identifier") ||
|
|
28509
|
-
|
|
28510
|
-
if (methodName === null || !WRITE_METHODS.has(methodName)) return;
|
|
28083
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
28084
|
+
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
28511
28085
|
context.report({
|
|
28512
28086
|
node,
|
|
28513
28087
|
message: MESSAGE$24
|
|
@@ -29314,14 +28888,6 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
29314
28888
|
//#endregion
|
|
29315
28889
|
//#region src/plugin/rules/security/no-eval.ts
|
|
29316
28890
|
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
|
-
};
|
|
29325
28891
|
const noEval = defineRule({
|
|
29326
28892
|
id: "no-eval",
|
|
29327
28893
|
title: "eval() runs untrusted code strings",
|
|
@@ -29331,21 +28897,20 @@ const noEval = defineRule({
|
|
|
29331
28897
|
if (SANDBOX_SURFACE_PATH_PATTERN.test(normalizeFilename(context.filename ?? ""))) return {};
|
|
29332
28898
|
return {
|
|
29333
28899
|
CallExpression(node) {
|
|
29334
|
-
|
|
29335
|
-
if (executableGlobalName === "eval") {
|
|
28900
|
+
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "eval") {
|
|
29336
28901
|
context.report({
|
|
29337
28902
|
node,
|
|
29338
28903
|
message: "eval() is a code-injection vulnerability: it runs any string as code."
|
|
29339
28904
|
});
|
|
29340
28905
|
return;
|
|
29341
28906
|
}
|
|
29342
|
-
if ((
|
|
28907
|
+
if (isNodeOfType(node.callee, "Identifier") && (node.callee.name === "setTimeout" || node.callee.name === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
|
|
29343
28908
|
node,
|
|
29344
|
-
message: `Passing a string to ${
|
|
28909
|
+
message: `Passing a string to ${node.callee.name}() is a code-injection vulnerability, since it runs that string as code.`
|
|
29345
28910
|
});
|
|
29346
28911
|
},
|
|
29347
28912
|
NewExpression(node) {
|
|
29348
|
-
if (
|
|
28913
|
+
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "Function") {
|
|
29349
28914
|
const onlyArgument = node.arguments.length === 1 ? node.arguments[0] : void 0;
|
|
29350
28915
|
if (isNodeOfType(onlyArgument, "Literal") && typeof onlyArgument.value === "string" && onlyArgument.value.trim() === "return this") return;
|
|
29351
28916
|
context.report({
|
|
@@ -30604,11 +30169,7 @@ const noFetchInEffect = defineRule({
|
|
|
30604
30169
|
severity: "warn",
|
|
30605
30170
|
recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
|
|
30606
30171
|
create: (context) => ({ CallExpression(node) {
|
|
30607
|
-
if (!
|
|
30608
|
-
allowGlobalReactNamespace: true,
|
|
30609
|
-
allowUnboundBareCalls: true,
|
|
30610
|
-
resolveNamedAliases: true
|
|
30611
|
-
})) return;
|
|
30172
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
30612
30173
|
const callback = getEffectCallback(node);
|
|
30613
30174
|
if (!callback) return;
|
|
30614
30175
|
const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
|
|
@@ -34818,6 +34379,19 @@ const DATA_SINK_METHOD_NAMES = new Set([
|
|
|
34818
34379
|
"deserialize"
|
|
34819
34380
|
]);
|
|
34820
34381
|
//#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
|
|
34821
34395
|
//#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
|
|
34822
34396
|
const isUseStateIdentifier = (identifier) => {
|
|
34823
34397
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
@@ -35443,30 +35017,6 @@ const guardsRenderShape = (comparison) => {
|
|
|
35443
35017
|
}
|
|
35444
35018
|
return false;
|
|
35445
35019
|
};
|
|
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
|
-
};
|
|
35470
35020
|
const noPolymorphicChildren = defineRule({
|
|
35471
35021
|
id: "no-polymorphic-children",
|
|
35472
35022
|
title: "Children type checked at runtime",
|
|
@@ -35480,7 +35030,6 @@ const noPolymorphicChildren = defineRule({
|
|
|
35480
35030
|
const isStringLiteral = (operand) => isNodeOfType(operand, "Literal") && operand.value === "string";
|
|
35481
35031
|
if (!isStringLiteral(node.left) && !isStringLiteral(node.right)) return;
|
|
35482
35032
|
if (!guardsRenderShape(node)) return;
|
|
35483
|
-
if (isLargeStringOptimizationGuard(node, context.scopes)) return;
|
|
35484
35033
|
context.report({
|
|
35485
35034
|
node,
|
|
35486
35035
|
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."
|
|
@@ -35698,27 +35247,6 @@ const hasPreviousValueDep = (effectNode, depElements) => {
|
|
|
35698
35247
|
}
|
|
35699
35248
|
return false;
|
|
35700
35249
|
};
|
|
35701
|
-
const isStateLikeDependency = (analysis, element, isPropName) => {
|
|
35702
|
-
if (!isNodeOfType(element, "Identifier") || isPropName(element.name)) return false;
|
|
35703
|
-
if (!analysis) return true;
|
|
35704
|
-
const reference = getRef(analysis, element);
|
|
35705
|
-
if (!reference) return true;
|
|
35706
|
-
const upstreamReferences = getUpstreamRefs(analysis, reference);
|
|
35707
|
-
if (upstreamReferences.some((upstreamReference) => isState(analysis, upstreamReference))) return true;
|
|
35708
|
-
return !upstreamReferences.some((upstreamReference) => isProp(analysis, upstreamReference));
|
|
35709
|
-
};
|
|
35710
|
-
const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
35711
|
-
const callee = stripParenExpression(callExpression.callee);
|
|
35712
|
-
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "current") return null;
|
|
35713
|
-
const receiver = stripParenExpression(callee.object);
|
|
35714
|
-
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
35715
|
-
const binding = findVariableInitializer(callExpression, receiver.name);
|
|
35716
|
-
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
35717
|
-
if (getCalleeName$2(binding.initializer) !== "useRef") return null;
|
|
35718
|
-
const callbackArgument = binding.initializer.arguments?.[0];
|
|
35719
|
-
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
35720
|
-
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
35721
|
-
};
|
|
35722
35250
|
const noPropCallbackInEffect = defineRule({
|
|
35723
35251
|
id: "no-prop-callback-in-effect",
|
|
35724
35252
|
title: "Parent kept in sync with a callback effect",
|
|
@@ -35735,11 +35263,11 @@ const noPropCallbackInEffect = defineRule({
|
|
|
35735
35263
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
35736
35264
|
const depsNode = node.arguments[1];
|
|
35737
35265
|
if (!isNodeOfType(depsNode, "ArrayExpression") || !depsNode.elements?.length) return;
|
|
35738
|
-
const
|
|
35739
|
-
const stateLikeDeps = (depsNode.elements ?? []).filter((element) => element && isStateLikeDependency(analysis, element, propStackTracker.isPropName));
|
|
35266
|
+
const stateLikeDeps = (depsNode.elements ?? []).filter((element) => isNodeOfType(element, "Identifier") && !propStackTracker.isPropName(element.name));
|
|
35740
35267
|
if (stateLikeDeps.length === 0) return;
|
|
35741
35268
|
if (isRefLatchGuardedEffect(callback.body)) return;
|
|
35742
35269
|
if (hasPreviousValueDep(node, depsNode.elements ?? [])) return;
|
|
35270
|
+
const analysis = getProgramAnalysis(node);
|
|
35743
35271
|
if (analysis) {
|
|
35744
35272
|
const stateLikeDepRefs = [];
|
|
35745
35273
|
for (const element of stateLikeDeps) {
|
|
@@ -35751,9 +35279,9 @@ const noPropCallbackInEffect = defineRule({
|
|
|
35751
35279
|
const reportedNodes = /* @__PURE__ */ new Set();
|
|
35752
35280
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
35753
35281
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
35754
|
-
|
|
35755
|
-
const calleeName =
|
|
35756
|
-
if (!calleeName) return;
|
|
35282
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
35283
|
+
const calleeName = child.callee.name;
|
|
35284
|
+
if (!propStackTracker.isPropName(calleeName)) return;
|
|
35757
35285
|
if (!isResultDiscardedCall(child)) return;
|
|
35758
35286
|
if (reportedNodes.has(child)) return;
|
|
35759
35287
|
reportedNodes.add(child);
|
|
@@ -54906,6 +54434,12 @@ const webhookSignatureRisk = defineRule({
|
|
|
54906
54434
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
54907
54435
|
const ZOD_MODULE = "zod";
|
|
54908
54436
|
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
54437
|
+
const getStaticPropertyName = (member) => {
|
|
54438
|
+
const property = member.property;
|
|
54439
|
+
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
54440
|
+
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
54441
|
+
return null;
|
|
54442
|
+
};
|
|
54909
54443
|
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
54910
54444
|
const getImportInfoForIdentifier = (identifier) => {
|
|
54911
54445
|
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|