oxlint-plugin-react-doctor 0.7.6-dev.e877ca7 → 0.7.6-dev.eb09902
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 +389 -174
- 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) {
|
|
@@ -5995,7 +6021,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
5995
6021
|
//#endregion
|
|
5996
6022
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
5997
6023
|
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
5998
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
6024
|
+
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
5999
6025
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6000
6026
|
let symbol = scopes.symbolFor(identifier);
|
|
6001
6027
|
while (symbol?.kind === "const") {
|
|
@@ -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;
|
|
@@ -15809,14 +15972,21 @@ const jsxFilenameExtension = defineRule({
|
|
|
15809
15972
|
});
|
|
15810
15973
|
//#endregion
|
|
15811
15974
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
15812
|
-
const isJsxFragmentElement = (node) => {
|
|
15975
|
+
const isJsxFragmentElement = (node, scopes) => {
|
|
15813
15976
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
15814
15977
|
const elementName = node.name;
|
|
15815
|
-
if (isNodeOfType(elementName, "JSXIdentifier"))
|
|
15978
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
15979
|
+
if (!scopes) return elementName.name === "Fragment";
|
|
15980
|
+
const symbol = resolveConstIdentifierAlias(elementName, scopes);
|
|
15981
|
+
if (!symbol) return elementName.name === "Fragment" && scopes.isGlobalReference(elementName);
|
|
15982
|
+
return isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "Fragment";
|
|
15983
|
+
}
|
|
15816
15984
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
15817
15985
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
15818
|
-
if (elementName.
|
|
15819
|
-
return elementName.
|
|
15986
|
+
if (elementName.property.name !== "Fragment") return false;
|
|
15987
|
+
if (!scopes) return elementName.object.name === "React";
|
|
15988
|
+
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
15989
|
+
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15820
15990
|
}
|
|
15821
15991
|
return false;
|
|
15822
15992
|
};
|
|
@@ -15842,7 +16012,7 @@ const jsxFragments = defineRule({
|
|
|
15842
16012
|
if (mode !== "syntax") return;
|
|
15843
16013
|
if (!node.closingElement) return;
|
|
15844
16014
|
const openingElement = node.openingElement;
|
|
15845
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
16015
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
15846
16016
|
if (openingElement.attributes.length > 0) return;
|
|
15847
16017
|
context.report({
|
|
15848
16018
|
node: openingElement,
|
|
@@ -18523,10 +18693,6 @@ const resolveSettings$29 = (settings) => {
|
|
|
18523
18693
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18524
18694
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18525
18695
|
};
|
|
18526
|
-
const getElementName = (node) => {
|
|
18527
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18528
|
-
return null;
|
|
18529
|
-
};
|
|
18530
18696
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18531
18697
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18532
18698
|
const explicit = options.components?.[elementName];
|
|
@@ -18546,7 +18712,8 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18546
18712
|
create: (context) => {
|
|
18547
18713
|
const options = resolveSettings$29(context.settings);
|
|
18548
18714
|
return { JSXOpeningElement(node) {
|
|
18549
|
-
|
|
18715
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18716
|
+
const elementName = resolveJsxElementType(node);
|
|
18550
18717
|
if (!elementName) return;
|
|
18551
18718
|
for (const attribute of node.attributes) {
|
|
18552
18719
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -18733,11 +18900,6 @@ const checkTarget = (attributeValue) => {
|
|
|
18733
18900
|
alternate: false
|
|
18734
18901
|
};
|
|
18735
18902
|
};
|
|
18736
|
-
const getOpeningElementName = (node) => {
|
|
18737
|
-
const name = node.name;
|
|
18738
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18739
|
-
return null;
|
|
18740
|
-
};
|
|
18741
18903
|
const jsxNoTargetBlank = defineRule({
|
|
18742
18904
|
id: "jsx-no-target-blank",
|
|
18743
18905
|
title: "Unsafe target=_blank link",
|
|
@@ -18757,7 +18919,8 @@ const jsxNoTargetBlank = defineRule({
|
|
|
18757
18919
|
return settings.formComponents.has(tagName);
|
|
18758
18920
|
};
|
|
18759
18921
|
return { JSXOpeningElement(node) {
|
|
18760
|
-
|
|
18922
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18923
|
+
const tagName = resolveJsxElementType(node);
|
|
18761
18924
|
if (!tagName) return;
|
|
18762
18925
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
18763
18926
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -18990,7 +19153,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
18990
19153
|
return {
|
|
18991
19154
|
JSXElement(node) {
|
|
18992
19155
|
const openingElement = node.openingElement;
|
|
18993
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
19156
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
18994
19157
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
18995
19158
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
18996
19159
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20232,9 +20395,6 @@ const hasDirective = (programNode, directive) => {
|
|
|
20232
20395
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20233
20396
|
};
|
|
20234
20397
|
//#endregion
|
|
20235
|
-
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20236
|
-
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20237
|
-
//#endregion
|
|
20238
20398
|
//#region src/plugin/utils/is-component-assignment.ts
|
|
20239
20399
|
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
20400
|
//#endregion
|
|
@@ -20567,7 +20727,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20567
20727
|
severity: "warn",
|
|
20568
20728
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20569
20729
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20570
|
-
if (
|
|
20730
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
20571
20731
|
const attributes = node.attributes ?? [];
|
|
20572
20732
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20573
20733
|
if (downloadAttribute) {
|
|
@@ -20763,7 +20923,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
20763
20923
|
severity: "warn",
|
|
20764
20924
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
20765
20925
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20766
|
-
if (
|
|
20926
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20767
20927
|
const attributes = node.attributes ?? [];
|
|
20768
20928
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
20769
20929
|
if (!relAttribute?.value) return;
|
|
@@ -20871,7 +21031,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
20871
21031
|
severity: "warn",
|
|
20872
21032
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
20873
21033
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20874
|
-
if (
|
|
21034
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20875
21035
|
const attributes = node.attributes ?? [];
|
|
20876
21036
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
20877
21037
|
if (!hrefAttribute?.value) return;
|
|
@@ -21046,7 +21206,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21046
21206
|
create: (context) => {
|
|
21047
21207
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21048
21208
|
return { JSXOpeningElement(node) {
|
|
21049
|
-
if (
|
|
21209
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
21050
21210
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21051
21211
|
const programRoot = findProgramRoot(node);
|
|
21052
21212
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21093,8 +21253,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21093
21253
|
severity: "warn",
|
|
21094
21254
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21095
21255
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21096
|
-
|
|
21097
|
-
if (
|
|
21256
|
+
const elementName = resolveJsxElementType(node);
|
|
21257
|
+
if (elementName !== "script" && elementName !== "Script") return;
|
|
21098
21258
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21099
21259
|
if (!srcAttribute?.value) return;
|
|
21100
21260
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -22902,10 +23062,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
22902
23062
|
if (!declaringNode) return false;
|
|
22903
23063
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
22904
23064
|
}));
|
|
23065
|
+
const isWholePropsParameterBinding = (bindingNode) => {
|
|
23066
|
+
if (isFunctionLike$1(bindingNode.parent)) return true;
|
|
23067
|
+
let declaringFunction = bindingNode.parent;
|
|
23068
|
+
while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
|
|
23069
|
+
return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
|
|
23070
|
+
};
|
|
22905
23071
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
22906
23072
|
if (def.type !== "Parameter") return false;
|
|
22907
|
-
|
|
22908
|
-
return isFunctionLike$1(bindingParent);
|
|
23073
|
+
return isWholePropsParameterBinding(def.name);
|
|
22909
23074
|
}));
|
|
22910
23075
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
22911
23076
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -25902,6 +26067,56 @@ const noBarrelImport = defineRule({
|
|
|
25902
26067
|
}
|
|
25903
26068
|
});
|
|
25904
26069
|
//#endregion
|
|
26070
|
+
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
26071
|
+
const collectReturnedExpressions = (functionNode) => {
|
|
26072
|
+
if (!isFunctionLike$1(functionNode)) return [];
|
|
26073
|
+
const body = functionNode.body;
|
|
26074
|
+
if (!body) return [];
|
|
26075
|
+
if (!isNodeOfType(body, "BlockStatement")) return [body];
|
|
26076
|
+
const returnedExpressions = [];
|
|
26077
|
+
walkAst(body, (node) => {
|
|
26078
|
+
if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
26079
|
+
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
|
|
26080
|
+
});
|
|
26081
|
+
return returnedExpressions;
|
|
26082
|
+
};
|
|
26083
|
+
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
|
|
26084
|
+
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
26085
|
+
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
26086
|
+
const functionMatches = (candidateFunction) => {
|
|
26087
|
+
if (visitedFunctions.has(candidateFunction)) return false;
|
|
26088
|
+
visitedFunctions.add(candidateFunction);
|
|
26089
|
+
return collectReturnedExpressions(candidateFunction).some(expressionMatches);
|
|
26090
|
+
};
|
|
26091
|
+
const expressionMatches = (expression) => {
|
|
26092
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
26093
|
+
if (visitedExpressions.has(unwrappedExpression)) return false;
|
|
26094
|
+
visitedExpressions.add(unwrappedExpression);
|
|
26095
|
+
if (matchesExpression(unwrappedExpression)) return true;
|
|
26096
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
26097
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
26098
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
|
|
26099
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
26100
|
+
if (isFunctionLike$1(initializer)) return false;
|
|
26101
|
+
return expressionMatches(initializer);
|
|
26102
|
+
}
|
|
26103
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
26104
|
+
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
26105
|
+
if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
26106
|
+
const symbol = scopes.symbolFor(unwrappedExpression.callee);
|
|
26107
|
+
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
|
|
26108
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
26109
|
+
const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
|
|
26110
|
+
if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
|
|
26111
|
+
return functionMatches(candidateFunction);
|
|
26112
|
+
}
|
|
26113
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
|
|
26114
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
|
|
26115
|
+
return false;
|
|
26116
|
+
};
|
|
26117
|
+
return functionMatches(functionNode);
|
|
26118
|
+
};
|
|
26119
|
+
//#endregion
|
|
25905
26120
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
25906
26121
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
25907
26122
|
"FunctionDeclaration",
|
|
@@ -25921,12 +26136,13 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
25921
26136
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
25922
26137
|
};
|
|
25923
26138
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
26139
|
+
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
25924
26140
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
25925
26141
|
let hasRenderOutput = false;
|
|
25926
26142
|
walkAst(rootNode, (node) => {
|
|
25927
26143
|
if (hasRenderOutput) return false;
|
|
25928
26144
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
25929
|
-
if (
|
|
26145
|
+
if (isRenderOutputExpression(node, scopes)) {
|
|
25930
26146
|
hasRenderOutput = true;
|
|
25931
26147
|
return false;
|
|
25932
26148
|
}
|
|
@@ -25937,7 +26153,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
25937
26153
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
25938
26154
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
25939
26155
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
25940
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26156
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
|
|
25941
26157
|
renderOutputCache.set(functionNode, {
|
|
25942
26158
|
scopes,
|
|
25943
26159
|
hasRenderOutput
|
|
@@ -28402,7 +28618,7 @@ const noDisabledZoom = defineRule({
|
|
|
28402
28618
|
category: "Accessibility",
|
|
28403
28619
|
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
28620
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28405
|
-
if (
|
|
28621
|
+
if (resolveJsxElementType(node) !== "meta") return;
|
|
28406
28622
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28407
28623
|
if (!nameAttr?.value) return;
|
|
28408
28624
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -28792,7 +29008,7 @@ const findTopLevelEffectCalls = (componentBody) => {
|
|
|
28792
29008
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
28793
29009
|
for (const statement of componentBody.body ?? []) {
|
|
28794
29010
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
28795
|
-
const expression = statement
|
|
29011
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
28796
29012
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
28797
29013
|
if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
|
|
28798
29014
|
effectCalls.push(expression);
|
|
@@ -31574,7 +31790,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31574
31790
|
severity: "warn",
|
|
31575
31791
|
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
31792
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31577
|
-
if (
|
|
31793
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
31578
31794
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31579
31795
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31580
31796
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -31815,7 +32031,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
31815
32031
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
31816
32032
|
return {
|
|
31817
32033
|
JSXOpeningElement(node) {
|
|
31818
|
-
if (
|
|
32034
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
31819
32035
|
let typeAttribute = null;
|
|
31820
32036
|
let typeAttributeIndex = null;
|
|
31821
32037
|
let indeterminateAttribute = null;
|
|
@@ -32887,11 +33103,13 @@ const noManyBooleanProps = defineRule({
|
|
|
32887
33103
|
};
|
|
32888
33104
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
32889
33105
|
if (!param) return;
|
|
33106
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33107
|
+
if (!propsBinding) return;
|
|
32890
33108
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
32891
|
-
if (isNodeOfType(
|
|
33109
|
+
if (isNodeOfType(propsBinding, "ObjectPattern")) {
|
|
32892
33110
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
32893
33111
|
const booleanLikePropNames = [];
|
|
32894
|
-
for (const property of
|
|
33112
|
+
for (const property of propsBinding.properties ?? []) {
|
|
32895
33113
|
if (!isNodeOfType(property, "Property")) continue;
|
|
32896
33114
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
32897
33115
|
if (!keyName) continue;
|
|
@@ -32902,7 +33120,7 @@ const noManyBooleanProps = defineRule({
|
|
|
32902
33120
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
32903
33121
|
return;
|
|
32904
33122
|
}
|
|
32905
|
-
if (isNodeOfType(
|
|
33123
|
+
if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
|
|
32906
33124
|
};
|
|
32907
33125
|
return {
|
|
32908
33126
|
FunctionDeclaration(node) {
|
|
@@ -33024,7 +33242,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33024
33242
|
if (mirrorBindings.length === 0) return;
|
|
33025
33243
|
for (const statement of componentBody.body ?? []) {
|
|
33026
33244
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
33027
|
-
const effectCall = statement
|
|
33245
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
33028
33246
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
33029
33247
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
33030
33248
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -33038,7 +33256,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33038
33256
|
const bodyStatements = getCallbackStatements(callback);
|
|
33039
33257
|
if (bodyStatements.length !== 1) continue;
|
|
33040
33258
|
const onlyStatement = bodyStatements[0];
|
|
33041
|
-
const expression =
|
|
33259
|
+
const expression = unwrapDiscardedExpression(onlyStatement);
|
|
33042
33260
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
33043
33261
|
if (!isNodeOfType(expression.callee, "Identifier")) continue;
|
|
33044
33262
|
if (!isSetterIdentifier(expression.callee.name)) continue;
|
|
@@ -35654,7 +35872,7 @@ const noPreventDefault = defineRule({
|
|
|
35654
35872
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
35655
35873
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
35656
35874
|
return { JSXOpeningElement(node) {
|
|
35657
|
-
const elementName =
|
|
35875
|
+
const elementName = resolveJsxElementType(node);
|
|
35658
35876
|
if (!elementName) return;
|
|
35659
35877
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
35660
35878
|
if (!targetEventProps) return;
|
|
@@ -37139,7 +37357,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
|
37139
37357
|
return expressionReadsStateValue(argument, stateName);
|
|
37140
37358
|
};
|
|
37141
37359
|
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
37142
|
-
const expression =
|
|
37360
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
37143
37361
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
37144
37362
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
37145
37363
|
if (!setterNames.has(expression.callee.name)) return null;
|
|
@@ -37488,7 +37706,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
37488
37706
|
const setterNames = new Set(setterNameToStateName.keys());
|
|
37489
37707
|
for (const statement of functionBody.body ?? []) {
|
|
37490
37708
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
37491
|
-
const effectCall = statement
|
|
37709
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
37492
37710
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
37493
37711
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
37494
37712
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -38118,9 +38336,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38118
38336
|
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
38337
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38120
38338
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38121
|
-
const
|
|
38339
|
+
const elementName = resolveJsxElementType(node);
|
|
38340
|
+
const firstCharacter = elementName.charCodeAt(0);
|
|
38122
38341
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38123
|
-
if (
|
|
38342
|
+
if (elementName.includes("-")) return;
|
|
38124
38343
|
for (const attribute of node.attributes) {
|
|
38125
38344
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38126
38345
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38508,8 +38727,7 @@ const noUncontrolledInput = defineRule({
|
|
|
38508
38727
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38509
38728
|
walkAst(componentBody, (child) => {
|
|
38510
38729
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38511
|
-
|
|
38512
|
-
const tagName = child.name.name;
|
|
38730
|
+
const tagName = resolveJsxElementType(child);
|
|
38513
38731
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38514
38732
|
const attributes = child.attributes ?? [];
|
|
38515
38733
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38570,7 +38788,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38570
38788
|
severity: "warn",
|
|
38571
38789
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38572
38790
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38573
|
-
if (
|
|
38791
|
+
if (resolveJsxElementType(node) !== "script") return;
|
|
38574
38792
|
const attributes = node.attributes ?? [];
|
|
38575
38793
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38576
38794
|
if (!srcAttribute) return;
|
|
@@ -39785,7 +40003,7 @@ const noUnknownProperty = defineRule({
|
|
|
39785
40003
|
}
|
|
39786
40004
|
if (fileIsNonReactJsx) return;
|
|
39787
40005
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
39788
|
-
const elementType = node
|
|
40006
|
+
const elementType = resolveJsxElementType(node);
|
|
39789
40007
|
const firstCharacter = elementType.charCodeAt(0);
|
|
39790
40008
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
39791
40009
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -39963,20 +40181,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
39963
40181
|
});
|
|
39964
40182
|
return found;
|
|
39965
40183
|
};
|
|
40184
|
+
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
39966
40185
|
const isReactClassComponent = (classNode) => {
|
|
39967
40186
|
if (isEs6Component(classNode)) return true;
|
|
39968
40187
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
39969
40188
|
};
|
|
39970
|
-
const findEnclosingComponent = (node) => {
|
|
40189
|
+
const findEnclosingComponent = (node, scopes) => {
|
|
39971
40190
|
let walker = node.parent;
|
|
39972
40191
|
while (walker) {
|
|
39973
40192
|
if (isFunctionLike$1(walker)) {
|
|
39974
40193
|
const componentName = inferFunctionLikeName(walker);
|
|
39975
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40194
|
+
if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
|
|
39976
40195
|
component: walker,
|
|
39977
40196
|
name: componentName
|
|
39978
40197
|
};
|
|
39979
|
-
if (!componentName &&
|
|
40198
|
+
if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
39980
40199
|
component: walker,
|
|
39981
40200
|
name: null
|
|
39982
40201
|
};
|
|
@@ -40215,7 +40434,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40215
40434
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40216
40435
|
if (settings.allowAsProps) return;
|
|
40217
40436
|
}
|
|
40218
|
-
const enclosing = findEnclosingComponent(candidateNode);
|
|
40437
|
+
const enclosing = findEnclosingComponent(candidateNode, context.scopes);
|
|
40219
40438
|
if (!enclosing) return;
|
|
40220
40439
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40221
40440
|
queuedReports.push({
|
|
@@ -40226,7 +40445,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40226
40445
|
});
|
|
40227
40446
|
};
|
|
40228
40447
|
const checkFunctionLike = (node) => {
|
|
40229
|
-
if (!
|
|
40448
|
+
if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
|
|
40230
40449
|
const inferredName = inferFunctionLikeName(node);
|
|
40231
40450
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40232
40451
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41486,7 +41705,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41486
41705
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41487
41706
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41488
41707
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41489
|
-
const tagName = node
|
|
41708
|
+
const tagName = resolveJsxElementType(node);
|
|
41490
41709
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41491
41710
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41492
41711
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41506,7 +41725,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41506
41725
|
]);
|
|
41507
41726
|
const isTextLikeInput = (openingElement) => {
|
|
41508
41727
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41509
|
-
const tagName = openingElement
|
|
41728
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
41510
41729
|
if (tagName === "textarea") return true;
|
|
41511
41730
|
if (tagName !== "input") return false;
|
|
41512
41731
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -41663,8 +41882,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
41663
41882
|
]);
|
|
41664
41883
|
const collectBooleanPropBindings = (param) => {
|
|
41665
41884
|
const bindings = /* @__PURE__ */ new Set();
|
|
41666
|
-
|
|
41667
|
-
|
|
41885
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
41886
|
+
if (!isNodeOfType(propsBinding, "ObjectPattern")) return bindings;
|
|
41887
|
+
for (const property of propsBinding.properties ?? []) {
|
|
41668
41888
|
if (!isNodeOfType(property, "Property")) continue;
|
|
41669
41889
|
if (property.computed) continue;
|
|
41670
41890
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -41925,7 +42145,7 @@ const preferHtmlDialog = defineRule({
|
|
|
41925
42145
|
},
|
|
41926
42146
|
JSXOpeningElement(node) {
|
|
41927
42147
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41928
|
-
const tagName = node
|
|
42148
|
+
const tagName = resolveJsxElementType(node);
|
|
41929
42149
|
if (tagName === "dialog") return;
|
|
41930
42150
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41931
42151
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -43962,7 +44182,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
43962
44182
|
severity: "warn",
|
|
43963
44183
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
43964
44184
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
43965
|
-
if (
|
|
44185
|
+
if (resolveJsxElementType(node) !== "svg") return;
|
|
43966
44186
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
43967
44187
|
node,
|
|
43968
44188
|
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 +45475,10 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45255
45475
|
//#endregion
|
|
45256
45476
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45257
45477
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45258
|
-
const callbackReturnsJsx = (callback) => {
|
|
45478
|
+
const callbackReturnsJsx = (callback, scopes) => {
|
|
45259
45479
|
if (!callback) return false;
|
|
45260
45480
|
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;
|
|
45481
|
+
return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
|
|
45266
45482
|
};
|
|
45267
45483
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45268
45484
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45340,7 +45556,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45340
45556
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45341
45557
|
for (const declarator of stmt.declarations ?? []) {
|
|
45342
45558
|
const init = declarator.init;
|
|
45343
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
45559
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
|
|
45344
45560
|
memoNode = declarator;
|
|
45345
45561
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45346
45562
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45406,8 +45622,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45406
45622
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45407
45623
|
const bindings = /* @__PURE__ */ new Map();
|
|
45408
45624
|
const params = functionNode.params ?? [];
|
|
45409
|
-
for (const param of params)
|
|
45410
|
-
|
|
45625
|
+
for (const [parameterIndex, param] of params.entries()) {
|
|
45626
|
+
const parameterBinding = parameterIndex === 0 ? resolveFirstArgumentBinding(param) : param;
|
|
45627
|
+
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45628
|
+
}
|
|
45629
|
+
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45411
45630
|
const body = functionNode.body;
|
|
45412
45631
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45413
45632
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -45913,8 +46132,8 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
45913
46132
|
if (statements.length !== 1) return;
|
|
45914
46133
|
const onlyStatement = statements[0];
|
|
45915
46134
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
45916
|
-
singleAssignment = onlyStatement
|
|
45917
|
-
} else if (body) singleAssignment = body;
|
|
46135
|
+
singleAssignment = unwrapDiscardedExpression(onlyStatement);
|
|
46136
|
+
} else if (body) singleAssignment = unwrapDiscardedExpression(body);
|
|
45918
46137
|
if (!singleAssignment) return;
|
|
45919
46138
|
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
46139
|
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 +53740,6 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53521
53740
|
}
|
|
53522
53741
|
return false;
|
|
53523
53742
|
};
|
|
53524
|
-
const getJsxOpeningElementName = (node) => {
|
|
53525
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53526
|
-
return null;
|
|
53527
|
-
};
|
|
53528
53743
|
const stylePropObject = defineRule({
|
|
53529
53744
|
id: "style-prop-object",
|
|
53530
53745
|
title: "Style prop is not an object",
|
|
@@ -53536,7 +53751,8 @@ const stylePropObject = defineRule({
|
|
|
53536
53751
|
const allowSet = new Set(allow);
|
|
53537
53752
|
return {
|
|
53538
53753
|
JSXOpeningElement(node) {
|
|
53539
|
-
|
|
53754
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
53755
|
+
const elementName = resolveJsxElementType(node);
|
|
53540
53756
|
if (elementName && allowSet.has(elementName)) return;
|
|
53541
53757
|
if (elementName) {
|
|
53542
53758
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54241,7 +54457,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54241
54457
|
create: (context) => {
|
|
54242
54458
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54243
54459
|
return { JSXOpeningElement(node) {
|
|
54244
|
-
if (
|
|
54460
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
54245
54461
|
const attributes = node.attributes ?? [];
|
|
54246
54462
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54247
54463
|
if (!hrefAttribute?.value) return;
|
|
@@ -54862,8 +55078,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
54862
55078
|
create: (context) => ({
|
|
54863
55079
|
JSXElement(node) {
|
|
54864
55080
|
const openingElement = node.openingElement;
|
|
54865
|
-
|
|
54866
|
-
const tagName = openingElement.name.name;
|
|
55081
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
54867
55082
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
54868
55083
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
54869
55084
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|