oxlint-plugin-react-doctor 0.7.5-dev.d9676e2 → 0.7.5-dev.dbd4067

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 +259 -130
  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
@@ -7618,7 +7712,7 @@ const isCreateClassLikeCall = (node) => {
7618
7712
  if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$2(callee) === "createClass";
7619
7713
  return false;
7620
7714
  };
7621
- const isCreateContextCall = (node) => {
7715
+ const isCreateContextCall$1 = (node) => {
7622
7716
  if (!isNodeOfType(node, "CallExpression")) return false;
7623
7717
  const callee = node.callee;
7624
7718
  if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
@@ -7795,7 +7889,7 @@ const displayName = defineRule({
7795
7889
  }
7796
7890
  },
7797
7891
  CallExpression(node) {
7798
- if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall(node)) {
7892
+ if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall$1(node)) {
7799
7893
  const assignedName = getAssignedName(node);
7800
7894
  if (assignedName) {
7801
7895
  const programRoot = findProgramRoot(node);
@@ -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) => {
@@ -7887,9 +7951,9 @@ const isImportedFromReact = (symbol) => {
7887
7951
  const importDeclaration = symbol.declarationNode.parent;
7888
7952
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
7889
7953
  };
7890
- const isNamedReactApiImport = (identifier, apiNames, scopes) => {
7954
+ const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
7891
7955
  if (!isNodeOfType(identifier, "Identifier")) return false;
7892
- const symbol = scopes.symbolFor(identifier);
7956
+ const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
7893
7957
  if (!symbol || !isImportedFromReact(symbol)) return false;
7894
7958
  const importedName = getImportedName(symbol.declarationNode);
7895
7959
  return Boolean(importedName && includesApiName(apiNames, importedName));
@@ -7903,7 +7967,7 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
7903
7967
  if (!isNodeOfType(node, "CallExpression")) return false;
7904
7968
  const callee = stripParenExpression(node.callee);
7905
7969
  if (isNodeOfType(callee, "Identifier")) {
7906
- if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
7970
+ if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
7907
7971
  return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
7908
7972
  }
7909
7973
  if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
@@ -11112,6 +11176,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
11112
11176
  return { CallExpression(node) {
11113
11177
  const hookName = getHookName(node.callee, context.scopes);
11114
11178
  if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
11179
+ if (HOOKS_REQUIRING_DEPS_MATCH.has(hookName) && !isReactApiCall(node, HOOKS_REQUIRING_DEPS_MATCH, context.scopes, {
11180
+ allowGlobalReactNamespace: true,
11181
+ allowUnboundBareCalls: true,
11182
+ resolveNamedAliases: true
11183
+ })) return;
11115
11184
  const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
11116
11185
  const depsArgumentIndex = getDepsArgumentIndex(hookName);
11117
11186
  const callbackArgument = node.arguments[callbackArgumentIndex];
@@ -23039,61 +23108,73 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
23039
23108
  "Array",
23040
23109
  "BigInt",
23041
23110
  "Boolean",
23111
+ "encodeURIComponent",
23042
23112
  "Number",
23043
23113
  "Object",
23044
23114
  "String",
23045
23115
  "parseFloat",
23046
- "parseInt"
23116
+ "parseInt",
23117
+ "structuredClone"
23118
+ ]);
23119
+ const PURE_GLOBAL_CONSTRUCTOR_NAMES = new Set(["Date", "Set"]);
23120
+ const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([
23121
+ ["Array", new Set(["from"])],
23122
+ ["JSON", new Set([
23123
+ "isRawJSON",
23124
+ "parse",
23125
+ "rawJSON",
23126
+ "stringify"
23127
+ ])],
23128
+ ["Math", new Set([
23129
+ "abs",
23130
+ "acos",
23131
+ "acosh",
23132
+ "asin",
23133
+ "asinh",
23134
+ "atan",
23135
+ "atan2",
23136
+ "atanh",
23137
+ "cbrt",
23138
+ "ceil",
23139
+ "clz32",
23140
+ "cos",
23141
+ "cosh",
23142
+ "exp",
23143
+ "floor",
23144
+ "fround",
23145
+ "hypot",
23146
+ "imul",
23147
+ "log",
23148
+ "log10",
23149
+ "log1p",
23150
+ "log2",
23151
+ "max",
23152
+ "min",
23153
+ "pow",
23154
+ "round",
23155
+ "sign",
23156
+ "sin",
23157
+ "sinh",
23158
+ "sqrt",
23159
+ "tan",
23160
+ "tanh",
23161
+ "trunc"
23162
+ ])],
23163
+ ["Object", new Set(["assign"])]
23047
23164
  ]);
23048
- const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
23049
- const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
23050
- "isRawJSON",
23051
- "parse",
23052
- "rawJSON",
23053
- "stringify"
23054
- ])], ["Math", new Set([
23055
- "abs",
23056
- "acos",
23057
- "acosh",
23058
- "asin",
23059
- "asinh",
23060
- "atan",
23061
- "atan2",
23062
- "atanh",
23063
- "cbrt",
23064
- "ceil",
23065
- "clz32",
23066
- "cos",
23067
- "cosh",
23068
- "exp",
23069
- "floor",
23070
- "fround",
23071
- "hypot",
23072
- "imul",
23073
- "log",
23074
- "log10",
23075
- "log1p",
23076
- "log2",
23077
- "max",
23078
- "min",
23079
- "pow",
23080
- "round",
23081
- "sign",
23082
- "sin",
23083
- "sinh",
23084
- "sqrt",
23085
- "tan",
23086
- "tanh",
23087
- "trunc"
23088
- ])]]);
23089
23165
  const PURE_MEMBER_TRANSFORM_NAMES = new Set([
23090
23166
  "concat",
23091
23167
  "filter",
23168
+ "flatMap",
23092
23169
  "join",
23093
23170
  "map",
23171
+ "reduce",
23172
+ "replace",
23173
+ "slice",
23094
23174
  "split",
23095
23175
  "toLowerCase",
23096
23176
  "toString",
23177
+ "toSorted",
23097
23178
  "toUpperCase",
23098
23179
  "trim"
23099
23180
  ]);
