oxlint-plugin-react-doctor 0.7.5-dev.20cd922 → 0.7.5-dev.22bb155

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 +551 -138
  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;
@@ -7468,7 +7712,7 @@ const isCreateClassLikeCall = (node) => {
7468
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";
@@ -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;
@@ -8856,6 +9070,30 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
8856
9070
  }
8857
9071
  return matchingBlocks.size > 0;
8858
9072
  };
9073
+ const doMatchingNodesCoverEveryPathFromFunctionEntry = (owner, matchingNodes, context) => {
9074
+ const functionCfg = context.cfg.cfgFor(owner);
9075
+ if (!functionCfg) return false;
9076
+ const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
9077
+ if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
9078
+ const matchingBlock = functionCfg.blockOf(matchingNode);
9079
+ return matchingBlock ? [matchingBlock] : [];
9080
+ }));
9081
+ if (matchingBlocks.size === 0) return false;
9082
+ const visitedBlocks = new Set([functionCfg.entry]);
9083
+ const pendingBlocks = [functionCfg.entry];
9084
+ while (pendingBlocks.length > 0) {
9085
+ const currentBlock = pendingBlocks.pop();
9086
+ if (!currentBlock) break;
9087
+ if (matchingBlocks.has(currentBlock)) continue;
9088
+ for (const edge of currentBlock.successors) {
9089
+ if (edge.to === functionCfg.exit) return false;
9090
+ if (visitedBlocks.has(edge.to)) continue;
9091
+ visitedBlocks.add(edge.to);
9092
+ pendingBlocks.push(edge.to);
9093
+ }
9094
+ }
9095
+ return true;
9096
+ };
8859
9097
  const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
8860
9098
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8861
9099
  if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
@@ -9056,6 +9294,83 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
9056
9294
  });
9057
9295
  return didCleanupFunctionMatch;
9058
9296
  };
