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.
Files changed (2) hide show
  1. package/dist/index.js +389 -174
  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) {
@@ -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(`(?: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;
@@ -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")) return elementName.name === "Fragment";
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.object.name !== "React") return false;
15819
- return elementName.property.name === "Fragment";
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
- const elementName = getElementName(node);
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
- const tagName = getOpeningElementName(node);
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
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
- if (!isNodeOfType(node.name, "JSXIdentifier")) return;
21097
- if (node.name.name !== "script" && node.name.name !== "Script") return;
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
- const bindingParent = def.name.parent;
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 (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
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.expression;
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
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(param, "ObjectPattern")) {
33109
+ if (isNodeOfType(propsBinding, "ObjectPattern")) {
32892
33110
  const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
32893
33111
  const booleanLikePropNames = [];
32894
- for (const property of param.properties ?? []) {
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(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
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.expression;
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 = isNodeOfType(onlyStatement, "ExpressionStatement") ? onlyStatement.expression : onlyStatement;
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 = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
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 = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
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.expression;
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 firstCharacter = node.name.name.charCodeAt(0);
38339
+ const elementName = resolveJsxElementType(node);
38340
+ const firstCharacter = elementName.charCodeAt(0);
38122
38341
  if (firstCharacter < 97 || firstCharacter > 122) return;
38123
- if (node.name.name.includes("-")) return;
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
- if (!isNodeOfType(child.name, "JSXIdentifier")) return;
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
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.name.name;
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) && expressionContainsJsxOrCreateElement(walker)) return {
40194
+ if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
39976
40195
  component: walker,
39977
40196
  name: componentName
39978
40197
  };
39979
- if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
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 (!expressionContainsJsxOrCreateElement(node)) return;
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.name.name;
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.name.name;
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
- if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
41667
- for (const property of param.properties ?? []) {
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.name.name;
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
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
- 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;
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) collectFromObjectPattern(param, bindings);
45410
- const propsParam = params[0];
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.expression;
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
- const elementName = getJsxOpeningElementName(node);
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 (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
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
- if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
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);
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.eb09902",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",