oxlint-plugin-react-doctor 0.7.5-dev.654849 → 0.7.5-dev.70eff9a

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 +331 -96
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5934,6 +5934,21 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
5934
5934
  }
5935
5935
  });
5936
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
5937
5952
  //#region src/plugin/utils/is-presentation-role.ts
5938
5953
  const isPresentationRole = (openingElement) => {
5939
5954
  const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
@@ -5978,6 +5993,21 @@ const isPureEventBlockerHandler = (attribute) => {
5978
5993
  return false;
5979
5994
  };
5980
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
5981
6011
  //#region src/plugin/rules/a11y/click-events-have-key-events.ts
5982
6012
  const MESSAGE$57 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
5983
6013
  const KEY_HANDLERS = [
@@ -5988,6 +6018,63 @@ const KEY_HANDLERS = [
5988
6018
  "onKeyDownCapture",
5989
6019
  "onKeyPressCapture"
5990
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
+ };
5991
6078
  const FOCUSLESS_CONTAINER_TAGS = new Set([
5992
6079
  "tr",
5993
6080
  "td",
@@ -6035,7 +6122,10 @@ const isFocusForwardingFunctionBody = (body) => {
6035
6122
  };
6036
6123
  const resolveHandlerFunction = (attribute) => {
6037
6124
  if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
6038
- let expression = attribute.value.expression;
6125
+ return resolveHandlerFunctionExpression(attribute.value.expression);
6126
+ };
6127
+ const resolveHandlerFunctionExpression = (handlerExpression) => {
6128
+ let expression = handlerExpression;
6039
6129
  if (isNodeOfType(expression, "Identifier")) {
6040
6130
  const binding = findVariableInitializer(expression, expression.name);
6041
6131
  if (!binding?.initializer) return null;
@@ -6134,18 +6224,22 @@ const clickEventsHaveKeyEvents = defineRule({
6134
6224
  if (!HTML_TAGS.has(tag)) return;
6135
6225
  if (tag === "label") return;
6136
6226
  if (!FOCUSLESS_CONTAINER_TAGS.has(tag) && isInteractiveElement(tag, node)) return;
6227
+ const spreadEventValues = getTransparentSpreadEventValues(node.attributes, context.scopes);
6228
+ if (!spreadEventValues) return;
6137
6229
  const onClick = hasJsxPropIgnoreCase(node.attributes, "onClick") ?? hasJsxPropIgnoreCase(node.attributes, "onClickCapture");
6138
- if (!onClick) return;
6139
- if (isPureEventBlockerHandler(onClick)) return;
6140
- if (isFocusForwardingHandler(onClick)) return;
6141
- 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;
6142
6236
  if (hasCompositeItemRole(node)) return;
6143
6237
  if (isHoverSelectionListItem(tag, node)) return;
6144
- if (isBackdropDismissHandler(onClick)) return;
6238
+ if (onClick && isBackdropDismissHandler(onClick)) return;
6145
6239
  if (containsKeyboardActivatableDescendant(node.parent)) return;
6146
6240
  if (isHiddenFromScreenReader(node, context.settings)) return;
6147
6241
  if (isPresentationRole(node)) return;
6148
- 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;
6149
6243
  context.report({
6150
6244
  node: node.name,
6151
6245
  message: MESSAGE$57
@@ -7842,21 +7936,6 @@ const displayName = defineRule({
7842
7936
  }
7843
7937
  });
7844
7938
  //#endregion
7845
- //#region src/plugin/utils/get-static-property-key-name.ts
7846
- const getStaticPropertyKeyName = (node, options = {}) => {
7847
- if (!isNodeOfType(node, "Property")) return null;
7848
- if (node.computed) {
7849
- if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
7850
- return null;
7851
- }
7852
- if (isNodeOfType(node.key, "Identifier")) return node.key.name;
7853
- if (isNodeOfType(node.key, "Literal")) {
7854
- if (typeof node.key.value === "string") return node.key.value;
7855
- if (options.stringifyNonStringLiterals) return String(node.key.value);
7856
- }
7857
- return null;
7858
- };
7859
- //#endregion
7860
7939
  //#region src/plugin/utils/read-static-boolean.ts
7861
7940
  const readStaticBoolean = (node) => {
7862
7941
  if (!node) return null;
@@ -7865,21 +7944,6 @@ const readStaticBoolean = (node) => {
7865
7944
  return unwrappedNode.value;
7866
7945
  };
7867
7946
  //#endregion
7868
- //#region src/plugin/utils/resolve-const-identifier-alias.ts
7869
- const resolveConstIdentifierAlias = (identifier, scopes) => {
7870
- if (!isNodeOfType(identifier, "Identifier")) return null;
7871
- const visitedSymbolIds = /* @__PURE__ */ new Set();
7872
- let symbol = scopes.symbolFor(identifier);
7873
- while (symbol?.kind === "const") {
7874
- if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
7875
- visitedSymbolIds.add(symbol.id);
7876
- const initializer = stripParenExpression(symbol.initializer);
7877
- if (!isNodeOfType(initializer, "Identifier")) return symbol;
7878
- symbol = scopes.symbolFor(initializer);
7879
- }
7880
- return symbol;
7881
- };
7882
- //#endregion
7883
7947
  //#region src/plugin/utils/is-react-api-call.ts
7884
7948
  const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
7885
7949
  const isImportedFromReact = (symbol) => {
@@ -9006,6 +9070,30 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
9006
9070
  }
9007
9071
  return matchingBlocks.size > 0;
9008
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
+ };
9009
9097
  const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
9010
9098
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
9011
9099
  if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
@@ -9206,6 +9294,83 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
9206
9294
  });
9207
9295
  return didCleanupFunctionMatch;
9208
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);
9209
9374
  const effectHasCleanupForUsage = (callback, usage, context) => {
9210
9375
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
9211
9376
  if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
@@ -9218,6 +9383,10 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9218
9383
  if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
9219
9384
  const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
9220
9385
  if (!returnedValue) return;
9386
+ if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) {
9387
+ matchingCleanupReturns.push(child);
9388
+ return;
9389
+ }
9221
9390
  if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
9222
9391
  matchingCleanupReturns.push(child);
9223
9392
  return;
@@ -9233,13 +9402,17 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
9233
9402
  if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
9234
9403
  }
9235
9404
  const cleanupFunction = resolveStableValue(returnedValue, context);
9405
+ if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) {
9406
+ matchingCleanupReturns.push(child);
9407
+ return;
9408
+ }
9236
9409
  if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
9237
9410
  if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
9238
9411
  });
9239
9412
  return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
9240
9413
  };
9241
9414
  const findFirstUsageWithoutCleanup = (callback, usages, context) => {
9242
- 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;
9243
9416
  return null;
9244
9417
  };
9245
9418
  const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
@@ -23044,61 +23217,73 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
23044
23217
  "Array",
23045
23218
  "BigInt",
23046
23219
  "Boolean",
23220
+ "encodeURIComponent",
23047
23221
  "Number",
23048
23222
  "Object",
23049
23223
  "String",
23050
23224
  "parseFloat",
23051
- "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"])]
23052
23273
  ]);