9297
+ const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
9298
+ const callExpression = stripParenExpression(expression);
9299
+ if (!isNodeOfType(callExpression, "CallExpression")) return false;
9300
+ const bindCallee = stripParenExpression(callExpression.callee);
9301
+ if (!isNodeOfType(bindCallee, "MemberExpression") || bindCallee.computed || !isNodeOfType(bindCallee.property, "Identifier") || bindCallee.property.name !== "bind") return false;
9302
+ const releaseMember = stripParenExpression(bindCallee.object);
9303
+ if (!isNodeOfType(releaseMember, "MemberExpression") || releaseMember.computed || !isNodeOfType(releaseMember.property, "Identifier")) return false;
9304
+ const releaseReceiverKey = resolveExpressionKey$1(releaseMember.object, context);
9305
+ if (releaseReceiverKey === null || releaseReceiverKey !== resolveExpressionKey$1(callExpression.arguments?.[0], context)) return false;
9306
+ const releaseVerbName = releaseMember.property.name;
9307
+ if (usage.kind === "socket") return usage.handleKey === releaseReceiverKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
9308
+ return usage.kind === "subscribe" && usage.handleKey === releaseReceiverKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName));
9309
+ };
9310
+ const callbackReturnsCleanupForUsage = (callback, usage, context) => {
9311
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
9312
+ const doesReturnedValueReleaseUsage = (returnedValue) => {
9313
+ if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) return true;
9314
+ const cleanupFunction = resolveStableValue(returnedValue, context);
9315
+ if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) return true;
9316
+ return Boolean(cleanupFunction && isFunctionLike$1(cleanupFunction) && doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context));
9317
+ };
9318
+ if (!isNodeOfType(callback.body, "BlockStatement")) return doesReturnedValueReleaseUsage(stripParenExpression(callback.body));
9319
+ const matchingCleanupReturns = [];
9320
+ walkInsideStatementBlocks(callback.body, (child) => {
9321
+ if (isNodeOfType(child, "ReturnStatement") && child.argument && doesReturnedValueReleaseUsage(stripParenExpression(child.argument))) matchingCleanupReturns.push(child);
9322
+ });
9323
+ return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
9324
+ };
9325
+ const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
9326
+ if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
9327
+ const functionCfg = context.cfg.cfgFor(callback);
9328
+ const usageBlock = functionCfg?.blockOf(usage.node);
9329
+ const usageStart = getRangeStart(usage.node);
9330
+ if (!functionCfg || !usageBlock || usageStart === null) return false;
9331
+ const findHandleGuard = (releaseCall) => {
9332
+ if (usage.handleKey === null) return null;
9333
+ let ancestor = releaseCall.parent;
9334
+ while (ancestor && ancestor !== callback.body) {
9335
+ if (isNodeOfType(ancestor, "IfStatement")) return ancestor.alternate === null && resolveExpressionKey$1(ancestor.test, context) === usage.handleKey && getRangeStart(ancestor) !== null && (getRangeStart(ancestor) ?? usageStart) < usageStart ? ancestor : null;
9336
+ ancestor = ancestor.parent;
9337
+ }
9338
+ return null;
9339
+ };
9340
+ const matchingReleaseAnchors = [];
9341
+ walkInsideStatementBlocks(callback.body, (child) => {
9342
+ if (!isNodeOfType(child, "CallExpression")) return;
9343
+ const releaseStart = getRangeStart(child);
9344
+ const handleGuard = findHandleGuard(child);
9345
+ if (releaseStart === null || releaseStart >= usageStart || functionCfg.blockOf(child) !== usageBlock && !handleGuard) return;
9346
+ if (doesReleaseCallMatchUsage(child, usage, context)) {
9347
+ matchingReleaseAnchors.push(handleGuard ?? child);
9348
+ return;
9349
+ }
9350
+ const helperFunction = resolveStableValue(child.callee, context);
9351
+ if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context)) matchingReleaseAnchors.push(handleGuard ?? child);
9352
+ });
9353
+ return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingReleaseAnchors, context);
9354
+ };
9355
+ const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
9356
+ const componentFunction = findEnclosingFunction(callback);
9357
+ if (!componentFunction || !isNodeOfType(componentFunction, "ArrowFunctionExpression") && !isNodeOfType(componentFunction, "FunctionExpression") && !isNodeOfType(componentFunction, "FunctionDeclaration")) return false;
9358
+ let didFindUnmountCleanup = false;
9359
+ walkAst(componentFunction.body, (child) => {
9360
+ if (didFindUnmountCleanup) return false;
9361
+ if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction(child) !== componentFunction) return;
9362
+ if (!isHookCall$2(child, CLEANUP_EFFECT_HOOK_NAMES)) return;
9363
+ const dependencyList = child.arguments?.[1];
9364
+ if (!isNodeOfType(dependencyList, "ArrayExpression") || dependencyList.elements.length > 0) return;
9365
+ const cleanupCallback = getEffectCallback(child);
9366
+ if (cleanupCallback && cleanupCallback !== callback && callbackReturnsCleanupForUsage(cleanupCallback, usage, context)) {
9367
+ didFindUnmountCleanup = true;
9368
+ return false;
9369
+ }
9370
+ });
9371
+ return didFindUnmountCleanup;
9372
+ };
9373
+ const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
9059
9374
  const effectHasCleanupForUsage = (callback, usage, context) => {
9060
9375
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
9061
9376
  if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
@@ -9068,6 +9383,10 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9068
9383
  if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
9069
9384
  const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
9070
9385
  if (!returnedValue) return;
9386
+ if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) {
9387
+ matchingCleanupReturns.push(child);
9388
+ return;
9389
+ }
9071
9390
  if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
9072
9391
  matchingCleanupReturns.push(child);
9073
9392
  return;
@@ -9083,13 +9402,17 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9083
9402
  if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
9084
9403
  }
9085
9404
  const cleanupFunction = resolveStableValue(returnedValue, context);
9405
+ if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) {
9406
+ matchingCleanupReturns.push(child);
9407
+ return;
9408
+ }
9086
9409
  if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
9087
9410
  if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
9088
9411
  });
9089
9412
  return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
9090
9413
  };
9091
9414
  const findFirstUsageWithoutCleanup = (callback, usages, context) => {
9092
- for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
9415
+ for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context) && !hasSplitLifecycleCleanup(callback, usage, context)) return usage;
9093
9416
  return null;
9094
9417
  };
9095
9418
  const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
