oxlint-plugin-react-doctor 0.7.6-dev.e877ca7 → 0.7.6-dev.f215ace
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 +375 -167
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1518,6 +1518,37 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1518
1518
|
}
|
|
1519
1519
|
};
|
|
1520
1520
|
//#endregion
|
|
1521
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
1522
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
1523
|
+
//#endregion
|
|
1524
|
+
//#region src/plugin/utils/resolve-jsx-element-type.ts
|
|
1525
|
+
const resolveConstantStringBinding = (name) => {
|
|
1526
|
+
const binding = findVariableInitializer(name, name.name);
|
|
1527
|
+
if (!binding?.initializer) return null;
|
|
1528
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
1529
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
1530
|
+
if (declarator.id !== binding.bindingIdentifier) return null;
|
|
1531
|
+
const declaration = declarator.parent;
|
|
1532
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
1533
|
+
if (declaration.kind !== "const") return null;
|
|
1534
|
+
const initializer = stripParenExpression(binding.initializer);
|
|
1535
|
+
return isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
|
|
1536
|
+
};
|
|
1537
|
+
const flattenJsxName$2 = (name) => {
|
|
1538
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1539
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1540
|
+
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1541
|
+
return "";
|
|
1542
|
+
};
|
|
1543
|
+
const resolveJsxElementType = (openingElement) => {
|
|
1544
|
+
const name = openingElement.name;
|
|
1545
|
+
if (isNodeOfType(name, "JSXIdentifier")) {
|
|
1546
|
+
if (!isUppercaseName(name.name)) return name.name;
|
|
1547
|
+
return resolveConstantStringBinding(name) ?? name.name;
|
|
1548
|
+
}
|
|
1549
|
+
return flattenJsxName$2(name);
|
|
1550
|
+
};
|
|
1551
|
+
//#endregion
|
|
1521
1552
|
//#region src/plugin/utils/get-element-type.ts
|
|
1522
1553
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1523
1554
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1530,14 +1561,8 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1530
1561
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1531
1562
|
return a11ySettings;
|
|
1532
1563
|
};
|
|
1533
|
-
const flattenJsxName$2 = (name) => {
|
|
1534
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1535
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1536
|
-
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1537
|
-
return "";
|
|
1538
|
-
};
|
|
1539
1564
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1540
|
-
const baseName =
|
|
1565
|
+
const baseName = resolveJsxElementType(openingElement);
|
|
1541
1566
|
if (a11ySettings.polymorphicPropName) {
|
|
1542
1567
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1543
1568
|
if (polymorphicAttribute) {
|
|
@@ -2372,8 +2397,9 @@ const anchorIsValid = defineRule({
|
|
|
2372
2397
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2373
2398
|
return { JSXOpeningElement(node) {
|
|
2374
2399
|
if (isTestlikeFile) return;
|
|
2375
|
-
|
|
2376
|
-
if (
|
|
2400
|
+
const tag = getElementType(node, context.settings);
|
|
2401
|
+
if (!fileHasJsxA11ySettings && tag !== "a") return;
|
|
2402
|
+
if (tag !== "a") return;
|
|
2377
2403
|
let hrefAttribute;
|
|
2378
2404
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2379
2405
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5728,7 +5754,7 @@ const buttonHasType = defineRule({
|
|
|
5728
5754
|
return {
|
|
5729
5755
|
JSXOpeningElement(node) {
|
|
5730
5756
|
if (isTestlikeFile) return;
|
|
5731
|
-
if (
|
|
5757
|
+
if (resolveJsxElementType(node) !== "button") return;
|
|
5732
5758
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5733
5759
|
if (!typeAttr) {
|
|
5734
5760
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5918,7 +5944,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5918
5944
|
};
|
|
5919
5945
|
return {
|
|
5920
5946
|
JSXOpeningElement(node) {
|
|
5921
|
-
if (
|
|
5947
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
5922
5948
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5923
5949
|
},
|
|
5924
5950
|
CallExpression(node) {
|
|
@@ -6929,6 +6955,30 @@ const corsCookieTrustRisk = defineRule({
|
|
|
6929
6955
|
})
|
|
6930
6956
|
});
|
|
6931
6957
|
//#endregion
|
|
6958
|
+
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
6959
|
+
const findMatchingBracket = (content, openIndex) => {
|
|
6960
|
+
const open = content[openIndex];
|
|
6961
|
+
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
6962
|
+
if (close === "") return -1;
|
|
6963
|
+
let depth = 0;
|
|
6964
|
+
let stringDelimiter = null;
|
|
6965
|
+
for (let index = openIndex; index < content.length; index += 1) {
|
|
6966
|
+
const character = content[index];
|
|
6967
|
+
if (stringDelimiter !== null) {
|
|
6968
|
+
if (character === "\\") index += 1;
|
|
6969
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
6970
|
+
continue;
|
|
6971
|
+
}
|
|
6972
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
6973
|
+
else if (character === open) depth += 1;
|
|
6974
|
+
else if (character === close) {
|
|
6975
|
+
depth -= 1;
|
|
6976
|
+
if (depth === 0) return index;
|
|
6977
|
+
}
|
|
6978
|
+
}
|
|
6979
|
+
return -1;
|
|
6980
|
+
};
|
|
6981
|
+
//#endregion
|
|
6932
6982
|
//#region src/plugin/rules/security-scan/dangerous-html-sink.ts
|
|
6933
6983
|
const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
|
|
6934
6984
|
const HTML_VALUE_START_PATTERN = /(?:__html\s*:|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(\s*[^,]*,|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\()\s*([\s\S]*)/;
|
|
@@ -6944,6 +6994,7 @@ const I18N_VALUE_PATTERN = /\b(?:t|i18n|translate|formatMessage|intl)\s*[.(]/;
|
|
|
6944
6994
|
const ESCAPING_SERIALIZER_CALL_PATTERN = /^(?:[\w$.]+\.)?(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*)\s*\(/;
|
|
6945
6995
|
const HIGHLIGHTER_LIBRARY_PATTERN = /\b(?:shiki|prism|hljs|highlightjs|getHighlighter|codeToHtml|codeToHast|refractor|lowlight|starry-night)\b|highlight\.js/i;
|
|
6946
6996
|
const SERIALIZER_ASSIGNMENT_PATTERN = /[:=]\s*[^\n;]*(?:\b(?:katex|shiki|hljs|prism|mermaid)\b|hast-util-to-html|renderHtmlFromRichText|(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast)\s*\()/i;
|
|
6997
|
+
const SERIALIZER_CALL_PROVENANCE_PATTERN = /\b(?:(?:katex|shiki|hljs|prism|mermaid|highlighter)[\w$]*\.(?:render\w*|highlight\w*|codeTo(?:Html|Hast))|(?:toHtml|render(?:Html|HTML)[A-Za-z]*|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*))\s*\(/i;
|
|
6947
6998
|
const BARE_IDENTIFIER_VALUE_PATTERN = /^[\w$]+\s*(?:[;,})\n]|$)/;
|
|
6948
6999
|
const IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN = /^[\w$]+\s*(?:\|\||\?\?)\s*(?:"[^"\n]*"|'[^'\n]*'|`[^`$]*`)\s*(?:[;,})\n]|$)/;
|
|
6949
7000
|
const MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN = /^[\w$]+(?:\.[\w$]+|\[[^\]]*\])+\s*(?:[;,})\n]|$)/;
|
|
@@ -7103,22 +7154,66 @@ const findContainingBlockEndIndex = (fileContent, targetIndex) => {
|
|
|
7103
7154
|
return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
|
|
7104
7155
|
};
|
|
7105
7156
|
const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
|
|
7106
|
-
const initializerPattern = new RegExp(`(
|
|
7157
|
+
const initializerPattern = new RegExp(`(const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
|
|
7107
7158
|
let nearestDeclaration = null;
|
|
7108
7159
|
let nearestDeclarationIndex = -1;
|
|
7109
7160
|
for (const match of fileContent.matchAll(initializerPattern)) {
|
|
7110
7161
|
const declarationIndex = match.index;
|
|
7111
7162
|
if (declarationIndex === void 0 || declarationIndex >= sinkIndex || declarationIndex <= nearestDeclarationIndex || findContainingBlockEndIndex(fileContent, declarationIndex) < sinkIndex) continue;
|
|
7112
7163
|
nearestDeclarationIndex = declarationIndex;
|
|
7113
|
-
const initializer = match[
|
|
7164
|
+
const initializer = match[2];
|
|
7114
7165
|
if (initializer === void 0) continue;
|
|
7115
7166
|
nearestDeclaration = {
|
|
7167
|
+
declarationIndex,
|
|
7116
7168
|
initializer,
|
|
7117
|
-
initializerStartIndex: declarationIndex + match[0].length - initializer.length
|
|
7169
|
+
initializerStartIndex: declarationIndex + match[0].length - initializer.length,
|
|
7170
|
+
isImmutable: match[1] === "const"
|
|
7118
7171
|
};
|
|
7119
7172
|
}
|
|
7120
7173
|
return nearestDeclaration;
|
|
7121
7174
|
};
|
|
7175
|
+
const isDeclarationStable = (identifier, declaration, usageIndex, fileContent) => {
|
|
7176
|
+
if (declaration.isImmutable) return true;
|
|
7177
|
+
const textAfterInitializer = fileContent.slice(declaration.initializerStartIndex + declaration.initializer.length, usageIndex);
|
|
7178
|
+
return !new RegExp(`(?:^|[^\\w$.])${escapeRegExp(identifier)}\\s*=(?!=)`).test(textAfterInitializer);
|
|
7179
|
+
};
|
|
7180
|
+
const isTrustedHighlighterValue = (valueExpression, fileContent, sinkIndex) => {
|
|
7181
|
+
if (!/highlight/i.test(valueExpression)) return false;
|
|
7182
|
+
const identifier = valueExpression.match(/^([\w$]+)/)?.[1];
|
|
7183
|
+
if (identifier === void 0) return false;
|
|
7184
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7185
|
+
if (declaration !== null) return isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(declaration.initializer);
|
|
7186
|
+
return /highlighted/i.test(valueExpression) || HIGHLIGHTER_LIBRARY_PATTERN.test(fileContent);
|
|
7187
|
+
};
|
|
7188
|
+
const isExplicitlyTrustedHtmlValue = (valueExpression, fileContent, sinkIndex, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7189
|
+
const trimmedExpression = valueExpression.trim();
|
|
7190
|
+
const concatenatedParts = splitTopLevelByPlus(trimmedExpression);
|
|
7191
|
+
if (concatenatedParts.length > 1) return concatenatedParts.every((part) => isExplicitlyTrustedHtmlValue(part, fileContent, sinkIndex, visitedIdentifiers));
|
|
7192
|
+
const interpolationParts = [...trimmedExpression.matchAll(/\$\{([^}]*)\}/g)].flatMap((match) => match[1] === void 0 ? [] : [match[1]]);
|
|
7193
|
+
if (interpolationParts.length > 1) return interpolationParts.every((part) => isExplicitlyTrustedHtmlValue(part, fileContent, sinkIndex, visitedIdentifiers));
|
|
7194
|
+
const identifier = trimmedExpression.match(/^([\w$]+)(?:(?:\??\.[\w$]+)|(?:\[\s*(?:"[^"\n]*"|'[^'\n]*'|\d+)\s*\]))*$/)?.[1];
|
|
7195
|
+
if (identifier && !visitedIdentifiers.has(identifier)) {
|
|
7196
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7197
|
+
if (declaration && isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) {
|
|
7198
|
+
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7199
|
+
nextVisitedIdentifiers.add(identifier);
|
|
7200
|
+
return isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex, nextVisitedIdentifiers);
|
|
7201
|
+
}
|
|
7202
|
+
}
|
|
7203
|
+
if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`) || MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`) || SANITIZER_PATTERN.test(trimmedExpression) || ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression) || I18N_VALUE_PATTERN.test(trimmedExpression) || ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression) || isTrustedHighlighterValue(trimmedExpression, fileContent, sinkIndex)) return true;
|
|
7204
|
+
return false;
|
|
7205
|
+
};
|
|
7206
|
+
const doesExpressionAliasIdentifier = (expression, targetIdentifier, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7207
|
+
const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
|
|
7208
|
+
if (!identifier) return false;
|
|
7209
|
+
if (identifier === targetIdentifier) return true;
|
|
7210
|
+
if (visitedIdentifiers.has(identifier)) return false;
|
|
7211
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, expressionIndex, fileContent);
|
|
7212
|
+
if (!declaration?.isImmutable) return false;
|
|
7213
|
+
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7214
|
+
nextVisitedIdentifiers.add(identifier);
|
|
7215
|
+
return doesExpressionAliasIdentifier(declaration.initializer, targetIdentifier, declaration.initializerStartIndex, fileContent, nextVisitedIdentifiers);
|
|
7216
|
+
};
|
|
7122
7217
|
const findContainingFunctionParameterSource = (identifier, sinkIndex, fileContent) => {
|
|
7123
7218
|
const patterns = [/function\s+([\w$]+)\s*\(([^)]*)\)\s*\{/g, /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/g];
|
|
7124
7219
|
let closestSource = null;
|
|
@@ -7126,12 +7221,16 @@ const findContainingFunctionParameterSource = (identifier, sinkIndex, fileConten
|
|
|
7126
7221
|
for (const pattern of patterns) for (const match of fileContent.matchAll(pattern)) {
|
|
7127
7222
|
const matchIndex = match.index;
|
|
7128
7223
|
if (matchIndex === void 0 || matchIndex >= sinkIndex || matchIndex < closestStartIndex) continue;
|
|
7129
|
-
|
|
7224
|
+
const bodyEndIndex = findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{"));
|
|
7225
|
+
if (bodyEndIndex < sinkIndex) continue;
|
|
7130
7226
|
const parameterIndex = (match[2] ?? "").split(",").findIndex((parameter) => parameter.trim().match(/^(?:\.\.\.)?([\w$]+)/)?.[1] === identifier);
|
|
7131
7227
|
if (parameterIndex < 0) continue;
|
|
7132
7228
|
closestStartIndex = matchIndex;
|
|
7229
|
+
const functionName = match[1] ?? "";
|
|
7133
7230
|
closestSource = {
|
|
7134
|
-
|
|
7231
|
+
bodyEndIndex,
|
|
7232
|
+
declarationNameIndex: matchIndex + match[0].indexOf(functionName),
|
|
7233
|
+
functionName,
|
|
7135
7234
|
parameterIndex
|
|
7136
7235
|
};
|
|
7137
7236
|
}
|
|
@@ -7162,28 +7261,56 @@ const splitTopLevelArguments = (argumentText) => {
|
|
|
7162
7261
|
argumentsList.push(argumentText.slice(startIndex).trim());
|
|
7163
7262
|
return argumentsList;
|
|
7164
7263
|
};
|
|
7165
|
-
const
|
|
7166
|
-
const
|
|
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];
|
|
7264
|
+
const isBackedByFunctionParameter = (expression, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7265
|
+
const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
|
|
7175
7266
|
if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
|
|
7267
|
+
if (findContainingFunctionParameterSource(identifier, expressionIndex, fileContent) !== null) return true;
|
|
7268
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, expressionIndex, fileContent);
|
|
7269
|
+
if (!declaration || !isDeclarationStable(identifier, declaration, expressionIndex, fileContent)) return false;
|
|
7270
|
+
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7271
|
+
nextVisitedIdentifiers.add(identifier);
|
|
7272
|
+
return isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent, nextVisitedIdentifiers);
|
|
7273
|
+
};
|
|
7274
|
+
const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers, visitedCallSites) => {
|
|
7275
|
+
const trimmedExpression = expression.trim();
|
|
7276
|
+
if (isExplicitlyTrustedHtmlValue(trimmedExpression, fileContent, sinkIndex)) return false;
|
|
7277
|
+
const identifier = trimmedExpression.match(/^([\w$]+)(?:\.|\s*(?:[;,})\n]|$))/)?.[1];
|
|
7278
|
+
if (identifier === void 0) return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7279
|
+
if (visitedIdentifiers.has(identifier)) return false;
|
|
7176
7280
|
visitedIdentifiers.add(identifier);
|
|
7177
7281
|
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7178
|
-
if (declaration !== null && isHtmlTainted(declaration.initializer, fileContent, sinkIndex, visitedIdentifiers)) return true;
|
|
7179
7282
|
const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
|
|
7180
|
-
if (parameterSource === null ||
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
if (
|
|
7185
|
-
|
|
7186
|
-
|
|
7283
|
+
if (declaration !== null && (parameterSource === null || declaration.declarationIndex > parameterSource.declarationNameIndex)) {
|
|
7284
|
+
if (isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex)) return false;
|
|
7285
|
+
if (!isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) return true;
|
|
7286
|
+
if (isHtmlTainted(declaration.initializer, fileContent, declaration.initializerStartIndex, visitedIdentifiers, visitedCallSites)) return true;
|
|
7287
|
+
if (isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent)) return false;
|
|
7288
|
+
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7289
|
+
}
|
|
7290
|
+
if (parameterSource !== null && parameterSource.functionName.length > 0) {
|
|
7291
|
+
const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(`, "g");
|
|
7292
|
+
let didInspectCallArgument = false;
|
|
7293
|
+
let didInspectOnlyExplicitlyTrustedArguments = true;
|
|
7294
|
+
for (const callMatch of fileContent.matchAll(callPattern)) {
|
|
7295
|
+
if (callMatch.index === parameterSource.declarationNameIndex || visitedCallSites.has(callMatch.index)) continue;
|
|
7296
|
+
const openingParenthesisIndex = callMatch.index + callMatch[0].lastIndexOf("(");
|
|
7297
|
+
const closingParenthesisIndex = findMatchingBracket(fileContent, openingParenthesisIndex);
|
|
7298
|
+
if (closingParenthesisIndex < 0) continue;
|
|
7299
|
+
const argument = splitTopLevelArguments(fileContent.slice(openingParenthesisIndex + 1, closingParenthesisIndex))[parameterSource.parameterIndex];
|
|
7300
|
+
if (argument === void 0) continue;
|
|
7301
|
+
if (callMatch.index >= parameterSource.declarationNameIndex && callMatch.index <= parameterSource.bodyEndIndex && doesExpressionAliasIdentifier(argument, identifier, callMatch.index, fileContent)) continue;
|
|
7302
|
+
didInspectCallArgument = true;
|
|
7303
|
+
didInspectOnlyExplicitlyTrustedArguments &&= isExplicitlyTrustedHtmlValue(argument, fileContent, callMatch.index);
|
|
7304
|
+
const callVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7305
|
+
callVisitedIdentifiers.delete(identifier);
|
|
7306
|
+
const nextVisitedCallSites = new Set(visitedCallSites);
|
|
7307
|
+
nextVisitedCallSites.add(callMatch.index);
|
|
7308
|
+
if (isHtmlTainted(argument, fileContent, callMatch.index, callVisitedIdentifiers, nextVisitedCallSites)) return true;
|
|
7309
|
+
}
|
|
7310
|
+
if (didInspectCallArgument) return !didInspectOnlyExplicitlyTrustedArguments && HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7311
|
+
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7312
|
+
}
|
|
7313
|
+
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7187
7314
|
};
|
|
7188
7315
|
const dangerousHtmlSink = defineRule({
|
|
7189
7316
|
id: "dangerous-html-sink",
|
|
@@ -7222,7 +7349,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
7222
7349
|
const longValueTail = HTML_VALUE_START_PATTERN.exec(lines.slice(lineIndex, lineIndex + 1 + STATIC_TEMPLATE_LOOKAHEAD_LINES).join("\n"))?.[1]?.trimStart();
|
|
7223
7350
|
let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
|
|
7224
7351
|
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
|
-
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
|
|
7352
|
+
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) && findContainingFunctionParameterSource(valueIdentifier, sinkIndex, file.content) === null) {
|
|
7226
7353
|
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
|
|
7227
7354
|
if (declarationInitializer !== null) {
|
|
7228
7355
|
if (isPureStringLiteralConcat(declarationInitializer)) continue;
|
|
@@ -7231,17 +7358,23 @@ const dangerousHtmlSink = defineRule({
|
|
|
7231
7358
|
}
|
|
7232
7359
|
if (templateInterpolations === "") continue;
|
|
7233
7360
|
const judgedExpression = templateInterpolations ?? valueExpression;
|
|
7234
|
-
|
|
7235
|
-
if (
|
|
7236
|
-
if (
|
|
7237
|
-
if (!
|
|
7361
|
+
const doesJudgedExpressionCombineValues = splitTopLevelByPlus(judgedExpression).length > 1 || (templateInterpolations?.match(/\$\{/g)?.length ?? 0) > 1;
|
|
7362
|
+
if (!doesJudgedExpressionCombineValues && SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
7363
|
+
if (!doesJudgedExpressionCombineValues && ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7364
|
+
if (!doesJudgedExpressionCombineValues && I18N_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7365
|
+
if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set())) continue;
|
|
7238
7366
|
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
|
|
7239
|
-
if (
|
|
7240
|
-
if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
7367
|
+
if (isTrustedHighlighterValue(valueExpression, file.content, sinkIndex)) continue;
|
|
7241
7368
|
if (valueIdentifier !== void 0) {
|
|
7242
7369
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
7243
|
-
|
|
7244
|
-
|
|
7370
|
+
const visibleDeclaration = findVisibleIdentifierDeclaration(valueIdentifier, sinkIndex, file.content);
|
|
7371
|
+
const visibleInitializer = visibleDeclaration?.initializer;
|
|
7372
|
+
if (!(visibleInitializer !== void 0 && (splitTopLevelByPlus(visibleInitializer).length > 1 || (visibleInitializer.match(/\$\{/g)?.length ?? 0) > 1))) {
|
|
7373
|
+
const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7374
|
+
if (visibleInitializer === void 0 ? fromSerializer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(visibleInitializer)) continue;
|
|
7375
|
+
const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7376
|
+
if (visibleInitializer === void 0 ? fromSanitizer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SANITIZED_ASSIGNMENT_PATTERN.test(`=${visibleInitializer}`)) continue;
|
|
7377
|
+
}
|
|
7245
7378
|
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
7246
7379
|
if (new RegExp(`highlight[\\w$]*\\s*\\.map\\(\\s*(?:async\\s+)?\\(?\\s*${escapedIdentifier}\\b`, "i").test(file.content) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
7247
7380
|
}
|
|
@@ -7326,7 +7459,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7326
7459
|
const getOpeningElementTagName = (openingElement) => {
|
|
7327
7460
|
if (!openingElement) return null;
|
|
7328
7461
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7329
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement
|
|
7462
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return resolveJsxElementType(openingElement);
|
|
7330
7463
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7331
7464
|
let cursor = openingElement.name;
|
|
7332
7465
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7578,7 +7711,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7578
7711
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7579
7712
|
return { JSXOpeningElement(node) {
|
|
7580
7713
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7581
|
-
const tagName = node
|
|
7714
|
+
const tagName = resolveJsxElementType(node);
|
|
7582
7715
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7583
7716
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7584
7717
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -11810,7 +11943,7 @@ const forbidDomProps = defineRule({
|
|
|
11810
11943
|
return { JSXOpeningElement(node) {
|
|
11811
11944
|
if (forbidMap.size === 0) return;
|
|
11812
11945
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
11813
|
-
const elementName = node
|
|
11946
|
+
const elementName = resolveJsxElementType(node);
|
|
11814
11947
|
if (isReactComponentName(elementName)) return;
|
|
11815
11948
|
for (const attribute of node.attributes) {
|
|
11816
11949
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11888,7 +12021,7 @@ const forbidElements = defineRule({
|
|
|
11888
12021
|
return {
|
|
11889
12022
|
JSXOpeningElement(node) {
|
|
11890
12023
|
if (forbidMap.size === 0) return;
|
|
11891
|
-
const fullName =
|
|
12024
|
+
const fullName = resolveJsxElementType(node);
|
|
11892
12025
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
11893
12026
|
context.report({
|
|
11894
12027
|
node: node.name,
|
|
@@ -12250,8 +12383,7 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
12250
12383
|
const isParagraphElement = (candidate) => {
|
|
12251
12384
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12252
12385
|
const opening = candidate.openingElement;
|
|
12253
|
-
|
|
12254
|
-
return opening.name.name === "p";
|
|
12386
|
+
return resolveJsxElementType(opening) === "p";
|
|
12255
12387
|
};
|
|
12256
12388
|
const findEnclosingParagraph = (openingElement) => {
|
|
12257
12389
|
const owningElement = openingElement.parent;
|
|
@@ -12272,8 +12404,7 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
12272
12404
|
severity: "warn",
|
|
12273
12405
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
12274
12406
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12275
|
-
|
|
12276
|
-
const childTagName = node.name.name;
|
|
12407
|
+
const childTagName = resolveJsxElementType(node);
|
|
12277
12408
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
12278
12409
|
if (!findEnclosingParagraph(node)) return;
|
|
12279
12410
|
context.report({
|
|
@@ -12304,7 +12435,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
12304
12435
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
12305
12436
|
const opening = jsxElement.openingElement;
|
|
12306
12437
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
12307
|
-
const tagName = opening
|
|
12438
|
+
const tagName = resolveJsxElementType(opening);
|
|
12308
12439
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
12309
12440
|
return tagName;
|
|
12310
12441
|
};
|
|
@@ -12334,7 +12465,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
12334
12465
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
12335
12466
|
const opening = ancestor.openingElement;
|
|
12336
12467
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
12337
|
-
const ancestorTag = opening
|
|
12468
|
+
const ancestorTag = resolveJsxElementType(opening);
|
|
12338
12469
|
if (ancestorTag.length === 0) {
|
|
12339
12470
|
previous = ancestor;
|
|
12340
12471
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12416,8 +12547,7 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12416
12547
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12417
12548
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12418
12549
|
const opening = candidate.openingElement;
|
|
12419
|
-
|
|
12420
|
-
return opening.name.name === tagName;
|
|
12550
|
+
return resolveJsxElementType(opening) === tagName;
|
|
12421
12551
|
};
|
|
12422
12552
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12423
12553
|
const owningElement = openingElement.parent;
|
|
@@ -12438,8 +12568,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12438
12568
|
severity: "warn",
|
|
12439
12569
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12440
12570
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12441
|
-
|
|
12442
|
-
const tagName = node.name.name;
|
|
12571
|
+
const tagName = resolveJsxElementType(node);
|
|
12443
12572
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12444
12573
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12445
12574
|
context.report({
|
|
@@ -12554,7 +12683,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12554
12683
|
matchByOccurrence: true,
|
|
12555
12684
|
create: skipNonProductionFiles((context) => ({
|
|
12556
12685
|
JSXOpeningElement(node) {
|
|
12557
|
-
if (
|
|
12686
|
+
if (resolveJsxElementType(node) !== "iframe") return;
|
|
12558
12687
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12559
12688
|
if (!sandboxAttr) {
|
|
12560
12689
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -12790,30 +12919,6 @@ const insecureCryptoRisk = defineRule({
|
|
|
12790
12919
|
}
|
|
12791
12920
|
});
|
|
12792
12921
|
//#endregion
|
|
12793
|
-
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
12794
|
-
const findMatchingBracket = (content, openIndex) => {
|
|
12795
|
-
const open = content[openIndex];
|
|
12796
|
-
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
12797
|
-
if (close === "") return -1;
|
|
12798
|
-
let depth = 0;
|
|
12799
|
-
let stringDelimiter = null;
|
|
12800
|
-
for (let index = openIndex; index < content.length; index += 1) {
|
|
12801
|
-
const character = content[index];
|
|
12802
|
-
if (stringDelimiter !== null) {
|
|
12803
|
-
if (character === "\\") index += 1;
|
|
12804
|
-
else if (character === stringDelimiter) stringDelimiter = null;
|
|
12805
|
-
continue;
|
|
12806
|
-
}
|
|
12807
|
-
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
12808
|
-
else if (character === open) depth += 1;
|
|
12809
|
-
else if (character === close) {
|
|
12810
|
-
depth -= 1;
|
|
12811
|
-
if (depth === 0) return index;
|
|
12812
|
-
}
|
|
12813
|
-
}
|
|
12814
|
-
return -1;
|
|
12815
|
-
};
|
|
12816
|
-
//#endregion
|
|
12817
12922
|
//#region src/plugin/rules/security-scan/insecure-session-cookie.ts
|
|
12818
12923
|
const AUTH_COOKIE_NAME_TOKEN = `(?<![A-Za-z0-9])(?:session|sess|sid|connect\\.sid|auth|jwt|access[_-]?token|refresh[_-]?token|id[_-]?token)(?![A-Za-z0-9])`;
|
|
12819
12924
|
const AUTH_COOKIE_NAME_LITERAL = `[\`"'][^\`"']*?${AUTH_COOKIE_NAME_TOKEN}[^\`"']*[\`"']`;
|
|
@@ -13570,6 +13675,28 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
13570
13675
|
} })
|
|
13571
13676
|
});
|
|
13572
13677
|
//#endregion
|
|
13678
|
+
//#region src/plugin/utils/unwrap-discarded-expression.ts
|
|
13679
|
+
const unwrapDiscardedExpression = (node) => {
|
|
13680
|
+
let expression = isNodeOfType(node, "ExpressionStatement") ? node.expression : node;
|
|
13681
|
+
for (;;) {
|
|
13682
|
+
expression = stripParenExpression(expression);
|
|
13683
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") {
|
|
13684
|
+
expression = expression.argument;
|
|
13685
|
+
continue;
|
|
13686
|
+
}
|
|
13687
|
+
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
13688
|
+
const expressions = expression.expressions ?? [];
|
|
13689
|
+
const finalExpression = expressions.at(-1);
|
|
13690
|
+
const prefixExpressions = expressions.slice(0, -1);
|
|
13691
|
+
if (finalExpression && prefixExpressions.length > 0 && prefixExpressions.every((prefixExpression) => isNodeOfType(stripParenExpression(prefixExpression), "Literal"))) {
|
|
13692
|
+
expression = finalExpression;
|
|
13693
|
+
continue;
|
|
13694
|
+
}
|
|
13695
|
+
}
|
|
13696
|
+
return expression;
|
|
13697
|
+
}
|
|
13698
|
+
};
|
|
13699
|
+
//#endregion
|
|
13573
13700
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
13574
13701
|
const ITERATOR_METHOD_NAMES$2 = new Set([
|
|
13575
13702
|
"forEach",
|
|
@@ -13754,9 +13881,19 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
13754
13881
|
});
|
|
13755
13882
|
return foundAttachment;
|
|
13756
13883
|
};
|
|
13884
|
+
const getStyleAssignment = (node) => {
|
|
13885
|
+
if (!isNodeOfType(node, "ExpressionStatement")) return null;
|
|
13886
|
+
const expression = unwrapDiscardedExpression(node);
|
|
13887
|
+
if (!isNodeOfType(expression, "AssignmentExpression")) return null;
|
|
13888
|
+
if (!isNodeOfType(expression.left, "MemberExpression")) return null;
|
|
13889
|
+
if (!isNodeOfType(expression.left.object, "MemberExpression")) return null;
|
|
13890
|
+
if (!isNodeOfType(expression.left.object.property, "Identifier")) return null;
|
|
13891
|
+
return expression.left.object.property.name === "style" ? expression : null;
|
|
13892
|
+
};
|
|
13757
13893
|
const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
13758
|
-
|
|
13759
|
-
|
|
13894
|
+
const assignment = getStyleAssignment(styleWriteStatement);
|
|
13895
|
+
if (!assignment || !isNodeOfType(assignment.left, "MemberExpression") || !isNodeOfType(assignment.left.object, "MemberExpression")) return false;
|
|
13896
|
+
const elementExpression = assignment.left.object.object;
|
|
13760
13897
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
13761
13898
|
if (!creationRoot) return false;
|
|
13762
13899
|
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
|
|
@@ -13768,15 +13905,17 @@ const jsBatchDomCss = defineRule({
|
|
|
13768
13905
|
severity: "warn",
|
|
13769
13906
|
recommendation: "Do all your reads first, then all your writes. Mixing them inside a loop makes the browser recalculate the layout again and again, which is slow",
|
|
13770
13907
|
create: (context) => {
|
|
13771
|
-
const
|
|
13772
|
-
|
|
13908
|
+
const writesLayoutAffectingProperty = (node) => {
|
|
13909
|
+
const assignment = getStyleAssignment(node);
|
|
13910
|
+
return assignment !== null && isNodeOfType(assignment.left, "MemberExpression") && isNodeOfType(assignment.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(assignment.left.property.name);
|
|
13911
|
+
};
|
|
13773
13912
|
return { BlockStatement(node) {
|
|
13774
13913
|
const perIterationBody = findEnclosingPerIterationBody(node);
|
|
13775
13914
|
if (!perIterationBody) return;
|
|
13776
13915
|
const statements = node.body ?? [];
|
|
13777
13916
|
let layoutReads = null;
|
|
13778
13917
|
for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
|
|
13779
|
-
if (
|
|
13918
|
+
if (getStyleAssignment(statements[statementIndex]) === null || getStyleAssignment(statements[statementIndex - 1]) === null) continue;
|
|
13780
13919
|
if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
|
|
13781
13920
|
layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
|
|
13782
13921
|
if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
|
|
@@ -14472,6 +14611,18 @@ const jsHoistRegexp = defineRule({
|
|
|
14472
14611
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14473
14612
|
});
|
|
14474
14613
|
//#endregion
|
|
14614
|
+
//#region src/plugin/utils/resolve-first-argument-binding.ts
|
|
14615
|
+
const resolveFirstArgumentBinding = (firstParameter) => {
|
|
14616
|
+
if (!firstParameter) return null;
|
|
14617
|
+
if (!isNodeOfType(firstParameter, "RestElement")) return firstParameter;
|
|
14618
|
+
if (!isNodeOfType(firstParameter.argument, "ArrayPattern")) return null;
|
|
14619
|
+
const elements = firstParameter.argument.elements ?? [];
|
|
14620
|
+
if (elements.length !== 1) return null;
|
|
14621
|
+
const firstBinding = elements[0];
|
|
14622
|
+
if (!firstBinding || isNodeOfType(firstBinding, "RestElement")) return null;
|
|
14623
|
+
return firstBinding;
|
|
14624
|
+
};
|
|
14625
|
+
//#endregion
|
|
14475
14626
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14476
14627
|
const referencesParameter = (expression, parameterName) => {
|
|
14477
14628
|
if (!expression) return false;
|
|
@@ -14482,7 +14633,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14482
14633
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14483
14634
|
const callback = node.arguments?.[0];
|
|
14484
14635
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14485
|
-
const firstParameter = callback.params?.[0];
|
|
14636
|
+
const firstParameter = resolveFirstArgumentBinding(callback.params?.[0]);
|
|
14486
14637
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14487
14638
|
let predicate = null;
|
|
14488
14639
|
const body = callback.body;
|
|
@@ -15174,9 +15325,21 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
15174
15325
|
};
|
|
15175
15326
|
const getResolvedInitializer = (receiver) => {
|
|
15176
15327
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
15177
|
-
const
|
|
15178
|
-
|
|
15179
|
-
return
|
|
15328
|
+
const binding = findVariableInitializer(receiver, receiver.name);
|
|
15329
|
+
const initializer = binding?.initializer ?? null;
|
|
15330
|
+
if (!binding || !initializer) return null;
|
|
15331
|
+
const isDefault = isNodeOfType(binding.bindingIdentifier.parent, "AssignmentPattern");
|
|
15332
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
15333
|
+
const aliased = findVariableInitializer(initializer, initializer.name);
|
|
15334
|
+
if (aliased?.initializer) return {
|
|
15335
|
+
initializer: aliased.initializer,
|
|
15336
|
+
isDefault: isDefault || isNodeOfType(aliased.bindingIdentifier.parent, "AssignmentPattern")
|
|
15337
|
+
};
|
|
15338
|
+
}
|
|
15339
|
+
return {
|
|
15340
|
+
initializer,
|
|
15341
|
+
isDefault
|
|
15342
|
+
};
|
|
15180
15343
|
};
|
|
15181
15344
|
const isSplitCall = (expression) => {
|
|
15182
15345
|
if (!expression) return false;
|
|
@@ -15342,8 +15505,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
15342
15505
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
15343
15506
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
15344
15507
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
15345
|
-
const
|
|
15346
|
-
if (
|
|
15508
|
+
const resolved = getResolvedInitializer(stripped);
|
|
15509
|
+
if (resolved && !resolved.isDefault && isSmallInlineLiteralArray(resolved.initializer)) return true;
|
|
15347
15510
|
}
|
|
15348
15511
|
return false;
|
|
15349
15512
|
};
|
|
@@ -15395,8 +15558,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15395
15558
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15396
15559
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15397
15560
|
if (resolvedInitializer) {
|
|
15398
|
-
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15399
|
-
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15561
|
+
if (isLikelyStringReceiver(resolvedInitializer.initializer)) return;
|
|
15562
|
+
if (!resolvedInitializer.isDefault && isSmallInlineLiteralArray(resolvedInitializer.initializer)) return;
|
|
15400
15563
|
}
|
|
15401
15564
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15402
15565
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -18523,10 +18686,6 @@ const resolveSettings$29 = (settings) => {
|
|
|
18523
18686
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18524
18687
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18525
18688
|
};
|
|
18526
|
-
const getElementName = (node) => {
|
|
18527
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18528
|
-
return null;
|
|
18529
|
-
};
|
|
18530
18689
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18531
18690
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18532
18691
|
const explicit = options.components?.[elementName];
|
|
@@ -18546,7 +18705,8 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18546
18705
|
create: (context) => {
|
|
18547
18706
|
const options = resolveSettings$29(context.settings);
|
|
18548
18707
|
return { JSXOpeningElement(node) {
|
|
18549
|
-
|
|
18708
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18709
|
+
const elementName = resolveJsxElementType(node);
|
|
18550
18710
|
if (!elementName) return;
|
|
18551
18711
|
for (const attribute of node.attributes) {
|
|
18552
18712
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -18733,11 +18893,6 @@ const checkTarget = (attributeValue) => {
|
|
|
18733
18893
|
alternate: false
|
|
18734
18894
|
};
|
|
18735
18895
|
};
|
|
18736
|
-
const getOpeningElementName = (node) => {
|
|
18737
|
-
const name = node.name;
|
|
18738
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18739
|
-
return null;
|
|
18740
|
-
};
|
|
18741
18896
|
const jsxNoTargetBlank = defineRule({
|
|
18742
18897
|
id: "jsx-no-target-blank",
|
|
18743
18898
|
title: "Unsafe target=_blank link",
|
|
@@ -18757,7 +18912,8 @@ const jsxNoTargetBlank = defineRule({
|
|
|
18757
18912
|
return settings.formComponents.has(tagName);
|
|
18758
18913
|
};
|
|
18759
18914
|
return { JSXOpeningElement(node) {
|
|
18760
|
-
|
|
18915
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18916
|
+
const tagName = resolveJsxElementType(node);
|
|
18761
18917
|
if (!tagName) return;
|
|
18762
18918
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
18763
18919
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -20232,9 +20388,6 @@ const hasDirective = (programNode, directive) => {
|
|
|
20232
20388
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20233
20389
|
};
|
|
20234
20390
|
//#endregion
|
|
20235
|
-
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20236
|
-
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20237
|
-
//#endregion
|
|
20238
20391
|
//#region src/plugin/utils/is-component-assignment.ts
|
|
20239
20392
|
const isComponentAssignment = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && isUppercaseName(node.id.name) && Boolean(node.init) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
|
|
20240
20393
|
//#endregion
|
|
@@ -20567,7 +20720,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20567
20720
|
severity: "warn",
|
|
20568
20721
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20569
20722
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20570
|
-
if (
|
|
20723
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
20571
20724
|
const attributes = node.attributes ?? [];
|
|
20572
20725
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20573
20726
|
if (downloadAttribute) {
|
|
@@ -20763,7 +20916,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
20763
20916
|
severity: "warn",
|
|
20764
20917
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
20765
20918
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20766
|
-
if (
|
|
20919
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20767
20920
|
const attributes = node.attributes ?? [];
|
|
20768
20921
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
20769
20922
|
if (!relAttribute?.value) return;
|
|
@@ -20871,7 +21024,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
20871
21024
|
severity: "warn",
|
|
20872
21025
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
20873
21026
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20874
|
-
if (
|
|
21027
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20875
21028
|
const attributes = node.attributes ?? [];
|
|
20876
21029
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
20877
21030
|
if (!hrefAttribute?.value) return;
|
|
@@ -21046,7 +21199,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21046
21199
|
create: (context) => {
|
|
21047
21200
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21048
21201
|
return { JSXOpeningElement(node) {
|
|
21049
|
-
if (
|
|
21202
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
21050
21203
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21051
21204
|
const programRoot = findProgramRoot(node);
|
|
21052
21205
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21093,8 +21246,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21093
21246
|
severity: "warn",
|
|
21094
21247
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21095
21248
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21096
|
-
|
|
21097
|
-
if (
|
|
21249
|
+
const elementName = resolveJsxElementType(node);
|
|
21250
|
+
if (elementName !== "script" && elementName !== "Script") return;
|
|
21098
21251
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21099
21252
|
if (!srcAttribute?.value) return;
|
|
21100
21253
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -22902,10 +23055,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
22902
23055
|
if (!declaringNode) return false;
|
|
22903
23056
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
22904
23057
|
}));
|
|
23058
|
+
const isWholePropsParameterBinding = (bindingNode) => {
|
|
23059
|
+
if (isFunctionLike$1(bindingNode.parent)) return true;
|
|
23060
|
+
let declaringFunction = bindingNode.parent;
|
|
23061
|
+
while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
|
|
23062
|
+
return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
|
|
23063
|
+
};
|
|
22905
23064
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
22906
23065
|
if (def.type !== "Parameter") return false;
|
|
22907
|
-
|
|
22908
|
-
return isFunctionLike$1(bindingParent);
|
|
23066
|
+
return isWholePropsParameterBinding(def.name);
|
|
22909
23067
|
}));
|
|
22910
23068
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
22911
23069
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -25902,6 +26060,56 @@ const noBarrelImport = defineRule({
|
|
|
25902
26060
|
}
|
|
25903
26061
|
});
|
|
25904
26062
|
//#endregion
|
|
26063
|
+
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
26064
|
+
const collectReturnedExpressions = (functionNode) => {
|
|
26065
|
+
if (!isFunctionLike$1(functionNode)) return [];
|
|
26066
|
+
const body = functionNode.body;
|
|
26067
|
+
if (!body) return [];
|
|
26068
|
+
if (!isNodeOfType(body, "BlockStatement")) return [body];
|
|
26069
|
+
const returnedExpressions = [];
|
|
26070
|
+
walkAst(body, (node) => {
|
|
26071
|
+
if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
26072
|
+
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
|
|
26073
|
+
});
|
|
26074
|
+
return returnedExpressions;
|
|
26075
|
+
};
|
|
26076
|
+
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
|
|
26077
|
+
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
26078
|
+
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
26079
|
+
const functionMatches = (candidateFunction) => {
|
|
26080
|
+
if (visitedFunctions.has(candidateFunction)) return false;
|
|
26081
|
+
visitedFunctions.add(candidateFunction);
|
|
26082
|
+
return collectReturnedExpressions(candidateFunction).some(expressionMatches);
|
|
26083
|
+
};
|
|
26084
|
+
const expressionMatches = (expression) => {
|
|
26085
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
26086
|
+
if (visitedExpressions.has(unwrappedExpression)) return false;
|
|
26087
|
+
visitedExpressions.add(unwrappedExpression);
|
|
26088
|
+
if (matchesExpression(unwrappedExpression)) return true;
|
|
26089
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
26090
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
26091
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
|
|
26092
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
26093
|
+
if (isFunctionLike$1(initializer)) return false;
|
|
26094
|
+
return expressionMatches(initializer);
|
|
26095
|
+
}
|
|
26096
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
26097
|
+
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
26098
|
+
if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
26099
|
+
const symbol = scopes.symbolFor(unwrappedExpression.callee);
|
|
26100
|
+
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
|
|
26101
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
26102
|
+
const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
|
|
26103
|
+
if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
|
|
26104
|
+
return functionMatches(candidateFunction);
|
|
26105
|
+
}
|
|
26106
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
|
|
26107
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
|
|
26108
|
+
return false;
|
|
26109
|
+
};
|
|
26110
|
+
return functionMatches(functionNode);
|
|
26111
|
+
};
|
|
26112
|
+
//#endregion
|
|
25905
26113
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
25906
26114
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
25907
26115
|
"FunctionDeclaration",
|
|
@@ -25921,12 +26129,13 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
25921
26129
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
25922
26130
|
};
|
|
25923
26131
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
26132
|
+
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
25924
26133
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
25925
26134
|
let hasRenderOutput = false;
|
|
25926
26135
|
walkAst(rootNode, (node) => {
|
|
25927
26136
|
if (hasRenderOutput) return false;
|
|
25928
26137
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
25929
|
-
if (
|
|
26138
|
+
if (isRenderOutputExpression(node, scopes)) {
|
|
25930
26139
|
hasRenderOutput = true;
|
|
25931
26140
|
return false;
|
|
25932
26141
|
}
|
|
@@ -25937,7 +26146,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
25937
26146
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
25938
26147
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
25939
26148
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
25940
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26149
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
|
|
25941
26150
|
renderOutputCache.set(functionNode, {
|
|
25942
26151
|
scopes,
|
|
25943
26152
|
hasRenderOutput
|
|
@@ -28402,7 +28611,7 @@ const noDisabledZoom = defineRule({
|
|
|
28402
28611
|
category: "Accessibility",
|
|
28403
28612
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28404
28613
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28405
|
-
if (
|
|
28614
|
+
if (resolveJsxElementType(node) !== "meta") return;
|
|
28406
28615
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28407
28616
|
if (!nameAttr?.value) return;
|
|
28408
28617
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -28792,7 +29001,7 @@ const findTopLevelEffectCalls = (componentBody) => {
|
|
|
28792
29001
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
28793
29002
|
for (const statement of componentBody.body ?? []) {
|
|
28794
29003
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
28795
|
-
const expression = statement
|
|
29004
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
28796
29005
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
28797
29006
|
if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
|
|
28798
29007
|
effectCalls.push(expression);
|
|
@@ -31574,7 +31783,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31574
31783
|
severity: "warn",
|
|
31575
31784
|
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
31576
31785
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31577
|
-
if (
|
|
31786
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
31578
31787
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31579
31788
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31580
31789
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -31815,7 +32024,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
31815
32024
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
31816
32025
|
return {
|
|
31817
32026
|
JSXOpeningElement(node) {
|
|
31818
|
-
if (
|
|
32027
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
31819
32028
|
let typeAttribute = null;
|
|
31820
32029
|
let typeAttributeIndex = null;
|
|
31821
32030
|
let indeterminateAttribute = null;
|
|
@@ -32887,11 +33096,13 @@ const noManyBooleanProps = defineRule({
|
|
|
32887
33096
|
};
|
|
32888
33097
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
32889
33098
|
if (!param) return;
|
|
33099
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33100
|
+
if (!propsBinding) return;
|
|
32890
33101
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
32891
|
-
if (isNodeOfType(
|
|
33102
|
+
if (isNodeOfType(propsBinding, "ObjectPattern")) {
|
|
32892
33103
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
32893
33104
|
const booleanLikePropNames = [];
|
|
32894
|
-
for (const property of
|
|
33105
|
+
for (const property of propsBinding.properties ?? []) {
|
|
32895
33106
|
if (!isNodeOfType(property, "Property")) continue;
|
|
32896
33107
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
32897
33108
|
if (!keyName) continue;
|
|
@@ -32902,7 +33113,7 @@ const noManyBooleanProps = defineRule({
|
|
|
32902
33113
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
32903
33114
|
return;
|
|
32904
33115
|
}
|
|
32905
|
-
if (isNodeOfType(
|
|
33116
|
+
if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
|
|
32906
33117
|
};
|
|
32907
33118
|
return {
|
|
32908
33119
|
FunctionDeclaration(node) {
|
|
@@ -33024,7 +33235,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33024
33235
|
if (mirrorBindings.length === 0) return;
|
|
33025
33236
|
for (const statement of componentBody.body ?? []) {
|
|
33026
33237
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
33027
|
-
const effectCall = statement
|
|
33238
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
33028
33239
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
33029
33240
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
33030
33241
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -33038,7 +33249,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33038
33249
|
const bodyStatements = getCallbackStatements(callback);
|
|
33039
33250
|
if (bodyStatements.length !== 1) continue;
|
|
33040
33251
|
const onlyStatement = bodyStatements[0];
|
|
33041
|
-
const expression =
|
|
33252
|
+
const expression = unwrapDiscardedExpression(onlyStatement);
|
|
33042
33253
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
33043
33254
|
if (!isNodeOfType(expression.callee, "Identifier")) continue;
|
|
33044
33255
|
if (!isSetterIdentifier(expression.callee.name)) continue;
|
|
@@ -35654,7 +35865,7 @@ const noPreventDefault = defineRule({
|
|
|
35654
35865
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
35655
35866
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
35656
35867
|
return { JSXOpeningElement(node) {
|
|
35657
|
-
const elementName =
|
|
35868
|
+
const elementName = resolveJsxElementType(node);
|
|
35658
35869
|
if (!elementName) return;
|
|
35659
35870
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
35660
35871
|
if (!targetEventProps) return;
|
|
@@ -37139,7 +37350,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
|
37139
37350
|
return expressionReadsStateValue(argument, stateName);
|
|
37140
37351
|
};
|
|
37141
37352
|
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
37142
|
-
const expression =
|
|
37353
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
37143
37354
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
37144
37355
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
37145
37356
|
if (!setterNames.has(expression.callee.name)) return null;
|
|
@@ -37488,7 +37699,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
37488
37699
|
const setterNames = new Set(setterNameToStateName.keys());
|
|
37489
37700
|
for (const statement of functionBody.body ?? []) {
|
|
37490
37701
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
37491
|
-
const effectCall = statement
|
|
37702
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
37492
37703
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
37493
37704
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
37494
37705
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -38118,9 +38329,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38118
38329
|
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
38119
38330
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38120
38331
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38121
|
-
const
|
|
38332
|
+
const elementName = resolveJsxElementType(node);
|
|
38333
|
+
const firstCharacter = elementName.charCodeAt(0);
|
|
38122
38334
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38123
|
-
if (
|
|
38335
|
+
if (elementName.includes("-")) return;
|
|
38124
38336
|
for (const attribute of node.attributes) {
|
|
38125
38337
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38126
38338
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38508,8 +38720,7 @@ const noUncontrolledInput = defineRule({
|
|
|
38508
38720
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38509
38721
|
walkAst(componentBody, (child) => {
|
|
38510
38722
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38511
|
-
|
|
38512
|
-
const tagName = child.name.name;
|
|
38723
|
+
const tagName = resolveJsxElementType(child);
|
|
38513
38724
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38514
38725
|
const attributes = child.attributes ?? [];
|
|
38515
38726
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38570,7 +38781,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38570
38781
|
severity: "warn",
|
|
38571
38782
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38572
38783
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38573
|
-
if (
|
|
38784
|
+
if (resolveJsxElementType(node) !== "script") return;
|
|
38574
38785
|
const attributes = node.attributes ?? [];
|
|
38575
38786
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38576
38787
|
if (!srcAttribute) return;
|
|
@@ -39785,7 +39996,7 @@ const noUnknownProperty = defineRule({
|
|
|
39785
39996
|
}
|
|
39786
39997
|
if (fileIsNonReactJsx) return;
|
|
39787
39998
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
39788
|
-
const elementType = node
|
|
39999
|
+
const elementType = resolveJsxElementType(node);
|
|
39789
40000
|
const firstCharacter = elementType.charCodeAt(0);
|
|
39790
40001
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
39791
40002
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -39963,20 +40174,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
39963
40174
|
});
|
|
39964
40175
|
return found;
|
|
39965
40176
|
};
|
|
40177
|
+
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
39966
40178
|
const isReactClassComponent = (classNode) => {
|
|
39967
40179
|
if (isEs6Component(classNode)) return true;
|
|
39968
40180
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
39969
40181
|
};
|
|
39970
|
-
const findEnclosingComponent = (node) => {
|
|
40182
|
+
const findEnclosingComponent = (node, scopes) => {
|
|
39971
40183
|
let walker = node.parent;
|
|
39972
40184
|
while (walker) {
|
|
39973
40185
|
if (isFunctionLike$1(walker)) {
|
|
39974
40186
|
const componentName = inferFunctionLikeName(walker);
|
|
39975
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40187
|
+
if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
|
|
39976
40188
|
component: walker,
|
|
39977
40189
|
name: componentName
|
|
39978
40190
|
};
|
|
39979
|
-
if (!componentName &&
|
|
40191
|
+
if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
39980
40192
|
component: walker,
|
|
39981
40193
|
name: null
|
|
39982
40194
|
};
|
|
@@ -40215,7 +40427,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40215
40427
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40216
40428
|
if (settings.allowAsProps) return;
|
|
40217
40429
|
}
|
|
40218
|
-
const enclosing = findEnclosingComponent(candidateNode);
|
|
40430
|
+
const enclosing = findEnclosingComponent(candidateNode, context.scopes);
|
|
40219
40431
|
if (!enclosing) return;
|
|
40220
40432
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40221
40433
|
queuedReports.push({
|
|
@@ -40226,7 +40438,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40226
40438
|
});
|
|
40227
40439
|
};
|
|
40228
40440
|
const checkFunctionLike = (node) => {
|
|
40229
|
-
if (!
|
|
40441
|
+
if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
|
|
40230
40442
|
const inferredName = inferFunctionLikeName(node);
|
|
40231
40443
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40232
40444
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41486,7 +41698,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41486
41698
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41487
41699
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41488
41700
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41489
|
-
const tagName = node
|
|
41701
|
+
const tagName = resolveJsxElementType(node);
|
|
41490
41702
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41491
41703
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41492
41704
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41506,7 +41718,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41506
41718
|
]);
|
|
41507
41719
|
const isTextLikeInput = (openingElement) => {
|
|
41508
41720
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41509
|
-
const tagName = openingElement
|
|
41721
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
41510
41722
|
if (tagName === "textarea") return true;
|
|
41511
41723
|
if (tagName !== "input") return false;
|
|
41512
41724
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -41663,8 +41875,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
41663
41875
|
]);
|
|
41664
41876
|
const collectBooleanPropBindings = (param) => {
|
|
41665
41877
|
const bindings = /* @__PURE__ */ new Set();
|
|
41666
|
-
|
|
41667
|
-
|
|
41878
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
41879
|
+
if (!isNodeOfType(propsBinding, "ObjectPattern")) return bindings;
|
|
41880
|
+
for (const property of propsBinding.properties ?? []) {
|
|
41668
41881
|
if (!isNodeOfType(property, "Property")) continue;
|
|
41669
41882
|
if (property.computed) continue;
|
|
41670
41883
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -41925,7 +42138,7 @@ const preferHtmlDialog = defineRule({
|
|
|
41925
42138
|
},
|
|
41926
42139
|
JSXOpeningElement(node) {
|
|
41927
42140
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41928
|
-
const tagName = node
|
|
42141
|
+
const tagName = resolveJsxElementType(node);
|
|
41929
42142
|
if (tagName === "dialog") return;
|
|
41930
42143
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41931
42144
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -43962,7 +44175,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
43962
44175
|
severity: "warn",
|
|
43963
44176
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
43964
44177
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
43965
|
-
if (
|
|
44178
|
+
if (resolveJsxElementType(node) !== "svg") return;
|
|
43966
44179
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
43967
44180
|
node,
|
|
43968
44181
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -45255,14 +45468,10 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45255
45468
|
//#endregion
|
|
45256
45469
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45257
45470
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45258
|
-
const callbackReturnsJsx = (callback) => {
|
|
45471
|
+
const callbackReturnsJsx = (callback, scopes) => {
|
|
45259
45472
|
if (!callback) return false;
|
|
45260
45473
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45261
|
-
|
|
45262
|
-
if (isJsxExpression(body)) return true;
|
|
45263
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
45264
|
-
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
|
|
45265
|
-
return false;
|
|
45474
|
+
return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
|
|
45266
45475
|
};
|
|
45267
45476
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45268
45477
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45340,7 +45549,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45340
45549
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45341
45550
|
for (const declarator of stmt.declarations ?? []) {
|
|
45342
45551
|
const init = declarator.init;
|
|
45343
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
45552
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
|
|
45344
45553
|
memoNode = declarator;
|
|
45345
45554
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45346
45555
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45406,8 +45615,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45406
45615
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45407
45616
|
const bindings = /* @__PURE__ */ new Map();
|
|
45408
45617
|
const params = functionNode.params ?? [];
|
|
45409
|
-
for (const param of params)
|
|
45410
|
-
|
|
45618
|
+
for (const [parameterIndex, param] of params.entries()) {
|
|
45619
|
+
const parameterBinding = parameterIndex === 0 ? resolveFirstArgumentBinding(param) : param;
|
|
45620
|
+
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45621
|
+
}
|
|
45622
|
+
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45411
45623
|
const body = functionNode.body;
|
|
45412
45624
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45413
45625
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -45913,8 +46125,8 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
45913
46125
|
if (statements.length !== 1) return;
|
|
45914
46126
|
const onlyStatement = statements[0];
|
|
45915
46127
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
45916
|
-
singleAssignment = onlyStatement
|
|
45917
|
-
} else if (body) singleAssignment = body;
|
|
46128
|
+
singleAssignment = unwrapDiscardedExpression(onlyStatement);
|
|
46129
|
+
} else if (body) singleAssignment = unwrapDiscardedExpression(body);
|
|
45918
46130
|
if (!singleAssignment) return;
|
|
45919
46131
|
const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
|
|
45920
46132
|
const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
|
|
@@ -53521,10 +53733,6 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53521
53733
|
}
|
|
53522
53734
|
return false;
|
|
53523
53735
|
};
|
|
53524
|
-
const getJsxOpeningElementName = (node) => {
|
|
53525
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53526
|
-
return null;
|
|
53527
|
-
};
|
|
53528
53736
|
const stylePropObject = defineRule({
|
|
53529
53737
|
id: "style-prop-object",
|
|
53530
53738
|
title: "Style prop is not an object",
|
|
@@ -53536,7 +53744,8 @@ const stylePropObject = defineRule({
|
|
|
53536
53744
|
const allowSet = new Set(allow);
|
|
53537
53745
|
return {
|
|
53538
53746
|
JSXOpeningElement(node) {
|
|
53539
|
-
|
|
53747
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
53748
|
+
const elementName = resolveJsxElementType(node);
|
|
53540
53749
|
if (elementName && allowSet.has(elementName)) return;
|
|
53541
53750
|
if (elementName) {
|
|
53542
53751
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54241,7 +54450,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54241
54450
|
create: (context) => {
|
|
54242
54451
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54243
54452
|
return { JSXOpeningElement(node) {
|
|
54244
|
-
if (
|
|
54453
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
54245
54454
|
const attributes = node.attributes ?? [];
|
|
54246
54455
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54247
54456
|
if (!hrefAttribute?.value) return;
|
|
@@ -54862,8 +55071,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
54862
55071
|
create: (context) => ({
|
|
54863
55072
|
JSXElement(node) {
|
|
54864
55073
|
const openingElement = node.openingElement;
|
|
54865
|
-
|
|
54866
|
-
const tagName = openingElement.name.name;
|
|
55074
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
54867
55075
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
54868
55076
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
54869
55077
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|