23053
- const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
23054
- const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
23055
- "isRawJSON",
23056
- "parse",
23057
- "rawJSON",
23058
- "stringify"
23059
- ])], ["Math", new Set([
23060
- "abs",
23061
- "acos",
23062
- "acosh",
23063
- "asin",
23064
- "asinh",
23065
- "atan",
23066
- "atan2",
23067
- "atanh",
23068
- "cbrt",
23069
- "ceil",
23070
- "clz32",
23071
- "cos",
23072
- "cosh",
23073
- "exp",
23074
- "floor",
23075
- "fround",
23076
- "hypot",
23077
- "imul",
23078
- "log",
23079
- "log10",
23080
- "log1p",
23081
- "log2",
23082
- "max",
23083
- "min",
23084
- "pow",
23085
- "round",
23086
- "sign",
23087
- "sin",
23088
- "sinh",
23089
- "sqrt",
23090
- "tan",
23091
- "tanh",
23092
- "trunc"
23093
- ])]]);
23094
23274
  const PURE_MEMBER_TRANSFORM_NAMES = new Set([
23095
23275
  "concat",
23096
23276
  "filter",
23277
+ "flatMap",
23097
23278
  "join",
23098
23279
  "map",
23280
+ "reduce",
23281
+ "replace",
23282
+ "slice",
23099
23283
  "split",
23100
23284
  "toLowerCase",
23101
23285
  "toString",
23286
+ "toSorted",
23102
23287
  "toUpperCase",
23103
23288
  "trim"
23104
23289
  ]);