@@ -23475,6 +23556,11 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
23475
23556
  const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
23476
23557
  return callbackSummary.isValid && !callbackSummary.canContinue;
23477
23558
  }
23559
+ if (isNodeOfType(node, "NewExpression")) {
23560
+ const callee = stripParenExpression(node.callee);
23561
+ 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;
23562
+ return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
23563
+ }
23478
23564
  if (isNodeOfType(node, "CallExpression")) {
23479
23565
  const callee = stripParenExpression(node.callee);
23480
23566
  const calleeRoot = getMemberRoot(callee);
@@ -23677,7 +23763,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23677
23763
  }
23678
23764
  const callee = stripParenExpression(node.callee);
23679
23765
  const calleeRoot = getMemberRoot(callee);
23680
- 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;
23766
+ 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;
23681
23767
  const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
23682
23768
  if (isPureGlobalCall || isPureMemberTransform) {
23683
23769
  if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
@@ -23730,6 +23816,15 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23730
23816
  }
23731
23817
  return evidence;
23732
23818
  }
23819
+ if (isNodeOfType(node, "NewExpression")) {
23820
+ const callee = stripParenExpression(node.callee);
23821
+ if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || getIdentifierBindingIdentity(analysis, callee) !== null) {
23822
+ evidence.hasUnknownSource = true;
23823
+ return evidence;
23824
+ }
23825
+ for (const argument of node.arguments ?? []) mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
23826
+ return evidence;
23827
+ }
23733
23828
  if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
23734
23829
  evidence.hasUnknownSource = true;
23735
23830
  return evidence;
