oxlint-plugin-react-doctor 0.7.4-dev.5113067 → 0.7.4-dev.593824d

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 +264 -51
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22370,6 +22370,26 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
22370
22370
  const importDeclaration = declarationNode.parent;
22371
22371
  return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22372
22372
  }));
22373
+ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
22374
+ if (def.type !== "ImportBinding") return false;
22375
+ const declarationNode = def.node;
22376
+ if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
22377
+ const importDeclaration = declarationNode.parent;
22378
+ return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
22379
+ }));
22380
+ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
22381
+ if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
22382
+ const callee = stripParenExpression(declarator.init.callee);
22383
+ if (isNodeOfType(callee, "Identifier")) {
22384
+ const reference = getRef(analysis, callee);
22385
+ if (!reference?.resolved) return callee.name === hookName;
22386
+ return isReactNamedImportReference(reference, hookName);
22387
+ }
22388
+ if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
22389
+ const namespaceReference = getRef(analysis, callee.object);
22390
+ if (!namespaceReference?.resolved) return callee.object.name === "React";
22391
+ return isReactNamespaceImportReference(namespaceReference);
22392
+ };
22373
22393
  const isHookCallee$1 = (analysis, node, hookName) => {
22374
22394
  if (!node) return false;
22375
22395
  if (isNodeOfType(node, "Identifier")) {
@@ -23424,6 +23444,20 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
23424
23444
  }
23425
23445
  return evidence;
23426
23446
  };
23447
+ const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
23448
+ const evidence = collectValueEvidence(analysis, expression, {
23449
+ functionNode: componentFunction,
23450
+ invocation: null,
23451
+ isDeferred: false,
23452
+ introducedBindings: /* @__PURE__ */ new Set(),
23453
+ substitutions: /* @__PURE__ */ new Map(),
23454
+ currentFilename
23455
+ }, 1);
23456
+ return {
23457
+ sourceReferences: evidence.sourceReferences,
23458
+ isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
23459
+ };
23460
+ };
23427
23461
  const findStateSetterReference = (analysis, callExpression) => {
23428
23462
  if (!isNodeOfType(callExpression, "CallExpression")) return null;
23429
23463
  const callee = stripParenExpression(callExpression.callee);
@@ -26476,57 +26510,6 @@ const noDefaultProps = defineRule({
26476
26510
  } })
26477
26511
  });
26478
26512
  //#endregion
26479
- //#region src/plugin/rules/state-and-effects/no-derived-state.ts
26480
- const getStateName$1 = (stateDeclarator) => {
26481
- if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
26482
- if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
26483
- const stateBinding = stateDeclarator.id.elements?.[0];
26484
- if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
26485
- const setterBinding = stateDeclarator.id.elements?.[1];
26486
- if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
26487
- if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
26488
- return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
26489
- };
26490
- const noDerivedState = defineRule({
26491
- id: "no-derived-state",
26492
- title: "Derived value copied into state",
26493
- severity: "warn",
26494
- tags: ["test-noise"],
26495
- recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into useState through a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
26496
- create: (context) => ({ CallExpression(node) {
26497
- if (!isUseEffect(node)) return;
26498
- const analysis = getProgramAnalysis(node);
26499
- if (!analysis) return;
26500
- for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
26501
- if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
26502
- const stateName = getStateName$1(fact.stateDeclarator);
26503
- context.report({
26504
- node: fact.callExpression,
26505
- message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
26506
- });
26507
- }
26508
- } })
26509
- });
26510
- //#endregion
26511
- //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
26512
- const noDerivedStateEffect = defineRule({
26513
- id: "no-derived-state-effect",
26514
- title: "Derived state stored in an effect",
26515
- severity: "warn",
26516
- tags: ["test-noise"],
26517
- recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
26518
- create: (context) => ({ CallExpression(node) {
26519
- if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
26520
- const analysis = getProgramAnalysis(node);
26521
- if (!analysis) return;
26522
- if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
26523
- context.report({
26524
- node,
26525
- message: "You pay an extra render for state you can derive from other values."
26526
- });
26527
- } })
26528
- });
26529
- //#endregion
26530
26513
  //#region src/plugin/utils/is-component-function.ts
26531
26514
  const isFunctionAssignedToComponent = (functionNode) => {
26532
26515
  let cursor = functionNode.parent ?? null;
@@ -26660,6 +26643,236 @@ const createComponentPropStackTracker = (callbacks) => {
26660
26643
  };
26661
26644
  };
26662
26645
  //#endregion