@@ -23480,6 +23665,11 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
23480
23665
  const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
23481
23666
  return callbackSummary.isValid && !callbackSummary.canContinue;
23482
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
+ }
23483
23673
  if (isNodeOfType(node, "CallExpression")) {
23484
23674
  const callee = stripParenExpression(node.callee);
23485
23675
  const calleeRoot = getMemberRoot(callee);
@@ -23682,7 +23872,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23682
23872
  }
23683
23873
  const callee = stripParenExpression(node.callee);
23684
23874
  const calleeRoot = getMemberRoot(callee);
23685
- 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;
23686
23876
  const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
23687
23877
  if (isPureGlobalCall || isPureMemberTransform) {
23688
23878
  if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
@@ -23735,6 +23925,15 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23735
23925
  }
23736
23926
  return evidence;
23737
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
+ }
23738
23937
  if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
23739
23938
  evidence.hasUnknownSource = true;
23740
23939
  return evidence;
@@ -28284,20 +28483,31 @@ const noDocumentStartViewTransition = defineRule({
28284
28483
  } })
28285
28484
  });
28286
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
28287
28495
  //#region src/plugin/rules/js-performance/no-document-write.ts
28288
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.";
28289
28497
  const WRITE_METHODS = new Set(["write", "writeln"]);