@@ -25070,7 +25165,11 @@ const noAsyncEffectCallback = defineRule({
25070
25165
  severity: "warn",
25071
25166
  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.",
25072
25167
  create: (context) => ({ CallExpression(node) {
25073
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
25168
+ if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
25169
+ allowGlobalReactNamespace: true,
25170
+ allowUnboundBareCalls: true,
25171
+ resolveNamedAliases: true
25172
+ })) return;
25074
25173
  const callback = getEffectCallback(node);
25075
25174
  if (!callback) return;
25076
25175
  if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
@@ -26268,6 +26367,19 @@ const noCloneElement = defineRule({
26268
26367
  } })
26269
26368
  });
26270
26369
  //#endregion
26370
+ //#region src/plugin/utils/get-call-method-name.ts
26371
+ /**
26372
+ * Returns the static method name of a call's callee when it's a
26373
+ * non-computed MemberExpression (`obj.method` → `"method"`), or
26374
+ * `null` otherwise. Used by `no-pass-data-to-parent` and
26375
+ * `no-pass-live-state-to-parent` to match the receiver-bound
26376
+ * method-call shape against an allow/block list.
26377
+ */
26378
+ const getCallMethodName = (callee) => {
26379
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
26380
+ return null;
26381
+ };
26382
+ //#endregion
26271
26383
  //#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
26272
26384
  const MESSAGE$31 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