26646
+ //#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
26647
+ const findReferenceDeclarator = (reference) => {
26648
+ for (const definition of reference.resolved?.defs ?? []) {
26649
+ const definitionNode = definition.node;
26650
+ if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
26651
+ }
26652
+ return null;
26653
+ };
26654
+ const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
26655
+ const node = stripParenExpression(expression);
26656
+ if (!isNodeOfType(node, "Identifier")) return null;
26657
+ const reference = getRef(analysis, node);
26658
+ if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
26659
+ if (isProp(analysis, reference)) {
26660
+ if (isWholePropsObjectReference(analysis, reference)) return null;
26661
+ return { bindingIdentity: reference.resolved };
26662
+ }
26663
+ if (isState(analysis, reference)) {
26664
+ const stateDeclarator = getUseStateDecl(analysis, reference);
26665
+ if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
26666
+ return { bindingIdentity: reference.resolved };
26667
+ }
26668
+ if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
26669
+ const declarator = findReferenceDeclarator(reference);
26670
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
26671
+ const nextVisitedBindings = new Set(visitedBindings);
26672
+ nextVisitedBindings.add(reference.resolved);
26673
+ return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
26674
+ };
26675
+ const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
26676
+ const getDirectBranchStatements = (branch) => {
26677
+ if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
26678
+ return [branch];
26679
+ };
26680
+ const getDirectCallExpression = (statement) => {
26681
+ if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26682
+ const expression = stripParenExpression(statement.expression);
26683
+ return isNodeOfType(expression, "CallExpression") ? expression : null;
26684
+ };
26685
+ const getDirectAssignmentExpression = (statement) => {
26686
+ if (!isNodeOfType(statement, "ExpressionStatement")) return null;
26687
+ const expression = stripParenExpression(statement.expression);
26688
+ return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
26689
+ };
26690
+ const findDirectStateSetterReference = (analysis, callExpression) => {
26691
+ if (!isNodeOfType(callExpression, "CallExpression")) return null;
26692
+ const callee = stripParenExpression(callExpression.callee);
26693
+ if (!isNodeOfType(callee, "Identifier")) return null;
26694
+ const reference = getRef(analysis, callee);
26695
+ return reference && isStateSetter(analysis, reference) ? reference : null;
26696
+ };
26697
+ const getStaticRefCurrentReference = (analysis, expression) => {
26698
+ const node = stripParenExpression(expression);
26699
+ if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
26700
+ const reference = getRef(analysis, node.object);
26701
+ if (!reference) return null;
26702
+ const declarator = findReferenceDeclarator(reference);
26703
+ return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
26704
+ };
26705
+ const getStateTracker = (analysis, expression) => {
26706
+ const node = stripParenExpression(expression);
26707
+ if (!isNodeOfType(node, "Identifier")) return null;
26708
+ const reference = getRef(analysis, node);
26709
+ if (!reference || !isState(analysis, reference)) return null;
26710
+ const declarator = getUseStateDecl(analysis, reference);
26711
+ if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
26712
+ return {
26713
+ kind: "state",
26714
+ declarator
26715
+ };
26716
+ };
26717
+ const getRefTracker = (analysis, expression) => {
26718
+ const reference = getStaticRefCurrentReference(analysis, expression);
26719
+ return reference ? {
26720
+ kind: "ref",
26721
+ reference
26722
+ } : null;
26723
+ };
26724
+ const getTrackerInitializer = (tracker) => {
26725
+ const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
26726
+ if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
26727
+ const initializer = declarator.init.arguments?.[0];
26728
+ return initializer ? initializer : null;
26729
+ };
26730
+ const isTrackerSynchronized = (analysis, guard) => {
26731
+ for (const statement of guard.statements) {
26732
+ if (guard.tracker.kind === "state") {
26733
+ const callExpression = getDirectCallExpression(statement);
26734
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26735
+ const setterReference = findDirectStateSetterReference(analysis, callExpression);
26736
+ if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
26737
+ const writtenValue = callExpression.arguments?.[0];
26738
+ if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
26739
+ continue;
26740
+ }
26741
+ const assignmentExpression = getDirectAssignmentExpression(statement);
26742
+ if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
26743
+ if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
26744
+ if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
26745
+ }
26746
+ return false;
26747
+ };
26748
+ const parseRenderTrackerGuard = (analysis, statement) => {
26749
+ if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
26750
+ const left = statement.test.left;
26751
+ const right = statement.test.right;
26752
+ const candidates = [{
26753
+ sourceExpression: left,
26754
+ trackerExpression: right
26755
+ }, {
26756
+ sourceExpression: right,
26757
+ trackerExpression: left
26758
+ }];
26759
+ for (const candidate of candidates) {
26760
+ const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
26761
+ if (!source) continue;
26762
+ const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
26763
+ if (!tracker) continue;
26764
+ const trackerInitializer = getTrackerInitializer(tracker);
26765
+ if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
26766
+ const guard = {
26767
+ source,
26768
+ tracker,
26769
+ statements: getDirectBranchStatements(statement.consequent)
26770
+ };
26771
+ return isTrackerSynchronized(analysis, guard) ? guard : null;
26772
+ }
26773
+ return null;
26774
+ };
26775
+ const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
26776
+ if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
26777
+ const references = setterReference.resolved.references.filter((reference) => !reference.init);
26778
+ if (references.length !== 1) return false;
26779
+ return references[0].identifier === callExpression.callee;
26780
+ };
26781
+ const getStateInitializer = (stateDeclarator) => {
26782
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
26783
+ const initializer = stateDeclarator.init.arguments?.[0];
26784
+ return initializer ? initializer : null;
26785
+ };
26786
+ const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
26787
+ if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
26788
+ const componentFunction = componentBody.parent;
26789
+ const facts = [];
26790
+ for (const statement of componentBody.body ?? []) {
26791
+ const guard = parseRenderTrackerGuard(analysis, statement);
26792
+ if (!guard) continue;
26793
+ for (const branchStatement of guard.statements) {
26794
+ const callExpression = getDirectCallExpression(branchStatement);
26795
+ if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
26796
+ const setterReference = findDirectStateSetterReference(analysis, callExpression);
26797
+ if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
26798
+ const stateDeclarator = getUseStateDecl(analysis, setterReference);
26799
+ if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
26800
+ const writtenValue = callExpression.arguments?.[0];
26801
+ const initializer = getStateInitializer(stateDeclarator);
26802
+ if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
26803
+ facts.push({
26804
+ callExpression,
26805
+ stateDeclarator
26806
+ });
26807
+ }
26808
+ }
26809
+ return facts;
26810
+ };
26811
+ //#endregion
26812
+ //#region src/plugin/rules/state-and-effects/no-derived-state.ts
26813
+ const getStateName$1 = (stateDeclarator) => {
26814
+ if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
26815
+ if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
26816
+ const stateBinding = stateDeclarator.id.elements?.[0];
26817
+ if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
26818
+ const setterBinding = stateDeclarator.id.elements?.[1];
26819
+ if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
26820
+ if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
26821
+ return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
26822
+ };
26823
+ const noDerivedState = defineRule({
26824
+ id: "no-derived-state",
26825
+ title: "Derived value copied into state",
26826
+ severity: "warn",
26827
+ tags: ["test-noise"],
26828
+ recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into state and synchronizing it during render or through an effect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
26829
+ create: (context) => {
26830
+ const reportStateWrite = (callExpression, stateDeclarator) => {
26831
+ const stateName = getStateName$1(stateDeclarator);
26832
+ context.report({
26833
+ node: callExpression,
26834
+ message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
26835
+ });
26836
+ };
26837
+ return {
26838
+ ...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
26839
+ if (!componentBody) return;
26840
+ const analysis = getProgramAnalysis(componentBody);
26841
+ if (!analysis) return;
26842
+ for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
26843
+ } }).visitors,
26844
+ CallExpression(node) {
26845
+ if (!isUseEffect(node)) return;
26846
+ const analysis = getProgramAnalysis(node);
26847
+ if (!analysis) return;
26848
+ for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
26849
+ if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
26850
+ reportStateWrite(fact.callExpression, fact.stateDeclarator);
26851
+ }
26852
+ }
26853
+ };
26854
+ }
26855
+ });
26856
+ //#endregion
26857
+ //#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
26858
+ const noDerivedStateEffect = defineRule({
26859
+ id: "no-derived-state-effect",
26860
+ title: "Derived state stored in an effect",
26861
+ severity: "warn",
26862
+ tags: ["test-noise"],
26863
+ recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
26864
+ create: (context) => ({ CallExpression(node) {
26865
+ if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
26866
+ const analysis = getProgramAnalysis(node);
26867
+ if (!analysis) return;
26868
+ if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
26869
+ context.report({
26870
+ node,
26871
+ message: "You pay an extra render for state you can derive from other values."
26872
+ });
26873
+ } })
26874
+ });
26875
+ //#endregion
26663
26876
  //#region src/plugin/utils/is-initial-only-prop-name.ts
26664
26877
  const isInitialOnlyPropName = (propName) => {
26665
26878
  if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.7.4-dev.5113067",
3
+ "version": "0.7.4-dev.593824d",
4
4
  "description": "React Doctor rules for oxlint.",
5
5
  "keywords": [
6
6
  "accessibility",