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