oxlint-plugin-react-doctor 0.7.4-dev.2b5a18e → 0.7.4-dev.5113067
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.
- package/dist/index.d.ts +0 -92
- package/dist/index.js +60 -471
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -5296,29 +5296,6 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
5296
5296
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5297
5297
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5298
5298
|
};
|
|
5299
|
-
}, {
|
|
5300
|
-
readonly key: "react-doctor/no-prop-callback-in-render";
|
|
5301
|
-
readonly id: "no-prop-callback-in-render";
|
|
5302
|
-
readonly source: "react-doctor";
|
|
5303
|
-
readonly originallyExternal: false;
|
|
5304
|
-
readonly rule: {
|
|
5305
|
-
readonly framework: "global";
|
|
5306
|
-
readonly category: "Bugs";
|
|
5307
|
-
readonly requires: readonly Capability[];
|
|
5308
|
-
readonly id: string;
|
|
5309
|
-
readonly title?: string;
|
|
5310
|
-
readonly severity: RuleSeverity;
|
|
5311
|
-
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
5312
|
-
readonly tags?: ReadonlyArray<string>;
|
|
5313
|
-
readonly matchByOccurrence?: boolean;
|
|
5314
|
-
readonly defaultEnabled?: boolean;
|
|
5315
|
-
readonly lifecycle?: "retired";
|
|
5316
|
-
readonly scan?: FileScan;
|
|
5317
|
-
readonly committedFilesOnly?: boolean;
|
|
5318
|
-
readonly recommendation?: string;
|
|
5319
|
-
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5320
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5321
|
-
};
|
|
5322
5299
|
}, {
|
|
5323
5300
|
readonly key: "react-doctor/no-prop-types";
|
|
5324
5301
|
readonly id: "no-prop-types";
|
|
@@ -5503,29 +5480,6 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
5503
5480
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5504
5481
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5505
5482
|
};
|
|
5506
|
-
}, {
|
|
5507
|
-
readonly key: "react-doctor/no-ref-current-in-render";
|
|
5508
|
-
readonly id: "no-ref-current-in-render";
|
|
5509
|
-
readonly source: "react-doctor";
|
|
5510
|
-
readonly originallyExternal: false;
|
|
5511
|
-
readonly rule: {
|
|
5512
|
-
readonly framework: "global";
|
|
5513
|
-
readonly category: "Bugs";
|
|
5514
|
-
readonly requires: readonly Capability[];
|
|
5515
|
-
readonly id: string;
|
|
5516
|
-
readonly title?: string;
|
|
5517
|
-
readonly severity: RuleSeverity;
|
|
5518
|
-
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
5519
|
-
readonly tags?: ReadonlyArray<string>;
|
|
5520
|
-
readonly matchByOccurrence?: boolean;
|
|
5521
|
-
readonly defaultEnabled?: boolean;
|
|
5522
|
-
readonly lifecycle?: "retired";
|
|
5523
|
-
readonly scan?: FileScan;
|
|
5524
|
-
readonly committedFilesOnly?: boolean;
|
|
5525
|
-
readonly recommendation?: string;
|
|
5526
|
-
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5527
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5528
|
-
};
|
|
5529
5483
|
}, {
|
|
5530
5484
|
readonly key: "react-doctor/no-render-in-render";
|
|
5531
5485
|
readonly id: "no-render-in-render";
|
|
@@ -14562,29 +14516,6 @@ declare const RULES: readonly [{
|
|
|
14562
14516
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14563
14517
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14564
14518
|
};
|
|
14565
|
-
}, {
|
|
14566
|
-
readonly key: "react-doctor/no-prop-callback-in-render";
|
|
14567
|
-
readonly id: "no-prop-callback-in-render";
|
|
14568
|
-
readonly source: "react-doctor";
|
|
14569
|
-
readonly originallyExternal: false;
|
|
14570
|
-
readonly rule: {
|
|
14571
|
-
readonly framework: "global";
|
|
14572
|
-
readonly category: "Bugs";
|
|
14573
|
-
readonly requires: readonly Capability[];
|
|
14574
|
-
readonly id: string;
|
|
14575
|
-
readonly title?: string;
|
|
14576
|
-
readonly severity: RuleSeverity;
|
|
14577
|
-
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
14578
|
-
readonly tags?: ReadonlyArray<string>;
|
|
14579
|
-
readonly matchByOccurrence?: boolean;
|
|
14580
|
-
readonly defaultEnabled?: boolean;
|
|
14581
|
-
readonly lifecycle?: "retired";
|
|
14582
|
-
readonly scan?: FileScan;
|
|
14583
|
-
readonly committedFilesOnly?: boolean;
|
|
14584
|
-
readonly recommendation?: string;
|
|
14585
|
-
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14586
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14587
|
-
};
|
|
14588
14519
|
}, {
|
|
14589
14520
|
readonly key: "react-doctor/no-prop-types";
|
|
14590
14521
|
readonly id: "no-prop-types";
|
|
@@ -14769,29 +14700,6 @@ declare const RULES: readonly [{
|
|
|
14769
14700
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14770
14701
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14771
14702
|
};
|
|
14772
|
-
}, {
|
|
14773
|
-
readonly key: "react-doctor/no-ref-current-in-render";
|
|
14774
|
-
readonly id: "no-ref-current-in-render";
|
|
14775
|
-
readonly source: "react-doctor";
|
|
14776
|
-
readonly originallyExternal: false;
|
|
14777
|
-
readonly rule: {
|
|
14778
|
-
readonly framework: "global";
|
|
14779
|
-
readonly category: "Bugs";
|
|
14780
|
-
readonly requires: readonly Capability[];
|
|
14781
|
-
readonly id: string;
|
|
14782
|
-
readonly title?: string;
|
|
14783
|
-
readonly severity: RuleSeverity;
|
|
14784
|
-
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
14785
|
-
readonly tags?: ReadonlyArray<string>;
|
|
14786
|
-
readonly matchByOccurrence?: boolean;
|
|
14787
|
-
readonly defaultEnabled?: boolean;
|
|
14788
|
-
readonly lifecycle?: "retired";
|
|
14789
|
-
readonly scan?: FileScan;
|
|
14790
|
-
readonly committedFilesOnly?: boolean;
|
|
14791
|
-
readonly recommendation?: string;
|
|
14792
|
-
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14793
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14794
|
-
};
|
|
14795
14703
|
}, {
|
|
14796
14704
|
readonly key: "react-doctor/no-render-in-render";
|
|
14797
14705
|
readonly id: "no-render-in-render";
|
package/dist/index.js
CHANGED
|
@@ -10589,19 +10589,6 @@ const isUnstableInitializer = (node) => {
|
|
|
10589
10589
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
10590
10590
|
return isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment") || isNodeOfType(stripped, "AssignmentExpression") || isNodeOfType(stripped, "NewExpression");
|
|
10591
10591
|
};
|
|
10592
|
-
const isPotentiallyFreshComparedValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
10593
|
-
const candidate = unwrapExpression$3(node);
|
|
10594
|
-
if (isUnstableInitializer(candidate)) return true;
|
|
10595
|
-
if (isNodeOfType(candidate, "ConditionalExpression")) return isPotentiallyFreshComparedValue(candidate.consequent, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.alternate, scopes, visitedSymbolIds);
|
|
10596
|
-
if (isNodeOfType(candidate, "LogicalExpression")) return isPotentiallyFreshComparedValue(candidate.left, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.right, scopes, visitedSymbolIds);
|
|
10597
|
-
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
10598
|
-
const symbol = scopes.symbolFor(candidate);
|
|
10599
|
-
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
10600
|
-
if (symbol.kind === "let" || symbol.kind === "var") return true;
|
|
10601
|
-
if (symbol.kind !== "const" || !symbol.initializer) return false;
|
|
10602
|
-
visitedSymbolIds.add(symbol.id);
|
|
10603
|
-
return isPotentiallyFreshComparedValue(symbol.initializer, scopes, visitedSymbolIds);
|
|
10604
|
-
};
|
|
10605
10592
|
const isExtraDepAllowedForHook = (hookName, node, scopes) => {
|
|
10606
10593
|
if (!isExtraReactiveDepAllowed(node, scopes)) return false;
|
|
10607
10594
|
if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
|
|
@@ -10615,52 +10602,6 @@ const isStableSetterLikeSymbol = (symbol, scopes) => {
|
|
|
10615
10602
|
if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
|
|
10616
10603
|
return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
|
|
10617
10604
|
};
|
|
10618
|
-
const isConvergingFunctionalUpdater = (node, scopes) => {
|
|
10619
|
-
const updater = unwrapExpression$3(node);
|
|
10620
|
-
if (!isNodeOfType(updater, "ArrowFunctionExpression") && !isNodeOfType(updater, "FunctionExpression")) return false;
|
|
10621
|
-
const previousValueParameter = updater.params?.[0];
|
|
10622
|
-
if (!previousValueParameter || !isNodeOfType(previousValueParameter, "Identifier")) return false;
|
|
10623
|
-
let returnedExpression = updater.body;
|
|
10624
|
-
if (isNodeOfType(updater.body, "BlockStatement")) {
|
|
10625
|
-
returnedExpression = null;
|
|
10626
|
-
for (const statement of updater.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) {
|
|
10627
|
-
returnedExpression = statement.argument;
|
|
10628
|
-
break;
|
|
10629
|
-
}
|
|
10630
|
-
}
|
|
10631
|
-
const conditional = returnedExpression ? unwrapExpression$3(returnedExpression) : null;
|
|
10632
|
-
if (!conditional || !isNodeOfType(conditional, "ConditionalExpression")) return false;
|
|
10633
|
-
const test = unwrapExpression$3(conditional.test);
|
|
10634
|
-
if (!isNodeOfType(test, "BinaryExpression") || ![
|
|
10635
|
-
"===",
|
|
10636
|
-
"!==",
|
|
10637
|
-
"==",
|
|
10638
|
-
"!="
|
|
10639
|
-
].includes(test.operator)) return false;
|
|
10640
|
-
const isPreviousValue = (expression) => {
|
|
10641
|
-
const candidate = unwrapExpression$3(expression);
|
|
10642
|
-
return isNodeOfType(candidate, "Identifier") && candidate.name === previousValueParameter.name;
|
|
10643
|
-
};
|
|
10644
|
-
let comparedValue = null;
|
|
10645
|
-
if (isPreviousValue(test.left)) comparedValue = test.right;
|
|
10646
|
-
else if (isPreviousValue(test.right)) comparedValue = test.left;
|
|
10647
|
-
if (!comparedValue) return false;
|
|
10648
|
-
if (isPotentiallyFreshComparedValue(comparedValue, scopes)) return false;
|
|
10649
|
-
const isSameComparedValue = (expression) => {
|
|
10650
|
-
const candidate = unwrapExpression$3(expression);
|
|
10651
|
-
const compared = unwrapExpression$3(comparedValue);
|
|
10652
|
-
if (isNodeOfType(candidate, "Identifier") && isNodeOfType(compared, "Identifier")) return candidate.name === compared.name;
|
|
10653
|
-
if (isNodeOfType(candidate, "Literal") && isNodeOfType(compared, "Literal")) return candidate.value === compared.value;
|
|
10654
|
-
return false;
|
|
10655
|
-
};
|
|
10656
|
-
return test.operator === "===" || test.operator === "==" ? isPreviousValue(conditional.consequent) && isSameComparedValue(conditional.alternate) : isSameComparedValue(conditional.consequent) && isPreviousValue(conditional.alternate);
|
|
10657
|
-
};
|
|
10658
|
-
const isGuardedStableSetterCall = (identifier, symbol, scopes) => {
|
|
10659
|
-
const parent = identifier.parent;
|
|
10660
|
-
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier || !isStableSetterLikeSymbol(symbol, scopes)) return false;
|
|
10661
|
-
const writtenValue = parent.arguments?.[0];
|
|
10662
|
-
return Boolean(writtenValue && isConvergingFunctionalUpdater(writtenValue, scopes));
|
|
10663
|
-
};
|
|
10664
10605
|
const findStableSetterReference = (node, scopes) => {
|
|
10665
10606
|
let setterName = null;
|
|
10666
10607
|
const visit = (current) => {
|
|
@@ -10668,7 +10609,7 @@ const findStableSetterReference = (node, scopes) => {
|
|
|
10668
10609
|
if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
|
|
10669
10610
|
if (isNodeOfType(current, "Identifier")) {
|
|
10670
10611
|
const symbol = scopes.referenceFor(current)?.resolvedSymbol;
|
|
10671
|
-
if (symbol && isStableSetterLikeSymbol(symbol, scopes)
|
|
10612
|
+
if (symbol && isStableSetterLikeSymbol(symbol, scopes)) {
|
|
10672
10613
|
setterName = symbol.name;
|
|
10673
10614
|
return;
|
|
10674
10615
|
}
|
|
@@ -22429,26 +22370,6 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
22429
22370
|
const importDeclaration = declarationNode.parent;
|
|
22430
22371
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22431
22372
|
}));
|
|
22432
|
-
const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
|
|
22433
|
-
if (def.type !== "ImportBinding") return false;
|
|
22434
|
-
const declarationNode = def.node;
|
|
22435
|
-
if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
|
|
22436
|
-
const importDeclaration = declarationNode.parent;
|
|
22437
|
-
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22438
|
-
}));
|
|
22439
|
-
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
22440
|
-
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
22441
|
-
const callee = stripParenExpression(declarator.init.callee);
|
|
22442
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
22443
|
-
const reference = getRef(analysis, callee);
|
|
22444
|
-
if (!reference?.resolved) return callee.name === hookName;
|
|
22445
|
-
return isReactNamedImportReference(reference, hookName);
|
|
22446
|
-
}
|
|
22447
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
22448
|
-
const namespaceReference = getRef(analysis, callee.object);
|
|
22449
|
-
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
22450
|
-
return isReactNamespaceImportReference(namespaceReference);
|
|
22451
|
-
};
|
|
22452
22373
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
22453
22374
|
if (!node) return false;
|
|
22454
22375
|
if (isNodeOfType(node, "Identifier")) {
|
|
@@ -23503,20 +23424,6 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23503
23424
|
}
|
|
23504
23425
|
return evidence;
|
|
23505
23426
|
};
|
|
23506
|
-
const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
|
|
23507
|
-
const evidence = collectValueEvidence(analysis, expression, {
|
|
23508
|
-
functionNode: componentFunction,
|
|
23509
|
-
invocation: null,
|
|
23510
|
-
isDeferred: false,
|
|
23511
|
-
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23512
|
-
substitutions: /* @__PURE__ */ new Map(),
|
|
23513
|
-
currentFilename
|
|
23514
|
-
}, 1);
|
|
23515
|
-
return {
|
|
23516
|
-
sourceReferences: evidence.sourceReferences,
|
|
23517
|
-
isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
|
|
23518
|
-
};
|
|
23519
|
-
};
|
|
23520
23427
|
const findStateSetterReference = (analysis, callExpression) => {
|
|
23521
23428
|
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
23522
23429
|
const callee = stripParenExpression(callExpression.callee);
|
|
@@ -26569,6 +26476,57 @@ const noDefaultProps = defineRule({
|
|
|
26569
26476
|
} })
|
|
26570
26477
|
});
|
|
26571
26478
|
//#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
|
|
26572
26530
|
//#region src/plugin/utils/is-component-function.ts
|
|
26573
26531
|
const isFunctionAssignedToComponent = (functionNode) => {
|
|
26574
26532
|
let cursor = functionNode.parent ?? null;
|
|
@@ -26702,236 +26660,6 @@ const createComponentPropStackTracker = (callbacks) => {
|
|
|
26702
26660
|
};
|
|
26703
26661
|
};
|
|
26704
26662
|
//#endregion
|
|
26705
|
-
//#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
|
|
26706
|
-
const findReferenceDeclarator = (reference) => {
|
|
26707
|
-
for (const definition of reference.resolved?.defs ?? []) {
|
|
26708
|
-
const definitionNode = definition.node;
|
|
26709
|
-
if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
|
|
26710
|
-
}
|
|
26711
|
-
return null;
|
|
26712
|
-
};
|
|
26713
|
-
const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
26714
|
-
const node = stripParenExpression(expression);
|
|
26715
|
-
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26716
|
-
const reference = getRef(analysis, node);
|
|
26717
|
-
if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
|
|
26718
|
-
if (isProp(analysis, reference)) {
|
|
26719
|
-
if (isWholePropsObjectReference(analysis, reference)) return null;
|
|
26720
|
-
return { bindingIdentity: reference.resolved };
|
|
26721
|
-
}
|
|
26722
|
-
if (isState(analysis, reference)) {
|
|
26723
|
-
const stateDeclarator = getUseStateDecl(analysis, reference);
|
|
26724
|
-
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
|
|
26725
|
-
return { bindingIdentity: reference.resolved };
|
|
26726
|
-
}
|
|
26727
|
-
if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
|
|
26728
|
-
const declarator = findReferenceDeclarator(reference);
|
|
26729
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
26730
|
-
const nextVisitedBindings = new Set(visitedBindings);
|
|
26731
|
-
nextVisitedBindings.add(reference.resolved);
|
|
26732
|
-
return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
|
|
26733
|
-
};
|
|
26734
|
-
const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
|
|
26735
|
-
const getDirectBranchStatements = (branch) => {
|
|
26736
|
-
if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
|
|
26737
|
-
return [branch];
|
|
26738
|
-
};
|
|
26739
|
-
const getDirectCallExpression = (statement) => {
|
|
26740
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26741
|
-
const expression = stripParenExpression(statement.expression);
|
|
26742
|
-
return isNodeOfType(expression, "CallExpression") ? expression : null;
|
|
26743
|
-
};
|
|
26744
|
-
const getDirectAssignmentExpression = (statement) => {
|
|
26745
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26746
|
-
const expression = stripParenExpression(statement.expression);
|
|
26747
|
-
return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
|
|
26748
|
-
};
|
|
26749
|
-
const findDirectStateSetterReference = (analysis, callExpression) => {
|
|
26750
|
-
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
26751
|
-
const callee = stripParenExpression(callExpression.callee);
|
|
26752
|
-
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
26753
|
-
const reference = getRef(analysis, callee);
|
|
26754
|
-
return reference && isStateSetter(analysis, reference) ? reference : null;
|
|
26755
|
-
};
|
|
26756
|
-
const getStaticRefCurrentReference = (analysis, expression) => {
|
|
26757
|
-
const node = stripParenExpression(expression);
|
|
26758
|
-
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
26759
|
-
const reference = getRef(analysis, node.object);
|
|
26760
|
-
if (!reference) return null;
|
|
26761
|
-
const declarator = findReferenceDeclarator(reference);
|
|
26762
|
-
return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
|
|
26763
|
-
};
|
|
26764
|
-
const getStateTracker = (analysis, expression) => {
|
|
26765
|
-
const node = stripParenExpression(expression);
|
|
26766
|
-
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26767
|
-
const reference = getRef(analysis, node);
|
|
26768
|
-
if (!reference || !isState(analysis, reference)) return null;
|
|
26769
|
-
const declarator = getUseStateDecl(analysis, reference);
|
|
26770
|
-
if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
|
|
26771
|
-
return {
|
|
26772
|
-
kind: "state",
|
|
26773
|
-
declarator
|
|
26774
|
-
};
|
|
26775
|
-
};
|
|
26776
|
-
const getRefTracker = (analysis, expression) => {
|
|
26777
|
-
const reference = getStaticRefCurrentReference(analysis, expression);
|
|
26778
|
-
return reference ? {
|
|
26779
|
-
kind: "ref",
|
|
26780
|
-
reference
|
|
26781
|
-
} : null;
|
|
26782
|
-
};
|
|
26783
|
-
const getTrackerInitializer = (tracker) => {
|
|
26784
|
-
const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
|
|
26785
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
|
|
26786
|
-
const initializer = declarator.init.arguments?.[0];
|
|
26787
|
-
return initializer ? initializer : null;
|
|
26788
|
-
};
|
|
26789
|
-
const isTrackerSynchronized = (analysis, guard) => {
|
|
26790
|
-
for (const statement of guard.statements) {
|
|
26791
|
-
if (guard.tracker.kind === "state") {
|
|
26792
|
-
const callExpression = getDirectCallExpression(statement);
|
|
26793
|
-
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26794
|
-
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26795
|
-
if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
|
|
26796
|
-
const writtenValue = callExpression.arguments?.[0];
|
|
26797
|
-
if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26798
|
-
continue;
|
|
26799
|
-
}
|
|
26800
|
-
const assignmentExpression = getDirectAssignmentExpression(statement);
|
|
26801
|
-
if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
|
|
26802
|
-
if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
|
|
26803
|
-
if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26804
|
-
}
|
|
26805
|
-
return false;
|
|
26806
|
-
};
|
|
26807
|
-
const parseRenderTrackerGuard = (analysis, statement) => {
|
|
26808
|
-
if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
|
|
26809
|
-
const left = statement.test.left;
|
|
26810
|
-
const right = statement.test.right;
|
|
26811
|
-
const candidates = [{
|
|
26812
|
-
sourceExpression: left,
|
|
26813
|
-
trackerExpression: right
|
|
26814
|
-
}, {
|
|
26815
|
-
sourceExpression: right,
|
|
26816
|
-
trackerExpression: left
|
|
26817
|
-
}];
|
|
26818
|
-
for (const candidate of candidates) {
|
|
26819
|
-
const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
|
|
26820
|
-
if (!source) continue;
|
|
26821
|
-
const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
|
|
26822
|
-
if (!tracker) continue;
|
|
26823
|
-
const trackerInitializer = getTrackerInitializer(tracker);
|
|
26824
|
-
if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
|
|
26825
|
-
const guard = {
|
|
26826
|
-
source,
|
|
26827
|
-
tracker,
|
|
26828
|
-
statements: getDirectBranchStatements(statement.consequent)
|
|
26829
|
-
};
|
|
26830
|
-
return isTrackerSynchronized(analysis, guard) ? guard : null;
|
|
26831
|
-
}
|
|
26832
|
-
return null;
|
|
26833
|
-
};
|
|
26834
|
-
const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
|
|
26835
|
-
if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
|
|
26836
|
-
const references = setterReference.resolved.references.filter((reference) => !reference.init);
|
|
26837
|
-
if (references.length !== 1) return false;
|
|
26838
|
-
return references[0].identifier === callExpression.callee;
|
|
26839
|
-
};
|
|
26840
|
-
const getStateInitializer = (stateDeclarator) => {
|
|
26841
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
|
|
26842
|
-
const initializer = stateDeclarator.init.arguments?.[0];
|
|
26843
|
-
return initializer ? initializer : null;
|
|
26844
|
-
};
|
|
26845
|
-
const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
|
|
26846
|
-
if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
|
|
26847
|
-
const componentFunction = componentBody.parent;
|
|
26848
|
-
const facts = [];
|
|
26849
|
-
for (const statement of componentBody.body ?? []) {
|
|
26850
|
-
const guard = parseRenderTrackerGuard(analysis, statement);
|
|
26851
|
-
if (!guard) continue;
|
|
26852
|
-
for (const branchStatement of guard.statements) {
|
|
26853
|
-
const callExpression = getDirectCallExpression(branchStatement);
|
|
26854
|
-
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26855
|
-
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26856
|
-
if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
|
|
26857
|
-
const stateDeclarator = getUseStateDecl(analysis, setterReference);
|
|
26858
|
-
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
|
|
26859
|
-
const writtenValue = callExpression.arguments?.[0];
|
|
26860
|
-
const initializer = getStateInitializer(stateDeclarator);
|
|
26861
|
-
if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
|
|
26862
|
-
facts.push({
|
|
26863
|
-
callExpression,
|
|
26864
|
-
stateDeclarator
|
|
26865
|
-
});
|
|
26866
|
-
}
|
|
26867
|
-
}
|
|
26868
|
-
return facts;
|
|
26869
|
-
};
|
|
26870
|
-
//#endregion
|
|
26871
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
26872
|
-
const getStateName$1 = (stateDeclarator) => {
|
|
26873
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
26874
|
-
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
26875
|
-
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
26876
|
-
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
26877
|
-
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
26878
|
-
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
26879
|
-
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
26880
|
-
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
26881
|
-
};
|
|
26882
|
-
const noDerivedState = defineRule({
|
|
26883
|
-
id: "no-derived-state",
|
|
26884
|
-
title: "Derived value copied into state",
|
|
26885
|
-
severity: "warn",
|
|
26886
|
-
tags: ["test-noise"],
|
|
26887
|
-
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",
|
|
26888
|
-
create: (context) => {
|
|
26889
|
-
const reportStateWrite = (callExpression, stateDeclarator) => {
|
|
26890
|
-
const stateName = getStateName$1(stateDeclarator);
|
|
26891
|
-
context.report({
|
|
26892
|
-
node: callExpression,
|
|
26893
|
-
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
26894
|
-
});
|
|
26895
|
-
};
|
|
26896
|
-
return {
|
|
26897
|
-
...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
|
|
26898
|
-
if (!componentBody) return;
|
|
26899
|
-
const analysis = getProgramAnalysis(componentBody);
|
|
26900
|
-
if (!analysis) return;
|
|
26901
|
-
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26902
|
-
} }).visitors,
|
|
26903
|
-
CallExpression(node) {
|
|
26904
|
-
if (!isUseEffect(node)) return;
|
|
26905
|
-
const analysis = getProgramAnalysis(node);
|
|
26906
|
-
if (!analysis) return;
|
|
26907
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
26908
|
-
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
26909
|
-
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26910
|
-
}
|
|
26911
|
-
}
|
|
26912
|
-
};
|
|
26913
|
-
}
|
|
26914
|
-
});
|
|
26915
|
-
//#endregion
|
|
26916
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
26917
|
-
const noDerivedStateEffect = defineRule({
|
|
26918
|
-
id: "no-derived-state-effect",
|
|
26919
|
-
title: "Derived state stored in an effect",
|
|
26920
|
-
severity: "warn",
|
|
26921
|
-
tags: ["test-noise"],
|
|
26922
|
-
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",
|
|
26923
|
-
create: (context) => ({ CallExpression(node) {
|
|
26924
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
26925
|
-
const analysis = getProgramAnalysis(node);
|
|
26926
|
-
if (!analysis) return;
|
|
26927
|
-
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
26928
|
-
context.report({
|
|
26929
|
-
node,
|
|
26930
|
-
message: "You pay an extra render for state you can derive from other values."
|
|
26931
|
-
});
|
|
26932
|
-
} })
|
|
26933
|
-
});
|
|
26934
|
-
//#endregion
|
|
26935
26663
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
26936
26664
|
const isInitialOnlyPropName = (propName) => {
|
|
26937
26665
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -30295,26 +30023,28 @@ const noGiantComponent = defineRule({
|
|
|
30295
30023
|
const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
|
|
30296
30024
|
return lineCount > 300 ? lineCount : null;
|
|
30297
30025
|
};
|
|
30298
|
-
const reportOversizedComponent = (nameNode, componentName) => {
|
|
30026
|
+
const reportOversizedComponent = (nameNode, componentName, lineCount) => {
|
|
30299
30027
|
context.report({
|
|
30300
30028
|
node: nameNode,
|
|
30301
|
-
message: `Component "${componentName}" is
|
|
30029
|
+
message: `Component "${componentName}" is ${lineCount} lines long, which is hard to read & change. Split it into a few smaller components.`
|
|
30302
30030
|
});
|
|
30303
30031
|
};
|
|
30304
30032
|
return {
|
|
30305
30033
|
FunctionDeclaration(node) {
|
|
30306
30034
|
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
30307
|
-
|
|
30035
|
+
const lineCount = getOversizedComponentLineCount(node);
|
|
30036
|
+
if (lineCount === null) return;
|
|
30308
30037
|
if (!functionContainsReactRenderOutput(node, context.scopes)) return;
|
|
30309
|
-
reportOversizedComponent(node.id, node.id.name);
|
|
30038
|
+
reportOversizedComponent(node.id, node.id.name, lineCount);
|
|
30310
30039
|
},
|
|
30311
30040
|
VariableDeclarator(node) {
|
|
30312
30041
|
if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
|
|
30313
30042
|
const functionNode = unwrapReactHocFunction(node.init);
|
|
30314
30043
|
if (!functionNode) return;
|
|
30315
|
-
|
|
30044
|
+
const lineCount = getOversizedComponentLineCount(functionNode);
|
|
30045
|
+
if (lineCount === null) return;
|
|
30316
30046
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
30317
|
-
reportOversizedComponent(node.id, node.id.name);
|
|
30047
|
+
reportOversizedComponent(node.id, node.id.name, lineCount);
|
|
30318
30048
|
}
|
|
30319
30049
|
};
|
|
30320
30050
|
}
|
|
@@ -35085,66 +34815,6 @@ const noPropCallbackInEffect = defineRule({
|
|
|
35085
34815
|
}
|
|
35086
34816
|
});
|
|
35087
34817
|
//#endregion
|
|
35088
|
-
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
|
|
35089
|
-
const isPreservedThroughConciseArrow = (callExpression, scopes) => {
|
|
35090
|
-
let node = callExpression;
|
|
35091
|
-
let parent = node.parent;
|
|
35092
|
-
while (parent) {
|
|
35093
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
35094
|
-
node = parent;
|
|
35095
|
-
parent = node.parent;
|
|
35096
|
-
continue;
|
|
35097
|
-
}
|
|
35098
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
35099
|
-
node = parent;
|
|
35100
|
-
parent = node.parent;
|
|
35101
|
-
continue;
|
|
35102
|
-
}
|
|
35103
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
35104
|
-
node = parent;
|
|
35105
|
-
parent = node.parent;
|
|
35106
|
-
continue;
|
|
35107
|
-
}
|
|
35108
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
35109
|
-
const expressions = parent.expressions ?? [];
|
|
35110
|
-
if (expressions[expressions.length - 1] !== node) return false;
|
|
35111
|
-
node = parent;
|
|
35112
|
-
parent = node.parent;
|
|
35113
|
-
continue;
|
|
35114
|
-
}
|
|
35115
|
-
if (!isNodeOfType(parent, "ArrowFunctionExpression") || parent.body !== node) return !isResultDiscardedCall(node);
|
|
35116
|
-
const invocation = parent.parent;
|
|
35117
|
-
if (!isNodeOfType(invocation, "CallExpression") || !executesDuringRender(parent, scopes)) return true;
|
|
35118
|
-
if (invocation.arguments?.[0] === parent || invocation.arguments?.[1] === parent) {
|
|
35119
|
-
const callee = stripParenExpression(invocation.callee);
|
|
35120
|
-
return !(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" && invocation.arguments[0] === parent);
|
|
35121
|
-
}
|
|
35122
|
-
node = invocation;
|
|
35123
|
-
parent = node.parent;
|
|
35124
|
-
}
|
|
35125
|
-
return false;
|
|
35126
|
-
};
|
|
35127
|
-
const noPropCallbackInRender = defineRule({
|
|
35128
|
-
id: "no-prop-callback-in-render",
|
|
35129
|
-
title: "Prop callback invoked during render",
|
|
35130
|
-
severity: "error",
|
|
35131
|
-
recommendation: "Invoke the callback from the event or asynchronous operation that produced the value, or from an effect when synchronizing with an external system. Render must stay pure because React can replay or discard it.",
|
|
35132
|
-
create: (context) => ({ CallExpression(node) {
|
|
35133
|
-
if (!isResultDiscardedCall(node)) return;
|
|
35134
|
-
if (isPreservedThroughConciseArrow(node, context.scopes)) return;
|
|
35135
|
-
if (!findRenderPhaseComponentOrHook(node, context.scopes)) return;
|
|
35136
|
-
const analysis = getProgramAnalysis(node);
|
|
35137
|
-
if (!analysis) return;
|
|
35138
|
-
const callee = stripParenExpression(node.callee);
|
|
35139
|
-
if (isFunctionLike$1(callee)) return;
|
|
35140
|
-
if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
|
|
35141
|
-
context.report({
|
|
35142
|
-
node,
|
|
35143
|
-
message: "This prop callback runs during render. React can replay or discard render work, so the callback can fire more than once or for UI that never commits."
|
|
35144
|
-
});
|
|
35145
|
-
} })
|
|
35146
|
-
});
|
|
35147
|
-
//#endregion
|
|
35148
34818
|
//#region src/plugin/rules/architecture/no-prop-types.ts
|
|
35149
34819
|
const PROP_TYPES_PROPERTY = "propTypes";
|
|
35150
34820
|
const isPropTypesKey = (key, computed) => {
|
|
@@ -35839,63 +35509,6 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
35839
35509
|
}
|
|
35840
35510
|
});
|
|
35841
35511
|
//#endregion
|
|
35842
|
-
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
35843
|
-
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
35844
|
-
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
35845
|
-
if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
35846
|
-
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
35847
|
-
if (!symbol?.initializer) return null;
|
|
35848
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
35849
|
-
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
35850
|
-
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
35851
|
-
};
|
|
35852
|
-
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
35853
|
-
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
|
|
35854
|
-
const receiver = stripParenExpression(node.object);
|
|
35855
|
-
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
35856
|
-
};
|
|
35857
|
-
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
35858
|
-
if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
|
|
35859
|
-
let descendant = assignmentExpression;
|
|
35860
|
-
let ancestor = descendant.parent;
|
|
35861
|
-
while (ancestor) {
|
|
35862
|
-
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
|
|
35863
|
-
const { left, right } = ancestor.test;
|
|
35864
|
-
if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
|
|
35865
|
-
}
|
|
35866
|
-
descendant = ancestor;
|
|
35867
|
-
ancestor = descendant.parent;
|
|
35868
|
-
}
|
|
35869
|
-
return false;
|
|
35870
|
-
};
|
|
35871
|
-
const noRefCurrentInRender = defineRule({
|
|
35872
|
-
id: "no-ref-current-in-render",
|
|
35873
|
-
title: "Ref mutated during render",
|
|
35874
|
-
severity: "error",
|
|
35875
|
-
recommendation: "Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.",
|
|
35876
|
-
create: (context) => {
|
|
35877
|
-
const report = (memberExpression) => {
|
|
35878
|
-
if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
|
|
35879
|
-
if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
|
|
35880
|
-
context.report({
|
|
35881
|
-
node: memberExpression,
|
|
35882
|
-
message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
|
|
35883
|
-
});
|
|
35884
|
-
};
|
|
35885
|
-
return {
|
|
35886
|
-
AssignmentExpression(node) {
|
|
35887
|
-
const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
|
|
35888
|
-
if (!refSymbol) return;
|
|
35889
|
-
if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
|
|
35890
|
-
report(node.left);
|
|
35891
|
-
},
|
|
35892
|
-
UpdateExpression(node) {
|
|
35893
|
-
report(node.argument);
|
|
35894
|
-
}
|
|
35895
|
-
};
|
|
35896
|
-
}
|
|
35897
|
-
});
|
|
35898
|
-
//#endregion
|
|
35899
35512
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
35900
35513
|
const isInsideComponentContext = (node) => {
|
|
35901
35514
|
let cursor = node.parent;
|
|
@@ -57286,18 +56899,6 @@ const reactDoctorRules = [
|
|
|
57286
56899
|
requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
|
|
57287
56900
|
}
|
|
57288
56901
|
},
|
|
57289
|
-
{
|
|
57290
|
-
key: "react-doctor/no-prop-callback-in-render",
|
|
57291
|
-
id: "no-prop-callback-in-render",
|
|
57292
|
-
source: "react-doctor",
|
|
57293
|
-
originallyExternal: false,
|
|
57294
|
-
rule: {
|
|
57295
|
-
...noPropCallbackInRender,
|
|
57296
|
-
framework: "global",
|
|
57297
|
-
category: "Bugs",
|
|
57298
|
-
requires: [...new Set(["react", ...noPropCallbackInRender.requires ?? []])]
|
|
57299
|
-
}
|
|
57300
|
-
},
|
|
57301
56902
|
{
|
|
57302
56903
|
key: "react-doctor/no-prop-types",
|
|
57303
56904
|
id: "no-prop-types",
|
|
@@ -57389,18 +56990,6 @@ const reactDoctorRules = [
|
|
|
57389
56990
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
57390
56991
|
}
|
|
57391
56992
|
},
|
|
57392
|
-
{
|
|
57393
|
-
key: "react-doctor/no-ref-current-in-render",
|
|
57394
|
-
id: "no-ref-current-in-render",
|
|
57395
|
-
source: "react-doctor",
|
|
57396
|
-
originallyExternal: false,
|
|
57397
|
-
rule: {
|
|
57398
|
-
...noRefCurrentInRender,
|
|
57399
|
-
framework: "global",
|
|
57400
|
-
category: "Bugs",
|
|
57401
|
-
requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
|
|
57402
|
-
}
|
|
57403
|
-
},
|
|
57404
56993
|
{
|
|
57405
56994
|
key: "react-doctor/no-render-in-render",
|
|
57406
56995
|
id: "no-render-in-render",
|