oxlint-plugin-react-doctor 0.2.18-dev.eba20ae → 0.3.0-dev.6130583

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 +45 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import path from "node:path";
1
+ import * as path from "node:path";
2
2
  import { analyze } from "eslint-scope";
3
3
  import * as eslintVisitorKeys from "eslint-visitor-keys";
4
- import fs from "node:fs";
4
+ import * as fs from "node:fs";
5
5
  import { parseSync } from "oxc-parser";
6
6
  //#region src/plugin/utils/is-testlike-filename.ts
7
7
  const NON_PRODUCTION_PATH_SEGMENTS = [
@@ -14786,6 +14786,37 @@ const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
14786
14786
  //#region src/plugin/utils/is-setter-call.ts
14787
14787
  const isSetterCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && isSetterIdentifier(node.callee.name);
14788
14788
  //#endregion
14789
+ //#region src/plugin/utils/is-hook-binding-in-scope.ts
14790
+ const isHookBindingInScope = (node, query) => {
14791
+ const { bindingName, hookName, destructureIndex } = query;
14792
+ let cursor = node;
14793
+ while (cursor) {
14794
+ if (isNodeOfType(cursor, "BlockStatement") || isNodeOfType(cursor, "Program")) for (const statement of cursor.body ?? []) {
14795
+ if (!isNodeOfType(statement, "VariableDeclaration")) continue;
14796
+ for (const declarator of statement.declarations ?? []) {
14797
+ if (!isNodeOfType(declarator.init, "CallExpression")) continue;
14798
+ if (!isHookCall$1(declarator.init, hookName)) continue;
14799
+ if (destructureIndex !== void 0) {
14800
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
14801
+ const elements = declarator.id.elements ?? [];
14802
+ if (elements.length <= destructureIndex) continue;
14803
+ const element = elements[destructureIndex];
14804
+ if (isNodeOfType(element, "Identifier") && element.name === bindingName) return true;
14805
+ } else if (isNodeOfType(declarator.id, "Identifier") && declarator.id.name === bindingName) return true;
14806
+ }
14807
+ }
14808
+ cursor = cursor.parent ?? null;
14809
+ }
14810
+ return false;
14811
+ };
14812
+ //#endregion
14813
+ //#region src/plugin/utils/is-use-state-setter-in-scope.ts
14814
+ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node, {
14815
+ bindingName: setterName,
14816
+ hookName: "useState",
14817
+ destructureIndex: 1
14818
+ });
14819
+ //#endregion
14789
14820
  //#region src/plugin/rules/state-and-effects/no-cascading-set-state.ts
14790
14821
  const isAsyncFunctionLike = (node) => {
14791
14822
  if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
@@ -14827,7 +14858,7 @@ const countMaxPathSetStateCalls = (node) => {
14827
14858
  const finallyCount = node.finalizer ? countMaxPathSetStateCalls(node.finalizer) : 0;
14828
14859
  return Math.max(tryCount, catchCount) + finallyCount;
14829
14860
  }
14830
- if (isSetterCall(node)) {
14861
+ if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
14831
14862
  let nestedSettersInArgs = 0;
14832
14863
  for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument);
14833
14864
  return 1 + nestedSettersInArgs;
@@ -15628,7 +15659,11 @@ const noDerivedStateEffect = defineRule({
15628
15659
  if (statements.length === 0) return;
15629
15660
  if (!statements.every((statement) => {
15630
15661
  if (!isNodeOfType(statement, "ExpressionStatement")) return false;
15631
- return isSetterCall(statement.expression);
15662
+ const expression = statement.expression;
15663
+ if (!isSetterCall(expression)) return false;
15664
+ if (!isNodeOfType(expression, "CallExpression")) return false;
15665
+ if (!isNodeOfType(expression.callee, "Identifier")) return false;
15666
+ return isUseStateSetterInScope(expression, expression.callee.name);
15632
15667
  })) return;
15633
15668
  let allArgumentsDeriveFromDeps = true;
15634
15669
  let hasAnyDependencyReference = false;
@@ -25968,7 +26003,9 @@ const renderingHydrationNoFlicker = defineRule({
25968
26003
  const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
25969
26004
  if (!bodyStatements || bodyStatements.length !== 1) return;
25970
26005
  const soleStatement = bodyStatements[0];
25971
- if (isNodeOfType(soleStatement, "ExpressionStatement") && isSetterCall(soleStatement.expression)) context.report({
26006
+ if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
26007
+ const expression = soleStatement.expression;
26008
+ if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) context.report({
25972
26009
  node,
25973
26010
  message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
25974
26011
  });
@@ -26491,6 +26528,7 @@ const rerenderFunctionalSetstate = defineRule({
26491
26528
  if (!isSetterCall(node)) return;
26492
26529
  if (!node.arguments?.length) return;
26493
26530
  if (!isNodeOfType(node.callee, "Identifier")) return;
26531
+ if (!isUseStateSetterInScope(node, node.callee.name)) return;
26494
26532
  const calleeName = node.callee.name;
26495
26533
  const argument = node.arguments[0];
26496
26534
  const expectedStateName = deriveStateVariableName(calleeName);
@@ -26825,7 +26863,7 @@ const handlerCallsSetState = (handler) => {
26825
26863
  let setStateCall = null;
26826
26864
  walkAst(handler.body, (child) => {
26827
26865
  if (setStateCall) return;
26828
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && /^set[A-Z]/.test(child.callee.name)) setStateCall = child;
26866
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && /^set[A-Z]/.test(child.callee.name) && isUseStateSetterInScope(child, child.callee.name)) setStateCall = child;
26829
26867
  });
26830
26868
  return setStateCall;
26831
26869
  };
@@ -28073,7 +28111,7 @@ const findSetStateInBody = (body) => {
28073
28111
  let setStateCallNode = null;
28074
28112
  walkAst(body, (child) => {
28075
28113
  if (setStateCallNode) return;
28076
- if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && SET_STATE_PATTERN.test(child.callee.name)) setStateCallNode = child;
28114
+ if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && SET_STATE_PATTERN.test(child.callee.name) && isUseStateSetterInScope(child, child.callee.name)) setStateCallNode = child;
28077
28115
  });
28078
28116
  return setStateCallNode;
28079
28117
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.2.18-dev.eba20ae",
3
+ "version": "0.3.0-dev.6130583",
4
4
  "description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",