oxlint-plugin-react-doctor 0.7.5-dev.21da48f → 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 +524 -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
@@ -5933,6 +5934,21 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
5933
5934
  }
5934
5935
  });
5935
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
5936
5952
  //#region src/plugin/utils/is-presentation-role.ts
5937
5953
  const isPresentationRole = (openingElement) => {
5938
5954
  const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
@@ -5977,6 +5993,21 @@ const isPureEventBlockerHandler = (attribute) => {
5977
5993
  return false;
5978
5994
  };
5979
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
5980
6011
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
5981
6012
  const MESSAGE$57 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
5982
6013
  const KEY_HANDLERS = [
@@ -5987,6 +6018,63 @@ const KEY_HANDLERS = [
5987
6018
  "onKeyDownCapture",
5988
6019
  "onKeyPressCapture"
5989
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
+ };
5990
6078
  const FOCUSLESS_CONTAINER_TAGS = new Set([
5991
6079
  "tr",
5992
6080
  "td",
@@ -6034,7 +6122,10 @@ const isFocusForwardingFunctionBody = (body) => {
6034
6122
  };
6035
6123
  const resolveHandlerFunction = (attribute) => {
6036
6124
  if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
6037
- let expression = attribute.value.expression;
6125
+ return resolveHandlerFunctionExpression(attribute.value.expression);
6126
+ };
6127
+ const resolveHandlerFunctionExpression = (handlerExpression) => {
6128
+ let expression = handlerExpression;
6038
6129
  if (isNodeOfType(expression, "Identifier")) {
6039
6130
  const binding = findVariableInitializer(expression, expression.name);
6040
6131
  if (!binding?.initializer) return null;
@@ -6133,18 +6224,22 @@ const clickEventsHaveKeyEvents = defineRule({
6133
6224
  if (!HTML_TAGS.has(tag)) return;
6134
6225
  if (tag === "label") return;
6135
6226
  if (!FOCUSLESS_CONTAINER_TAGS.has(tag) && isInteractiveElement(tag, node)) return;
6227
+ const spreadEventValues = getTransparentSpreadEventValues(node.attributes, context.scopes);
6228
+ if (!spreadEventValues) return;
6136
6229
  const onClick = hasJsxPropIgnoreCase(node.attributes, "onClick") ?? hasJsxPropIgnoreCase(node.attributes, "onClickCapture");
6137
- if (!onClick) return;
6138
- if (isPureEventBlockerHandler(onClick)) return;
6139
- if (isFocusForwardingHandler(onClick)) return;
6140
- 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;
6141
6236
  if (hasCompositeItemRole(node)) return;
6142
6237
  if (isHoverSelectionListItem(tag, node)) return;
6143
- if (isBackdropDismissHandler(onClick)) return;
6238
+ if (onClick && isBackdropDismissHandler(onClick)) return;
6144
6239
  if (containsKeyboardActivatableDescendant(node.parent)) return;
6145
6240
  if (isHiddenFromScreenReader(node, context.settings)) return;
6146
6241
  if (isPresentationRole(node)) return;
6147
- 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;
6148
6243
  context.report({
6149
6244
  node: node.name,
6150
6245
  message: MESSAGE$57
@@ -6870,11 +6965,10 @@ const getTemplateInterpolations = (valueTail) => {
6870
6965
  const interpolations = valueTail.slice(1, closingBacktickIndex).match(/\$\{[^}]*\}/g);
6871
6966
  return interpolations === null ? "" : interpolations.join(" ");
6872
6967
  };
6873
- const getIdentifierDeclarationInitializer = (identifier, fileContent) => {
6874
- const declarationMatch = new RegExp(`(?:const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]{0,120})?=\\s*`).exec(fileContent);
6875
- if (declarationMatch === null) return null;
6876
- const initializerStartIndex = declarationMatch.index + declarationMatch[0].length;
6877
- 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);
6878
6972
  };
6879
6973
  const isPureStringLiteralConcat = (initializerText) => {
6880
6974
  if (!/^["']/.test(initializerText)) return false;
@@ -6944,6 +7038,153 @@ const isAllOperandsDomContentConcat = (valueExpression) => {
6944
7038
  return !HTML_TAINT_PATTERN.test(withoutCast.replace(DOM_CONTENT_SOURCE_VALUE_PATTERN, ""));
6945
7039
  });
6946
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
+ };
6947
7188
  const dangerousHtmlSink = defineRule({
6948
7189
  id: "dangerous-html-sink",
6949
7190
  title: "HTML injection sink with dynamic content",
@@ -6970,6 +7211,7 @@ const dangerousHtmlSink = defineRule({
6970
7211
  const valueTail = fullValueTail.slice(0, VALUE_EXPRESSION_MAX_CHARS);
6971
7212
  const terminatorIndex = valueTail.search(/[;}]/);
6972
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);
6973
7215
  if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
6974
7216
  if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
6975
7217
  if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
@@ -6981,7 +7223,7 @@ const dangerousHtmlSink = defineRule({
6981
7223
  let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
6982
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;
6983
7225
  if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
6984
- const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, file.content);
7226
+ const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
6985
7227
  if (declarationInitializer !== null) {
6986
7228
  if (isPureStringLiteralConcat(declarationInitializer)) continue;
6987
7229
  templateInterpolations = getTemplateInterpolations(declarationInitializer);
@@ -6992,7 +7234,7 @@ const dangerousHtmlSink = defineRule({
6992
7234
  if (SANITIZER_PATTERN.test(judgedExpression)) continue;
6993
7235
  if (ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
6994
7236
  if (I18N_VALUE_PATTERN.test(judgedExpression)) continue;
6995
- if (!HTML_TAINT_PATTERN.test(judgedExpression)) continue;
7237
+ if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set())) continue;
6996
7238
  if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
6997
7239
  if (/highlighted/i.test(valueExpression)) continue;
6998
7240
  if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
@@ -7470,7 +7712,7 @@ const isCreateClassLikeCall = (node) => {
7470
7712
  if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$2(callee) === "createClass";
7471
7713
  return false;
7472
7714
  };
7473
- const isCreateContextCall = (node) => {
7715
+ const isCreateContextCall$1 = (node) => {
7474
7716
  if (!isNodeOfType(node, "CallExpression")) return false;
7475
7717
  const callee = node.callee;
7476
7718
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
@@ -7647,7 +7889,7 @@ const displayName = defineRule({
7647
7889
  }
7648
7890
  },
7649
7891
  CallExpression(node) {
7650
- if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall(node)) {
7892
+ if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall$1(node)) {
7651
7893
  const assignedName = getAssignedName(node);
7652
7894
  if (assignedName) {
7653
7895
  const programRoot = findProgramRoot(node);
@@ -7694,21 +7936,6 @@ const displayName = defineRule({
7694
7936
  }
7695
7937
  });
7696
7938
  //#endregion
7697
- //#region src/plugin/utils/get-static-property-key-name.ts
7698
- const getStaticPropertyKeyName = (node, options = {}) => {
7699
- if (!isNodeOfType(node, "Property")) return null;
7700
- if (node.computed) {
7701
- if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
7702
- return null;
7703
- }
7704
- if (isNodeOfType(node.key, "Identifier")) return node.key.name;
7705
- if (isNodeOfType(node.key, "Literal")) {
7706
- if (typeof node.key.value === "string") return node.key.value;
7707
- if (options.stringifyNonStringLiterals) return String(node.key.value);
7708
- }
7709
- return null;
7710
- };
7711
- //#endregion
7712
7939
  //#region src/plugin/utils/read-static-boolean.ts
7713
7940
  const readStaticBoolean = (node) => {
7714
7941
  if (!node) return null;
@@ -7717,21 +7944,6 @@ const readStaticBoolean = (node) => {
7717
7944
  return unwrappedNode.value;
7718
7945
  };
7719
7946
  //#endregion
7720
- //#region src/plugin/utils/resolve-const-identifier-alias.ts
7721
- const resolveConstIdentifierAlias = (identifier, scopes) => {
7722
- if (!isNodeOfType(identifier, "Identifier")) return null;
7723
- const visitedSymbolIds = /* @__PURE__ */ new Set();
7724
- let symbol = scopes.symbolFor(identifier);
7725
- while (symbol?.kind === "const") {
7726
- if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
7727
- visitedSymbolIds.add(symbol.id);
7728
- const initializer = stripParenExpression(symbol.initializer);
7729
- if (!isNodeOfType(initializer, "Identifier")) return symbol;
7730
- symbol = scopes.symbolFor(initializer);
7731
- }
7732
- return symbol;
7733
- };
7734
- //#endregion
7735
7947
  //#region src/plugin/utils/is-react-api-call.ts
7736
7948
  const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
7737
7949
  const isImportedFromReact = (symbol) => {
@@ -7739,9 +7951,9 @@ const isImportedFromReact = (symbol) => {
7739
7951
  const importDeclaration = symbol.declarationNode.parent;
7740
7952
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
7741
7953
  };
7742
- const isNamedReactApiImport = (identifier, apiNames, scopes) => {
7954
+ const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
7743
7955
  if (!isNodeOfType(identifier, "Identifier")) return false;
7744
- const symbol = scopes.symbolFor(identifier);
7956
+ const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
7745
7957
  if (!symbol || !isImportedFromReact(symbol)) return false;
7746
7958
  const importedName = getImportedName(symbol.declarationNode);
7747
7959
  return Boolean(importedName && includesApiName(apiNames, importedName));
@@ -7755,7 +7967,7 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
7755
7967
  if (!isNodeOfType(node, "CallExpression")) return false;
7756
7968
  const callee = stripParenExpression(node.callee);
7757
7969
  if (isNodeOfType(callee, "Identifier")) {
7758
- if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
7970
+ if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
7759
7971
  return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
7760
7972
  }
7761
7973
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
@@ -8858,6 +9070,30 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
8858
9070
  }
8859
9071
  return matchingBlocks.size > 0;
8860
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
+ };
8861
9097
  const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
8862
9098
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
8863
9099
  if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
@@ -9058,6 +9294,83 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
9058
9294
  });
9059
9295
  return didCleanupFunctionMatch;
9060
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);
9061
9374
  const effectHasCleanupForUsage = (callback, usage, context) => {
9062
9375
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
9063
9376
  if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
@@ -9070,6 +9383,10 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9070
9383
  if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
9071
9384
  const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
9072
9385
  if (!returnedValue) return;
9386
+ if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) {
9387
+ matchingCleanupReturns.push(child);
9388
+ return;
9389
+ }
9073
9390
  if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
9074
9391
  matchingCleanupReturns.push(child);
9075
9392
  return;
@@ -9085,13 +9402,17 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9085
9402
  if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
9086
9403
  }
9087
9404
  const cleanupFunction = resolveStableValue(returnedValue, context);
9405
+ if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) {
9406
+ matchingCleanupReturns.push(child);
9407
+ return;
9408
+ }
9088
9409
  if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
9089
9410
  if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
9090
9411
  });
9091
9412
  return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
9092
9413
  };
9093
9414
  const findFirstUsageWithoutCleanup = (callback, usages, context) => {
9094
- 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;
9095
9416
  return null;
9096
9417
  };
9097
9418
  const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
@@ -10964,6 +11285,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
10964
11285
  return { CallExpression(node) {
10965
11286
  const hookName = getHookName(node.callee, context.scopes);
10966
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;
10967
11293
  const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
10968
11294
  const depsArgumentIndex = getDepsArgumentIndex(hookName);
10969
11295
  const callbackArgument = node.arguments[callbackArgumentIndex];
@@ -22891,61 +23217,73 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
22891
23217
  "Array",
22892
23218
  "BigInt",
22893
23219
  "Boolean",
23220
+ "encodeURIComponent",
22894
23221
  "Number",
22895
23222
  "Object",
22896
23223
  "String",
22897
23224
  "parseFloat",
22898
- "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"])]
22899
23273
  ]);
22900
- const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
22901
- const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
22902
- "isRawJSON",
22903
- "parse",
22904
- "rawJSON",
22905
- "stringify"
22906
- ])], ["Math", new Set([
22907
- "abs",
22908
- "acos",
22909
- "acosh",
22910
- "asin",
22911
- "asinh",
22912
- "atan",
22913
- "atan2",
22914
- "atanh",
22915
- "cbrt",
22916
- "ceil",
22917
- "clz32",
22918
- "cos",
22919
- "cosh",
22920
- "exp",
22921
- "floor",
22922
- "fround",
22923
- "hypot",
22924
- "imul",
22925
- "log",
22926
- "log10",
22927
- "log1p",
22928
- "log2",
22929
- "max",
22930
- "min",
22931
- "pow",
22932
- "round",
22933
- "sign",
22934
- "sin",
22935
- "sinh",
22936
- "sqrt",
22937
- "tan",
22938
- "tanh",
22939
- "trunc"
22940
- ])]]);
22941
23274
  const PURE_MEMBER_TRANSFORM_NAMES = new Set([
22942
23275
  "concat",
22943
23276
  "filter",
23277
+ "flatMap",
22944
23278
  "join",
22945
23279
  "map",
23280
+ "reduce",
23281
+ "replace",
23282
+ "slice",
22946
23283
  "split",
22947
23284
  "toLowerCase",
22948
23285
  "toString",
23286
+ "toSorted",
22949
23287
  "toUpperCase",
22950
23288
  "trim"
22951
23289
  ]);
@@ -23327,6 +23665,11 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
23327
23665
  const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
23328
23666
  return callbackSummary.isValid && !callbackSummary.canContinue;
23329
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
+ }
23330
23673
  if (isNodeOfType(node, "CallExpression")) {
23331
23674
  const callee = stripParenExpression(node.callee);
23332
23675
  const calleeRoot = getMemberRoot(callee);
@@ -23529,7 +23872,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23529
23872
  }
23530
23873
  const callee = stripParenExpression(node.callee);
23531
23874
  const calleeRoot = getMemberRoot(callee);
23532
- 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;
23533
23876
  const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
23534
23877
  if (isPureGlobalCall || isPureMemberTransform) {
23535
23878
  if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
@@ -23582,6 +23925,15 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23582
23925
  }
23583
23926
  return evidence;
23584
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
+ }
23585
23937
  if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
