oxlint-plugin-react-doctor 0.7.6-dev.55cebeb → 0.7.6-dev.5699529

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +49 -270
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,7 +63,7 @@ Each rule can be set to `"error"`, `"warn"`, or `"off"`:
63
63
  ```jsonc
64
64
  {
65
65
  "rules": {
66
- "react-doctor/no-cascading-set-state": "error",
66
+ "react-doctor/effect-needs-cleanup": "error",
67
67
  "react-doctor/no-array-index-as-key": "warn",
68
68
  },
69
69
  }
package/dist/index.js CHANGED
@@ -10948,7 +10948,7 @@ const findDeclaratorForBinding = (bindingIdentifier) => {
10948
10948
  };
10949
10949
  //#endregion
10950
10950
  //#region src/plugin/utils/executes-during-render.ts
10951
- const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
10951
+ const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
10952
10952
  "map",
10953
10953
  "filter",
10954
10954
  "forEach",
@@ -10984,7 +10984,7 @@ const executesDuringRender = (functionNode, scopes) => {
10984
10984
  if (parent.callee === functionNode) return true;
10985
10985
  if ((scopes ? isReactApiCall(parent, REACT_RENDER_PHASE_HOOK_NAMES, scopes, { allowGlobalReactNamespace: true }) : isHookCall$2(parent, REACT_RENDER_PHASE_HOOK_NAMES)) && parent.arguments?.[0] === functionNode) return true;
10986
10986
  if (scopes && parent.arguments?.[1] === functionNode && (isGlobalArrayFromMember(parent.callee, scopes) || isConstAliasOfGlobalArrayFrom(parent.callee, scopes))) return true;
10987
- return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
10987
+ return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
10988
10988
  };
10989
10989
  //#endregion
10990
10990
  //#region src/plugin/utils/find-render-phase-component-or-hook.ts
@@ -15826,7 +15826,7 @@ const jotaiTqUseRawQueryAtom = defineRule({
15826
15826
  });
15827
15827
  //#endregion
15828
15828
  //#region src/plugin/rules/js-performance/js-async-reduce-without-awaited-acc.ts
15829
- const isAsyncFunctionLike$1 = (node) => {
15829
+ const isAsyncFunctionLike = (node) => {
15830
15830
  if (!node) return false;
15831
15831
  if (!isNodeOfType(node, "ArrowFunctionExpression") && !isNodeOfType(node, "FunctionExpression")) return false;
15832
15832
  return node.async === true;
@@ -15893,7 +15893,7 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
15893
15893
  const args = node.arguments ?? [];
15894
15894
  if (args.length === 0) return;
15895
15895
  const reducerCandidate = stripParenExpression(args[0]);
15896
- if (!isAsyncFunctionLike$1(reducerCandidate)) return;
15896
+ if (!isAsyncFunctionLike(reducerCandidate)) return;
15897
15897
  const reducer = reducerCandidate;
15898
15898
  if (!containsDirectAwait(reducer.body)) return;
15899
15899
  const firstParameter = classifyFirstParameter(reducer);
@@ -28479,271 +28479,21 @@ const noCallComponentAsFunction = defineRule({
28479
28479
  }
28480
28480
  });
28481
28481
  //#endregion
28482
- //#region src/plugin/utils/is-setter-identifier.ts
28483
- const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
28484
- //#endregion
28485
- //#region src/plugin/utils/is-setter-call.ts
28486
- const isSetterCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && isSetterIdentifier(node.callee.name);
28487
- //#endregion
28488
- //#region src/plugin/utils/is-hook-binding-in-scope.ts
28489
- const isHookBindingInScope = (node, query) => {
28490
- const { bindingName, hookName, destructureIndex } = query;
28491
- let cursor = node;
28492
- while (cursor) {
28493
- if (isNodeOfType(cursor, "BlockStatement") || isNodeOfType(cursor, "Program")) for (const statement of cursor.body ?? []) {
28494
- if (!isNodeOfType(statement, "VariableDeclaration")) continue;
28495
- for (const declarator of statement.declarations ?? []) {
28496
- if (!isNodeOfType(declarator.init, "CallExpression")) continue;
28497
- if (!isHookCall$2(declarator.init, hookName)) continue;
28498
- if (destructureIndex !== void 0) {
28499
- if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
28500
- const elements = declarator.id.elements ?? [];
28501
- if (elements.length <= destructureIndex) continue;
28502
- const element = elements[destructureIndex];
28503
- if (isNodeOfType(element, "Identifier") && element.name === bindingName) return true;
28504
- } else if (isNodeOfType(declarator.id, "Identifier") && declarator.id.name === bindingName) return true;
28505
- }
28506
- }
28507
- cursor = cursor.parent ?? null;
28508
- }
28509
- return false;
28510
- };
28511
- //#endregion
28512
- //#region src/plugin/utils/is-use-state-setter-in-scope.ts
28513
- const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node, {
28514
- bindingName: setterName,
28515
- hookName: "useState",
28516
- destructureIndex: 1
28482
+ //#region src/plugin/utils/define-retired-rule.ts
28483
+ const defineRetiredRule = (rule) => defineRule({
28484
+ ...rule,
28485
+ defaultEnabled: false,
28486
+ lifecycle: "retired",
28487
+ create: () => ({})
28517
28488
  });
28518
28489
  //#endregion
28519
28490
  //#region src/plugin/rules/state-and-effects/no-cascading-set-state.ts
28520
- const isAsyncFunctionLike = (node) => {
28521
- if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
28522
- return false;
28523
- };
28524
- const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
28525
- "forEach",
28526
- "map",
28527
- "filter",
28528
- "reduce",
28529
- "reduceRight",
28530
- "flatMap",
28531
- "some",
28532
- "every",
28533
- "find",
28534
- "findIndex",
28535
- "findLast",
28536
- "findLastIndex",
28537
- "sort"
28538
- ]);
28539
- const runsOnEffectDispatch = (functionNode) => {
28540
- const parent = functionNode.parent;
28541
- if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
28542
- if (parent.callee === functionNode) return true;
28543
- if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
28544
- const callee = parent.callee;
28545
- return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
28546
- };
28547
- const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
28548
- const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
28549
- const analyzeStatementSequence = (statements, context) => {
28550
- let fallThroughCount = 0;
28551
- let maxTerminatedCount = 0;
28552
- for (const statement of statements) {
28553
- if (isNodeOfType(statement, "IfStatement")) {
28554
- fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
28555
- const thenSummary = analyzeBranchStatements(statement.consequent, context);
28556
- const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
28557
- fallThroughCount: 0,
28558
- maxTerminatedCount: 0,
28559
- doAllPathsTerminate: false
28560
- };
28561
- maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
28562
- if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
28563
- fallThroughCount: 0,
28564
- maxTerminatedCount,
28565
- doAllPathsTerminate: true
28566
- };
28567
- const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
28568
- fallThroughCount += Math.max(...fallThroughBranchCounts);
28569
- continue;
28570
- }
28571
- if (isTerminatingStatement(statement)) {
28572
- const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
28573
- return {
28574
- fallThroughCount: 0,
28575
- maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
28576
- doAllPathsTerminate: true
28577
- };
28578
- }
28579
- fallThroughCount += countMaxPathSetStateCalls(statement, context);
28580
- }
28581
- return {
28582
- fallThroughCount,
28583
- maxTerminatedCount,
28584
- doAllPathsTerminate: false
28585
- };
28586
- };
28587
- const countStatementSequenceSetStateCalls = (statements, context) => {
28588
- const summary = analyzeStatementSequence(statements, context);
28589
- return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
28590
- };
28591
- const collectLocalHelperFunctions = (root) => {
28592
- const helpersByName = /* @__PURE__ */ new Map();
28593
- const visit = (node) => {
28594
- if (isNodeOfType(node, "FunctionDeclaration") && node.id) helpersByName.set(node.id.name, node);
28595
- if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.init && isFunctionLike$2(node.init)) helpersByName.set(node.id.name, node.init);
28596
- if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.init && isNodeOfType(node.init, "CallExpression") && isNodeOfType(node.init.callee, "Identifier") && node.init.callee.name === "useCallback" && node.init.arguments?.[0] && isFunctionLike$2(node.init.arguments[0])) helpersByName.set(node.id.name, node.init.arguments[0]);
28597
- const record = node;
28598
- for (const key of Object.keys(record)) {
28599
- if (key === "parent") continue;
28600
- const child = record[key];
28601
- if (Array.isArray(child)) {
28602
- for (const item of child) if (item && typeof item === "object" && "type" in item) visit(item);
28603
- } else if (child && typeof child === "object" && "type" in child) visit(child);
28604
- }
28605
- };
28606
- visit(root);
28607
- return helpersByName;
28608
- };
28609
- const isWholesaleDelegationCall = (callNode, effectCallback) => {
28610
- const parent = callNode.parent;
28611
- if (!parent) return false;
28612
- if (parent === effectCallback) return true;
28613
- if (!isNodeOfType(parent, "ExpressionStatement")) return false;
28614
- return parent.parent === effectCallback.body;
28615
- };
28616
- const countFunctionBodySetStateCalls = (functionNode, context) => {
28617
- if (isAsyncFunctionLike(functionNode)) return 0;
28618
- const body = functionNode.body;
28619
- if (!body) return 0;
28620
- return countMaxPathSetStateCalls(body, context);
28621
- };
28622
- const countMaxPathSetStateCalls = (node, context) => {
28623
- if (!node || typeof node !== "object") return 0;
28624
- if (isFunctionLike$2(node)) return 0;
28625
- if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
28626
- if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
28627
- const consequent = node.consequent;
28628
- const alternate = node.alternate;
28629
- const testCount = countMaxPathSetStateCalls(node.test, context);
28630
- const thenCount = countMaxPathSetStateCalls(consequent, context);
28631
- const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
28632
- return testCount + Math.max(thenCount, elseCount);
28633
- }
28634
- if (isNodeOfType(node, "SwitchStatement")) {
28635
- let maxRunSetters = 0;
28636
- let currentRunSetters = 0;
28637
- for (const switchCase of node.cases ?? []) {
28638
- const consequent = switchCase.consequent ?? [];
28639
- let caseSetters = 0;
28640
- let runEnds = false;
28641
- for (const statement of consequent) {
28642
- caseSetters += countMaxPathSetStateCalls(statement, context);
28643
- if (isTerminatingStatement(statement)) runEnds = true;
28644
- }
28645
- currentRunSetters += caseSetters;
28646
- if (runEnds) {
28647
- if (currentRunSetters > maxRunSetters) maxRunSetters = currentRunSetters;
28648
- currentRunSetters = 0;
28649
- }
28650
- }
28651
- if (currentRunSetters > maxRunSetters) maxRunSetters = currentRunSetters;
28652
- return maxRunSetters;
28653
- }
28654
- if (isNodeOfType(node, "TryStatement")) {
28655
- const tryCount = countMaxPathSetStateCalls(node.block, context);
28656
- const catchCount = node.handler ? countMaxPathSetStateCalls(node.handler.body, context) : 0;
28657
- const finallyCount = node.finalizer ? countMaxPathSetStateCalls(node.finalizer, context) : 0;
28658
- return Math.max(tryCount, catchCount) + finallyCount;
28659
- }
28660
- if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
28661
- let nestedSettersInArgs = 0;
28662
- for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$2(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
28663
- return 1 + nestedSettersInArgs;
28664
- }
28665
- if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
28666
- const helperFunction = context.helpersByName.get(node.callee.name);
28667
- if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
28668
- context.activeHelpers.add(helperFunction);
28669
- let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
28670
- context.activeHelpers.delete(helperFunction);
28671
- for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
28672
- return helperCount;
28673
- }
28674
- }
28675
- const countChild = (child) => {
28676
- if (isFunctionLike$2(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
28677
- return countMaxPathSetStateCalls(child, context);
28678
- };
28679
- let total = 0;
28680
- const nodeRecord = node;
28681
- for (const key of Object.keys(nodeRecord)) {
28682
- if (key === "parent") continue;
28683
- const child = nodeRecord[key];
28684
- if (Array.isArray(child)) {
28685
- for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
28686
- } else if (child && typeof child === "object" && "type" in child) total += countChild(child);
28687
- }
28688
- return total;
28689
- };
28690
- const isInitOnlyEffect = (node) => {
28691
- const depsArg = node.arguments?.[1];
28692
- if (!depsArg) return false;
28693
- if (!isNodeOfType(depsArg, "ArrayExpression")) return false;
28694
- return (depsArg.elements ?? []).length === 0;
28695
- };
28696
- const DEV_ENV_FLAG_NAMES = new Set([
28697
- "DEV",
28698
- "PROD",
28699
- "MODE",
28700
- "NODE_ENV"
28701
- ]);
28702
- const mentionsDevEnvFlag = (node) => {
28703
- if (!node || typeof node !== "object") return false;
28704
- if (isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && DEV_ENV_FLAG_NAMES.has(node.property.name) && isNodeOfType(node.object, "MemberExpression") && isNodeOfType(node.object.property, "Identifier") && node.object.property.name === "env") return true;
28705
- const record = node;
28706
- for (const key of Object.keys(record)) {
28707
- if (key === "parent") continue;
28708
- const child = record[key];
28709
- if (Array.isArray(child)) {
28710
- for (const item of child) if (item && typeof item === "object" && "type" in item && mentionsDevEnvFlag(item)) return true;
28711
- } else if (child && typeof child === "object" && "type" in child && mentionsDevEnvFlag(child)) return true;
28712
- }
28713
- return false;
28714
- };
28715
- const isDevOnlyGuardedEffect = (callback) => {
28716
- const body = callback.body;
28717
- if (!body || !isNodeOfType(body, "BlockStatement")) return false;
28718
- const firstStatement = (body.body ?? [])[0];
28719
- if (!firstStatement) return false;
28720
- if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
28721
- const consequent = firstStatement.consequent;
28722
- if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
28723
- return mentionsDevEnvFlag(firstStatement.test);
28724
- };
28725
- const noCascadingSetState = defineRule({
28491
+ const noCascadingSetState = defineRetiredRule({
28726
28492
  id: "no-cascading-set-state",
28727
28493
  title: "Multiple setState calls in one effect",
28728
28494
  severity: "warn",
28729
28495
  tags: ["test-noise"],
28730
- recommendation: "Combine related updates in `useReducer` so one effect does not redraw the screen once per `setState` call.",
28731
- create: (context) => ({ CallExpression(node) {
28732
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
28733
- if (isInitOnlyEffect(node)) return;
28734
- const callback = getEffectCallback(node, context.scopes);
28735
- if (!callback) return;
28736
- if (isDevOnlyGuardedEffect(callback)) return;
28737
- const setStateCallCount = countFunctionBodySetStateCalls(callback, {
28738
- helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
28739
- activeHelpers: /* @__PURE__ */ new Set(),
28740
- effectCallback: callback
28741
- });
28742
- if (setStateCallCount >= 3) context.report({
28743
- node,
28744
- message: `${setStateCallCount} setState calls in one useEffect redraw your screen each time they run together.`
28745
- });
28746
- } })
28496
+ recommendation: "Retired: React batches synchronous state updates from one effect into the same follow-up commit, so setter count does not prove repeated redraws."
28747
28497
  });
28748
28498
  //#endregion
28749
28499
  //#region src/plugin/rules/state-and-effects/no-chain-state-updates.ts
@@ -30617,6 +30367,9 @@ const noDirectMutationState = defineRule({
30617
30367
  })
30618
30368
  });
30619
30369
  //#endregion
30370
+ //#region src/plugin/utils/is-setter-identifier.ts
30371
+ const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
30372
+ //#endregion
30620
30373
  //#region src/plugin/rules/state-and-effects/utils/collect-use-state-bindings.ts
30621
30374
  const collectUseStateBindings = (componentBody) => {
30622
30375
  const bindings = [];
@@ -32887,6 +32640,9 @@ const noEventTriggerState = defineRule({
32887
32640
  }
32888
32641
  });
32889
32642
  //#endregion
32643
+ //#region src/plugin/utils/is-setter-call.ts
32644
+ const isSetterCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && isSetterIdentifier(node.callee.name);
32645
+ //#endregion
32890
32646
  //#region src/plugin/rules/state-and-effects/no-fetch-in-effect.ts
32891
32647
  const IMPORT_INITIALIZER_TYPES = new Set([
32892
32648
  "ImportSpecifier",
@@ -47248,6 +47004,37 @@ const containsLocaleEnvironmentRead = (expression) => {
47248
47004
  return readsLocaleEnvironment;
47249
47005
  };
47250
47006
  //#endregion
47007
+ //#region src/plugin/utils/is-hook-binding-in-scope.ts
47008
+ const isHookBindingInScope = (node, query) => {
47009
+ const { bindingName, hookName, destructureIndex } = query;
47010
+ let cursor = node;
47011
+ while (cursor) {
47012
+ if (isNodeOfType(cursor, "BlockStatement") || isNodeOfType(cursor, "Program")) for (const statement of cursor.body ?? []) {
47013
+ if (!isNodeOfType(statement, "VariableDeclaration")) continue;
47014
+ for (const declarator of statement.declarations ?? []) {
47015
+ if (!isNodeOfType(declarator.init, "CallExpression")) continue;
47016
+ if (!isHookCall$2(declarator.init, hookName)) continue;
47017
+ if (destructureIndex !== void 0) {
47018
+ if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
47019
+ const elements = declarator.id.elements ?? [];
47020
+ if (elements.length <= destructureIndex) continue;
47021
+ const element = elements[destructureIndex];
47022
+ if (isNodeOfType(element, "Identifier") && element.name === bindingName) return true;
47023
+ } else if (isNodeOfType(declarator.id, "Identifier") && declarator.id.name === bindingName) return true;
47024
+ }
47025
+ }
47026
+ cursor = cursor.parent ?? null;
47027
+ }
47028
+ return false;
47029
+ };
47030
+ //#endregion
47031
+ //#region src/plugin/utils/is-use-state-setter-in-scope.ts
47032
+ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node, {
47033
+ bindingName: setterName,
47034
+ hookName: "useState",
47035
+ destructureIndex: 1
47036
+ });
47037
+ //#endregion
47251
47038
  //#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
47252
47039
  const USE_EFFECT_ONLY = new Set(["useEffect"]);
47253
47040
  const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
@@ -48937,14 +48724,6 @@ const rerenderTransitionsScroll = defineRule({
48937
48724
  } })
48938
48725
  });
48939
48726
  //#endregion
48940
- //#region src/plugin/utils/define-retired-rule.ts
48941
- const defineRetiredRule = (rule) => defineRule({
48942
- ...rule,
48943
- defaultEnabled: false,
48944
- lifecycle: "retired",
48945
- create: () => ({})
48946
- });
48947
- //#endregion
48948
48727
  //#region src/plugin/rules/react-native/rn-animate-layout-property.ts
48949
48728
  const rnAnimateLayoutProperty = defineRetiredRule({
48950
48729
  id: "rn-animate-layout-property",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.6-dev.55cebeb",
3
+ "version": "0.7.6-dev.5699529",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",