28498
+ const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
28290
28499
  const noDocumentWrite = defineRule({
28291
28500
  id: "no-document-write",
28292
28501
  title: "document.write/writeln",
28293
28502
  severity: "warn",
28294
28503
  recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
28295
28504
  create: (context) => ({ CallExpression(node) {
28296
- const callee = node.callee;
28297
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
28505
+ const callee = stripParenExpression(node.callee);
28506
+ if (!isNodeOfType(callee, "MemberExpression")) return;
28298
28507
  const receiver = stripParenExpression(callee.object);
28299
- if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
28300
- 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;
28301
28511
  context.report({
28302
28512
  node,
28303
28513
  message: MESSAGE$24
@@ -29104,6 +29314,14 @@ const noEffectWithFreshDeps = defineRule({
29104
29314
  //#endregion
29105
29315
  //#region src/plugin/rules/security/no-eval.ts
29106
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
+ };
29107
29325
  const noEval = defineRule({
29108
29326
  id: "no-eval",
29109
29327
  title: "eval() runs untrusted code strings",
@@ -29113,20 +29331,21 @@ const noEval = defineRule({
29113
29331
  if (SANDBOX_SURFACE_PATH_PATTERN.test(normalizeFilename(context.filename ?? ""))) return {};
29114
29332
  return {
29115
29333
  CallExpression(node) {
29116
- if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "eval") {
29334
+ const executableGlobalName = getExecutableGlobalName(node.callee, context);
29335
+ if (executableGlobalName === "eval") {
29117
29336
  context.report({
29118
29337
  node,
29119
29338
  message: "eval() is a code-injection vulnerability: it runs any string as code."
29120
29339
  });
29121
29340
  return;
29122
29341
  }
29123
- 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({
29124
29343
  node,
29125
- 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.`
29126
29345
  });
29127
29346
  },
29128
29347
  NewExpression(node) {
29129
- if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "Function") {
29348
+ if (getExecutableGlobalName(node.callee, context) === "Function") {
29130
29349
  const onlyArgument = node.arguments.length === 1 ? node.arguments[0] : void 0;
29131
29350
  if (isNodeOfType(onlyArgument, "Literal") && typeof onlyArgument.value === "string" && onlyArgument.value.trim() === "return this") return;
29132
29351
  context.report({
@@ -34843,6 +35062,14 @@ const isHandlerBagArgument = (analysis, argument) => {
34843
35062
  };
34844
35063
  const getFunctionalUpdaterDataRefs = (analysis, updater) => getDownstreamRefs(analysis, updater).filter((updaterRef) => !updaterRef.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updater));
34845
35064
  const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
35065
+ const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
35066
+ "useIntersectionObserver",
35067
+ "useMatchMedia",
35068
+ "useMediaQuery",
35069
+ "useResizeObserver",
35070
+ "useVisibility",
35071
+ "useWindowSize"
35072
+ ]);
34846
35073
  const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
34847
35074
  const node = def.node;
34848
35075
  if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
@@ -34865,6 +35092,19 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
34865
35092
  if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
34866
35093
  return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isProp(analysis, downstreamRef)));
34867
35094
  };
35095
+ const isExternalSubscriptionHookRef = (ref) => {
35096
+ const identifier = ref.identifier;
35097
+ if (!isNodeOfType(identifier, "Identifier")) return false;
35098
+ if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(identifier.name) && isCalleePosition(identifier)) return true;
35099
+ return Boolean(ref.resolved?.defs.some((def) => {
35100
+ const node = def.node;
35101
+ if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
35102
+ const initializer = stripParenExpression(node.init);
35103
+ if (!isNodeOfType(initializer, "CallExpression")) return false;
35104
+ const callee = stripParenExpression(initializer.callee);
35105
+ return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(callee.name);
35106
+ }));
35107
+ };
34868
35108
  const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
34869
35109
  const isCalleePosition = (identifier) => {
34870
35110
  const parent = identifier.parent;
@@ -34926,10 +35166,11 @@ const noPassDataToParent = defineRule({
34926
35166
  if (argumentRef && resolveToFunction(argumentRef)) return [];
34927
35167
  }
34928
35168
  return getDownstreamRefs(analysis, argument);
34929
- }).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
35169
+ }).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
34930
35170
  if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
34931
35171
  if (!argsUpstreamRefs.some((argRef) => {
34932
35172
  if (isUseStateIdentifier(argRef.identifier)) return false;
35173
+ if (isExternalSubscriptionHookRef(argRef)) return false;
34933
35174
  if (isProp(analysis, argRef)) return false;
34934
35175
  if (isUseRefIdentifier(argRef.identifier)) return false;
34935
35176
  if (isRefCurrent(argRef)) return false;
@@ -54687,12 +54928,6 @@ const webhookSignatureRisk = defineRule({
54687
54928
  //#region src/plugin/rules/zod/utils/zod-ast.ts
54688
54929
  const ZOD_MODULE = "zod";
54689
54930
  const ZOD_MODULE_SOURCES = [ZOD_MODULE];
54690
- const getStaticPropertyName = (member) => {
54691
- const property = member.property;
54692
- if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
54693
- if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
54694
- return null;
54695
- };
54696
54931
  const importInfoCache = /* @__PURE__ */ new WeakMap();
54697
54932
  const getImportInfoForIdentifier = (identifier) => {
54698
54933
  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.0654849",
3
+ "version": "0.7.5-dev.70eff9a",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",