oxlint-plugin-react-doctor 0.7.2-dev.e872767 → 0.7.2-dev.f10f9ca

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 +111 -44
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8987,8 +8987,8 @@ const isAstDescendant = (inner, outer) => {
8987
8987
  * One cohesive concept: "given a captured symbol, is its value
8988
8988
  * structurally stable across re-renders (and therefore unnecessary
8989
8989
  * in a deps array)?". The rule reads `symbolHasStableValue` /
8990
- * `symbolHasStableHookOrigin` / `symbolHasUseEffectEventOrigin` /
8991
- * `isRecursiveInitializerCapture` at multiple sites — extracting
8990
+ * `symbolHasStableHookOrigin` / `isRecursiveInitializerCapture` at
8991
+ * multiple sites — extracting
8992
8992
  * them lets the rule body stay focused on the diff-the-captured-vs-
8993
8993
  * declared logic.
8994
8994
  *
@@ -9044,11 +9044,6 @@ const symbolHasStableHookOrigin = (symbol) => {
9044
9044
  }
9045
9045
  return false;
9046
9046
  };
9047
- const symbolHasUseEffectEventOrigin = (symbol) => {
9048
- const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
9049
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
9050
- return getHookName(initializer.callee) === "useEffectEvent";
9051
- };
9052
9047
  const getFunctionValueNode = (symbol) => {
9053
9048
  if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
9054
9049
  const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
@@ -9134,6 +9129,37 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
9134
9129
  };
9135
9130
  const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
9136
9131
  //#endregion
9132
+ //#region src/plugin/utils/is-imported-from-non-react-module.ts
9133
+ const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
9134
+ const importSource = getImportSourceForName(contextNode, localIdentifierName);
9135
+ if (importSource === null) return false;
9136
+ return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
9137
+ };
9138
+ //#endregion
9139
+ //#region src/plugin/utils/is-non-react-effect-event-callee.ts
9140
+ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
9141
+ const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
9142
+ return Boolean(symbol && symbol.kind !== "import");
9143
+ };
9144
+ const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
9145
+ if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
9146
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
9147
+ return false;
9148
+ };
9149
+ //#endregion
9150
+ //#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
9151
+ const getUseEffectEventCalleeName = (callee) => {
9152
+ if (isNodeOfType(callee, "Identifier")) return callee.name;
9153
+ if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
9154
+ return null;
9155
+ };
9156
+ const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
9157
+ const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
9158
+ if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
9159
+ if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
9160
+ return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
9161
+ };
9162
+ //#endregion
9137
9163
  //#region src/plugin/rules/react-builtins/exhaustive-deps.ts
