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

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