oxlint-plugin-react-doctor 0.7.6-dev.e877ca7 → 0.7.6-dev.f215ace

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +375 -167
  2. 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 = flattenJsxName$2(openingElement.name);
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
- if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
2376
- if (getElementType(node, context.settings) !== "a") return;
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "button") return;
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
5947
+ if (resolveJsxElementType(node) !== "input") return;
5922
5948
  reportFromPresence(collectFromJsxAttributes(node.attributes));
5923
5949
  },
5924
5950
  CallExpression(node) {
@@ -6929,6 +6955,30 @@ const corsCookieTrustRisk = defineRule({
6929
6955
  })
6930
6956
  });
6931
6957
  //#endregion
6958
+ //#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
6959
+ const findMatchingBracket = (content, openIndex) => {
6960
+ const open = content[openIndex];
6961
+ const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
6962
+ if (close === "") return -1;
6963
+ let depth = 0;
6964
+ let stringDelimiter = null;
6965
+ for (let index = openIndex; index < content.length; index += 1) {
6966
+ const character = content[index];
6967
+ if (stringDelimiter !== null) {
6968
+ if (character === "\\") index += 1;
6969
+ else if (character === stringDelimiter) stringDelimiter = null;
6970
+ continue;
6971
+ }
6972
+ if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
6973
+ else if (character === open) depth += 1;
6974
+ else if (character === close) {
6975
+ depth -= 1;
6976
+ if (depth === 0) return index;
6977
+ }
6978
+ }
6979
+ return -1;
6980
+ };
6981
+ //#endregion
6932
6982
  //#region src/plugin/rules/security-scan/dangerous-html-sink.ts
6933
6983
  const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