@@ -10962,6 +11285,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
10962
11285
  return { CallExpression(node) {
10963
11286
  const hookName = getHookName(node.callee, context.scopes);
10964
11287
  if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
11288
+ if (HOOKS_REQUIRING_DEPS_MATCH.has(hookName) && !isReactApiCall(node, HOOKS_REQUIRING_DEPS_MATCH, context.scopes, {
11289
+ allowGlobalReactNamespace: true,
11290
+ allowUnboundBareCalls: true,
11291
+ resolveNamedAliases: true
11292
+ })) return;
10965
11293
  const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
10966
11294
  const depsArgumentIndex = getDepsArgumentIndex(hookName);
10967
11295
  const callbackArgument = node.arguments[callbackArgumentIndex];
@@ -22889,61 +23217,73 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
22889
23217
  "Array",
22890
23218
  "BigInt",
22891
23219
  "Boolean",
23220
+ "encodeURIComponent",
22892
23221
  "Number",
22893
23222
  "Object",
22894
23223
  "String",
22895
23224
  "parseFloat",
22896
- "parseInt"
23225
+ "parseInt",
23226
+ "structuredClone"
23227
+ ]);
23228
+ const PURE_GLOBAL_CONSTRUCTOR_NAMES = new Set(["Date", "Set"]);
23229
+ const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([
23230
+ ["Array", new Set(["from"])],
23231
+ ["JSON", new Set([
23232
+ "isRawJSON",
23233
+ "parse",
23234
+ "rawJSON",
23235
+ "stringify"
23236
+ ])],
23237
+ ["Math", new Set([
23238
+ "abs",
23239
+ "acos",
23240
+ "acosh",
23241
+ "asin",
23242
+ "asinh",
23243
+ "atan",
23244
+ "atan2",
23245
+ "atanh",
23246
+ "cbrt",
23247
+ "ceil",
23248
+ "clz32",
23249
+ "cos",
23250
+ "cosh",
23251
+ "exp",
23252
+ "floor",
23253
+ "fround",
23254
+ "hypot",
23255
+ "imul",
23256
+ "log",
23257
+ "log10",
23258
+ "log1p",
23259
+ "log2",
23260
+ "max",
23261
+ "min",
23262
+ "pow",
23263
+ "round",
23264
+ "sign",
23265
+ "sin",
23266
+ "sinh",
23267
+ "sqrt",
23268
+ "tan",
23269
+ "tanh",
23270
+ "trunc"
23271
+ ])],
23272
+ ["Object", new Set(["assign"])]
22897
23273
  ]);
22898
- const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
22899
- const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
22900
- "isRawJSON",
22901
- "parse",
22902
- "rawJSON",
22903
- "stringify"
22904
- ])], ["Math", new Set([
22905
- "abs",
22906
- "acos",
22907
- "acosh",
22908
- "asin",
22909
- "asinh",
22910
- "atan",
22911
- "atan2",
22912
- "atanh",
22913
- "cbrt",
22914
- "ceil",
22915
- "clz32",
22916
- "cos",
22917
- "cosh",
22918
- "exp",
22919
- "floor",
22920
- "fround",
22921
- "hypot",
22922
- "imul",
22923
- "log",
22924
- "log10",
22925
- "log1p",
22926
- "log2",
22927
- "max",
22928
- "min",
22929
- "pow",
22930
- "round",
22931
- "sign",
22932
- "sin",
22933
- "sinh",
22934
- "sqrt",
22935
- "tan",
22936
- "tanh",
22937
- "trunc"
22938
- ])]]);
22939
23274
  const PURE_MEMBER_TRANSFORM_NAMES = new Set([
22940
23275
  "concat",
22941
23276
  "filter",
23277
+ "flatMap",
22942
23278
  "join",
22943
23279
  "map",
23280
+ "reduce",
23281
+ "replace",
23282
+ "slice",
22944
23283
  "split",
22945
23284
  "toLowerCase",
22946
23285
  "toString",
23286
+ "toSorted",
22947
23287
  "toUpperCase",
22948
23288
  "trim"
22949
23289
  ]);
@@ -23325,6 +23665,11 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
23325
23665
  const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
23326
23666
  return callbackSummary.isValid && !callbackSummary.canContinue;
