oxlint-plugin-react-doctor 0.7.5-dev.76cd6be → 0.7.5-dev.bc49aaa

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 +503 -125
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -397,6 +397,7 @@ const isProductionFilePath = (relativePath, sourceFilePattern) => {
397
397
  //#endregion
398
398
  //#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
399
399
  const isProductionSourcePath = (relativePath) => {
400
+ if (/\.d\.[cm]?[jt]s$/i.test(relativePath)) return false;
400
401
  return isProductionFilePath(relativePath, SOURCE_FILE_PATTERN);
401
402
  };
402
403
  //#endregion
@@ -5178,8 +5179,10 @@ const STORAGE_GLOBALS = new Set([
5178
5179
  const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
5179
5180
  const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
5180
5181
  const STRONG_AUTH_KEY_PATTERN = /jwt|secret|password|passwd|credential|private[-_]?key|api[-_]?key|bearer|access[-_]?token|refresh[-_]?token|auth[-_]?token|id[-_]?token|session/i;
5182
+ const PRODUCT_API_KEY_RECORDS_PATTERN = /(?:^|[._:-])(?:created|saved|integration|mailing)[-_]?api[-_]?keys$/i;
5181
5183
  const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
5182
5184
  const isAuthCredentialKey = (key) => {
5185
+ if (PRODUCT_API_KEY_RECORDS_PATTERN.test(key)) return false;
5183
5186
  if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
5184
5187
  if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
5185
5188
  if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
@@ -5931,6 +5934,21 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
5931
5934
  }
5932
5935
  });
5933
5936
  //#endregion
5937
+ //#region src/plugin/utils/get-static-property-key-name.ts
5938
+ const getStaticPropertyKeyName = (node, options = {}) => {
5939
+ if (!isNodeOfType(node, "Property")) return null;
5940
+ if (node.computed) {
5941
+ if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
5942
+ return null;
5943
+ }
5944
+ if (isNodeOfType(node.key, "Identifier")) return node.key.name;
5945
+ if (isNodeOfType(node.key, "Literal")) {
5946
+ if (typeof node.key.value === "string") return node.key.value;
5947
+ if (options.stringifyNonStringLiterals) return String(node.key.value);
5948
+ }
5949
+ return null;
5950
+ };
5951
+ //#endregion
5934
5952
  //#region src/plugin/utils/is-presentation-role.ts
5935
5953
  const isPresentationRole = (openingElement) => {
5936
5954
  const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
@@ -5975,6 +5993,21 @@ const isPureEventBlockerHandler = (attribute) => {
5975
5993
  return false;
5976
5994
  };
5977
5995
  //#endregion
5996
+ //#region src/plugin/utils/resolve-const-identifier-alias.ts
5997
+ const resolveConstIdentifierAlias = (identifier, scopes) => {
5998
+ if (!isNodeOfType(identifier, "Identifier")) return null;
5999
+ const visitedSymbolIds = /* @__PURE__ */ new Set();
6000
+ let symbol = scopes.symbolFor(identifier);
6001
+ while (symbol?.kind === "const") {
6002
+ if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
6003
+ visitedSymbolIds.add(symbol.id);
6004
+ const initializer = stripParenExpression(symbol.initializer);
6005
+ if (!isNodeOfType(initializer, "Identifier")) return symbol;
6006
+ symbol = scopes.symbolFor(initializer);
6007
+ }
6008
+ return symbol;
6009
+ };
6010
+ //#endregion
5978
6011
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
5979
6012
  const MESSAGE$57 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
5980
6013
  const KEY_HANDLERS = [
@@ -5985,6 +6018,63 @@ const KEY_HANDLERS = [
5985
6018
  "onKeyDownCapture",
5986
6019
  "onKeyPressCapture"
5987
6020
  ];
6021
+ const CLICK_HANDLERS = ["onClick", "onClickCapture"];
6022
+ const TRANSPARENT_SPREAD_EVENT_NAMES = new Set([...CLICK_HANDLERS, ...KEY_HANDLERS].map((eventName) => eventName.toLowerCase()));
6023
+ const CONSERVATIVE_SPREAD_PROP_NAMES = new Set([
6024
+ "aria-hidden",
6025
+ "onmouseenter",
6026
+ "onmouseover",
6027
+ "role"
6028
+ ]);
6029
+ const resolveSpreadObjectExpression = (expression, scopes) => {
6030
+ const innerExpression = stripParenExpression(expression);
6031
+ if (isNodeOfType(innerExpression, "ObjectExpression")) return innerExpression;
6032
+ if (!isNodeOfType(innerExpression, "Identifier")) return null;
6033
+ const symbol = resolveConstIdentifierAlias(innerExpression, scopes);
6034
+ if (symbol?.kind !== "const" || !symbol.initializer) return null;
6035
+ const initializer = stripParenExpression(symbol.initializer);
6036
+ return isNodeOfType(initializer, "ObjectExpression") ? initializer : null;
6037
+ };
6038
+ const collectTransparentSpreadEventNames = (expression, scopes, eventValues, visitedObjectExpressions) => {
6039
+ const objectExpression = resolveSpreadObjectExpression(expression, scopes);
6040
+ if (!objectExpression || visitedObjectExpressions.has(objectExpression)) return false;
6041
+ visitedObjectExpressions.add(objectExpression);
6042
+ let isTransparent = true;
6043
+ for (const property of objectExpression.properties) {
6044
+ if (isNodeOfType(property, "SpreadElement")) {
6045
+ if (!collectTransparentSpreadEventNames(property.argument, scopes, eventValues, visitedObjectExpressions)) {
6046
+ isTransparent = false;
6047
+ break;
6048
+ }
6049
+ continue;
6050
+ }
6051
+ if (!isNodeOfType(property, "Property")) {
6052
+ isTransparent = false;
6053
+ break;
6054
+ }
6055
+ const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
6056
+ if (!propertyName) {
6057
+ isTransparent = false;
6058
+ break;
6059
+ }
6060
+ const normalizedPropertyName = propertyName.toLowerCase();
6061
+ if (CONSERVATIVE_SPREAD_PROP_NAMES.has(normalizedPropertyName)) {
6062
+ isTransparent = false;
6063
+ break;
6064
+ }
6065
+ if (TRANSPARENT_SPREAD_EVENT_NAMES.has(normalizedPropertyName)) eventValues.set(normalizedPropertyName, property.value);
6066
+ }
6067
+ visitedObjectExpressions.delete(objectExpression);
6068
+ return isTransparent;
6069
+ };
6070
+ const getTransparentSpreadEventValues = (attributes, scopes) => {
6071
+ const eventValues = /* @__PURE__ */ new Map();
6072
+ for (const attribute of attributes) {
6073
+ if (!isNodeOfType(attribute, "JSXSpreadAttribute")) continue;
6074
+ if (!collectTransparentSpreadEventNames(attribute.argument, scopes, eventValues, /* @__PURE__ */ new Set())) return null;
6075
+ }
6076
+ return eventValues;
6077
+ };
5988
6078
  const FOCUSLESS_CONTAINER_TAGS = new Set([
5989
6079
  "tr",
5990
6080
  "td",
@@ -6032,7 +6122,10 @@ const isFocusForwardingFunctionBody = (body) => {
6032
6122
  };
6033
6123
  const resolveHandlerFunction = (attribute) => {
6034
6124
  if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
6035
- let expression = attribute.value.expression;
6125
+ return resolveHandlerFunctionExpression(attribute.value.expression);
6126
+ };
6127
+ const resolveHandlerFunctionExpression = (handlerExpression) => {
6128
+ let expression = handlerExpression;
6036
6129
  if (isNodeOfType(expression, "Identifier")) {
6037
6130
  const binding = findVariableInitializer(expression, expression.name);
6038
6131
  if (!binding?.initializer) return null;
@@ -6131,18 +6224,22 @@ const clickEventsHaveKeyEvents = defineRule({
6131
6224
  if (!HTML_TAGS.has(tag)) return;
6132
6225
  if (tag === "label") return;
6133
6226
  if (!FOCUSLESS_CONTAINER_TAGS.has(tag) && isInteractiveElement(tag, node)) return;
6227
+ const spreadEventValues = getTransparentSpreadEventValues(node.attributes, context.scopes);
6228
+ if (!spreadEventValues) return;
6134
6229
  const onClick = hasJsxPropIgnoreCase(node.attributes, "onClick") ?? hasJsxPropIgnoreCase(node.attributes, "onClickCapture");
6135
- if (!onClick) return;
6136
- if (isPureEventBlockerHandler(onClick)) return;
6137
- if (isFocusForwardingHandler(onClick)) return;
6138
- if (hasJsxSpreadAttribute$1(node.attributes)) return;
6230
+ const spreadOnClickExpression = CLICK_HANDLERS.map((name) => spreadEventValues.get(name.toLowerCase())).find((expression) => expression !== void 0);
6231
+ if (!onClick && !spreadOnClickExpression) return;
6232
+ if (onClick && isPureEventBlockerHandler(onClick)) return;
6233
+ if (onClick && isFocusForwardingHandler(onClick)) return;
6234
+ const spreadHandlerFunction = spreadOnClickExpression ? resolveHandlerFunctionExpression(spreadOnClickExpression) : null;
6235
+ if (spreadHandlerFunction && (isFocusForwardingFunctionBody(spreadHandlerFunction.body ?? null) || containsBackdropDismissComparison(spreadHandlerFunction.body ?? null))) return;
6139
6236
  if (hasCompositeItemRole(node)) return;
6140
6237
  if (isHoverSelectionListItem(tag, node)) return;
6141
- if (isBackdropDismissHandler(onClick)) return;
6238
+ if (onClick && isBackdropDismissHandler(onClick)) return;
6142
6239
  if (containsKeyboardActivatableDescendant(node.parent)) return;
6143
6240
  if (isHiddenFromScreenReader(node, context.settings)) return;
6144
6241
  if (isPresentationRole(node)) return;
6145
- if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
6242
+ if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler) || spreadEventValues.has(handler.toLowerCase()))) return;
6146
6243
  context.report({
6147
6244
  node: node.name,
6148
6245
  message: MESSAGE$57
@@ -6868,11 +6965,10 @@ const getTemplateInterpolations = (valueTail) => {
6868
6965
  const interpolations = valueTail.slice(1, closingBacktickIndex).match(/\$\{[^}]*\}/g);
6869
6966
  return interpolations === null ? "" : interpolations.join(" ");
6870
6967
  };
6871
- const getIdentifierDeclarationInitializer = (identifier, fileContent) => {
6872
- const declarationMatch = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]{0,120})?=\\s*`).exec(fileContent);
6873
- if (declarationMatch === null) return null;
6874
- const initializerStartIndex = declarationMatch.index + declarationMatch[0].length;
6875
- return fileContent.slice(initializerStartIndex, initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
6968
+ const getIdentifierDeclarationInitializer = (identifier, sinkIndex, fileContent) => {
6969
+ const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
6970
+ if (declaration === null) return null;
6971
+ return fileContent.slice(declaration.initializerStartIndex, declaration.initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
6876
6972
  };
6877
6973
  const isPureStringLiteralConcat = (initializerText) => {
6878
6974
  if (!/^["']/.test(initializerText)) return false;
@@ -6942,6 +7038,153 @@ const isAllOperandsDomContentConcat = (valueExpression) => {
6942
7038
  return !HTML_TAINT_PATTERN.test(withoutCast.replace(DOM_CONTENT_SOURCE_VALUE_PATTERN, ""));
6943
7039
  });
6944
7040
  };
7041
+ const findMatchingBraceIndex = (fileContent, openingBraceIndex) => {
7042
+ let depth = 0;
7043
+ let quote = null;
7044
+ for (let index = openingBraceIndex; index < fileContent.length; index += 1) {
7045
+ const character = fileContent[index];
7046
+ if (quote !== null) {
7047
+ if (character === quote && fileContent[index - 1] !== "\\") quote = null;
7048
+ continue;
7049
+ }
7050
+ if (character === "\"" || character === "'" || character === "`") {
7051
+ quote = character;
7052
+ continue;
7053
+ }
7054
+ if (character === "{") depth += 1;
7055
+ if (character === "}") {
7056
+ depth -= 1;
7057
+ if (depth === 0) return index;
7058
+ }
7059
+ }
7060
+ return fileContent.length;
7061
+ };
7062
+ const findContainingBlockEndIndex = (fileContent, targetIndex) => {
7063
+ const openingBraceIndexes = [];
7064
+ let quote = null;
7065
+ let isLineComment = false;
7066
+ let isBlockComment = false;
7067
+ for (let index = 0; index < targetIndex; index += 1) {
7068
+ const character = fileContent[index];
7069
+ const nextCharacter = fileContent[index + 1];
7070
+ if (isLineComment) {
7071
+ if (character === "\n") isLineComment = false;
7072
+ continue;
7073
+ }
7074
+ if (isBlockComment) {
7075
+ if (character === "*" && nextCharacter === "/") {
7076
+ isBlockComment = false;
7077
+ index += 1;
7078
+ }
7079
+ continue;
7080
+ }
7081
+ if (quote !== null) {
7082
+ if (character === quote && fileContent[index - 1] !== "\\") quote = null;
7083
+ continue;
7084
+ }
7085
+ if (character === "/" && nextCharacter === "/") {
7086
+ isLineComment = true;
7087
+ index += 1;
7088
+ continue;
7089
+ }
7090
+ if (character === "/" && nextCharacter === "*") {
7091
+ isBlockComment = true;
7092
+ index += 1;
7093
+ continue;
7094
+ }
7095
+ if (character === "\"" || character === "'" || character === "`") {
7096
+ quote = character;
7097
+ continue;
7098
+ }
7099
+ if (character === "{") openingBraceIndexes.push(index);
7100
+ if (character === "}") openingBraceIndexes.pop();
7101
+ }
7102
+ const openingBraceIndex = openingBraceIndexes.at(-1);
7103
+ return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
7104
+ };
7105
+ const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
7106
+ const initializerPattern = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
7107
+ let nearestDeclaration = null;
7108
+ let nearestDeclarationIndex = -1;
7109
+ for (const match of fileContent.matchAll(initializerPattern)) {
7110
+ const declarationIndex = match.index;
7111
+ if (declarationIndex === void 0 || declarationIndex >= sinkIndex || declarationIndex <= nearestDeclarationIndex || findContainingBlockEndIndex(fileContent, declarationIndex) < sinkIndex) continue;
7112
+ nearestDeclarationIndex = declarationIndex;
7113
+ const initializer = match[1];
7114
+ if (initializer === void 0) continue;
7115
+ nearestDeclaration = {
7116
+ initializer,
7117
+ initializerStartIndex: declarationIndex + match[0].length - initializer.length
7118
+ };
7119
+ }
7120
+ return nearestDeclaration;
7121
+ };
7122
+ const findContainingFunctionParameterSource = (identifier, sinkIndex, fileContent) => {
7123
+ const patterns = [/function\s+([\w$]+)\s*\(([^)]*)\)\s*\{/g, /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/g];
7124
+ let closestSource = null;
7125
+ let closestStartIndex = -1;
7126
+ for (const pattern of patterns) for (const match of fileContent.matchAll(pattern)) {
7127
+ const matchIndex = match.index;
7128
+ if (matchIndex === void 0 || matchIndex >= sinkIndex || matchIndex < closestStartIndex) continue;
7129
+ if (findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{")) < sinkIndex) continue;
7130
+ const parameterIndex = (match[2] ?? "").split(",").findIndex((parameter) => parameter.trim().match(/^(?:\.\.\.)?([\w$]+)/)?.[1] === identifier);
7131
+ if (parameterIndex < 0) continue;
7132
+ closestStartIndex = matchIndex;
7133
+ closestSource = {
7134
+ functionName: match[1] ?? "",
7135
+ parameterIndex
7136
+ };
7137
+ }
7138
+ return closestSource;
7139
+ };
7140
+ const splitTopLevelArguments = (argumentText) => {
7141
+ const argumentsList = [];
7142
+ let startIndex = 0;
7143
+ let depth = 0;
7144
+ let quote = null;
7145
+ for (let index = 0; index < argumentText.length; index += 1) {
7146
+ const character = argumentText[index];
7147
+ if (quote !== null) {
7148
+ if (character === quote && argumentText[index - 1] !== "\\") quote = null;
7149
+ continue;
7150
+ }
7151
+ if (character === "\"" || character === "'" || character === "`") {
7152
+ quote = character;
7153
+ continue;
7154
+ }
7155
+ if (character === "(" || character === "[" || character === "{") depth += 1;
7156
+ if (character === ")" || character === "]" || character === "}") depth -= 1;
7157
+ if (character === "," && depth === 0) {
7158
+ argumentsList.push(argumentText.slice(startIndex, index).trim());
7159
+ startIndex = index + 1;
7160
+ }
7161
+ }
7162
+ argumentsList.push(argumentText.slice(startIndex).trim());
7163
+ return argumentsList;
7164
+ };
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];
7175
+ if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
7176
+ visitedIdentifiers.add(identifier);
7177
+ const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
7178
+ if (declaration !== null && isHtmlTainted(declaration.initializer, fileContent, sinkIndex, visitedIdentifiers)) return true;
7179
+ 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;
7187
+ };
6945
7188
  const dangerousHtmlSink = defineRule({
6946
7189
  id: "dangerous-html-sink",
6947
7190
  title: "HTML injection sink with dynamic content",
@@ -6968,6 +7211,7 @@ const dangerousHtmlSink = defineRule({
6968
7211
  const valueTail = fullValueTail.slice(0, VALUE_EXPRESSION_MAX_CHARS);
6969
7212
  const terminatorIndex = valueTail.search(/[;}]/);
6970
7213
  const valueExpression = terminatorIndex >= 0 ? valueTail.slice(0, terminatorIndex + 1) : valueTail;
7214
+ const sinkIndex = lines.slice(0, lineIndex).join("\n").length + (lineIndex > 0 ? 1 : 0) + line.search(DANGEROUS_HTML_PATTERN);
6971
7215
  if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
6972
7216
  if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
6973
7217
  if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
@@ -6979,7 +7223,7 @@ const dangerousHtmlSink = defineRule({
6979
7223
  let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
6980
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;
6981
7225
  if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
6982
- const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, file.content);
7226
+ const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
6983
7227
  if (declarationInitializer !== null) {
6984
7228
  if (isPureStringLiteralConcat(declarationInitializer)) continue;
6985
7229
  templateInterpolations = getTemplateInterpolations(declarationInitializer);
@@ -6990,7 +7234,7 @@ const dangerousHtmlSink = defineRule({
6990
7234
  if (SANITIZER_PATTERN.test(judgedExpression)) continue;
6991
7235
  if (ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
6992
7236
  if (I18N_VALUE_PATTERN.test(judgedExpression)) continue;
6993
- if (!HTML_TAINT_PATTERN.test(judgedExpression)) continue;
7237
+ if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set())) continue;
6994
7238
  if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
6995
7239
  if (/highlighted/i.test(valueExpression)) continue;
6996
7240
  if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
@@ -7437,7 +7681,7 @@ const containsJsx$1 = (root) => {
7437
7681
  containsJsxCache.set(root, found);
7438
7682
  return found;
7439
7683
  };
7440
- const getStaticMemberName$1 = (node) => {
7684
+ const getStaticMemberName$2 = (node) => {
7441
7685
  if (!isNodeOfType(node, "MemberExpression")) return null;
7442
7686
  if (!node.computed && isNodeOfType(node.property, "Identifier")) return node.property.name;
7443
7687
  if (node.computed && isNodeOfType(node.property, "Literal") && typeof node.property.value === "string") return node.property.value;
@@ -7451,7 +7695,7 @@ const getAssignedName = (node) => {
7451
7695
  if (isNodeOfType(parent, "AssignmentExpression")) {
7452
7696
  const left = parent.left;
7453
7697
  if (isNodeOfType(left, "Identifier")) return left.name;
7454
- if (isNodeOfType(left, "MemberExpression")) return getStaticMemberName$1(left);
7698
+ if (isNodeOfType(left, "MemberExpression")) return getStaticMemberName$2(left);
7455
7699
  }
7456
7700
  return null;
7457
7701
  };
@@ -7459,20 +7703,20 @@ const isModuleExportsAssignment = (node) => {
7459
7703
  const parent = node.parent;
7460
7704
  if (!parent || !isNodeOfType(parent, "AssignmentExpression")) return false;
7461
7705
  const left = parent.left;
7462
- return isNodeOfType(left, "MemberExpression") && isNodeOfType(left.object, "Identifier") && left.object.name === "module" && getStaticMemberName$1(left) === "exports";
7706
+ return isNodeOfType(left, "MemberExpression") && isNodeOfType(left.object, "Identifier") && left.object.name === "module" && getStaticMemberName$2(left) === "exports";
7463
7707
  };
7464
7708
  const isCreateClassLikeCall = (node) => {
7465
7709
  if (!isNodeOfType(node, "CallExpression")) return false;
7466
7710
  if (isEs5Component(node)) return true;
7467
7711
  const callee = node.callee;
7468
- if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$1(callee) === "createClass";
7712
+ if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$2(callee) === "createClass";
7469
7713
  return false;
7470
7714
  };
7471
- const isCreateContextCall = (node) => {
7715
+ const isCreateContextCall$1 = (node) => {
7472
7716
  if (!isNodeOfType(node, "CallExpression")) return false;
7473
7717
  const callee = node.callee;
7474
7718
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
7475
- return isNodeOfType(callee, "MemberExpression") && getStaticMemberName$1(callee) === "createContext";
7719
+ return isNodeOfType(callee, "MemberExpression") && getStaticMemberName$2(callee) === "createContext";
7476
7720
  };
7477
7721
  const isNamedFunctionLike = (node) => (isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) && Boolean(node.id?.name);
7478
7722
  const firstCallArgument = (node) => {
@@ -7530,7 +7774,7 @@ const memberExpressionPath = (node) => {
7530
7774
  if (isNodeOfType(node, "Identifier")) return [node.name];
7531
7775
  if (!isNodeOfType(node, "MemberExpression")) return [];
7532
7776
  const objectPath = memberExpressionPath(node.object);
7533
- const propertyName = getStaticMemberName$1(node);
7777
+ const propertyName = getStaticMemberName$2(node);
7534
7778
  return propertyName ? [...objectPath, propertyName] : objectPath;
7535
7779
  };
7536
7780
  const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
@@ -7540,7 +7784,7 @@ const getDisplayNameAssignmentIndex = (programRoot) => {
7540
7784
  const identifierTargets = /* @__PURE__ */ new Set();
7541
7785
  const memberPathSegments = /* @__PURE__ */ new Set();
7542
7786
  const visit = (node) => {
7543
- if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName$1(node.left) === "displayName") {
7787
+ if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName$2(node.left) === "displayName") {
7544
7788
  if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
7545
7789
  for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
7546
7790
  }
@@ -7645,7 +7889,7 @@ const displayName = defineRule({
7645
7889
  }
7646
7890
  },
7647
7891
  CallExpression(node) {
7648
- if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall(node)) {
7892
+ if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall$1(node)) {
7649
7893
  const assignedName = getAssignedName(node);
7650
7894
  if (assignedName) {
7651
7895
  const programRoot = findProgramRoot(node);
@@ -7692,21 +7936,6 @@ const displayName = defineRule({
7692
7936
  }
7693
7937
  });
7694
7938
  //#endregion
7695
- //#region src/plugin/utils/get-static-property-key-name.ts
7696
- const getStaticPropertyKeyName = (node, options = {}) => {
7697
- if (!isNodeOfType(node, "Property")) return null;
7698
- if (node.computed) {
7699
- if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
7700
- return null;
7701
- }
7702
- if (isNodeOfType(node.key, "Identifier")) return node.key.name;
7703
- if (isNodeOfType(node.key, "Literal")) {
7704
- if (typeof node.key.value === "string") return node.key.value;
7705
- if (options.stringifyNonStringLiterals) return String(node.key.value);
7706
- }
7707
- return null;
7708
- };
7709
- //#endregion
7710
7939
  //#region src/plugin/utils/read-static-boolean.ts
7711
7940
  const readStaticBoolean = (node) => {
7712
7941
  if (!node) return null;
@@ -7715,21 +7944,6 @@ const readStaticBoolean = (node) => {
7715
7944
  return unwrappedNode.value;
7716
7945
  };
7717
7946
  //#endregion
7718
- //#region src/plugin/utils/resolve-const-identifier-alias.ts
7719
- const resolveConstIdentifierAlias = (identifier, scopes) => {
7720
- if (!isNodeOfType(identifier, "Identifier")) return null;
7721
- const visitedSymbolIds = /* @__PURE__ */ new Set();
7722
- let symbol = scopes.symbolFor(identifier);
7723
- while (symbol?.kind === "const") {
7724
- if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
7725
- visitedSymbolIds.add(symbol.id);
7726
- const initializer = stripParenExpression(symbol.initializer);
7727
- if (!isNodeOfType(initializer, "Identifier")) return symbol;
7728
- symbol = scopes.symbolFor(initializer);
7729
- }
7730
- return symbol;
7731
- };
7732
- //#endregion
7733
7947
  //#region src/plugin/utils/is-react-api-call.ts
7734
7948
  const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
7735
7949
  const isImportedFromReact = (symbol) => {
@@ -7737,9 +7951,9 @@ const isImportedFromReact = (symbol) => {
7737
7951
  const importDeclaration = symbol.declarationNode.parent;
7738
7952
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
7739
7953
  };
7740
- const isNamedReactApiImport = (identifier, apiNames, scopes) => {
7954
+ const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
7741
7955
  if (!isNodeOfType(identifier, "Identifier")) return false;
7742
- const symbol = scopes.symbolFor(identifier);
7956
+ const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
7743
7957
  if (!symbol || !isImportedFromReact(symbol)) return false;
7744
7958
  const importedName = getImportedName(symbol.declarationNode);
7745
7959
  return Boolean(importedName && includesApiName(apiNames, importedName));
@@ -7753,7 +7967,7 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
7753
7967
  if (!isNodeOfType(node, "CallExpression")) return false;
7754
7968
  const callee = stripParenExpression(node.callee);
7755
7969
  if (isNodeOfType(callee, "Identifier")) {
7756
- if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
7970
+ if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
7757
7971
  return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
7758
7972
  }
7759
7973
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
@@ -10962,6 +11176,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
10962
11176
  return { CallExpression(node) {
10963
11177
  const hookName = getHookName(node.callee, context.scopes);
10964
11178
  if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
11179
+ if (HOOKS_REQUIRING_DEPS_MATCH.has(hookName) && !isReactApiCall(node, HOOKS_REQUIRING_DEPS_MATCH, context.scopes, {
11180
+ allowGlobalReactNamespace: true,
11181
+ allowUnboundBareCalls: true,
11182
+ resolveNamedAliases: true
11183
+ })) return;
10965
11184
  const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
10966
11185
  const depsArgumentIndex = getDepsArgumentIndex(hookName);
10967
11186
  const callbackArgument = node.arguments[callbackArgumentIndex];
@@ -14256,7 +14475,11 @@ const jsIndexMaps = defineRule({
14256
14475
  //#region src/plugin/utils/are-expressions-structurally-equal.ts
14257
14476
  const areExpressionsStructurallyEqual = (a, b) => {
14258
14477
  if (!a || !b) return a === b;
14478
+ const unwrappedA = stripParenExpression(a);
14479
+ const unwrappedB = stripParenExpression(b);
14480
+ if (unwrappedA !== a || unwrappedB !== b) return areExpressionsStructurallyEqual(unwrappedA, unwrappedB);
14259
14481
  if (a.type !== b.type) return false;
14482
+ if (isNodeOfType(a, "ThisExpression")) return true;
14260
14483
  if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
14261
14484
  if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
14262
14485
  if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
@@ -20799,8 +21022,8 @@ const catchClauseRethrowsCaught = (handler) => {
20799
21022
  return didRethrow;
20800
21023
  };
20801
21024
  //#endregion
20802
- //#region src/plugin/utils/find-guarding-try-statement.ts
20803
- const isImmediatelyInvokedFunctionCallee = (functionNode) => {
21025
+ //#region src/plugin/utils/is-immediately-invoked-function.ts
21026
+ const isImmediatelyInvokedFunction = (functionNode) => {
20804
21027
  let wrappedCallee = functionNode;
20805
21028
  let enclosing = functionNode.parent;
20806
21029
  while (enclosing && stripParenExpression(enclosing) === functionNode) {
@@ -20809,11 +21032,13 @@ const isImmediatelyInvokedFunctionCallee = (functionNode) => {
20809
21032
  }
20810
21033
  return Boolean(enclosing && isNodeOfType(enclosing, "CallExpression") && enclosing.callee === wrappedCallee);
20811
21034
  };
21035
+ //#endregion
21036
+ //#region src/plugin/utils/find-guarding-try-statement.ts
20812
21037
  const findGuardingTryStatement = (node) => {
20813
21038
  let child = node;
20814
21039
  let ancestor = node.parent;
20815
21040
  while (ancestor) {
20816
- if (isFunctionLike$1(ancestor) && !isImmediatelyInvokedFunctionCallee(ancestor)) return null;
21041
+ if (isFunctionLike$1(ancestor) && !isImmediatelyInvokedFunction(ancestor)) return null;
20817
21042
  if (isNodeOfType(ancestor, "TryStatement") && ancestor.block === child && ancestor.handler && !catchClauseRethrowsCaught(ancestor.handler)) return ancestor;
20818
21043
  child = ancestor;
20819
21044
  ancestor = ancestor.parent ?? null;
@@ -21946,6 +22171,8 @@ const DOM_QUERY_MEMBER_NAMES = new Set([
21946
22171
  ]);
21947
22172
  const LAYOUT_MEASUREMENT_MEMBER_NAMES = new Set([
21948
22173
  "current",
22174
+ "textContent",
22175
+ "innerText",
21949
22176
  "scrollWidth",
21950
22177
  "clientWidth",
21951
22178
  "offsetWidth",
@@ -21967,7 +22194,7 @@ const POST_MOUNT_GLOBAL_NAMES = new Set([
21967
22194
  "navigator"
21968
22195
  ]);
21969
22196
  const REF_FACTORY_CALLEE_NAMES = new Set(["useRef", "createRef"]);
21970
- const hasRefLikeName = (name) => name === "ref" || name.endsWith("Ref") || name.endsWith("ref");
22197
+ const hasRefLikeName = (name) => name === "ref" || name.endsWith("Ref") || name.endsWith("ref") || name.endsWith("Node") || name.endsWith("node") || name.endsWith("Element") || name.endsWith("element");
21971
22198
  const isRefFactoryInitializer = (init) => {
21972
22199
  if (!init || !isNodeOfType(init, "CallExpression")) return false;
21973
22200
  const callee = init.callee;
@@ -22944,7 +23171,7 @@ const STORAGE_GLOBAL_NAMES$1 = new Set([
22944
23171
  "localStorage",
22945
23172
  "sessionStorage"
22946
23173
  ]);
22947
- const getStaticMemberName = (node) => {
23174
+ const getStaticMemberName$1 = (node) => {
22948
23175
  if (!isNodeOfType(node, "MemberExpression")) return null;
22949
23176
  if (!node.computed && isNodeOfType(node.property, "Identifier")) return node.property.name;
22950
23177
  if (node.computed && isNodeOfType(node.property, "Literal")) return typeof node.property.value === "string" ? node.property.value : null;
@@ -22959,7 +23186,7 @@ const getCallCalleeName$1 = (callExpression) => {
22959
23186
  if (!isNodeOfType(callExpression, "CallExpression")) return null;
22960
23187
  const callee = stripParenExpression(callExpression.callee);
22961
23188
  if (isNodeOfType(callee, "Identifier")) return callee.name;
22962
- return getStaticMemberName(callee);
23189
+ return getStaticMemberName$1(callee);
22963
23190
  };
22964
23191
  const getFunctionParameters = (functionNode) => isFunctionLike$1(functionNode) ? functionNode.params ?? [] : [];
22965
23192
  const getIdentifierBindingIdentity = (analysis, identifier) => {
@@ -23075,14 +23302,14 @@ const localUseEventPreservesCallback = (analysis, callee) => {
23075
23302
  if (!isNodeOfType(child, "CallExpression")) return;
23076
23303
  const forwardedCallee = stripParenExpression(child.callee);
23077
23304
  if (!isNodeOfType(forwardedCallee, "MemberExpression")) return;
23078
- if (getStaticMemberName(forwardedCallee) !== "current") return;
23305
+ if (getStaticMemberName$1(forwardedCallee) !== "current") return;
23079
23306
  if (!isNodeOfType(forwardedCallee.object, "Identifier")) return;
23080
23307
  const refReference = getRef(analysis, forwardedCallee.object);
23081
23308
  if (!callbackRefDeclarators.find((declarator) => refReference?.resolved?.defs.some((definition) => definition.node === declarator)) || !refReference?.resolved) return;
23082
23309
  if (!refReference.resolved.references.some((candidateReference) => {
23083
23310
  const memberExpression = candidateReference.identifier.parent;
23084
23311
  const assignmentExpression = memberExpression?.parent;
23085
- if (!memberExpression || !isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName(memberExpression) !== "current" || !assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression") || assignmentExpression.left !== memberExpression) return false;
23312
+ if (!memberExpression || !isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName$1(memberExpression) !== "current" || !assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression") || assignmentExpression.left !== memberExpression) return false;
23086
23313
  return getIdentifierBindingIdentity(analysis, assignmentExpression.right) !== callbackBinding;
23087
23314
  })) forwardsCallback = true;
23088
23315
  });
@@ -23107,7 +23334,7 @@ const resolveWrappedCallable = (analysis, node) => {
23107
23334
  return callback && isFunctionLike$1(callback) ? callback : null;
23108
23335
  }
23109
23336
  if (!isNodeOfType(candidate, "MemberExpression")) return null;
23110
- if (getStaticMemberName(candidate) !== "current") return null;
23337
+ if (getStaticMemberName$1(candidate) !== "current") return null;
23111
23338
  if (!isNodeOfType(candidate.object, "Identifier")) return null;
23112
23339
  const reference = getRef(analysis, candidate.object);
23113
23340
  const declarator = reference?.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
@@ -23119,7 +23346,7 @@ const resolveWrappedCallable = (analysis, node) => {
23119
23346
  return Boolean(reference?.resolved?.references.some((candidateReference) => {
23120
23347
  const member = candidateReference.identifier.parent;
23121
23348
  const assignment = member?.parent;
23122
- return Boolean(member && isNodeOfType(member, "MemberExpression") && getStaticMemberName(member) === "current" && assignment && isNodeOfType(assignment, "AssignmentExpression") && assignment.left === member);
23349
+ return Boolean(member && isNodeOfType(member, "MemberExpression") && getStaticMemberName$1(member) === "current" && assignment && isNodeOfType(assignment, "AssignmentExpression") && assignment.left === member);
23123
23350
  })) ? null : initializer;
23124
23351
  };
23125
23352
  const functionInvokesItself = (analysis, functionNode) => {
@@ -23158,7 +23385,7 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilena
23158
23385
  if (!isNodeOfType(child, "CallExpression")) return;
23159
23386
  const callee = stripParenExpression(child.callee);
23160
23387
  const calleeName = getCallCalleeName$1(child);
23161
- const memberName = getStaticMemberName(callee);
23388
+ const memberName = getStaticMemberName$1(callee);
23162
23389
  const isDeferringCall = calleeName !== null && DEFERRING_CALLEE_NAMES.has(calleeName) || memberName !== null && DEFERRING_MEMBER_NAMES.has(memberName);
23163
23390
  const isIteratorCall = memberName !== null && SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(memberName) && isNodeOfType(callee, "MemberExpression");
23164
23391
  const enqueueFrame = (callableNode, argumentsForCallable, isDeferred, introducedBindings, allowWithoutInvocationEvidence = false) => {
@@ -23322,9 +23549,9 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
23322
23549
  const calleeRoot = getMemberRoot(callee);
23323
23550
  const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && !environment.parameterIndices.has(callee.name) && !environment.recursiveNames.has(callee.name) && !environment.shadowedGlobalNames.has(callee.name);
23324
23551
  const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
23325
- const namespaceMemberName = getStaticMemberName(callee);
23552
+ const namespaceMemberName = getStaticMemberName$1(callee);
23326
23553
  const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
23327
- const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
23554
+ const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
23328
23555
  if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
23329
23556
  if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
23330
23557
  return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
@@ -23398,7 +23625,7 @@ const isLocallyConstructedObjectMember = (reference, memberExpression) => isNode
23398
23625
  return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
23399
23626
  }) === true;
23400
23627
  const getUseRefDeclarator = (analysis, memberExpression) => {
23401
- if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName(memberExpression) !== "current" || !isNodeOfType(memberExpression.object, "Identifier")) return null;
23628
+ if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName$1(memberExpression) !== "current" || !isNodeOfType(memberExpression.object, "Identifier")) return null;
23402
23629
  return getRef(analysis, memberExpression.object)?.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator") && isNodeOfType(definitionNode.init, "CallExpression") && getCallCalleeName$1(definitionNode.init) === "useRef") ?? null;
23403
23630
  };
23404
23631
  const collectValueEvidence = (analysis, expression, frame, remainingCallFrames, visitedBindings = /* @__PURE__ */ new Set()) => {
@@ -23467,7 +23694,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23467
23694
  return collectValueEvidence(analysis, initializer.init, frame, remainingCallFrames, visitedBindings);
23468
23695
  }
23469
23696
  if (isNodeOfType(node, "MemberExpression")) {
23470
- if (getStaticMemberName(node) === "current" && isNodeOfType(node.object, "Identifier")) {
23697
+ if (getStaticMemberName$1(node) === "current" && isNodeOfType(node.object, "Identifier")) {
23471
23698
  const refBinding = getRef(analysis, node.object)?.resolved;
23472
23699
  const refDeclarator = useRefDeclarator;
23473
23700
  if (refBinding && refDeclarator && isNodeOfType(refDeclarator, "VariableDeclarator") && isNodeOfType(refDeclarator.init, "CallExpression")) {
@@ -23483,7 +23710,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23483
23710
  if (candidateReference.init) continue;
23484
23711
  const identifier = candidateReference.identifier;
23485
23712
  const member = identifier.parent;
23486
- if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier || getStaticMemberName(member) !== "current") {
23713
+ if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier || getStaticMemberName$1(member) !== "current") {
23487
23714
  evidence.hasUnknownSource = true;
23488
23715
  continue;
23489
23716
  }
@@ -23520,7 +23747,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23520
23747
  const callee = stripParenExpression(node.callee);
23521
23748
  const calleeRoot = getMemberRoot(callee);
23522
23749
  const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(calleeRoot, "Identifier") && PURE_GLOBAL_NAMESPACE_NAMES.has(calleeRoot.name) && getIdentifierBindingIdentity(analysis, calleeRoot) === null;
23523
- const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
23750
+ const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
23524
23751
  if (isPureGlobalCall || isPureMemberTransform) {
23525
23752
  if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
23526
23753
  for (const argument of node.arguments ?? []) {
@@ -23990,6 +24217,26 @@ const STATEFUL_HTML_DESCENDANT_TAGS = new Set([
23990
24217
  "dialog",
23991
24218
  "canvas"
23992
24219
  ]);
24220
+ const STATEFUL_HTML_ATTRIBUTE_NAMES = new Set([
24221
+ "autofocus",
24222
+ "contenteditable",
24223
+ "draggable",
24224
+ "tabindex"
24225
+ ]);
24226
+ const isStaticallyFalseAttributeValue = (attribute) => {
24227
+ if (!isNodeOfType(attribute, "JSXAttribute") || !attribute.value) return false;
24228
+ const value = isNodeOfType(attribute.value, "JSXExpressionContainer") ? attribute.value.expression : attribute.value;
24229
+ return isNodeOfType(value, "Literal") && (value.value === false || value.value === "false");
24230
+ };
24231
+ const hasStatefulHtmlAttribute = (openingElement) => {
24232
+ if (!isNodeOfType(openingElement, "JSXOpeningElement")) return false;
24233
+ return openingElement.attributes.some((attribute) => {
24234
+ if (!isNodeOfType(attribute, "JSXAttribute") || !isNodeOfType(attribute.name, "JSXIdentifier")) return false;
24235
+ const attributeName = attribute.name.name.toLowerCase();
24236
+ if (!STATEFUL_HTML_ATTRIBUTE_NAMES.has(attributeName)) return false;
24237
+ return attributeName === "tabindex" || !isStaticallyFalseAttributeValue(attribute);
24238
+ });
24239
+ };
23993
24240
  const STATEFUL_DESCENDANT_SCAN_BUDGET = 200;
23994
24241
  const rootIdentifierNameOf = (expression) => {
23995
24242
  let object = expression;
@@ -24007,7 +24254,8 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
24007
24254
  budget -= 1;
24008
24255
  const node = stack.pop();
24009
24256
  if (isNodeOfType(node, "JSXElement")) {
24010
- const name = node.openingElement.name;
24257
+ const opening = node.openingElement;
24258
+ const name = opening.name;
24011
24259
  if (name && isNodeOfType(name, "JSXIdentifier")) {
24012
24260
  const tagName = name.name;
24013
24261
  const firstChar = tagName.charCodeAt(0);
@@ -24015,6 +24263,7 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
24015
24263
  if (STATEFUL_HTML_DESCENDANT_TAGS.has(tagName)) return true;
24016
24264
  }
24017
24265
  if (name && isNodeOfType(name, "JSXMemberExpression")) return true;
24266
+ if (hasStatefulHtmlAttribute(opening)) return true;
24018
24267
  const children = node.children ?? [];
24019
24268
  for (const child of children) stack.push(child);
24020
24269
  continue;
@@ -24574,6 +24823,14 @@ const templateHasOuterMemberIdentity = (template, bindingFunction) => {
24574
24823
  }
24575
24824
  return false;
24576
24825
  };
24826
+ const findBareItemNamesReferencedByTemplate = (template, itemNames) => {
24827
+ const referencedItemNames = /* @__PURE__ */ new Set();
24828
+ for (const expression of template.expressions ?? []) {
24829
+ const unwrappedExpression = stripParenExpression(expression);
24830
+ if (isNodeOfType(unwrappedExpression, "Identifier") && itemNames.has(unwrappedExpression.name)) referencedItemNames.add(unwrappedExpression.name);
24831
+ }
24832
+ return referencedItemNames;
24833
+ };
24577
24834
  const forLoopTestReadsDataLength = (test) => {
24578
24835
  let didFindLengthRead = false;
24579
24836
  walkAst(test, (child) => {
@@ -24710,9 +24967,11 @@ const noArrayIndexAsKey = defineRule({
24710
24967
  } else if (STATELESS_HTML_LEAF_TAGS.has(elementName.name)) {
24711
24968
  const jsxElement = openingElement.parent;
24712
24969
  if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
24970
+ const isInlineTextRun = INLINE_TEXT_LEAF_TAGS.has(elementName.name);
24971
+ const primitiveItemNames = keyTemplate ? findBareItemNamesReferencedByTemplate(keyTemplate, itemNames) : EMPTY_NAME_SET;
24713
24972
  if (!containsStatefulDescendant(jsxElement, {
24714
- memberRootNames: INLINE_TEXT_LEAF_TAGS.has(elementName.name) ? itemNames : EMPTY_NAME_SET,
24715
- bareIdentifierNames: derivedNames
24973
+ memberRootNames: isInlineTextRun ? itemNames : EMPTY_NAME_SET,
24974
+ bareIdentifierNames: primitiveItemNames.size > 0 ? new Set([...derivedNames, ...primitiveItemNames]) : derivedNames
24716
24975
  })) return;
24717
24976
  }
24718
24977
  }
@@ -24880,7 +25139,11 @@ const noAsyncEffectCallback = defineRule({
24880
25139
  severity: "warn",
24881
25140
  recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
24882
25141
  create: (context) => ({ CallExpression(node) {
24883
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
25142
+ if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
25143
+ allowGlobalReactNamespace: true,
25144
+ allowUnboundBareCalls: true,
25145
+ resolveNamedAliases: true
25146
+ })) return;
24884
25147
  const callback = getEffectCallback(node);
24885
25148
  if (!callback) return;
24886
25149
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
@@ -26078,6 +26341,19 @@ const noCloneElement = defineRule({
26078
26341
  } })
26079
26342
  });
26080
26343
  //#endregion
26344
+ //#region src/plugin/utils/get-call-method-name.ts
26345
+ /**
26346
+ * Returns the static method name of a call's callee when it's a
26347
+ * non-computed MemberExpression (`obj.method` → `"method"`), or
26348
+ * `null` otherwise. Used by `no-pass-data-to-parent` and
26349
+ * `no-pass-live-state-to-parent` to match the receiver-bound
26350
+ * method-call shape against an allow/block list.
26351
+ */
26352
+ const getCallMethodName = (callee) => {
26353
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
26354
+ return null;
26355
+ };
26356
+ //#endregion
26081
26357
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
26082
26358
  const MESSAGE$31 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
26083
26359
  const CONTEXT_MODULES = [
@@ -26085,23 +26361,35 @@ const CONTEXT_MODULES = [
26085
26361
  "use-context-selector",
26086
26362
  "react-tracked"
26087
26363
  ];
26088
- const isCreateContextCallee = (callee) => {
26364
+ const getSupportedContextImportSource = (symbol) => {
26365
+ if (symbol?.kind !== "import") return null;
26366
+ const importDeclaration = symbol.declarationNode.parent;
26367
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration") || typeof importDeclaration.source.value !== "string" || !CONTEXT_MODULES.includes(importDeclaration.source.value)) return null;
26368
+ return importDeclaration.source.value;
26369
+ };
26370
+ const isSupportedNamespaceSymbol = (symbol) => getSupportedContextImportSource(symbol) !== null && Boolean(symbol && (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")));
26371
+ const isDestructuredCreateContextBinding = (identifier, scopes) => {
26372
+ if (!isNodeOfType(identifier, "Identifier")) return false;
26373
+ const symbol = scopes.symbolFor(identifier);
26374
+ if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
26375
+ const property = symbol.bindingIdentifier.parent;
26376
+ if (!property || !isNodeOfType(property, "Property") || getStaticPropertyKeyName(property, { allowComputedString: true }) !== "createContext") return false;
26377
+ const initializer = stripParenExpression(symbol.initializer);
26378
+ return isNodeOfType(initializer, "Identifier") && isSupportedNamespaceSymbol(resolveConstIdentifierAlias(initializer, scopes));
26379
+ };
26380
+ const isCreateContextCall = (node, scopes) => {
26381
+ const callee = stripParenExpression(node.callee);
26089
26382
  if (isNodeOfType(callee, "Identifier")) {
26090
- const binding = getImportBindingForName(callee, callee.name);
26091
- return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
26383
+ if (isDestructuredCreateContextBinding(callee, scopes)) return true;
26384
+ const symbol = resolveConstIdentifierAlias(callee, scopes);
26385
+ return getSupportedContextImportSource(symbol) !== null && Boolean(symbol && getImportedName(symbol.declarationNode) === "createContext");
26092
26386
  }
26093
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
26094
- const namespaceIdentifier = callee.object;
26095
- const propertyIdentifier = callee.property;
26096
- if (!isNodeOfType(namespaceIdentifier, "Identifier")) return false;
26097
- if (!isNodeOfType(propertyIdentifier, "Identifier")) return false;
26098
- if (propertyIdentifier.name !== "createContext") return false;
26099
- const namespaceName = namespaceIdentifier.name;
26100
- if (isCanonicalReactNamespaceName(namespaceName)) return true;
26101
- const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
26102
- return importSource !== null && CONTEXT_MODULES.includes(importSource);
26103
- }
26104
- return false;
26387
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
26388
+ if ((getCallMethodName(callee) ?? (callee.computed && isNodeOfType(callee.property, "Literal") && typeof callee.property.value === "string" ? callee.property.value : null)) !== "createContext") return false;
26389
+ const receiver = stripParenExpression(callee.object);
26390
+ if (!isNodeOfType(receiver, "Identifier")) return false;
26391
+ if (receiver.name === "React" && scopes.isGlobalReference(receiver)) return true;
26392
+ return isSupportedNamespaceSymbol(resolveConstIdentifierAlias(receiver, scopes));
26105
26393
  };
26106
26394
  const noCreateContextInRender = defineRule({
26107
26395
  id: "no-create-context-in-render",
@@ -26110,7 +26398,7 @@ const noCreateContextInRender = defineRule({
26110
26398
  category: "Correctness",
26111
26399
  recommendation: "Move `createContext(...)` outside the component, to the top level of the file, so it stays the same on every render.",
26112
26400
  create: (context) => ({ CallExpression(node) {
26113
- if (!isCreateContextCallee(node.callee)) return;
26401
+ if (!isCreateContextCall(node, context.scopes)) return;
26114
26402
  const componentOrHookName = enclosingComponentOrHookName(node);
26115
26403
  if (!componentOrHookName) return;
26116
26404
  context.report({
@@ -27345,7 +27633,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
27345
27633
  let ancestor = setStateCall.parent;
27346
27634
  while (ancestor) {
27347
27635
  if (!lifecycleMember) {
27348
- if (FUNCTION_NODE_TYPES$1.has(ancestor.type)) nestedFunctionCount += 1;
27636
+ if (FUNCTION_NODE_TYPES$1.has(ancestor.type) && (!isImmediatelyInvokedFunction(ancestor) || !("async" in ancestor) || ancestor.async === true)) nestedFunctionCount += 1;
27349
27637
  if (isLifecycleMember(ancestor, lifecycleNames)) lifecycleMember = ancestor;
27350
27638
  } else if (isEs5Component(ancestor) || isEs6Component(ancestor)) {
27351
27639
  if (nestedFunctionCount > 1 && !options.disallowInNestedFunctions) return false;
@@ -27547,7 +27835,7 @@ const collectDiffSourceLocalNames = (lifecycleFunction, paramNames) => {
27547
27835
  const body = lifecycleFunction.body;
27548
27836
  if (!body) return derivedNames;
27549
27837
  walkAst(body, (node) => {
27550
- if (FUNCTION_NODE_TYPES.has(node.type)) return false;
27838
+ if (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node)) return false;
27551
27839
  if (!isNodeOfType(node, "VariableDeclarator")) return;
27552
27840
  const init = node.init;
27553
27841
  if (!init) return;
@@ -27557,6 +27845,67 @@ const collectDiffSourceLocalNames = (lifecycleFunction, paramNames) => {
27557
27845
  return derivedNames;
27558
27846
  };
27559
27847
  const isStatefulOperand = (node, paramNames, derivedNames) => referencesAnyName(node, paramNames) || referencesAnyName(node, derivedNames) || containsThisStateOrProps(node);
27848
+ const getStaticMemberName = (node) => {
27849
+ if (!isNodeOfType(node, "MemberExpression") || node.computed === true) return null;
27850
+ return isNodeOfType(node.property, "Identifier") ? node.property.name : null;
27851
+ };
27852
+ const getThisStateFieldName = (node) => {
27853
+ const unwrappedNode = stripParenExpression(node);
27854
+ if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
27855
+ const object = stripParenExpression(unwrappedNode.object);
27856
+ if (!isNodeOfType(object, "MemberExpression") || !isNodeOfType(stripParenExpression(object.object), "ThisExpression") || getStaticMemberName(object) !== "state") return null;
27857
+ return getStaticMemberName(unwrappedNode);
27858
+ };
27859
+ const collectLocalInitializers = (lifecycleFunction) => {
27860
+ const initializers = /* @__PURE__ */ new Map();
27861
+ const body = lifecycleFunction.body;
27862
+ if (!body) return initializers;
27863
+ walkAst(body, (node) => {
27864
+ if (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node)) return false;
27865
+ if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.init) initializers.set(node.id.name, node.init);
27866
+ });
27867
+ return initializers;
27868
+ };
27869
+ const derivesFromPostMountValue = (node, localInitializers, visitedNames = /* @__PURE__ */ new Set()) => {
27870
+ if (readsPostMountValue(node)) return true;
27871
+ const referencedNames = /* @__PURE__ */ new Set();
27872
+ collectReferenceIdentifierNames(node, referencedNames);
27873
+ for (const referencedName of referencedNames) {
27874
+ if (visitedNames.has(referencedName)) continue;
27875
+ const initializer = localInitializers.get(referencedName);
27876
+ if (!initializer) continue;
27877
+ if (derivesFromPostMountValue(initializer, localInitializers, new Set([...visitedNames, referencedName]))) return true;
27878
+ }
27879
+ return false;
27880
+ };
27881
+ const getSetStateFieldValue = (setStateCall, fieldName) => {
27882
+ if (!isNodeOfType(setStateCall, "CallExpression")) return null;
27883
+ const argument = setStateCall.arguments?.[0];
27884
+ if (!argument || !isNodeOfType(argument, "ObjectExpression")) return null;
27885
+ for (const property of argument.properties ?? []) {
27886
+ if (!isNodeOfType(property, "Property") || property.computed === true) continue;
27887
+ if ((isNodeOfType(property.key, "Identifier") && property.key.name || isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" && property.key.value || null) === fieldName) return property.value;
27888
+ }
27889
+ return null;
27890
+ };
27891
+ const isConvergentPostMountGuard = (test, setStateCall, localInitializers) => {
27892
+ let qualifies = false;
27893
+ walkAst(test, (node) => {
27894
+ if (qualifies) return false;
27895
+ if (!isNodeOfType(node, "BinaryExpression") || !EQUALITY_OPERATORS.has(node.operator)) return;
27896
+ const leftFieldName = getThisStateFieldName(node.left);
27897
+ const rightFieldName = getThisStateFieldName(node.right);
27898
+ const fieldName = leftFieldName ?? rightFieldName;
27899
+ const comparedValue = leftFieldName ? node.right : node.left;
27900
+ if (!fieldName || !leftFieldName && !rightFieldName) return;
27901
+ const assignedValue = getSetStateFieldValue(setStateCall, fieldName);
27902
+ if (!assignedValue || !areExpressionsStructurallyEqual(comparedValue, assignedValue)) return;
27903
+ if (!derivesFromPostMountValue(comparedValue, localInitializers)) return;
27904
+ qualifies = true;
27905
+ return false;
27906
+ });
27907
+ return qualifies;
27908
+ };
27560
27909
  const isDiffGuardTest = (test, paramNames, derivedNames) => {
27561
27910
  if (referencesAnyName(test, paramNames)) return true;
27562
27911
  let qualifies = false;
@@ -27564,7 +27913,7 @@ const isDiffGuardTest = (test, paramNames, derivedNames) => {
27564
27913
  if (qualifies) return false;
27565
27914
  if (!isNodeOfType(node, "BinaryExpression")) return;
27566
27915
  if (!EQUALITY_OPERATORS.has(node.operator)) return;
27567
- if (isStatefulOperand(node.left, paramNames, derivedNames) && isStatefulOperand(node.right, paramNames, derivedNames)) {
27916
+ if (isStatefulOperand(node.left, paramNames, derivedNames) && isStatefulOperand(node.right, paramNames, derivedNames) && (referencesAnyName(node.left, derivedNames) || referencesAnyName(node.right, derivedNames))) {
27568
27917
  qualifies = true;
27569
27918
  return false;
27570
27919
  }
@@ -27577,11 +27926,12 @@ const isInsideDiffGuard = (setStateCall) => {
27577
27926
  const paramNames = /* @__PURE__ */ new Set();
27578
27927
  for (const param of lifecycleFunction.params ?? []) collectPatternNames(param, paramNames);
27579
27928
  const derivedNames = collectDiffSourceLocalNames(lifecycleFunction, paramNames);
27929
+ const localInitializers = collectLocalInitializers(lifecycleFunction);
27580
27930
  let child = setStateCall;
27581
27931
  let ancestor = setStateCall.parent;
27582
27932
  while (ancestor && ancestor !== lifecycleFunction) {
27583
27933
  const guardTest = isNodeOfType(ancestor, "IfStatement") && child !== ancestor.test && ancestor.test || isNodeOfType(ancestor, "ConditionalExpression") && child !== ancestor.test && ancestor.test || isNodeOfType(ancestor, "LogicalExpression") && ancestor.operator === "&&" && child === ancestor.right && ancestor.left || null;
27584
- if (guardTest && isDiffGuardTest(guardTest, paramNames, derivedNames)) return true;
27934
+ if (guardTest && (isDiffGuardTest(guardTest, paramNames, derivedNames) || isConvergentPostMountGuard(guardTest, setStateCall, localInitializers))) return true;
27585
27935
  child = ancestor;
27586
27936
  ancestor = ancestor.parent ?? null;
27587
27937
  }
@@ -30099,7 +30449,11 @@ const noFetchInEffect = defineRule({
30099
30449
  severity: "warn",
30100
30450
  recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
30101
30451
  create: (context) => ({ CallExpression(node) {
30102
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
30452
+ if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
30453
+ allowGlobalReactNamespace: true,
30454
+ allowUnboundBareCalls: true,
30455
+ resolveNamedAliases: true
30456
+ })) return;
30103
30457
  const callback = getEffectCallback(node);
30104
30458
  if (!callback) return;
30105
30459
  const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
@@ -34309,19 +34663,6 @@ const DATA_SINK_METHOD_NAMES = new Set([
34309
34663
  "deserialize"
34310
34664
  ]);
34311
34665
  //#endregion
34312
- //#region src/plugin/utils/get-call-method-name.ts
34313
- /**
34314
- * Returns the static method name of a call's callee when it's a
34315
- * non-computed MemberExpression (`obj.method` → `"method"`), or
34316
- * `null` otherwise. Used by `no-pass-data-to-parent` and
34317
- * `no-pass-live-state-to-parent` to match the receiver-bound
34318
- * method-call shape against an allow/block list.
34319
- */
34320
- const getCallMethodName = (callee) => {
34321
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
34322
- return null;
34323
- };
34324
- //#endregion
34325
34666
  //#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
34326
34667
  const isUseStateIdentifier = (identifier) => {
34327
34668
  if (!isNodeOfType(identifier, "Identifier")) return false;
@@ -34947,6 +35288,30 @@ const guardsRenderShape = (comparison) => {
34947
35288
  }
34948
35289
  return false;
34949
35290
  };
35291
+ const isPropsChildrenLength = (node, scopes) => {
35292
+ const unwrappedNode = stripParenExpression(node);
35293
+ return isNodeOfType(unwrappedNode, "MemberExpression") && !unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier") && unwrappedNode.property.name === "length" && resolvesToPropsChildren(stripParenExpression(unwrappedNode.object), scopes);
35294
+ };
35295
+ const isLargeTextLengthComparison = (node, scopes) => {
35296
+ const unwrappedNode = stripParenExpression(node);
35297
+ if (!isNodeOfType(unwrappedNode, "BinaryExpression")) return false;
35298
+ const leftIsLength = isPropsChildrenLength(unwrappedNode.left, scopes);
35299
+ const rightIsLength = isPropsChildrenLength(unwrappedNode.right, scopes);
35300
+ const thresholdNode = leftIsLength ? unwrappedNode.right : unwrappedNode.left;
35301
+ if (!leftIsLength && !rightIsLength || !isNodeOfType(thresholdNode, "Literal")) return false;
35302
+ if (typeof thresholdNode.value !== "number" || thresholdNode.value < 1e3) return false;
35303
+ return leftIsLength ? unwrappedNode.operator === ">" || unwrappedNode.operator === ">=" : unwrappedNode.operator === "<" || unwrappedNode.operator === "<=";
35304
+ };
35305
+ const isLargeStringOptimizationGuard = (comparison, scopes) => {
35306
+ let current = findTransparentExpressionRoot(comparison);
35307
+ while (current.parent) {
35308
+ const parent = current.parent;
35309
+ if (!isNodeOfType(parent, "LogicalExpression") || parent.operator !== "&&") return false;
35310
+ if (isLargeTextLengthComparison(parent.left === current ? parent.right : parent.left, scopes)) return true;
35311
+ current = findTransparentExpressionRoot(parent);
35312
+ }
35313
+ return false;
35314
+ };
34950
35315
  const noPolymorphicChildren = defineRule({
34951
35316
  id: "no-polymorphic-children",
34952
35317
  title: "Children type checked at runtime",
@@ -34960,6 +35325,7 @@ const noPolymorphicChildren = defineRule({
34960
35325
  const isStringLiteral = (operand) => isNodeOfType(operand, "Literal") && operand.value === "string";
34961
35326
  if (!isStringLiteral(node.left) && !isStringLiteral(node.right)) return;
34962
35327
  if (!guardsRenderShape(node)) return;
35328
+ if (isLargeStringOptimizationGuard(node, context.scopes)) return;
34963
35329
  context.report({
34964
35330
  node,
34965
35331
  message: "Your users hit inconsistent behavior because `typeof children === \"string\"` makes this component switch on what callers pass, so add clear subcomponents like `<Button.Text>` instead."
@@ -35177,6 +35543,27 @@ const hasPreviousValueDep = (effectNode, depElements) => {
35177
35543
  }
35178
35544
  return false;
35179
35545
  };
35546
+ const isStateLikeDependency = (analysis, element, isPropName) => {
35547
+ if (!isNodeOfType(element, "Identifier") || isPropName(element.name)) return false;
35548
+ if (!analysis) return true;
35549
+ const reference = getRef(analysis, element);
35550
+ if (!reference) return true;
35551
+ const upstreamReferences = getUpstreamRefs(analysis, reference);
35552
+ if (upstreamReferences.some((upstreamReference) => isState(analysis, upstreamReference))) return true;
35553
+ return !upstreamReferences.some((upstreamReference) => isProp(analysis, upstreamReference));
35554
+ };
35555
+ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
35556
+ const callee = stripParenExpression(callExpression.callee);
35557
+ if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "current") return null;
35558
+ const receiver = stripParenExpression(callee.object);
35559
+ if (!isNodeOfType(receiver, "Identifier")) return null;
35560
+ const binding = findVariableInitializer(callExpression, receiver.name);
35561
+ if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
35562
+ if (getCalleeName$2(binding.initializer) !== "useRef") return null;
35563
+ const callbackArgument = binding.initializer.arguments?.[0];
35564
+ if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
35565
+ return isPropName(callbackArgument.name) ? callbackArgument.name : null;
35566
+ };
35180
35567
  const noPropCallbackInEffect = defineRule({
35181
35568
  id: "no-prop-callback-in-effect",
35182
35569
  title: "Parent kept in sync with a callback effect",
@@ -35193,11 +35580,11 @@ const noPropCallbackInEffect = defineRule({
35193
35580
  if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
35194
35581
  const depsNode = node.arguments[1];
35195
35582
  if (!isNodeOfType(depsNode, "ArrayExpression") || !depsNode.elements?.length) return;
35196
- const stateLikeDeps = (depsNode.elements ?? []).filter((element) => isNodeOfType(element, "Identifier") && !propStackTracker.isPropName(element.name));
35583
+ const analysis = getProgramAnalysis(node);
35584
+ const stateLikeDeps = (depsNode.elements ?? []).filter((element) => element && isStateLikeDependency(analysis, element, propStackTracker.isPropName));
35197
35585
  if (stateLikeDeps.length === 0) return;
35198
35586
  if (isRefLatchGuardedEffect(callback.body)) return;
35199
35587
  if (hasPreviousValueDep(node, depsNode.elements ?? [])) return;
35200
- const analysis = getProgramAnalysis(node);
35201
35588
  if (analysis) {
35202
35589
  const stateLikeDepRefs = [];
35203
35590
  for (const element of stateLikeDeps) {
@@ -35209,9 +35596,9 @@ const noPropCallbackInEffect = defineRule({
35209
35596
  const reportedNodes = /* @__PURE__ */ new Set();
35210
35597
  walkInsideStatementBlocks(callback.body, (child) => {
35211
35598
  if (!isNodeOfType(child, "CallExpression")) return;
35212
- if (!isNodeOfType(child.callee, "Identifier")) return;
35213
- const calleeName = child.callee.name;
35214
- if (!propStackTracker.isPropName(calleeName)) return;
35599
+ const directCallee = stripParenExpression(child.callee);
35600
+ const calleeName = isNodeOfType(directCallee, "Identifier") && propStackTracker.isPropName(directCallee.name) && directCallee.name || getRefHeldPropCallbackName(child, propStackTracker.isPropName);
35601
+ if (!calleeName) return;
35215
35602
  if (!isResultDiscardedCall(child)) return;
35216
35603
  if (reportedNodes.has(child)) return;
35217
35604
  reportedNodes.add(child);
@@ -43052,15 +43439,6 @@ const isStableHookWrapperArgument = (node) => {
43052
43439
  const parent = node.parent;
43053
43440
  return isNodeOfType(parent, "CallExpression") && isNodeOfType(parent.callee, "Identifier") && STABLE_HOOK_WRAPPERS.has(parent.callee.name) && Boolean(parent.arguments?.some((argument) => argument === node));
43054
43441
  };
43055
- const isImmediatelyInvokedFunction = (functionNode) => {
43056
- let wrappedCallee = functionNode;
43057
- let enclosing = functionNode.parent;
43058
- while (enclosing && stripParenExpression(enclosing) === functionNode) {
43059
- wrappedCallee = enclosing;
43060
- enclosing = enclosing.parent ?? null;
43061
- }
43062
- return Boolean(enclosing && isNodeOfType(enclosing, "CallExpression") && enclosing.callee === wrappedCallee);
43063
- };
43064
43442
  const queryStableQueryClient = defineRule({
43065
43443
  id: "query-stable-query-client",
43066
43444
  title: "Unstable QueryClient in component",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.5-dev.76cd6be",
3
+ "version": "0.7.5-dev.bc49aaa",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",