23586
23938
  evidence.hasUnknownSource = true;
23587
23939
  return evidence;
@@ -24922,7 +25274,11 @@ const noAsyncEffectCallback = defineRule({
24922
25274
  severity: "warn",
24923
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.",
24924
25276
  create: (context) => ({ CallExpression(node) {
24925
- 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;
24926
25282
  const callback = getEffectCallback(node);
24927
25283
  if (!callback) return;
24928
25284
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
@@ -26120,6 +26476,19 @@ const noCloneElement = defineRule({
26120
26476
  } })
26121
26477
  });
26122
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
26123
26492
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
26124
26493
  const MESSAGE$31 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
26125
26494
  const CONTEXT_MODULES = [
@@ -26127,23 +26496,35 @@ const CONTEXT_MODULES = [
26127
26496
  "use-context-selector",
26128
26497
  "react-tracked"
26129
26498
  ];
26130
- 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);
26131
26517
  if (isNodeOfType(callee, "Identifier")) {
26132
- const binding = getImportBindingForName(callee, callee.name);
26133
- 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");
26134
26521
  }
26135
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
26136
- const namespaceIdentifier = callee.object;
26137
- const propertyIdentifier = callee.property;
26138
- if (!isNodeOfType(namespaceIdentifier, "Identifier")) return false;
26139
- if (!isNodeOfType(propertyIdentifier, "Identifier")) return false;
26140
- if (propertyIdentifier.name !== "createContext") return false;
26141
- const namespaceName = namespaceIdentifier.name;
26142
- if (isCanonicalReactNamespaceName(namespaceName)) return true;
26143
- const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
26144
- return importSource !== null && CONTEXT_MODULES.includes(importSource);
26145
- }
26146
- 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));
26147
26528
  };