23327
23667
  }
23668
+ if (isNodeOfType(node, "NewExpression")) {
23669
+ const callee = stripParenExpression(node.callee);
23670
+ if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || environment.parameterIndices.has(callee.name) || environment.recursiveNames.has(callee.name) || environment.shadowedGlobalNames.has(callee.name)) return false;
23671
+ return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
23672
+ }
23328
23673
  if (isNodeOfType(node, "CallExpression")) {
23329
23674
  const callee = stripParenExpression(node.callee);
23330
23675
  const calleeRoot = getMemberRoot(callee);
@@ -23527,7 +23872,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23527
23872
  }
23528
23873
  const callee = stripParenExpression(node.callee);
23529
23874
  const calleeRoot = getMemberRoot(callee);
23530
- 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;
23875
+ const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(callee, "MemberExpression") && isNodeOfType(calleeRoot, "Identifier") && getIdentifierBindingIdentity(analysis, calleeRoot) === null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(calleeRoot.name)?.has(getStaticMemberName$1(callee) ?? "") === true;
23531
23876
  const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
23532
23877
  if (isPureGlobalCall || isPureMemberTransform) {
23533
23878
  if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
@@ -23580,6 +23925,15 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23580
23925
  }
23581
23926
  return evidence;
23582
23927
  }
23928
+ if (isNodeOfType(node, "NewExpression")) {
23929
+ const callee = stripParenExpression(node.callee);
23930
+ if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || getIdentifierBindingIdentity(analysis, callee) !== null) {
23931
+ evidence.hasUnknownSource = true;
23932
+ return evidence;
23933
+ }
23934
+ for (const argument of node.arguments ?? []) mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
23935
+ return evidence;
23936
+ }
23583
23937
  if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
23584
23938
  evidence.hasUnknownSource = true;
23585
23939
  return evidence;
@@ -24920,7 +25274,11 @@ const noAsyncEffectCallback = defineRule({
24920
25274
  severity: "warn",
24921
25275
  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.",
24922
25276
  create: (context) => ({ CallExpression(node) {
24923
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
25277
+ if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
25278
+ allowGlobalReactNamespace: true,
25279
+ allowUnboundBareCalls: true,
25280
+ resolveNamedAliases: true
25281
+ })) return;
24924
25282
  const callback = getEffectCallback(node);
24925
25283
  if (!callback) return;
24926
25284
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
@@ -26118,6 +26476,19 @@ const noCloneElement = defineRule({
26118
26476
  } })
26119
26477
  });
26120
26478
  //#endregion
26479
+ //#region src/plugin/utils/get-call-method-name.ts
26480
+ /**
26481
+ * Returns the static method name of a call's callee when it's a
26482
+ * non-computed MemberExpression (`obj.method` → `"method"`), or
26483
+ * `null` otherwise. Used by `no-pass-data-to-parent` and
26484
+ * `no-pass-live-state-to-parent` to match the receiver-bound
26485
+ * method-call shape against an allow/block list.
26486
+ */
26487
+ const getCallMethodName = (callee) => {
26488
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
26489
+ return null;
26490
+ };
26491
+ //#endregion
26121
26492
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
26122
26493
  const MESSAGE$31 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