9138
9164
  const HOOKS_REQUIRING_DEPS_MATCH = new Set([
9139
9165
  "useEffect",
@@ -9784,7 +9810,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
9784
9810
  if (isLiteralOrEmptyTemplate(stripped)) continue;
9785
9811
  if (isNodeOfType(stripped, "Identifier")) {
9786
9812
  const depSymbol = context.scopes.symbolFor(stripped);
9787
- if (depSymbol && symbolHasUseEffectEventOrigin(depSymbol)) {
9813
+ if (depSymbol && symbolHasReactUseEffectEventOrigin(depSymbol, context.scopes)) {
9788
9814
  context.report({
9789
9815
  node: elementNode,
9790
9816
  message: buildEffectEventDepMessage()
@@ -25734,18 +25760,45 @@ const collectUseStateBindings = (componentBody) => {
25734
25760
  //#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
25735
25761
  const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
25736
25762
  const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
25763
+ const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
25764
+ const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
25765
+ "assign",
25766
+ "entries",
25767
+ "fromEntries",
25768
+ "keys",
25769
+ "values"
25770
+ ]);
25771
+ const ARRAY_COPY_METHOD_NAMES = new Set([
25772
+ "map",
25773
+ "filter",
25774
+ "slice",
25775
+ "concat",
25776
+ "flat",
25777
+ "flatMap",
25778
+ "toSorted",
25779
+ "toReversed",
25780
+ "toSpliced",
25781
+ "with"
25782
+ ]);
25737
25783
  const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
25738
25784
  const isPlainDataProducerCall = (expression) => {
25739
25785
  if (!isNodeOfType(expression, "CallExpression")) return false;
25740
25786
  const callee = expression.callee;
25741
25787
  if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
25742
25788
  if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
25743
- if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
25789
+ if (isNodeOfType(callee.object, "Identifier")) {
25790
+ if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
25791
+ if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
25792
+ if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
25793
+ }
25744
25794
  return producesPlainStateValue(callee.object);
25745
25795
  };
25796
+ const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
25797
+ const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
25746
25798
  const producesPlainStateValue = (expression) => {
25747
25799
  const unwrapped = stripParenExpression(expression);
25748
25800
  if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
25801
+ if (isPlainDataNewExpression(unwrapped)) return true;
25749
25802
  if (isNullOrUndefinedExpression(unwrapped)) return true;
25750
25803
  if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
25751
25804
  return isPlainDataProducerCall(unwrapped);
@@ -25760,6 +25813,38 @@ const initializerMarksPlainState = (initializerArgument) => {
25760
25813
  }
25761
25814
  return producesPlainStateValue(unwrapped);
25762
25815
  };
25816
+ const producesOpaqueInstanceValue = (expression) => {
25817
+ if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
25818
+ if (!isNodeOfType(expression, "CallExpression")) return false;
25819
+ const callee = expression.callee;
25820
+ if (!isNodeOfType(callee, "MemberExpression")) return false;
25821
+ if (isPlainDataProducerCall(expression)) return false;
25822
+ if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
25823
+ return true;
25824
+ };
25825
+ const collectSetterValueObservations = (componentBody, setterNames) => {
25826
+ const plainFedSetterNames = /* @__PURE__ */ new Set();
25827
+ const opaqueFedSetterNames = /* @__PURE__ */ new Set();
25828
+ walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
25829
+ if (!isNodeOfType(node, "CallExpression")) return;
25830
+ if (!isNodeOfType(node.callee, "Identifier")) return;
25831
+ const setterName = node.callee.name;
25832
+ if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
25833
+ const argument = node.arguments?.[0];
25834
+ if (!argument) return;
25835
+ const unwrapped = stripParenExpression(argument);
25836
+ if (isNullOrUndefinedExpression(unwrapped)) return;
25837
+ if (producesPlainStateValue(unwrapped)) {
25838
+ plainFedSetterNames.add(setterName);
25839
+ return;
25840
+ }
25841
+ if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
25842
+ }, true);
25843
+ return {
25844
+ plainFedSetterNames,
25845
+ opaqueFedSetterNames
25846
+ };
25847
+ };
25763
25848
  const collectCallbackRefSetterNames = (componentBody) => {
25764
25849
  const callbackRefSetterNames = /* @__PURE__ */ new Set();
25765
25850
  walkAst(componentBody, (node) => {
@@ -25829,11 +25914,15 @@ const noDirectStateMutation = defineRule({
25829
25914
  if (bindings.length === 0) return;
25830
25915
  const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
25831
25916
  const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
25917
+ const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
25832
25918
  const plainObjectStateValueNames = /* @__PURE__ */ new Set();
25833
25919
  for (const binding of bindings) {
25834
25920
  if (callbackRefSetterNames.has(binding.setterName)) continue;
25835
25921
  if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
25836
- if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
25922
+ const initializerArgument = binding.declarator.init.arguments?.[0];
25923
+ if (!initializerMarksPlainState(initializerArgument)) continue;
25924
+ if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
25925
+ plainObjectStateValueNames.add(binding.valueName);
25837
25926
  }
25838
25927
  const visitMutationCandidate = (child, currentlyShadowed) => {
25839
25928
  if (isNodeOfType(child, "AssignmentExpression")) {
@@ -26614,13 +26703,6 @@ const noEffectEventHandler = defineRule({
26614
26703
  }
26615
26704
  });
26616
26705
  //#endregion
26617
- //#region src/plugin/utils/is-imported-from-non-react-module.ts
26618
- const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
26619
- const importSource = getImportSourceForName(contextNode, localIdentifierName);
26620
- if (importSource === null) return false;
26621
- return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
26622
- };
26623
- //#endregion
26624
26706
  //#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
26625
26707
  const createComponentBindingStackTracker = (callbacks) => {
26626
26708
  const componentBindingStack = [];
@@ -26674,12 +26756,7 @@ const noEffectEventInDeps = defineRule({
26674
26756
  const initializer = declaratorNode.init;
26675
26757
  if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
26676
26758
  if (!isHookCall$2(initializer, "useEffectEvent")) return;
26677
- if (isNodeOfType(initializer.callee, "Identifier")) {
26678
- if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
26679
- const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
26680
- if (calleeSymbol && calleeSymbol.kind !== "import") return;
26681
- }
26682
- if (isNodeOfType(initializer.callee, "MemberExpression") && !initializer.callee.computed && isNodeOfType(initializer.callee.object, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.object.name)) return;
26759
+ if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
26683
26760
  componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
26684
26761
  } });
26685
26762
  return {
@@ -37350,6 +37427,10 @@ const isRouteFactoryCall = (expression) => {
37350
37427
  }
37351
37428
  return false;
37352
37429
  };
37430
+ const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
37431
+ const expression = skipTsExpression(argument);
37432
+ return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
37433
+ });
37353
37434
  const isReactHocName = (name, state) => state.customHocs.has(name);
37354
37435
  const isHocCallee = (callee, state) => {
37355
37436
  if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
@@ -37582,6 +37663,10 @@ const onlyExportComponents = defineRule({
37582
37663
  return false;
37583
37664
  })();
37584
37665
  if (isHoc && firstArgIsValid) hasReactExport = true;
37666
+ else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
37667
+ kind: "non-component",
37668
+ reportNode: stripped
37669
+ });
37585
37670
  else context.report({
37586
37671
  node: stripped,
37587
37672
  message: ANONYMOUS_MESSAGE
@@ -48549,25 +48634,6 @@ const isInsideLoop = (descendant, ancestor) => {
48549
48634
  }
48550
48635
  return false;
48551
48636
  };
48552
- const isUseEffectEventSymbol = (symbol) => {
48553
- const initializer = symbol.initializer;
48554
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
48555
- return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
48556
- };
48557
- const resolvesToLocalNonImportBinding = (identifier, scopes) => {
48558
- const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
48559
- return Boolean(symbol && symbol.kind !== "import");
48560
- };
48561
- const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
48562
- if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
48563
- if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
48564
- return false;
48565
- };
48566
- const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
48567
- const initializer = symbol.initializer;
48568
- if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
48569
- return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
48570
- };
48571
48637
  const findEnclosingComponentOrHookFunction = (node) => {
48572
48638
  let current = node.parent;
48573
48639
  while (current) {
@@ -48757,8 +48823,7 @@ const rulesOfHooks = defineRule({
48757
48823
  },
48758
48824
  Identifier(node) {
48759
48825
  const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
48760
- if (!symbol || !isUseEffectEventSymbol(symbol)) return;
48761
- if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
48826
+ if (!symbol || !symbolHasReactUseEffectEventOrigin(symbol, context.scopes)) return;
48762
48827
  if (!isSameComponentOrHookScope(symbol, node)) return;
48763
48828
  if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
48764
48829
  context.report({
@@ -56983,6 +57048,7 @@ const CROSS_FILE_RULE_IDS = new Set([
56983
57048
  "nextjs-no-use-search-params-without-suspense",
56984
57049
  "no-dynamic-import-path",
56985
57050
  "no-full-lodash-import",
57051
+ "no-locale-format-in-render",
56986
57052
  "no-mutating-reducer-state",
56987
57053
  "prefer-dynamic-import",
56988
57054
  "rendering-hydration-mismatch-time",
@@ -57117,6 +57183,7 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
57117
57183
  ["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
57118
57184
  ["no-dynamic-import-path", collectNearestManifestDependencies],
57119
57185
  ["no-full-lodash-import", collectNearestManifestDependencies],
57186
+ ["no-locale-format-in-render", collectNearestManifestDependencies],
57120
57187
  ["no-mutating-reducer-state", collectMutatingReducerDependencies],
57121
57188
  ["prefer-dynamic-import", collectNearestManifestDependencies],
57122
57189
  ["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.2-dev.e872767",
3
+ "version": "0.7.2-dev.f10f9ca",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",