26273
26385
  const CONTEXT_MODULES = [
@@ -26275,23 +26387,35 @@ const CONTEXT_MODULES = [
26275
26387
  "use-context-selector",
26276
26388
  "react-tracked"
26277
26389
  ];
26278
- const isCreateContextCallee = (callee) => {
26390
+ const getSupportedContextImportSource = (symbol) => {
26391
+ if (symbol?.kind !== "import") return null;
26392
+ const importDeclaration = symbol.declarationNode.parent;
26393
+ if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration") || typeof importDeclaration.source.value !== "string" || !CONTEXT_MODULES.includes(importDeclaration.source.value)) return null;
26394
+ return importDeclaration.source.value;
26395
+ };
26396
+ const isSupportedNamespaceSymbol = (symbol) => getSupportedContextImportSource(symbol) !== null && Boolean(symbol && (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")));
26397
+ const isDestructuredCreateContextBinding = (identifier, scopes) => {
26398
+ if (!isNodeOfType(identifier, "Identifier")) return false;
26399
+ const symbol = scopes.symbolFor(identifier);
26400
+ if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
26401
+ const property = symbol.bindingIdentifier.parent;
26402
+ if (!property || !isNodeOfType(property, "Property") || getStaticPropertyKeyName(property, { allowComputedString: true }) !== "createContext") return false;
26403
+ const initializer = stripParenExpression(symbol.initializer);
26404
+ return isNodeOfType(initializer, "Identifier") && isSupportedNamespaceSymbol(resolveConstIdentifierAlias(initializer, scopes));
26405
+ };
26406
+ const isCreateContextCall = (node, scopes) => {
26407
+ const callee = stripParenExpression(node.callee);
26279
26408
  if (isNodeOfType(callee, "Identifier")) {
26280
- const binding = getImportBindingForName(callee, callee.name);
26281
- return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
26409
+ if (isDestructuredCreateContextBinding(callee, scopes)) return true;
26410
+ const symbol = resolveConstIdentifierAlias(callee, scopes);
26411
+ return getSupportedContextImportSource(symbol) !== null && Boolean(symbol && getImportedName(symbol.declarationNode) === "createContext");
26282
26412
  }
26283
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
26284
- const namespaceIdentifier = callee.object;
26285
- const propertyIdentifier = callee.property;
26286
- if (!isNodeOfType(namespaceIdentifier, "Identifier")) return false;
26287
- if (!isNodeOfType(propertyIdentifier, "Identifier")) return false;
26288
- if (propertyIdentifier.name !== "createContext") return false;
26289
- const namespaceName = namespaceIdentifier.name;
26290
- if (isCanonicalReactNamespaceName(namespaceName)) return true;
26291
- const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
26292
- return importSource !== null && CONTEXT_MODULES.includes(importSource);
26293
- }
26294
- return false;
26413
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
26414
+ if ((getCallMethodName(callee) ?? (callee.computed && isNodeOfType(callee.property, "Literal") && typeof callee.property.value === "string" ? callee.property.value : null)) !== "createContext") return false;
26415
+ const receiver = stripParenExpression(callee.object);
26416
+ if (!isNodeOfType(receiver, "Identifier")) return false;
26417
+ if (receiver.name === "React" && scopes.isGlobalReference(receiver)) return true;
26418
+ return isSupportedNamespaceSymbol(resolveConstIdentifierAlias(receiver, scopes));
26295
26419
  };
26296
26420
  const noCreateContextInRender = defineRule({
26297
26421
  id: "no-create-context-in-render",
@@ -26300,7 +26424,7 @@ const noCreateContextInRender = defineRule({
26300
26424
  category: "Correctness",
26301
26425
  recommendation: "Move `createContext(...)` outside the component, to the top level of the file, so it stays the same on every render.",
26302
26426
  create: (context) => ({ CallExpression(node) {
26303
- if (!isCreateContextCallee(node.callee)) return;
26427
+ if (!isCreateContextCall(node, context.scopes)) return;
26304
26428
  const componentOrHookName = enclosingComponentOrHookName(node);
26305
26429
  if (!componentOrHookName) return;
26306
26430
  context.report({
@@ -28250,20 +28374,31 @@ const noDocumentStartViewTransition = defineRule({
28250
28374
  } })
28251
28375
  });
28252
28376
  //#endregion
28377
+ //#region src/plugin/utils/get-static-property-name.ts
28378
+ const getStaticPropertyName = (memberExpression) => {
28379
+ const property = memberExpression.property;
28380
+ if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
28381
+ if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
28382
+ if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
28383
+ return null;
28384
+ };
28385
+ //#endregion
28253
28386
  //#region src/plugin/rules/js-performance/no-document-write.ts
28254
28387
  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.";
28255
28388
  const WRITE_METHODS = new Set(["write", "writeln"]);
28389
+ const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
28256
28390
  const noDocumentWrite = defineRule({
28257
28391
  id: "no-document-write",
28258
28392
  title: "document.write/writeln",
28259
28393
  severity: "warn",
28260
28394
  recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
28261
28395
  create: (context) => ({ CallExpression(node) {
28262
- const callee = node.callee;
28263
- if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
28396
+ const callee = stripParenExpression(node.callee);
28397
+ if (!isNodeOfType(callee, "MemberExpression")) return;
28264
28398
  const receiver = stripParenExpression(callee.object);
28265
- if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
28266
- if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
28399
+ if (!isNodeOfType(receiver, "Identifier") || !isGlobalDocumentReference(receiver, context)) return;
28400
+ const methodName = getStaticPropertyName(callee);
28401
+ if (methodName === null || !WRITE_METHODS.has(methodName)) return;
28267
28402
  context.report({
28268
28403
  node,
28269
28404
  message: MESSAGE$24
@@ -29070,6 +29205,14 @@ const noEffectWithFreshDeps = defineRule({
29070
29205
  //#endregion
29071
29206
  //#region src/plugin/rules/security/no-eval.ts
29072
29207
  const SANDBOX_SURFACE_PATH_PATTERN = /(?:^|[/-])sandbox(?:$|[/-])|(?:^|\/)[\w.]+-sandbox(?:\/|\.[cm]?[jt]sx?$)/i;
29208
+ const getExecutableGlobalName = (node, context) => {
29209
+ const expression = stripParenExpression(node);
29210
+ if (isNodeOfType(expression, "Identifier")) return context.scopes.isGlobalReference(expression) ? expression.name : null;
29211
+ if (!isNodeOfType(expression, "MemberExpression")) return null;
29212
+ const receiver = stripParenExpression(expression.object);
29213
+ if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "globalThis" || !context.scopes.isGlobalReference(receiver)) return null;
29214
+ return getStaticPropertyName(expression);
29215
+ };
29073
29216
  const noEval = defineRule({
29074
29217
  id: "no-eval",
29075
29218
  title: "eval() runs untrusted code strings",
@@ -29079,20 +29222,21 @@ const noEval = defineRule({
29079
29222
  if (SANDBOX_SURFACE_PATH_PATTERN.test(normalizeFilename(context.filename ?? ""))) return {};
29080
29223
  return {
29081
29224
  CallExpression(node) {
29082
- if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "eval") {
29225
+ const executableGlobalName = getExecutableGlobalName(node.callee, context);
29226
+ if (executableGlobalName === "eval") {
29083
29227
  context.report({
29084
29228
  node,
29085
29229
  message: "eval() is a code-injection vulnerability: it runs any string as code."
29086
29230
  });
29087
29231
  return;
29088
29232
  }
29089
- 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({
29233
+ if ((executableGlobalName === "setTimeout" || executableGlobalName === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
29090
29234
  node,
29091
- message: `Passing a string to ${node.callee.name}() is a code-injection vulnerability, since it runs that string as code.`
29235
+ message: `Passing a string to ${executableGlobalName}() is a code-injection vulnerability, since it runs that string as code.`
29092
29236
  });
29093
29237
  },
29094
29238
  NewExpression(node) {
29095
- if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "Function") {
29239
+ if (getExecutableGlobalName(node.callee, context) === "Function") {
29096
29240
  const onlyArgument = node.arguments.length === 1 ? node.arguments[0] : void 0;
29097
29241
  if (isNodeOfType(onlyArgument, "Literal") && typeof onlyArgument.value === "string" && onlyArgument.value.trim() === "return this") return;
29098
29242
  context.report({
@@ -30351,7 +30495,11 @@ const noFetchInEffect = defineRule({
30351
30495
  severity: "warn",
30352
30496
  recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
30353
30497
  create: (context) => ({ CallExpression(node) {
30354
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
30498
+ if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
30499
+ allowGlobalReactNamespace: true,
30500
+ allowUnboundBareCalls: true,
30501
+ resolveNamedAliases: true
30502
+ })) return;
30355
30503
  const callback = getEffectCallback(node);
30356
30504
  if (!callback) return;
30357
30505
  const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
@@ -34561,19 +34709,6 @@ const DATA_SINK_METHOD_NAMES = new Set([
34561
34709
  "deserialize"
34562
34710
  ]);
34563
34711
  //#endregion
34564
- //#region src/plugin/utils/get-call-method-name.ts
34565
- /**
34566
- * Returns the static method name of a call's callee when it's a
34567
- * non-computed MemberExpression (`obj.method` → `"method"`), or
34568
- * `null` otherwise. Used by `no-pass-data-to-parent` and
34569
- * `no-pass-live-state-to-parent` to match the receiver-bound
34570
- * method-call shape against an allow/block list.
34571
- */
34572
- const getCallMethodName = (callee) => {
34573
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
34574
- return null;
34575
- };
34576
- //#endregion
34577
34712
  //#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
34578
34713
  const isUseStateIdentifier = (identifier) => {
34579
34714
  if (!isNodeOfType(identifier, "Identifier")) return false;
@@ -54662,12 +54797,6 @@ const webhookSignatureRisk = defineRule({
54662
54797
  //#region src/plugin/rules/zod/utils/zod-ast.ts
54663
54798
  const ZOD_MODULE = "zod";
54664
54799
  const ZOD_MODULE_SOURCES = [ZOD_MODULE];
54665
- const getStaticPropertyName = (member) => {
54666
- const property = member.property;
54667
- if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
54668
- if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
54669
- return null;
54670
- };
54671
54800
  const importInfoCache = /* @__PURE__ */ new WeakMap();
54672
54801
  const getImportInfoForIdentifier = (identifier) => {
54673
54802
  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.d9676e2",
3
+ "version": "0.7.5-dev.dbd4067",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",