26123
26494
  const CONTEXT_MODULES = [
@@ -26125,23 +26496,35 @@ const CONTEXT_MODULES = [
26125
26496
  "use-context-selector",
26126
26497
  "react-tracked"
26127
26498
  ];
26128
- const isCreateContextCallee = (callee) => {
26499
+ const getSupportedContextImportSource = (symbol) => {
26500
+ if (symbol?.kind !== "import") return null;
26501
+ const importDeclaration = symbol.declarationNode.parent;
26502
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration") || typeof importDeclaration.source.value !== "string" || !CONTEXT_MODULES.includes(importDeclaration.source.value)) return null;
26503
+ return importDeclaration.source.value;
26504
+ };
26505
+ const isSupportedNamespaceSymbol = (symbol) => getSupportedContextImportSource(symbol) !== null && Boolean(symbol && (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")));
26506
+ const isDestructuredCreateContextBinding = (identifier, scopes) => {
26507
+ if (!isNodeOfType(identifier, "Identifier")) return false;
26508
+ const symbol = scopes.symbolFor(identifier);
26509
+ if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
26510
+ const property = symbol.bindingIdentifier.parent;
26511
+ if (!property || !isNodeOfType(property, "Property") || getStaticPropertyKeyName(property, { allowComputedString: true }) !== "createContext") return false;
26512
+ const initializer = stripParenExpression(symbol.initializer);
26513
+ return isNodeOfType(initializer, "Identifier") && isSupportedNamespaceSymbol(resolveConstIdentifierAlias(initializer, scopes));
26514
+ };
26515
+ const isCreateContextCall = (node, scopes) => {
26516
+ const callee = stripParenExpression(node.callee);
26129
26517
  if (isNodeOfType(callee, "Identifier")) {
26130
- const binding = getImportBindingForName(callee, callee.name);
26131
- return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
26518
+ if (isDestructuredCreateContextBinding(callee, scopes)) return true;
26519
+ const symbol = resolveConstIdentifierAlias(callee, scopes);
26520
+ return getSupportedContextImportSource(symbol) !== null && Boolean(symbol && getImportedName(symbol.declarationNode) === "createContext");
26132
26521
  }
26133
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
26134
- const namespaceIdentifier = callee.object;
26135
- const propertyIdentifier = callee.property;
26136
- if (!isNodeOfType(namespaceIdentifier, "Identifier")) return false;
26137
- if (!isNodeOfType(propertyIdentifier, "Identifier")) return false;
26138
- if (propertyIdentifier.name !== "createContext") return false;
26139
- const namespaceName = namespaceIdentifier.name;
26140
- if (isCanonicalReactNamespaceName(namespaceName)) return true;
26141
- const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
26142
- return importSource !== null && CONTEXT_MODULES.includes(importSource);
26143
- }
26144
- return false;
26522
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
26523
+ if ((getCallMethodName(callee) ?? (callee.computed && isNodeOfType(callee.property, "Literal") && typeof callee.property.value === "string" ? callee.property.value : null)) !== "createContext") return false;
26524
+ const receiver = stripParenExpression(callee.object);
26525
+ if (!isNodeOfType(receiver, "Identifier")) return false;
26526
+ if (receiver.name === "React" && scopes.isGlobalReference(receiver)) return true;
26527
+ return isSupportedNamespaceSymbol(resolveConstIdentifierAlias(receiver, scopes));
26145
26528
  };
26146
26529
  const noCreateContextInRender = defineRule({
26147
26530
  id: "no-create-context-in-render",
@@ -26150,7 +26533,7 @@ const noCreateContextInRender = defineRule({
26150
26533
  category: "Correctness",
26151
26534
  recommendation: "Move `createContext(...)` outside the component, to the top level of the file, so it stays the same on every render.",
26152
26535
  create: (context) => ({ CallExpression(node) {
26153
- if (!isCreateContextCallee(node.callee)) return;
26536
+ if (!isCreateContextCall(node, context.scopes)) return;
26154
26537
  const componentOrHookName = enclosingComponentOrHookName(node);
26155
26538
  if (!componentOrHookName) return;
26156
26539
  context.report({
@@ -28100,20 +28483,31 @@ const noDocumentStartViewTransition = defineRule({
28100
28483
  } })
28101
28484
  });
28102
28485
  //#endregion
28486
+ //#region src/plugin/utils/get-static-property-name.ts
28487
+ const getStaticPropertyName = (memberExpression) => {
28488
+ const property = memberExpression.property;
28489
+ if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
28490
+ if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
28491
+ if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
28492
+ return null;
28493
+ };
28494
+ //#endregion
28103
28495
  //#region src/plugin/rules/js-performance/no-document-write.ts
28104
28496
  const MESSAGE$24 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
28105
28497
  const WRITE_METHODS = new Set(["write", "writeln"]);
28498
+ const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
28106
28499
  const noDocumentWrite = defineRule({
28107
28500
  id: "no-document-write",
28108
28501
  title: "document.write/writeln",
28109
28502
  severity: "warn",
28110
28503
  recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
28111
28504
  create: (context) => ({ CallExpression(node) {
28112
- const callee = node.callee;
28113
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
28505
+ const callee = stripParenExpression(node.callee);
28506
+ if (!isNodeOfType(callee, "MemberExpression")) return;
28114
28507
  const receiver = stripParenExpression(callee.object);
28115
- if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
28116
- if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
28508
+ if (!isNodeOfType(receiver, "Identifier") || !isGlobalDocumentReference(receiver, context)) return;
28509
+ const methodName = getStaticPropertyName(callee);
28510
+ if (methodName === null || !WRITE_METHODS.has(methodName)) return;
28117
28511
  context.report({
28118
28512
  node,
28119
28513
  message: MESSAGE$24
@@ -28920,6 +29314,14 @@ const noEffectWithFreshDeps = defineRule({
28920
29314
  //#endregion
28921
29315
  //#region src/plugin/rules/security/no-eval.ts
28922
29316
  const SANDBOX_SURFACE_PATH_PATTERN = /(?:^|[/-])sandbox(?:$|[/-])|(?:^|\/)[\w.]+-sandbox(?:\/|\.[cm]?[jt]sx?$)/i;
29317
+ const getExecutableGlobalName = (node, context) => {
29318
+ const expression = stripParenExpression(node);
29319
+ if (isNodeOfType(expression, "Identifier")) return context.scopes.isGlobalReference(expression) ? expression.name : null;
29320
+ if (!isNodeOfType(expression, "MemberExpression")) return null;
29321
+ const receiver = stripParenExpression(expression.object);
29322
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "globalThis" || !context.scopes.isGlobalReference(receiver)) return null;
29323
+ return getStaticPropertyName(expression);
29324
+ };
28923
29325
  const noEval = defineRule({
28924
29326
  id: "no-eval",
28925
29327
  title: "eval() runs untrusted code strings",
@@ -28929,20 +29331,21 @@ const noEval = defineRule({
28929
29331
  if (SANDBOX_SURFACE_PATH_PATTERN.test(normalizeFilename(context.filename ?? ""))) return {};
28930
29332
  return {
28931
29333
  CallExpression(node) {
28932
- if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "eval") {
29334
+ const executableGlobalName = getExecutableGlobalName(node.callee, context);
29335
+ if (executableGlobalName === "eval") {
28933
29336
  context.report({
28934
29337
  node,
28935
29338
  message: "eval() is a code-injection vulnerability: it runs any string as code."
28936
29339
  });
28937
29340
  return;
28938
29341
  }
28939
- if (isNodeOfType(node.callee, "Identifier") && (node.callee.name === "setTimeout" || node.callee.name === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
29342
+ if ((executableGlobalName === "setTimeout" || executableGlobalName === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
28940
29343
  node,
28941
- message: `Passing a string to ${node.callee.name}() is a code-injection vulnerability, since it runs that string as code.`
29344
+ message: `Passing a string to ${executableGlobalName}() is a code-injection vulnerability, since it runs that string as code.`
28942
29345
  });
28943
29346
  },
28944
29347
  NewExpression(node) {
28945
- if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "Function") {
29348
+ if (getExecutableGlobalName(node.callee, context) === "Function") {
28946
29349
  const onlyArgument = node.arguments.length === 1 ? node.arguments[0] : void 0;
28947
29350
  if (isNodeOfType(onlyArgument, "Literal") && typeof onlyArgument.value === "string" && onlyArgument.value.trim() === "return this") return;
28948
29351
  context.report({
@@ -30201,7 +30604,11 @@ const noFetchInEffect = defineRule({
30201
30604
  severity: "warn",
30202
30605
  recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
30203
30606
  create: (context) => ({ CallExpression(node) {
30204
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
30607
+ if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
30608
+ allowGlobalReactNamespace: true,
30609
+ allowUnboundBareCalls: true,
30610
+ resolveNamedAliases: true
30611
+ })) return;
30205
30612
  const callback = getEffectCallback(node);
30206
30613
  if (!callback) return;
30207
30614
  const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
@@ -34411,19 +34818,6 @@ const DATA_SINK_METHOD_NAMES = new Set([
34411
34818
  "deserialize"
34412
34819
  ]);
34413
34820
  //#endregion
34414
- //#region src/plugin/utils/get-call-method-name.ts
34415
- /**
34416
- * Returns the static method name of a call's callee when it's a
34417
- * non-computed MemberExpression (`obj.method` → `"method"`), or
34418
- * `null` otherwise. Used by `no-pass-data-to-parent` and
34419
- * `no-pass-live-state-to-parent` to match the receiver-bound
34420
- * method-call shape against an allow/block list.
34421
- */
34422
- const getCallMethodName = (callee) => {
34423
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
34424
- return null;
34425
- };
34426
- //#endregion
34427
34821
  //#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
34428
34822
  const isUseStateIdentifier = (identifier) => {
34429
34823
  if (!isNodeOfType(identifier, "Identifier")) return false;
@@ -35049,6 +35443,30 @@ const guardsRenderShape = (comparison) => {
35049
35443
  }
35050
35444
  return false;
35051
35445
  };
35446
+ const isPropsChildrenLength = (node, scopes) => {
35447
+ const unwrappedNode = stripParenExpression(node);
35448
+ return isNodeOfType(unwrappedNode, "MemberExpression") && !unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier") && unwrappedNode.property.name === "length" && resolvesToPropsChildren(stripParenExpression(unwrappedNode.object), scopes);
35449
+ };
35450
+ const isLargeTextLengthComparison = (node, scopes) => {
35451
+ const unwrappedNode = stripParenExpression(node);
35452
+ if (!isNodeOfType(unwrappedNode, "BinaryExpression")) return false;
35453
+ const leftIsLength = isPropsChildrenLength(unwrappedNode.left, scopes);
35454
+ const rightIsLength = isPropsChildrenLength(unwrappedNode.right, scopes);
35455
+ const thresholdNode = leftIsLength ? unwrappedNode.right : unwrappedNode.left;
35456
+ if (!leftIsLength && !rightIsLength || !isNodeOfType(thresholdNode, "Literal")) return false;
35457
+ if (typeof thresholdNode.value !== "number" || thresholdNode.value < 1e3) return false;
35458
+ return leftIsLength ? unwrappedNode.operator === ">" || unwrappedNode.operator === ">=" : unwrappedNode.operator === "<" || unwrappedNode.operator === "<=";
35459
+ };
35460
+ const isLargeStringOptimizationGuard = (comparison, scopes) => {
35461
+ let current = findTransparentExpressionRoot(comparison);
35462
+ while (current.parent) {
35463
+ const parent = current.parent;
35464
+ if (!isNodeOfType(parent, "LogicalExpression") || parent.operator !== "&&") return false;
35465
+ if (isLargeTextLengthComparison(parent.left === current ? parent.right : parent.left, scopes)) return true;
35466
+ current = findTransparentExpressionRoot(parent);
35467
+ }
35468
+ return false;
35469
+ };
35052
35470
  const noPolymorphicChildren = defineRule({
35053
35471
  id: "no-polymorphic-children",
35054
35472
  title: "Children type checked at runtime",
@@ -35062,6 +35480,7 @@ const noPolymorphicChildren = defineRule({
35062
35480
  const isStringLiteral = (operand) => isNodeOfType(operand, "Literal") && operand.value === "string";
35063
35481
  if (!isStringLiteral(node.left) && !isStringLiteral(node.right)) return;
35064
35482
  if (!guardsRenderShape(node)) return;
35483
+ if (isLargeStringOptimizationGuard(node, context.scopes)) return;
35065
35484
  context.report({
35066
35485
  node,
35067
35486
  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."
@@ -54487,12 +54906,6 @@ const webhookSignatureRisk = defineRule({
54487
54906
  //#region src/plugin/rules/zod/utils/zod-ast.ts
54488
54907
  const ZOD_MODULE = "zod";
54489
54908
  const ZOD_MODULE_SOURCES = [ZOD_MODULE];
54490
- const getStaticPropertyName = (member) => {
54491
- const property = member.property;
54492
- if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
54493
- if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
54494
- return null;
54495
- };
54496
54909
  const importInfoCache = /* @__PURE__ */ new WeakMap();
54497
54910
  const getImportInfoForIdentifier = (identifier) => {
54498
54911
  if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.5-dev.20cd922",
3
+ "version": "0.7.5-dev.22bb155",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",