26148
26529
  const noCreateContextInRender = defineRule({
26149
26530
  id: "no-create-context-in-render",
@@ -26152,7 +26533,7 @@ const noCreateContextInRender = defineRule({
26152
26533
  category: "Correctness",
26153
26534
  recommendation: "Move `createContext(...)` outside the component, to the top level of the file, so it stays the same on every render.",
26154
26535
  create: (context) => ({ CallExpression(node) {
26155
- if (!isCreateContextCallee(node.callee)) return;
26536
+ if (!isCreateContextCall(node, context.scopes)) return;
26156
26537
  const componentOrHookName = enclosingComponentOrHookName(node);
26157
26538
  if (!componentOrHookName) return;
26158
26539
  context.report({
@@ -28102,20 +28483,31 @@ const noDocumentStartViewTransition = defineRule({
28102
28483
  } })
28103
28484
  });
28104
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
28105
28495
  //#region src/plugin/rules/js-performance/no-document-write.ts
28106
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.";
28107
28497
  const WRITE_METHODS = new Set(["write", "writeln"]);
28498
+ const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
28108
28499
  const noDocumentWrite = defineRule({
28109
28500
  id: "no-document-write",
28110
28501
  title: "document.write/writeln",
28111
28502
  severity: "warn",
28112
28503
  recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
28113
28504
  create: (context) => ({ CallExpression(node) {
28114
- const callee = node.callee;
28115
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
28505
+ const callee = stripParenExpression(node.callee);
28506
+ if (!isNodeOfType(callee, "MemberExpression")) return;
28116
28507
  const receiver = stripParenExpression(callee.object);
28117
- if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
28118
- 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;
28119
28511
  context.report({
28120
28512
  node,
28121
28513
  message: MESSAGE$24
@@ -28922,6 +29314,14 @@ const noEffectWithFreshDeps = defineRule({
28922
29314
  //#endregion
28923
29315
  //#region src/plugin/rules/security/no-eval.ts
28924
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
+ };
28925
29325
  const noEval = defineRule({
28926
29326
  id: "no-eval",
28927
29327
  title: "eval() runs untrusted code strings",
@@ -28931,20 +29331,21 @@ const noEval = defineRule({
28931
29331
  if (SANDBOX_SURFACE_PATH_PATTERN.test(normalizeFilename(context.filename ?? ""))) return {};
28932
29332
  return {
28933
29333
  CallExpression(node) {
28934
- if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "eval") {
29334
+ const executableGlobalName = getExecutableGlobalName(node.callee, context);
29335
+ if (executableGlobalName === "eval") {
28935
29336
  context.report({
28936
29337
  node,
28937
29338
  message: "eval() is a code-injection vulnerability: it runs any string as code."
28938
29339
  });
28939
29340
  return;
28940
29341
  }
28941
- 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({
28942
29343
  node,
28943
- 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.`
28944
29345
  });
28945
29346
  },
28946
29347
  NewExpression(node) {
28947
- if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "Function") {
29348
+ if (getExecutableGlobalName(node.callee, context) === "Function") {
28948
29349
  const onlyArgument = node.arguments.length === 1 ? node.arguments[0] : void 0;
28949
29350
  if (isNodeOfType(onlyArgument, "Literal") && typeof onlyArgument.value === "string" && onlyArgument.value.trim() === "return this") return;
28950
29351
  context.report({
@@ -30203,7 +30604,11 @@ const noFetchInEffect = defineRule({
30203
30604
  severity: "warn",
30204
30605
  recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
30205
30606
  create: (context) => ({ CallExpression(node) {
30206
- 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;
30207
30612
  const callback = getEffectCallback(node);
30208
30613
  if (!callback) return;
30209
30614
  const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
@@ -34413,19 +34818,6 @@ const DATA_SINK_METHOD_NAMES = new Set([
34413
34818
  "deserialize"
34414
34819
  ]);
34415
34820
  //#endregion
34416
- //#region src/plugin/utils/get-call-method-name.ts
34417
- /**
34418
- * Returns the static method name of a call's callee when it's a
34419
- * non-computed MemberExpression (`obj.method` → `"method"`), or
34420
- * `null` otherwise. Used by `no-pass-data-to-parent` and
34421
- * `no-pass-live-state-to-parent` to match the receiver-bound
34422
- * method-call shape against an allow/block list.
34423
- */
34424
- const getCallMethodName = (callee) => {
34425
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
34426
- return null;
34427
- };
34428
- //#endregion
34429
34821
  //#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
34430
34822
  const isUseStateIdentifier = (identifier) => {
34431
34823
  if (!isNodeOfType(identifier, "Identifier")) return false;
@@ -54514,12 +54906,6 @@ const webhookSignatureRisk = defineRule({
54514
54906
  //#region src/plugin/rules/zod/utils/zod-ast.ts
54515
54907
  const ZOD_MODULE = "zod";
54516
54908
  const ZOD_MODULE_SOURCES = [ZOD_MODULE];
54517
- const getStaticPropertyName = (member) => {
54518
- const property = member.property;
54519
- if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
54520
- if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
54521
- return null;
54522
- };
54523
54909
  const importInfoCache = /* @__PURE__ */ new WeakMap();
54524
54910
  const getImportInfoForIdentifier = (identifier) => {
54525
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.21da48f",
3
+ "version": "0.7.5-dev.22bb155",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",