6934
6984
  const HTML_VALUE_START_PATTERN = /(?:__html\s*:|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(\s*[^,]*,|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\()\s*([\s\S]*)/;
@@ -6944,6 +6994,7 @@ const I18N_VALUE_PATTERN = /\b(?:t|i18n|translate|formatMessage|intl)\s*[.(]/;
6944
6994
  const ESCAPING_SERIALIZER_CALL_PATTERN = /^(?:[\w$.]+\.)?(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*)\s*\(/;
6945
6995
  const HIGHLIGHTER_LIBRARY_PATTERN = /\b(?:shiki|prism|hljs|highlightjs|getHighlighter|codeToHtml|codeToHast|refractor|lowlight|starry-night)\b|highlight\.js/i;
6946
6996
  const SERIALIZER_ASSIGNMENT_PATTERN = /[:=]\s*[^\n;]*(?:\b(?:katex|shiki|hljs|prism|mermaid)\b|hast-util-to-html|renderHtmlFromRichText|(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast)\s*\()/i;
6997
+ const SERIALIZER_CALL_PROVENANCE_PATTERN = /\b(?:(?:katex|shiki|hljs|prism|mermaid|highlighter)[\w$]*\.(?:render\w*|highlight\w*|codeTo(?:Html|Hast))|(?:toHtml|render(?:Html|HTML)[A-Za-z]*|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*))\s*\(/i;
6947
6998
  const BARE_IDENTIFIER_VALUE_PATTERN = /^[\w$]+\s*(?:[;,})\n]|$)/;
6948
6999
  const IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN = /^[\w$]+\s*(?:\|\||\?\?)\s*(?:"[^"\n]*"|'[^'\n]*'|`[^`$]*`)\s*(?:[;,})\n]|$)/;
6949
7000
  const MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN = /^[\w$]+(?:\.[\w$]+|\[[^\]]*\])+\s*(?:[;,})\n]|$)/;
@@ -7103,22 +7154,66 @@ const findContainingBlockEndIndex = (fileContent, targetIndex) => {
7103
7154
  return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
7104
7155
  };
7105
7156
  const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
7106
- const initializerPattern = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
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[1];
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
- if (findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{")) < sinkIndex) continue;
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
- functionName: match[1] ?? "",
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 isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers) => {
7166
- const trimmedExpression = expression.trim();
7167
- if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
7168
- if (MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`)) return false;
7169
- if (SANITIZER_PATTERN.test(trimmedExpression)) return false;
7170
- if (ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression)) return false;
7171
- if (I18N_VALUE_PATTERN.test(trimmedExpression)) return false;
7172
- if (ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression)) return false;
7173
- if (HTML_TAINT_PATTERN.test(trimmedExpression)) return true;
7174
- const identifier = trimmedExpression.match(/^([\w$]+)\s*(?:[;,})\n]|$)/)?.[1];
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 || 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;
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
- 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;
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 (/highlighted/i.test(valueExpression)) continue;
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
- 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;
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.name.name;
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.name.name;
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.name.name;
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 = flattenJsxName$1(node.name);
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
- if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
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
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
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.name.name;
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.name.name;
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
- if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
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
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "iframe") return;
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
- 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;
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 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);
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 (!isStyleAssignment(statements[statementIndex]) || !isStyleAssignment(statements[statementIndex - 1])) continue;
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 initializer = findVariableInitializer(receiver, receiver.name)?.initializer ?? null;
15178
- if (initializer && isNodeOfType(initializer, "Identifier")) return findVariableInitializer(initializer, initializer.name)?.initializer ?? initializer;
15179
- return initializer;
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 initializer = getResolvedInitializer(stripped);
15346
- if (initializer && isSmallInlineLiteralArray(initializer)) return true;
15508
+ const resolved = getResolvedInitializer(stripped);
15509
+ if (resolved && !resolved.isDefault && isSmallInlineLiteralArray(resolved.initializer)) return true;
15347
15510
  }
15348
15511
  return false;
15349
15512
  };
@@ -15395,8 +15558,8 @@ const jsSetMapLookups = defineRule({
15395
15558
  if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
15396
15559
  const resolvedInitializer = getResolvedInitializer(receiver);
15397
15560
  if (resolvedInitializer) {
15398
- if (isLikelyStringReceiver(resolvedInitializer)) return;
15399
- if (isSmallInlineLiteralArray(resolvedInitializer)) return;
15561
+ if (isLikelyStringReceiver(resolvedInitializer.initializer)) return;
15562
+ if (!resolvedInitializer.isDefault && isSmallInlineLiteralArray(resolvedInitializer.initializer)) return;
15400
15563
  }
15401
15564
  if (isStringElementOfSplitIteration(receiver)) return;
15402
15565
  if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
@@ -18523,10 +18686,6 @@ const resolveSettings$29 = (settings) => {
18523
18686
  if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
18524
18687
  return reactDoctor.jsxNoScriptUrl ?? {};
18525
18688
  };
18526
- const getElementName = (node) => {
18527
- if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
18528
- return null;
18529
- };
18530
18689
  const isLinkPropForElement = (elementName, attributeName, options) => {
18531
18690
  if (elementName === "a" && attributeName === "href") return true;
18532
18691
  const explicit = options.components?.[elementName];
@@ -18546,7 +18705,8 @@ const jsxNoScriptUrl = defineRule({
18546
18705
  create: (context) => {
18547
18706
  const options = resolveSettings$29(context.settings);
18548
18707
  return { JSXOpeningElement(node) {
18549
- const elementName = getElementName(node);
18708
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
18709
+ const elementName = resolveJsxElementType(node);
18550
18710
  if (!elementName) return;
18551
18711
  for (const attribute of node.attributes) {
18552
18712
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
@@ -18733,11 +18893,6 @@ const checkTarget = (attributeValue) => {
18733
18893
  alternate: false
18734
18894
  };
18735
18895
  };
18736
- const getOpeningElementName = (node) => {
18737
- const name = node.name;
18738
- if (isNodeOfType(name, "JSXIdentifier")) return name.name;
18739
- return null;
18740
- };
18741
18896
  const jsxNoTargetBlank = defineRule({
18742
18897
  id: "jsx-no-target-blank",
18743
18898
  title: "Unsafe target=_blank link",
@@ -18757,7 +18912,8 @@ const jsxNoTargetBlank = defineRule({
18757
18912
  return settings.formComponents.has(tagName);
18758
18913
  };
18759
18914
  return { JSXOpeningElement(node) {
18760
- const tagName = getOpeningElementName(node);
18915
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
18916
+ const tagName = resolveJsxElementType(node);
18761
18917
  if (!tagName) return;
18762
18918
  if (!isLink(tagName) && !isForm(tagName)) return;
18763
18919
  const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
@@ -20232,9 +20388,6 @@ const hasDirective = (programNode, directive) => {
20232
20388
  return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
20233
20389
  };
20234
20390
  //#endregion
20235
- //#region src/plugin/utils/is-uppercase-name.ts
20236
- const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
20237
- //#endregion
20238
20391
  //#region src/plugin/utils/is-component-assignment.ts
20239
20392
  const isComponentAssignment = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && isUppercaseName(node.id.name) && Boolean(node.init) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
20240
20393
  //#endregion
@@ -20567,7 +20720,7 @@ const nextjsNoAElement = defineRule({
20567
20720
  severity: "warn",
20568
20721
  recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
20569
20722
  create: (context) => ({ JSXOpeningElement(node) {
20570
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
20723
+ if (resolveJsxElementType(node) !== "a") return;
20571
20724
  const attributes = node.attributes ?? [];
20572
20725
  const downloadAttribute = findJsxAttribute(attributes, "download");
20573
20726
  if (downloadAttribute) {
@@ -20763,7 +20916,7 @@ const nextjsNoCssLink = defineRule({
20763
20916
  severity: "warn",
20764
20917
  recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
20765
20918
  create: (context) => ({ JSXOpeningElement(node) {
20766
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
20919
+ if (resolveJsxElementType(node) !== "link") return;
20767
20920
  const attributes = node.attributes ?? [];
20768
20921
  const relAttribute = findJsxAttribute(attributes, "rel");
20769
20922
  if (!relAttribute?.value) return;
@@ -20871,7 +21024,7 @@ const nextjsNoFontLink = defineRule({
20871
21024
  severity: "warn",
20872
21025
  recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
20873
21026
  create: (context) => ({ JSXOpeningElement(node) {
20874
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
21027
+ if (resolveJsxElementType(node) !== "link") return;
20875
21028
  const attributes = node.attributes ?? [];
20876
21029
  const hrefAttribute = findJsxAttribute(attributes, "href");
20877
21030
  if (!hrefAttribute?.value) return;
@@ -21046,7 +21199,7 @@ const nextjsNoImgElement = defineRule({
21046
21199
  create: (context) => {
21047
21200
  if (isGeneratedImageRenderContext(context)) return {};
21048
21201
  return { JSXOpeningElement(node) {
21049
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
21202
+ if (resolveJsxElementType(node) !== "img") return;
21050
21203
  if (isGeneratedImageRenderContext(context, node)) return;
21051
21204
  const programRoot = findProgramRoot(node);
21052
21205
  if (programRoot && hasEmailTemplateImport(programRoot)) return;
@@ -21093,8 +21246,8 @@ const nextjsNoPolyfillScript = defineRule({
21093
21246
  severity: "warn",
21094
21247
  recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
21095
21248
  create: (context) => ({ JSXOpeningElement(node) {
21096
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
21097
- if (node.name.name !== "script" && node.name.name !== "Script") return;
21249
+ const elementName = resolveJsxElementType(node);
21250
+ if (elementName !== "script" && elementName !== "Script") return;
21098
21251
  const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
21099
21252
  if (!srcAttribute?.value) return;
21100
21253
  const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
@@ -22902,10 +23055,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
22902
23055
  if (!declaringNode) return false;
22903
23056
  return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
22904
23057
  }));
23058
+ const isWholePropsParameterBinding = (bindingNode) => {
23059
+ if (isFunctionLike$1(bindingNode.parent)) return true;
23060
+ let declaringFunction = bindingNode.parent;
23061
+ while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
23062
+ return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
23063
+ };
22905
23064
  const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
22906
23065
  if (def.type !== "Parameter") return false;
22907
- const bindingParent = def.name.parent;
22908
- return isFunctionLike$1(bindingParent);
23066
+ return isWholePropsParameterBinding(def.name);
22909
23067
  }));
22910
23068
  const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
22911
23069
  const isPropAlias = (analysis, ref) => {
@@ -25902,6 +26060,56 @@ const noBarrelImport = defineRule({
25902
26060
  }
25903
26061
  });
25904
26062
  //#endregion
26063
+ //#region src/plugin/utils/function-returns-matching-expression.ts
26064
+ const collectReturnedExpressions = (functionNode) => {
26065
+ if (!isFunctionLike$1(functionNode)) return [];
26066
+ const body = functionNode.body;
26067
+ if (!body) return [];
26068
+ if (!isNodeOfType(body, "BlockStatement")) return [body];
26069
+ const returnedExpressions = [];
26070
+ walkAst(body, (node) => {
26071
+ if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
26072
+ if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
26073
+ });
26074
+ return returnedExpressions;
26075
+ };
26076
+ const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
26077
+ const visitedExpressions = /* @__PURE__ */ new Set();
26078
+ const visitedFunctions = /* @__PURE__ */ new Set();
26079
+ const functionMatches = (candidateFunction) => {
26080
+ if (visitedFunctions.has(candidateFunction)) return false;
26081
+ visitedFunctions.add(candidateFunction);
26082
+ return collectReturnedExpressions(candidateFunction).some(expressionMatches);
26083
+ };
26084
+ const expressionMatches = (expression) => {
26085
+ const unwrappedExpression = stripParenExpression(expression);
26086
+ if (visitedExpressions.has(unwrappedExpression)) return false;
26087
+ visitedExpressions.add(unwrappedExpression);
26088
+ if (matchesExpression(unwrappedExpression)) return true;
26089
+ if (isNodeOfType(unwrappedExpression, "Identifier")) {
26090
+ const symbol = scopes.symbolFor(unwrappedExpression);
26091
+ if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
26092
+ const initializer = stripParenExpression(symbol.initializer);
26093
+ if (isFunctionLike$1(initializer)) return false;
26094
+ return expressionMatches(initializer);
26095
+ }
26096
+ if (isNodeOfType(unwrappedExpression, "CallExpression")) {
26097
+ if (unwrappedExpression.arguments.length !== 0) return false;
26098
+ if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
26099
+ const symbol = scopes.symbolFor(unwrappedExpression.callee);
26100
+ if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
26101
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
26102
+ const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
26103
+ if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
26104
+ return functionMatches(candidateFunction);
26105
+ }
26106
+ if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
26107
+ if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
26108
+ return false;
26109
+ };
26110
+ return functionMatches(functionNode);
26111
+ };
26112
+ //#endregion
25905
26113
  //#region src/plugin/utils/function-contains-react-render-output.ts
25906
26114
  const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
25907
26115
  "FunctionDeclaration",
@@ -25921,12 +26129,13 @@ const isCallArgumentFunctionExpression = (node) => {
25921
26129
  return parent.arguments.some((argumentNode) => argumentNode === node);
25922
26130
  };
25923
26131
  const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
26132
+ const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
25924
26133
  const containsRenderOutput$1 = (rootNode, scopes) => {
25925
26134
  let hasRenderOutput = false;
25926
26135
  walkAst(rootNode, (node) => {
25927
26136
  if (hasRenderOutput) return false;
25928
26137
  if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
25929
- if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
26138
+ if (isRenderOutputExpression(node, scopes)) {
25930
26139
  hasRenderOutput = true;
25931
26140
  return false;
25932
26141
  }
@@ -25937,7 +26146,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
25937
26146
  const functionContainsReactRenderOutput = (functionNode, scopes) => {
25938
26147
  const cachedEntry = renderOutputCache.get(functionNode);
25939
26148
  if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
25940
- const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
26149
+ const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
25941
26150
  renderOutputCache.set(functionNode, {
25942
26151
  scopes,
25943
26152
  hasRenderOutput
@@ -28402,7 +28611,7 @@ const noDisabledZoom = defineRule({
28402
28611
  category: "Accessibility",
28403
28612
  recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
28404
28613
  create: (context) => ({ JSXOpeningElement(node) {
28405
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
28614
+ if (resolveJsxElementType(node) !== "meta") return;
28406
28615
  const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
28407
28616
  if (!nameAttr?.value) return;
28408
28617
  if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
@@ -28792,7 +29001,7 @@ const findTopLevelEffectCalls = (componentBody) => {
28792
29001
  if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
28793
29002
  for (const statement of componentBody.body ?? []) {
28794
29003
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
28795
- const expression = statement.expression;
29004
+ const expression = unwrapDiscardedExpression(statement);
28796
29005
  if (!isNodeOfType(expression, "CallExpression")) continue;
28797
29006
  if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
28798
29007
  effectCalls.push(expression);
@@ -31574,7 +31783,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
31574
31783
  severity: "warn",
31575
31784
  recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
31576
31785
  create: (context) => ({ JSXOpeningElement(node) {
31577
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
31786
+ if (resolveJsxElementType(node) !== "img") return;
31578
31787
  const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
31579
31788
  if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
31580
31789
  const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
@@ -31815,7 +32024,7 @@ const noIndeterminateAttribute = defineRule({
31815
32024
  if (classifyReactNativeFileTarget(context) === "react-native") return {};
31816
32025
  return {
31817
32026
  JSXOpeningElement(node) {
31818
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
32027
+ if (resolveJsxElementType(node) !== "input") return;
31819
32028
  let typeAttribute = null;
31820
32029
  let typeAttributeIndex = null;
31821
32030
  let indeterminateAttribute = null;
@@ -32887,11 +33096,13 @@ const noManyBooleanProps = defineRule({
32887
33096
  };
32888
33097
  const checkComponent = (functionNode, param, body, componentName, reportNode) => {
32889
33098
  if (!param) return;
33099
+ const propsBinding = resolveFirstArgumentBinding(param);
33100
+ if (!propsBinding) return;
32890
33101
  if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
32891
- if (isNodeOfType(param, "ObjectPattern")) {
33102
+ if (isNodeOfType(propsBinding, "ObjectPattern")) {
32892
33103
  const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
32893
33104
  const booleanLikePropNames = [];
32894
- for (const property of param.properties ?? []) {
33105
+ for (const property of propsBinding.properties ?? []) {
32895
33106
  if (!isNodeOfType(property, "Property")) continue;
32896
33107
  const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
32897
33108
  if (!keyName) continue;
@@ -32902,7 +33113,7 @@ const noManyBooleanProps = defineRule({
32902
33113
  reportIfMany(booleanLikePropNames, componentName, reportNode);
32903
33114
  return;
32904
33115
  }
32905
- if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
33116
+ if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
32906
33117
  };
32907
33118
  return {
32908
33119
  FunctionDeclaration(node) {
@@ -33024,7 +33235,7 @@ const noMirrorPropEffect = defineRule({
33024
33235
  if (mirrorBindings.length === 0) return;
33025
33236
  for (const statement of componentBody.body ?? []) {
33026
33237
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
33027
- const effectCall = statement.expression;
33238
+ const effectCall = unwrapDiscardedExpression(statement);
33028
33239
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
33029
33240
  if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
33030
33241
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
@@ -33038,7 +33249,7 @@ const noMirrorPropEffect = defineRule({
33038
33249
  const bodyStatements = getCallbackStatements(callback);
33039
33250
  if (bodyStatements.length !== 1) continue;
33040
33251
  const onlyStatement = bodyStatements[0];
33041
- const expression = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
33252
+ const expression = unwrapDiscardedExpression(onlyStatement);
33042
33253
  if (!isNodeOfType(expression, "CallExpression")) continue;
33043
33254
  if (!isNodeOfType(expression.callee, "Identifier")) continue;
33044
33255
  if (!isSetterIdentifier(expression.callee.name)) continue;
@@ -35654,7 +35865,7 @@ const noPreventDefault = defineRule({
35654
35865
  const isServerActionsFramework = hasCapability(context.settings, "server-actions");
35655
35866
  const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
35656
35867
  return { JSXOpeningElement(node) {
35657
- const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
35868
+ const elementName = resolveJsxElementType(node);
35658
35869
  if (!elementName) return;
35659
35870
  const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
35660
35871
  if (!targetEventProps) return;
@@ -37139,7 +37350,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
37139
37350
  return expressionReadsStateValue(argument, stateName);
37140
37351
  };
37141
37352
  const getUnconditionalSetterCall = (statement, setterNames) => {
37142
- const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
37353
+ const expression = unwrapDiscardedExpression(statement);
37143
37354
  if (!isNodeOfType(expression, "CallExpression")) return null;
37144
37355
  if (!isNodeOfType(expression.callee, "Identifier")) return null;
37145
37356
  if (!setterNames.has(expression.callee.name)) return null;
@@ -37488,7 +37699,7 @@ const noSelfUpdatingEffect = defineRule({
37488
37699
  const setterNames = new Set(setterNameToStateName.keys());
37489
37700
  for (const statement of functionBody.body ?? []) {
37490
37701
  if (!isNodeOfType(statement, "ExpressionStatement")) continue;
37491
- const effectCall = statement.expression;
37702
+ const effectCall = unwrapDiscardedExpression(statement);
37492
37703
  if (!isNodeOfType(effectCall, "CallExpression")) continue;
37493
37704
  if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
37494
37705
  if ((effectCall.arguments?.length ?? 0) < 2) continue;
@@ -38118,9 +38329,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
38118
38329
  recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
38119
38330
  create: (context) => ({ JSXOpeningElement(node) {
38120
38331
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
38121
- const firstCharacter = node.name.name.charCodeAt(0);
38332
+ const elementName = resolveJsxElementType(node);
38333
+ const firstCharacter = elementName.charCodeAt(0);
38122
38334
  if (firstCharacter < 97 || firstCharacter > 122) return;
38123
- if (node.name.name.includes("-")) return;
38335
+ if (elementName.includes("-")) return;
38124
38336
  for (const attribute of node.attributes) {
38125
38337
  if (!isNodeOfType(attribute, "JSXAttribute")) continue;
38126
38338
  if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
@@ -38508,8 +38720,7 @@ const noUncontrolledInput = defineRule({
38508
38720
  const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
38509
38721
  walkAst(componentBody, (child) => {
38510
38722
  if (!isNodeOfType(child, "JSXOpeningElement")) return;
38511
- if (!isNodeOfType(child.name, "JSXIdentifier")) return;
38512
- const tagName = child.name.name;
38723
+ const tagName = resolveJsxElementType(child);
38513
38724
  if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
38514
38725
  const attributes = child.attributes ?? [];
38515
38726
  if (hasJsxSpreadAttribute(attributes)) return;
@@ -38570,7 +38781,7 @@ const noUndeferredThirdParty = defineRule({
38570
38781
  severity: "warn",
38571
38782
  recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
38572
38783
  create: (context) => ({ JSXOpeningElement(node) {
38573
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
38784
+ if (resolveJsxElementType(node) !== "script") return;
38574
38785
  const attributes = node.attributes ?? [];
38575
38786
  const srcAttribute = findJsxAttribute(attributes, "src");
38576
38787
  if (!srcAttribute) return;
@@ -39785,7 +39996,7 @@ const noUnknownProperty = defineRule({
39785
39996
  }
39786
39997
  if (fileIsNonReactJsx) return;
39787
39998
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
39788
- const elementType = node.name.name;
39999
+ const elementType = resolveJsxElementType(node);
39789
40000
  const firstCharacter = elementType.charCodeAt(0);
39790
40001
  if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
39791
40002
  let isValidHtmlTag = isKnownDomTag(elementType);
@@ -39963,20 +40174,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
39963
40174
  });
39964
40175
  return found;
39965
40176
  };
40177
+ const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
39966
40178
  const isReactClassComponent = (classNode) => {
39967
40179
  if (isEs6Component(classNode)) return true;
39968
40180
  return expressionContainsJsxOrCreateElement(classNode);
39969
40181
  };
39970
- const findEnclosingComponent = (node) => {
40182
+ const findEnclosingComponent = (node, scopes) => {
39971
40183
  let walker = node.parent;
39972
40184
  while (walker) {
39973
40185
  if (isFunctionLike$1(walker)) {
39974
40186
  const componentName = inferFunctionLikeName(walker);
39975
- if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
40187
+ if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
39976
40188
  component: walker,
39977
40189
  name: componentName
39978
40190
  };
39979
- if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
40191
+ if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
39980
40192
  component: walker,
39981
40193
  name: null
39982
40194
  };
@@ -40215,7 +40427,7 @@ const noUnstableNestedComponents = defineRule({
40215
40427
  if (renderPropRegex.test(propInfo.propName)) return;
40216
40428
  if (settings.allowAsProps) return;
40217
40429
  }
40218
- const enclosing = findEnclosingComponent(candidateNode);
40430
+ const enclosing = findEnclosingComponent(candidateNode, context.scopes);
40219
40431
  if (!enclosing) return;
40220
40432
  const gatedName = propInfo ? null : requiredInstantiationName;
40221
40433
  queuedReports.push({
@@ -40226,7 +40438,7 @@ const noUnstableNestedComponents = defineRule({
40226
40438
  });
40227
40439
  };
40228
40440
  const checkFunctionLike = (node) => {
40229
- if (!expressionContainsJsxOrCreateElement(node)) return;
40441
+ if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
40230
40442
  const inferredName = inferFunctionLikeName(node);
40231
40443
  const propInfo = isComponentDeclaredInProp(node);
40232
40444
  const isObjectCallback = isObjectCallbackCandidate(node);
@@ -41486,7 +41698,7 @@ const preactPreferOndblclick = defineRule({
41486
41698
  recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
41487
41699
  create: (context) => ({ JSXOpeningElement(node) {
41488
41700
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
41489
- const tagName = node.name.name;
41701
+ const tagName = resolveJsxElementType(node);
41490
41702
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
41491
41703
  const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
41492
41704
  if (!onDoubleClickAttribute) return;
@@ -41506,7 +41718,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
41506
41718
  ]);
41507
41719
  const isTextLikeInput = (openingElement) => {
41508
41720
  if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
41509
- const tagName = openingElement.name.name;
41721
+ const tagName = resolveJsxElementType(openingElement);
41510
41722
  if (tagName === "textarea") return true;
41511
41723
  if (tagName !== "input") return false;
41512
41724
  const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
@@ -41663,8 +41875,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
41663
41875
  ]);
41664
41876
  const collectBooleanPropBindings = (param) => {
41665
41877
  const bindings = /* @__PURE__ */ new Set();
41666
- if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
41667
- for (const property of param.properties ?? []) {
41878
+ const propsBinding = resolveFirstArgumentBinding(param);
41879
+ if (!isNodeOfType(propsBinding, "ObjectPattern")) return bindings;
41880
+ for (const property of propsBinding.properties ?? []) {
41668
41881
  if (!isNodeOfType(property, "Property")) continue;
41669
41882
  if (property.computed) continue;
41670
41883
  if (!isNodeOfType(property.key, "Identifier")) continue;
@@ -41925,7 +42138,7 @@ const preferHtmlDialog = defineRule({
41925
42138
  },
41926
42139
  JSXOpeningElement(node) {
41927
42140
  if (!isNodeOfType(node.name, "JSXIdentifier")) return;
41928
- const tagName = node.name.name;
42141
+ const tagName = resolveJsxElementType(node);
41929
42142
  if (tagName === "dialog") return;
41930
42143
  if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
41931
42144
  if (!HTML_TAGS.has(tagName)) return;
@@ -43962,7 +44175,7 @@ const renderingAnimateSvgWrapper = defineRule({
43962
44175
  severity: "warn",
43963
44176
  recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
43964
44177
  create: (context) => ({ JSXOpeningElement(node) {
43965
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
44178
+ if (resolveJsxElementType(node) !== "svg") return;
43966
44179
  if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
43967
44180
  node,
43968
44181
  message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
@@ -45255,14 +45468,10 @@ const rerenderLazyStateInit = defineRule({
45255
45468
  //#endregion
45256
45469
  //#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
45257
45470
  const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
45258
- const callbackReturnsJsx = (callback) => {
45471
+ const callbackReturnsJsx = (callback, scopes) => {
45259
45472
  if (!callback) return false;
45260
45473
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
45261
- 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;
45474
+ return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
45266
45475
  };
45267
45476
  const returnArgumentUsesAnyName = (returnStatement, names) => {
45268
45477
  if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
@@ -45340,7 +45549,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
45340
45549
  if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
45341
45550
  for (const declarator of stmt.declarations ?? []) {
45342
45551
  const init = declarator.init;
45343
- if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
45552
+ if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
45344
45553
  memoNode = declarator;
45345
45554
  callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
45346
45555
  if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
@@ -45406,8 +45615,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
45406
45615
  const collectDefaultedEmptyBindings = (functionNode) => {
45407
45616
  const bindings = /* @__PURE__ */ new Map();
45408
45617
  const params = functionNode.params ?? [];
45409
- for (const param of params) collectFromObjectPattern(param, bindings);
45410
- const propsParam = params[0];
45618
+ for (const [parameterIndex, param] of params.entries()) {
45619
+ const parameterBinding = parameterIndex === 0 ? resolveFirstArgumentBinding(param) : param;
45620
+ if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
45621
+ }
45622
+ const propsParam = resolveFirstArgumentBinding(params[0]);
45411
45623
  const body = functionNode.body;
45412
45624
  if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
45413
45625
  if (!isNodeOfType(body, "BlockStatement")) return bindings;
@@ -45913,8 +46125,8 @@ const rnAnimationReactionAsDerived = defineRule({
45913
46125
  if (statements.length !== 1) return;
45914
46126
  const onlyStatement = statements[0];
45915
46127
  if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
45916
- singleAssignment = onlyStatement.expression;
45917
- } else if (body) singleAssignment = body;
46128
+ singleAssignment = unwrapDiscardedExpression(onlyStatement);
46129
+ } else if (body) singleAssignment = unwrapDiscardedExpression(body);
45918
46130
  if (!singleAssignment) return;
45919
46131
  const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
45920
46132
  const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
@@ -53521,10 +53733,6 @@ const isInvalidStyleExpression = (expression) => {
53521
53733
  }
53522
53734
  return false;
53523
53735
  };
53524
- const getJsxOpeningElementName = (node) => {
53525
- if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
53526
- return null;
53527
- };
53528
53736
  const stylePropObject = defineRule({
53529
53737
  id: "style-prop-object",
53530
53738
  title: "Style prop is not an object",
@@ -53536,7 +53744,8 @@ const stylePropObject = defineRule({
53536
53744
  const allowSet = new Set(allow);
53537
53745
  return {
53538
53746
  JSXOpeningElement(node) {
53539
- const elementName = getJsxOpeningElementName(node);
53747
+ if (!isNodeOfType(node.name, "JSXIdentifier")) return;
53748
+ const elementName = resolveJsxElementType(node);
53540
53749
  if (elementName && allowSet.has(elementName)) return;
53541
53750
  if (elementName) {
53542
53751
  const firstCharCode = elementName.charCodeAt(0);
@@ -54241,7 +54450,7 @@ const tanstackStartNoAnchorElement = defineRule({
54241
54450
  create: (context) => {
54242
54451
  if (!isInProjectDirectory(context, "routes")) return {};
54243
54452
  return { JSXOpeningElement(node) {
54244
- if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
54453
+ if (resolveJsxElementType(node) !== "a") return;
54245
54454
  const attributes = node.attributes ?? [];
54246
54455
  const hrefAttribute = findJsxAttribute(attributes, "href");
54247
54456
  if (!hrefAttribute?.value) return;
@@ -54862,8 +55071,7 @@ const voidDomElementsNoChildren = defineRule({
54862
55071
  create: (context) => ({
54863
55072
  JSXElement(node) {
54864
55073
  const openingElement = node.openingElement;
54865
- if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
54866
- const tagName = openingElement.name.name;
55074
+ const tagName = resolveJsxElementType(openingElement);
54867
55075
  if (!VOID_DOM_ELEMENTS.has(tagName)) return;
54868
55076
  const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
54869
55077
  const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.6-dev.e877ca7",
3
+ "version": "0.7.6-dev.f215ace",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",