oxlint-plugin-react-doctor 0.7.4-dev.5113067 → 0.7.4-dev.5aa82e8
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 +92 -0
- package/dist/index.js +607 -71
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -5296,6 +5296,29 @@ 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
|
+
};
|
|
5299
5322
|
}, {
|
|
5300
5323
|
readonly key: "react-doctor/no-prop-types";
|
|
5301
5324
|
readonly id: "no-prop-types";
|
|
@@ -5480,6 +5503,29 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
5480
5503
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5481
5504
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5482
5505
|
};
|
|
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
|
+
};
|
|
5483
5529
|
}, {
|
|
5484
5530
|
readonly key: "react-doctor/no-render-in-render";
|
|
5485
5531
|
readonly id: "no-render-in-render";
|
|
@@ -14516,6 +14562,29 @@ declare const RULES: readonly [{
|
|
|
14516
14562
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14517
14563
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14518
14564
|
};
|
|
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
|
+
};
|
|
14519
14588
|
}, {
|
|
14520
14589
|
readonly key: "react-doctor/no-prop-types";
|
|
14521
14590
|
readonly id: "no-prop-types";
|
|
@@ -14700,6 +14769,29 @@ declare const RULES: readonly [{
|
|
|
14700
14769
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14701
14770
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14702
14771
|
};
|
|
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
|
+
};
|
|
14703
14795
|
}, {
|
|
14704
14796
|
readonly key: "react-doctor/no-render-in-render";
|
|
14705
14797
|
readonly id: "no-render-in-render";
|
package/dist/index.js
CHANGED
|
@@ -553,7 +553,7 @@ const EXTERNAL_SYNC_OBSERVER_CONSTRUCTORS = new Set([
|
|
|
553
553
|
"ResizeObserver",
|
|
554
554
|
"PerformanceObserver"
|
|
555
555
|
]);
|
|
556
|
-
const STORAGE_OBJECTS
|
|
556
|
+
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
557
557
|
//#endregion
|
|
558
558
|
//#region src/plugin/constants/react.ts
|
|
559
559
|
const INDEX_PARAMETER_NAMES = new Set([
|
|
@@ -5139,8 +5139,10 @@ const STORAGE_GLOBALS = new Set([
|
|
|
5139
5139
|
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
5140
5140
|
const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
|
|
5141
5141
|
const STRONG_AUTH_KEY_PATTERN = /jwt|secret|password|passwd|credential|private[-_]?key|api[-_]?key|bearer|access[-_]?token|refresh[-_]?token|auth[-_]?token|id[-_]?token|session/i;
|
|
5142
|
+
const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
|
|
5142
5143
|
const isAuthCredentialKey = (key) => {
|
|
5143
5144
|
if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
|
|
5145
|
+
if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
|
|
5144
5146
|
if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
|
|
5145
5147
|
return true;
|
|
5146
5148
|
};
|
|
@@ -6135,7 +6137,6 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6135
6137
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6136
6138
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6137
6139
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
6138
|
-
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
6139
6140
|
const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
|
|
6140
6141
|
const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
|
|
6141
6142
|
const resolveStringKey = (keyArg, context) => {
|
|
@@ -6158,7 +6159,7 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6158
6159
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6159
6160
|
const receiver = stripParenExpression(node.callee.object);
|
|
6160
6161
|
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6161
|
-
if (
|
|
6162
|
+
if (receiver.name !== "localStorage") return;
|
|
6162
6163
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6163
6164
|
if (node.callee.property.name !== "setItem") return;
|
|
6164
6165
|
const keyArg = node.arguments?.[0];
|
|
@@ -8938,7 +8939,7 @@ const getListenerAbortControllerKey = (usage, context) => {
|
|
|
8938
8939
|
}
|
|
8939
8940
|
return null;
|
|
8940
8941
|
};
|
|
8941
|
-
const SYNCHRONOUS_ITERATOR_METHOD_NAMES$
|
|
8942
|
+
const SYNCHRONOUS_ITERATOR_METHOD_NAMES$2 = new Set([
|
|
8942
8943
|
"every",
|
|
8943
8944
|
"filter",
|
|
8944
8945
|
"flatMap",
|
|
@@ -8954,7 +8955,7 @@ const isSynchronousIteratorCallback = (functionNode) => {
|
|
|
8954
8955
|
const callee = stripParenExpression(callNode.callee);
|
|
8955
8956
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
8956
8957
|
if (isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && callee.property.name === "from") return callNode.arguments?.[1] === functionNode;
|
|
8957
|
-
return SYNCHRONOUS_ITERATOR_METHOD_NAMES$
|
|
8958
|
+
return SYNCHRONOUS_ITERATOR_METHOD_NAMES$2.has(callee.property.name) && callNode.arguments?.[0] === functionNode;
|
|
8958
8959
|
};
|
|
8959
8960
|
const findDirectCallForReference = (identifier) => {
|
|
8960
8961
|
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
@@ -10589,6 +10590,19 @@ const isUnstableInitializer = (node) => {
|
|
|
10589
10590
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
10590
10591
|
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
10592
|
};
|
|
10593
|
+
const isPotentiallyFreshComparedValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
10594
|
+
const candidate = unwrapExpression$3(node);
|
|
10595
|
+
if (isUnstableInitializer(candidate)) return true;
|
|
10596
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return isPotentiallyFreshComparedValue(candidate.consequent, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.alternate, scopes, visitedSymbolIds);
|
|
10597
|
+
if (isNodeOfType(candidate, "LogicalExpression")) return isPotentiallyFreshComparedValue(candidate.left, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.right, scopes, visitedSymbolIds);
|
|
10598
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
10599
|
+
const symbol = scopes.symbolFor(candidate);
|
|
10600
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
10601
|
+
if (symbol.kind === "let" || symbol.kind === "var") return true;
|
|
10602
|
+
if (symbol.kind !== "const" || !symbol.initializer) return false;
|
|
10603
|
+
visitedSymbolIds.add(symbol.id);
|
|
10604
|
+
return isPotentiallyFreshComparedValue(symbol.initializer, scopes, visitedSymbolIds);
|
|
10605
|
+
};
|
|
10592
10606
|
const isExtraDepAllowedForHook = (hookName, node, scopes) => {
|
|
10593
10607
|
if (!isExtraReactiveDepAllowed(node, scopes)) return false;
|
|
10594
10608
|
if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
|
|
@@ -10602,6 +10616,52 @@ const isStableSetterLikeSymbol = (symbol, scopes) => {
|
|
|
10602
10616
|
if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
|
|
10603
10617
|
return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
|
|
10604
10618
|
};
|
|
10619
|
+
const isConvergingFunctionalUpdater = (node, scopes) => {
|
|
10620
|
+
const updater = unwrapExpression$3(node);
|
|
10621
|
+
if (!isNodeOfType(updater, "ArrowFunctionExpression") && !isNodeOfType(updater, "FunctionExpression")) return false;
|
|
10622
|
+
const previousValueParameter = updater.params?.[0];
|
|
10623
|
+
if (!previousValueParameter || !isNodeOfType(previousValueParameter, "Identifier")) return false;
|
|
10624
|
+
let returnedExpression = updater.body;
|
|
10625
|
+
if (isNodeOfType(updater.body, "BlockStatement")) {
|
|
10626
|
+
returnedExpression = null;
|
|
10627
|
+
for (const statement of updater.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) {
|
|
10628
|
+
returnedExpression = statement.argument;
|
|
10629
|
+
break;
|
|
10630
|
+
}
|
|
10631
|
+
}
|
|
10632
|
+
const conditional = returnedExpression ? unwrapExpression$3(returnedExpression) : null;
|
|
10633
|
+
if (!conditional || !isNodeOfType(conditional, "ConditionalExpression")) return false;
|
|
10634
|
+
const test = unwrapExpression$3(conditional.test);
|
|
10635
|
+
if (!isNodeOfType(test, "BinaryExpression") || ![
|
|
10636
|
+
"===",
|
|
10637
|
+
"!==",
|
|
10638
|
+
"==",
|
|
10639
|
+
"!="
|
|
10640
|
+
].includes(test.operator)) return false;
|
|
10641
|
+
const isPreviousValue = (expression) => {
|
|
10642
|
+
const candidate = unwrapExpression$3(expression);
|
|
10643
|
+
return isNodeOfType(candidate, "Identifier") && candidate.name === previousValueParameter.name;
|
|
10644
|
+
};
|
|
10645
|
+
let comparedValue = null;
|
|
10646
|
+
if (isPreviousValue(test.left)) comparedValue = test.right;
|
|
10647
|
+
else if (isPreviousValue(test.right)) comparedValue = test.left;
|
|
10648
|
+
if (!comparedValue) return false;
|
|
10649
|
+
if (isPotentiallyFreshComparedValue(comparedValue, scopes)) return false;
|
|
10650
|
+
const isSameComparedValue = (expression) => {
|
|
10651
|
+
const candidate = unwrapExpression$3(expression);
|
|
10652
|
+
const compared = unwrapExpression$3(comparedValue);
|
|
10653
|
+
if (isNodeOfType(candidate, "Identifier") && isNodeOfType(compared, "Identifier")) return candidate.name === compared.name;
|
|
10654
|
+
if (isNodeOfType(candidate, "Literal") && isNodeOfType(compared, "Literal")) return candidate.value === compared.value;
|
|
10655
|
+
return false;
|
|
10656
|
+
};
|
|
10657
|
+
return test.operator === "===" || test.operator === "==" ? isPreviousValue(conditional.consequent) && isSameComparedValue(conditional.alternate) : isSameComparedValue(conditional.consequent) && isPreviousValue(conditional.alternate);
|
|
10658
|
+
};
|
|
10659
|
+
const isGuardedStableSetterCall = (identifier, symbol, scopes) => {
|
|
10660
|
+
const parent = identifier.parent;
|
|
10661
|
+
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier || !isStableSetterLikeSymbol(symbol, scopes)) return false;
|
|
10662
|
+
const writtenValue = parent.arguments?.[0];
|
|
10663
|
+
return Boolean(writtenValue && isConvergingFunctionalUpdater(writtenValue, scopes));
|
|
10664
|
+
};
|
|
10605
10665
|
const findStableSetterReference = (node, scopes) => {
|
|
10606
10666
|
let setterName = null;
|
|
10607
10667
|
const visit = (current) => {
|
|
@@ -10609,7 +10669,7 @@ const findStableSetterReference = (node, scopes) => {
|
|
|
10609
10669
|
if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
|
|
10610
10670
|
if (isNodeOfType(current, "Identifier")) {
|
|
10611
10671
|
const symbol = scopes.referenceFor(current)?.resolvedSymbol;
|
|
10612
|
-
if (symbol && isStableSetterLikeSymbol(symbol, scopes)) {
|
|
10672
|
+
if (symbol && isStableSetterLikeSymbol(symbol, scopes) && !isGuardedStableSetterCall(current, symbol, scopes)) {
|
|
10613
10673
|
setterName = symbol.name;
|
|
10614
10674
|
return;
|
|
10615
10675
|
}
|
|
@@ -13502,7 +13562,7 @@ const jsCacheStorage = defineRule({
|
|
|
13502
13562
|
CallExpression(node) {
|
|
13503
13563
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
13504
13564
|
const receiver = stripParenExpression(node.callee.object);
|
|
13505
|
-
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS
|
|
13565
|
+
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS.has(receiver.name)) return;
|
|
13506
13566
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
13507
13567
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
13508
13568
|
const storageKey = String(node.arguments[0].value);
|
|
@@ -13769,6 +13829,11 @@ const jsEarlyExit = defineRule({
|
|
|
13769
13829
|
});
|
|
13770
13830
|
//#endregion
|
|
13771
13831
|
//#region src/plugin/rules/js-performance/js-flatmap-filter.ts
|
|
13832
|
+
const BOUNDED_PIPELINE_SOURCE_METHOD_NAMES = new Set(["slice", "split"]);
|
|
13833
|
+
const isBoundedPipelineSource = (node) => {
|
|
13834
|
+
const receiver = stripParenExpression(node);
|
|
13835
|
+
return isNodeOfType(receiver, "CallExpression") && isNodeOfType(receiver.callee, "MemberExpression") && isNodeOfType(receiver.callee.property, "Identifier") && BOUNDED_PIPELINE_SOURCE_METHOD_NAMES.has(receiver.callee.property.name);
|
|
13836
|
+
};
|
|
13772
13837
|
const jsFlatmapFilter = defineRule({
|
|
13773
13838
|
id: "js-flatmap-filter",
|
|
13774
13839
|
title: ".map().filter(Boolean) loops twice",
|
|
@@ -13786,6 +13851,7 @@ const jsFlatmapFilter = defineRule({
|
|
|
13786
13851
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
13787
13852
|
if (innerCall.callee.property.name !== "map") return;
|
|
13788
13853
|
const receiver = stripParenExpression(innerCall.callee.object);
|
|
13854
|
+
if (receiver && isBoundedPipelineSource(receiver)) return;
|
|
13789
13855
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
13790
13856
|
const elements = receiver.elements ?? [];
|
|
13791
13857
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -15721,6 +15787,40 @@ const isArrayVariableRenderedAsList = (declarator) => {
|
|
|
15721
15787
|
renderedAsListCache.set(declarator, didFindRenderingUse);
|
|
15722
15788
|
return didFindRenderingUse;
|
|
15723
15789
|
};
|
|
15790
|
+
const namedCallbackIteratorCallCache = /* @__PURE__ */ new WeakMap();
|
|
15791
|
+
const findNamedCallbackIteratorCall = (functionNode) => {
|
|
15792
|
+
const cached = namedCallbackIteratorCallCache.get(functionNode);
|
|
15793
|
+
if (cached !== void 0) return cached;
|
|
15794
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
15795
|
+
if (!bindingIdentifier) {
|
|
15796
|
+
namedCallbackIteratorCallCache.set(functionNode, null);
|
|
15797
|
+
return null;
|
|
15798
|
+
}
|
|
15799
|
+
const programRoot = findProgramRoot(functionNode);
|
|
15800
|
+
if (!programRoot) {
|
|
15801
|
+
namedCallbackIteratorCallCache.set(functionNode, null);
|
|
15802
|
+
return null;
|
|
15803
|
+
}
|
|
15804
|
+
let iteratorCall = null;
|
|
15805
|
+
walkAst(programRoot, (node) => {
|
|
15806
|
+
if (iteratorCall) return false;
|
|
15807
|
+
if (!isNodeOfType(node, "Identifier") || node === bindingIdentifier || node.name !== bindingIdentifier.name) return;
|
|
15808
|
+
const binding = findVariableInitializer(node, node.name);
|
|
15809
|
+
if (!binding || binding.bindingIdentifier !== bindingIdentifier) return;
|
|
15810
|
+
const callbackExpression = findTransparentExpressionRoot(node);
|
|
15811
|
+
const callExpression = callbackExpression.parent;
|
|
15812
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) return;
|
|
15813
|
+
const callee = callExpression.callee;
|
|
15814
|
+
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !ITERATOR_METHOD_NAMES$1.has(callee.property.name)) return;
|
|
15815
|
+
const callbackIndex = callee.property.name === "from" ? 1 : 0;
|
|
15816
|
+
if (callExpression.arguments[callbackIndex] !== callbackExpression) return;
|
|
15817
|
+
if (isNonChildrenJsxAttributeValue(callExpression)) return;
|
|
15818
|
+
iteratorCall = callExpression;
|
|
15819
|
+
return false;
|
|
15820
|
+
});
|
|
15821
|
+
namedCallbackIteratorCallCache.set(functionNode, iteratorCall);
|
|
15822
|
+
return iteratorCall;
|
|
15823
|
+
};
|
|
15724
15824
|
const findEnclosingIteratorContext = (jsxNode) => {
|
|
15725
15825
|
let current = jsxNode;
|
|
15726
15826
|
let isOutsideContainingFunction = false;
|
|
@@ -15735,6 +15835,11 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
15735
15835
|
const grandparent = parent.parent;
|
|
15736
15836
|
if (grandparent && isNodeOfType(grandparent, "Property")) return null;
|
|
15737
15837
|
if (isOutsideContainingFunction) return null;
|
|
15838
|
+
const namedCallbackIteratorCall = findNamedCallbackIteratorCall(parent);
|
|
15839
|
+
if (namedCallbackIteratorCall) return {
|
|
15840
|
+
kind: "iterator",
|
|
15841
|
+
callExpression: namedCallbackIteratorCall
|
|
15842
|
+
};
|
|
15738
15843
|
isOutsideContainingFunction = true;
|
|
15739
15844
|
} else if (isNodeOfType(parent, "ArrayExpression")) {
|
|
15740
15845
|
if (isOutsideContainingFunction) return null;
|
|
@@ -15778,7 +15883,10 @@ const resolveIterationItemName = (callExpression) => {
|
|
|
15778
15883
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
15779
15884
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
15780
15885
|
const targetArgIndex = callee.property.name === "from" ? 1 : 0;
|
|
15781
|
-
const
|
|
15886
|
+
const callbackArgument = callExpression.arguments[targetArgIndex];
|
|
15887
|
+
if (!callbackArgument) return null;
|
|
15888
|
+
const unwrappedCallback = stripParenExpression(callbackArgument);
|
|
15889
|
+
const callback = isNodeOfType(unwrappedCallback, "Identifier") ? findVariableInitializer(unwrappedCallback, unwrappedCallback.name)?.initializer : unwrappedCallback;
|
|
15782
15890
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return null;
|
|
15783
15891
|
const firstParam = callback.params[0];
|
|
15784
15892
|
return firstParam && isNodeOfType(firstParam, "Identifier") ? firstParam.name : null;
|
|
@@ -22370,6 +22478,26 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
22370
22478
|
const importDeclaration = declarationNode.parent;
|
|
22371
22479
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22372
22480
|
}));
|
|
22481
|
+
const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
|
|
22482
|
+
if (def.type !== "ImportBinding") return false;
|
|
22483
|
+
const declarationNode = def.node;
|
|
22484
|
+
if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
|
|
22485
|
+
const importDeclaration = declarationNode.parent;
|
|
22486
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22487
|
+
}));
|
|
22488
|
+
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
22489
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
22490
|
+
const callee = stripParenExpression(declarator.init.callee);
|
|
22491
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
22492
|
+
const reference = getRef(analysis, callee);
|
|
22493
|
+
if (!reference?.resolved) return callee.name === hookName;
|
|
22494
|
+
return isReactNamedImportReference(reference, hookName);
|
|
22495
|
+
}
|
|
22496
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
22497
|
+
const namespaceReference = getRef(analysis, callee.object);
|
|
22498
|
+
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
22499
|
+
return isReactNamespaceImportReference(namespaceReference);
|
|
22500
|
+
};
|
|
22373
22501
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
22374
22502
|
if (!node) return false;
|
|
22375
22503
|
if (isNodeOfType(node, "Identifier")) {
|
|
@@ -22732,7 +22860,7 @@ const readsPostMountValueThroughLocals = (root, effectFn, options = {}, visitedL
|
|
|
22732
22860
|
};
|
|
22733
22861
|
//#endregion
|
|
22734
22862
|
//#region src/plugin/rules/state-and-effects/utils/collect-effect-state-write-facts.ts
|
|
22735
|
-
const SYNCHRONOUS_ITERATOR_METHOD_NAMES = new Set([
|
|
22863
|
+
const SYNCHRONOUS_ITERATOR_METHOD_NAMES$1 = new Set([
|
|
22736
22864
|
"every",
|
|
22737
22865
|
"filter",
|
|
22738
22866
|
"find",
|
|
@@ -23040,7 +23168,7 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilena
|
|
|
23040
23168
|
const calleeName = getCallCalleeName$1(child);
|
|
23041
23169
|
const memberName = getStaticMemberName(callee);
|
|
23042
23170
|
const isDeferringCall = calleeName !== null && DEFERRING_CALLEE_NAMES.has(calleeName) || memberName !== null && DEFERRING_MEMBER_NAMES.has(memberName);
|
|
23043
|
-
const isIteratorCall = memberName !== null && SYNCHRONOUS_ITERATOR_METHOD_NAMES.has(memberName) && isNodeOfType(callee, "MemberExpression");
|
|
23171
|
+
const isIteratorCall = memberName !== null && SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(memberName) && isNodeOfType(callee, "MemberExpression");
|
|
23044
23172
|
const enqueueFrame = (callableNode, argumentsForCallable, isDeferred, introducedBindings, allowWithoutInvocationEvidence = false) => {
|
|
23045
23173
|
const callable = resolveWrappedCallable(analysis, callableNode);
|
|
23046
23174
|
if (!callable || callable.async === true || callable === effectFunction) return;
|
|
@@ -23277,10 +23405,15 @@ const isLocallyConstructedObjectMember = (reference, memberExpression) => isNode
|
|
|
23277
23405
|
const definitionNode = definition.node;
|
|
23278
23406
|
return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
|
|
23279
23407
|
}) === true;
|
|
23408
|
+
const getUseRefDeclarator = (analysis, memberExpression) => {
|
|
23409
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName(memberExpression) !== "current" || !isNodeOfType(memberExpression.object, "Identifier")) return null;
|
|
23410
|
+
return getRef(analysis, memberExpression.object)?.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator") && isNodeOfType(definitionNode.init, "CallExpression") && getCallCalleeName$1(definitionNode.init) === "useRef") ?? null;
|
|
23411
|
+
};
|
|
23280
23412
|
const collectValueEvidence = (analysis, expression, frame, remainingCallFrames, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
23281
23413
|
const node = stripParenExpression(expression);
|
|
23282
23414
|
const evidence = emptyEvidence();
|
|
23283
|
-
|
|
23415
|
+
const useRefDeclarator = getUseRefDeclarator(analysis, node);
|
|
23416
|
+
if (!useRefDeclarator && (readsPostMountValue(node) || readsPostMountValueThroughLocals(node, frame.functionNode))) {
|
|
23284
23417
|
evidence.readsExternalValue = true;
|
|
23285
23418
|
return evidence;
|
|
23286
23419
|
}
|
|
@@ -23322,7 +23455,11 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23322
23455
|
evidence.hasUnknownSource = true;
|
|
23323
23456
|
return evidence;
|
|
23324
23457
|
}
|
|
23325
|
-
|
|
23458
|
+
const nonInitializerWrites = reference.resolved.references.filter((candidateReference) => candidateReference.isWrite() && !candidateReference.init);
|
|
23459
|
+
if (nonInitializerWrites.length > 0) {
|
|
23460
|
+
const writtenIdentifier = nonInitializerWrites[0]?.identifier;
|
|
23461
|
+
const assignment = writtenIdentifier.parent;
|
|
23462
|
+
if (nonInitializerWrites.length === 1 && assignment && isNodeOfType(assignment, "AssignmentExpression") && assignment.operator === "=" && assignment.left === writtenIdentifier) return collectValueEvidence(analysis, assignment.right, frame, remainingCallFrames, visitedBindings);
|
|
23326
23463
|
evidence.hasUnknownSource = true;
|
|
23327
23464
|
return evidence;
|
|
23328
23465
|
}
|
|
@@ -23338,6 +23475,40 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23338
23475
|
return collectValueEvidence(analysis, initializer.init, frame, remainingCallFrames, visitedBindings);
|
|
23339
23476
|
}
|
|
23340
23477
|
if (isNodeOfType(node, "MemberExpression")) {
|
|
23478
|
+
if (getStaticMemberName(node) === "current" && isNodeOfType(node.object, "Identifier")) {
|
|
23479
|
+
const refBinding = getRef(analysis, node.object)?.resolved;
|
|
23480
|
+
const refDeclarator = useRefDeclarator;
|
|
23481
|
+
if (refBinding && refDeclarator && isNodeOfType(refDeclarator, "VariableDeclarator") && isNodeOfType(refDeclarator.init, "CallExpression")) {
|
|
23482
|
+
if (visitedBindings.has(refBinding)) {
|
|
23483
|
+
evidence.hasUnknownSource = true;
|
|
23484
|
+
return evidence;
|
|
23485
|
+
}
|
|
23486
|
+
const refVisitedBindings = new Set(visitedBindings);
|
|
23487
|
+
refVisitedBindings.add(refBinding);
|
|
23488
|
+
const initialValue = refDeclarator.init.arguments?.[0];
|
|
23489
|
+
if (initialValue) mergeEvidence(evidence, collectValueEvidence(analysis, initialValue, frame, remainingCallFrames, new Set(refVisitedBindings)));
|
|
23490
|
+
for (const candidateReference of refBinding.references) {
|
|
23491
|
+
if (candidateReference.init) continue;
|
|
23492
|
+
const identifier = candidateReference.identifier;
|
|
23493
|
+
const member = identifier.parent;
|
|
23494
|
+
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier || getStaticMemberName(member) !== "current") {
|
|
23495
|
+
evidence.hasUnknownSource = true;
|
|
23496
|
+
continue;
|
|
23497
|
+
}
|
|
23498
|
+
const memberParent = member.parent;
|
|
23499
|
+
if (memberParent && isNodeOfType(memberParent, "AssignmentExpression") && memberParent.left === member) {
|
|
23500
|
+
if (memberParent.operator !== "=") {
|
|
23501
|
+
evidence.hasUnknownSource = true;
|
|
23502
|
+
continue;
|
|
23503
|
+
}
|
|
23504
|
+
mergeEvidence(evidence, collectValueEvidence(analysis, memberParent.right, frame, remainingCallFrames, new Set(refVisitedBindings)));
|
|
23505
|
+
continue;
|
|
23506
|
+
}
|
|
23507
|
+
if (memberParent && isNodeOfType(memberParent, "UpdateExpression")) evidence.hasUnknownSource = true;
|
|
23508
|
+
}
|
|
23509
|
+
return evidence;
|
|
23510
|
+
}
|
|
23511
|
+
}
|
|
23341
23512
|
if (isNodeOfType(node.object, "Identifier")) {
|
|
23342
23513
|
const objectReference = getRef(analysis, node.object);
|
|
23343
23514
|
if (objectReference && isLocallyConstructedObjectMember(objectReference, node)) {
|
|
@@ -23424,6 +23595,20 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23424
23595
|
}
|
|
23425
23596
|
return evidence;
|
|
23426
23597
|
};
|
|
23598
|
+
const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
|
|
23599
|
+
const evidence = collectValueEvidence(analysis, expression, {
|
|
23600
|
+
functionNode: componentFunction,
|
|
23601
|
+
invocation: null,
|
|
23602
|
+
isDeferred: false,
|
|
23603
|
+
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23604
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
23605
|
+
currentFilename
|
|
23606
|
+
}, 1);
|
|
23607
|
+
return {
|
|
23608
|
+
sourceReferences: evidence.sourceReferences,
|
|
23609
|
+
isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
|
|
23610
|
+
};
|
|
23611
|
+
};
|
|
23427
23612
|
const findStateSetterReference = (analysis, callExpression) => {
|
|
23428
23613
|
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
23429
23614
|
const callee = stripParenExpression(callExpression.callee);
|
|
@@ -26476,57 +26661,6 @@ const noDefaultProps = defineRule({
|
|
|
26476
26661
|
} })
|
|
26477
26662
|
});
|
|
26478
26663
|
//#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
26664
|
//#region src/plugin/utils/is-component-function.ts
|
|
26531
26665
|
const isFunctionAssignedToComponent = (functionNode) => {
|
|
26532
26666
|
let cursor = functionNode.parent ?? null;
|
|
@@ -26660,6 +26794,236 @@ const createComponentPropStackTracker = (callbacks) => {
|
|
|
26660
26794
|
};
|
|
26661
26795
|
};
|
|
26662
26796
|
//#endregion
|
|
26797
|
+
//#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
|
|
26798
|
+
const findReferenceDeclarator = (reference) => {
|
|
26799
|
+
for (const definition of reference.resolved?.defs ?? []) {
|
|
26800
|
+
const definitionNode = definition.node;
|
|
26801
|
+
if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
|
|
26802
|
+
}
|
|
26803
|
+
return null;
|
|
26804
|
+
};
|
|
26805
|
+
const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
26806
|
+
const node = stripParenExpression(expression);
|
|
26807
|
+
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26808
|
+
const reference = getRef(analysis, node);
|
|
26809
|
+
if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
|
|
26810
|
+
if (isProp(analysis, reference)) {
|
|
26811
|
+
if (isWholePropsObjectReference(analysis, reference)) return null;
|
|
26812
|
+
return { bindingIdentity: reference.resolved };
|
|
26813
|
+
}
|
|
26814
|
+
if (isState(analysis, reference)) {
|
|
26815
|
+
const stateDeclarator = getUseStateDecl(analysis, reference);
|
|
26816
|
+
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
|
|
26817
|
+
return { bindingIdentity: reference.resolved };
|
|
26818
|
+
}
|
|
26819
|
+
if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
|
|
26820
|
+
const declarator = findReferenceDeclarator(reference);
|
|
26821
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
26822
|
+
const nextVisitedBindings = new Set(visitedBindings);
|
|
26823
|
+
nextVisitedBindings.add(reference.resolved);
|
|
26824
|
+
return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
|
|
26825
|
+
};
|
|
26826
|
+
const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
|
|
26827
|
+
const getDirectBranchStatements = (branch) => {
|
|
26828
|
+
if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
|
|
26829
|
+
return [branch];
|
|
26830
|
+
};
|
|
26831
|
+
const getDirectCallExpression = (statement) => {
|
|
26832
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26833
|
+
const expression = stripParenExpression(statement.expression);
|
|
26834
|
+
return isNodeOfType(expression, "CallExpression") ? expression : null;
|
|
26835
|
+
};
|
|
26836
|
+
const getDirectAssignmentExpression = (statement) => {
|
|
26837
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26838
|
+
const expression = stripParenExpression(statement.expression);
|
|
26839
|
+
return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
|
|
26840
|
+
};
|
|
26841
|
+
const findDirectStateSetterReference = (analysis, callExpression) => {
|
|
26842
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
26843
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
26844
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
26845
|
+
const reference = getRef(analysis, callee);
|
|
26846
|
+
return reference && isStateSetter(analysis, reference) ? reference : null;
|
|
26847
|
+
};
|
|
26848
|
+
const getStaticRefCurrentReference = (analysis, expression) => {
|
|
26849
|
+
const node = stripParenExpression(expression);
|
|
26850
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
26851
|
+
const reference = getRef(analysis, node.object);
|
|
26852
|
+
if (!reference) return null;
|
|
26853
|
+
const declarator = findReferenceDeclarator(reference);
|
|
26854
|
+
return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
|
|
26855
|
+
};
|
|
26856
|
+
const getStateTracker = (analysis, expression) => {
|
|
26857
|
+
const node = stripParenExpression(expression);
|
|
26858
|
+
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26859
|
+
const reference = getRef(analysis, node);
|
|
26860
|
+
if (!reference || !isState(analysis, reference)) return null;
|
|
26861
|
+
const declarator = getUseStateDecl(analysis, reference);
|
|
26862
|
+
if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
|
|
26863
|
+
return {
|
|
26864
|
+
kind: "state",
|
|
26865
|
+
declarator
|
|
26866
|
+
};
|
|
26867
|
+
};
|
|
26868
|
+
const getRefTracker = (analysis, expression) => {
|
|
26869
|
+
const reference = getStaticRefCurrentReference(analysis, expression);
|
|
26870
|
+
return reference ? {
|
|
26871
|
+
kind: "ref",
|
|
26872
|
+
reference
|
|
26873
|
+
} : null;
|
|
26874
|
+
};
|
|
26875
|
+
const getTrackerInitializer = (tracker) => {
|
|
26876
|
+
const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
|
|
26877
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
|
|
26878
|
+
const initializer = declarator.init.arguments?.[0];
|
|
26879
|
+
return initializer ? initializer : null;
|
|
26880
|
+
};
|
|
26881
|
+
const isTrackerSynchronized = (analysis, guard) => {
|
|
26882
|
+
for (const statement of guard.statements) {
|
|
26883
|
+
if (guard.tracker.kind === "state") {
|
|
26884
|
+
const callExpression = getDirectCallExpression(statement);
|
|
26885
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26886
|
+
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26887
|
+
if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
|
|
26888
|
+
const writtenValue = callExpression.arguments?.[0];
|
|
26889
|
+
if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26890
|
+
continue;
|
|
26891
|
+
}
|
|
26892
|
+
const assignmentExpression = getDirectAssignmentExpression(statement);
|
|
26893
|
+
if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
|
|
26894
|
+
if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
|
|
26895
|
+
if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26896
|
+
}
|
|
26897
|
+
return false;
|
|
26898
|
+
};
|
|
26899
|
+
const parseRenderTrackerGuard = (analysis, statement) => {
|
|
26900
|
+
if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
|
|
26901
|
+
const left = statement.test.left;
|
|
26902
|
+
const right = statement.test.right;
|
|
26903
|
+
const candidates = [{
|
|
26904
|
+
sourceExpression: left,
|
|
26905
|
+
trackerExpression: right
|
|
26906
|
+
}, {
|
|
26907
|
+
sourceExpression: right,
|
|
26908
|
+
trackerExpression: left
|
|
26909
|
+
}];
|
|
26910
|
+
for (const candidate of candidates) {
|
|
26911
|
+
const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
|
|
26912
|
+
if (!source) continue;
|
|
26913
|
+
const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
|
|
26914
|
+
if (!tracker) continue;
|
|
26915
|
+
const trackerInitializer = getTrackerInitializer(tracker);
|
|
26916
|
+
if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
|
|
26917
|
+
const guard = {
|
|
26918
|
+
source,
|
|
26919
|
+
tracker,
|
|
26920
|
+
statements: getDirectBranchStatements(statement.consequent)
|
|
26921
|
+
};
|
|
26922
|
+
return isTrackerSynchronized(analysis, guard) ? guard : null;
|
|
26923
|
+
}
|
|
26924
|
+
return null;
|
|
26925
|
+
};
|
|
26926
|
+
const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
|
|
26927
|
+
if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
|
|
26928
|
+
const references = setterReference.resolved.references.filter((reference) => !reference.init);
|
|
26929
|
+
if (references.length !== 1) return false;
|
|
26930
|
+
return references[0].identifier === callExpression.callee;
|
|
26931
|
+
};
|
|
26932
|
+
const getStateInitializer = (stateDeclarator) => {
|
|
26933
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
|
|
26934
|
+
const initializer = stateDeclarator.init.arguments?.[0];
|
|
26935
|
+
return initializer ? initializer : null;
|
|
26936
|
+
};
|
|
26937
|
+
const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
|
|
26938
|
+
if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
|
|
26939
|
+
const componentFunction = componentBody.parent;
|
|
26940
|
+
const facts = [];
|
|
26941
|
+
for (const statement of componentBody.body ?? []) {
|
|
26942
|
+
const guard = parseRenderTrackerGuard(analysis, statement);
|
|
26943
|
+
if (!guard) continue;
|
|
26944
|
+
for (const branchStatement of guard.statements) {
|
|
26945
|
+
const callExpression = getDirectCallExpression(branchStatement);
|
|
26946
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26947
|
+
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26948
|
+
if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
|
|
26949
|
+
const stateDeclarator = getUseStateDecl(analysis, setterReference);
|
|
26950
|
+
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
|
|
26951
|
+
const writtenValue = callExpression.arguments?.[0];
|
|
26952
|
+
const initializer = getStateInitializer(stateDeclarator);
|
|
26953
|
+
if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
|
|
26954
|
+
facts.push({
|
|
26955
|
+
callExpression,
|
|
26956
|
+
stateDeclarator
|
|
26957
|
+
});
|
|
26958
|
+
}
|
|
26959
|
+
}
|
|
26960
|
+
return facts;
|
|
26961
|
+
};
|
|
26962
|
+
//#endregion
|
|
26963
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
26964
|
+
const getStateName$1 = (stateDeclarator) => {
|
|
26965
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
26966
|
+
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
26967
|
+
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
26968
|
+
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
26969
|
+
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
26970
|
+
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
26971
|
+
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
26972
|
+
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
26973
|
+
};
|
|
26974
|
+
const noDerivedState = defineRule({
|
|
26975
|
+
id: "no-derived-state",
|
|
26976
|
+
title: "Derived value copied into state",
|
|
26977
|
+
severity: "warn",
|
|
26978
|
+
tags: ["test-noise"],
|
|
26979
|
+
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",
|
|
26980
|
+
create: (context) => {
|
|
26981
|
+
const reportStateWrite = (callExpression, stateDeclarator) => {
|
|
26982
|
+
const stateName = getStateName$1(stateDeclarator);
|
|
26983
|
+
context.report({
|
|
26984
|
+
node: callExpression,
|
|
26985
|
+
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
26986
|
+
});
|
|
26987
|
+
};
|
|
26988
|
+
return {
|
|
26989
|
+
...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
|
|
26990
|
+
if (!componentBody) return;
|
|
26991
|
+
const analysis = getProgramAnalysis(componentBody);
|
|
26992
|
+
if (!analysis) return;
|
|
26993
|
+
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26994
|
+
} }).visitors,
|
|
26995
|
+
CallExpression(node) {
|
|
26996
|
+
if (!isUseEffect(node)) return;
|
|
26997
|
+
const analysis = getProgramAnalysis(node);
|
|
26998
|
+
if (!analysis) return;
|
|
26999
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
27000
|
+
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
27001
|
+
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
27002
|
+
}
|
|
27003
|
+
}
|
|
27004
|
+
};
|
|
27005
|
+
}
|
|
27006
|
+
});
|
|
27007
|
+
//#endregion
|
|
27008
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
27009
|
+
const noDerivedStateEffect = defineRule({
|
|
27010
|
+
id: "no-derived-state-effect",
|
|
27011
|
+
title: "Derived state stored in an effect",
|
|
27012
|
+
severity: "warn",
|
|
27013
|
+
tags: ["test-noise"],
|
|
27014
|
+
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",
|
|
27015
|
+
create: (context) => ({ CallExpression(node) {
|
|
27016
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
27017
|
+
const analysis = getProgramAnalysis(node);
|
|
27018
|
+
if (!analysis) return;
|
|
27019
|
+
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
27020
|
+
context.report({
|
|
27021
|
+
node,
|
|
27022
|
+
message: "You pay an extra render for state you can derive from other values."
|
|
27023
|
+
});
|
|
27024
|
+
} })
|
|
27025
|
+
});
|
|
27026
|
+
//#endregion
|
|
26663
27027
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
26664
27028
|
const isInitialOnlyPropName = (propName) => {
|
|
26665
27029
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -28923,6 +29287,18 @@ const MUTATING_COLLECTION_METHOD_NAMES = new Set([
|
|
|
28923
29287
|
"set",
|
|
28924
29288
|
"add"
|
|
28925
29289
|
]);
|
|
29290
|
+
const SYNCHRONOUS_ITERATOR_METHOD_NAMES = new Set([
|
|
29291
|
+
"every",
|
|
29292
|
+
"filter",
|
|
29293
|
+
"find",
|
|
29294
|
+
"findIndex",
|
|
29295
|
+
"flatMap",
|
|
29296
|
+
"forEach",
|
|
29297
|
+
"map",
|
|
29298
|
+
"reduce",
|
|
29299
|
+
"reduceRight",
|
|
29300
|
+
"some"
|
|
29301
|
+
]);
|
|
28926
29302
|
const getMemberRootBindingName = (node, scope) => {
|
|
28927
29303
|
let currentNode = node;
|
|
28928
29304
|
while (isNodeOfType(currentNode, "MemberExpression")) currentNode = currentNode.object;
|
|
@@ -28976,6 +29352,26 @@ const addMutatingCollectionCallDependencies = (graph, expression, scope, eventHa
|
|
|
28976
29352
|
addDependencyNames(dependencyNames, controlDependencyNames);
|
|
28977
29353
|
addDependencies(graph, receiverRootName, dependencyNames);
|
|
28978
29354
|
};
|
|
29355
|
+
const addIteratorMutationDependencies = (graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames) => {
|
|
29356
|
+
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
29357
|
+
if (!isNodeOfType(expression.callee, "MemberExpression")) return;
|
|
29358
|
+
const methodName = getStaticMemberPropertyName(expression.callee);
|
|
29359
|
+
if (!methodName || !SYNCHRONOUS_ITERATOR_METHOD_NAMES.has(methodName)) return;
|
|
29360
|
+
const iteratorDependencyNames = collectScopedReferenceNames(expression.callee.object, scope, eventHandlerReferenceNames);
|
|
29361
|
+
addDependencyNames(iteratorDependencyNames, controlDependencyNames);
|
|
29362
|
+
for (const argument of expression.arguments ?? []) {
|
|
29363
|
+
if (!isNodeOfType(argument, "ArrowFunctionExpression") && !isNodeOfType(argument, "FunctionExpression")) continue;
|
|
29364
|
+
walkAst(argument.body, (node) => {
|
|
29365
|
+
if (node !== argument.body && isFunctionLike$1(node)) return false;
|
|
29366
|
+
if (!isNodeOfType(node, "CallExpression")) return;
|
|
29367
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
29368
|
+
const nestedMethodName = getStaticMemberPropertyName(node.callee);
|
|
29369
|
+
if (!nestedMethodName || !MUTATING_COLLECTION_METHOD_NAMES.has(nestedMethodName)) return;
|
|
29370
|
+
const receiverRootName = getMemberRootBindingName(node.callee.object, scope);
|
|
29371
|
+
if (receiverRootName) addDependencies(graph, receiverRootName, iteratorDependencyNames);
|
|
29372
|
+
});
|
|
29373
|
+
}
|
|
29374
|
+
};
|
|
28979
29375
|
const collectExpressionDependencies = (graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames) => {
|
|
28980
29376
|
if (isNodeOfType(expression, "AssignmentExpression")) {
|
|
28981
29377
|
addAssignmentExpressionDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
|
|
@@ -28983,6 +29379,7 @@ const collectExpressionDependencies = (graph, expression, scope, eventHandlerRef
|
|
|
28983
29379
|
}
|
|
28984
29380
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
28985
29381
|
addMutatingCollectionCallDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
|
|
29382
|
+
addIteratorMutationDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
|
|
28986
29383
|
return;
|
|
28987
29384
|
}
|
|
28988
29385
|
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
@@ -30023,28 +30420,26 @@ const noGiantComponent = defineRule({
|
|
|
30023
30420
|
const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
|
|
30024
30421
|
return lineCount > 300 ? lineCount : null;
|
|
30025
30422
|
};
|
|
30026
|
-
const reportOversizedComponent = (nameNode, componentName
|
|
30423
|
+
const reportOversizedComponent = (nameNode, componentName) => {
|
|
30027
30424
|
context.report({
|
|
30028
30425
|
node: nameNode,
|
|
30029
|
-
message: `Component "${componentName}" is
|
|
30426
|
+
message: `Component "${componentName}" is over 300 lines long, which is hard to read & change. Split it into a few smaller components.`
|
|
30030
30427
|
});
|
|
30031
30428
|
};
|
|
30032
30429
|
return {
|
|
30033
30430
|
FunctionDeclaration(node) {
|
|
30034
30431
|
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
30035
|
-
|
|
30036
|
-
if (lineCount === null) return;
|
|
30432
|
+
if (getOversizedComponentLineCount(node) === null) return;
|
|
30037
30433
|
if (!functionContainsReactRenderOutput(node, context.scopes)) return;
|
|
30038
|
-
reportOversizedComponent(node.id, node.id.name
|
|
30434
|
+
reportOversizedComponent(node.id, node.id.name);
|
|
30039
30435
|
},
|
|
30040
30436
|
VariableDeclarator(node) {
|
|
30041
30437
|
if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
|
|
30042
30438
|
const functionNode = unwrapReactHocFunction(node.init);
|
|
30043
30439
|
if (!functionNode) return;
|
|
30044
|
-
|
|
30045
|
-
if (lineCount === null) return;
|
|
30440
|
+
if (getOversizedComponentLineCount(functionNode) === null) return;
|
|
30046
30441
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
30047
|
-
reportOversizedComponent(node.id, node.id.name
|
|
30442
|
+
reportOversizedComponent(node.id, node.id.name);
|
|
30048
30443
|
}
|
|
30049
30444
|
};
|
|
30050
30445
|
}
|
|
@@ -34815,6 +35210,66 @@ const noPropCallbackInEffect = defineRule({
|
|
|
34815
35210
|
}
|
|
34816
35211
|
});
|
|
34817
35212
|
//#endregion
|
|
35213
|
+
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
|
|
35214
|
+
const isPreservedThroughConciseArrow = (callExpression, scopes) => {
|
|
35215
|
+
let node = callExpression;
|
|
35216
|
+
let parent = node.parent;
|
|
35217
|
+
while (parent) {
|
|
35218
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
35219
|
+
node = parent;
|
|
35220
|
+
parent = node.parent;
|
|
35221
|
+
continue;
|
|
35222
|
+
}
|
|
35223
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
35224
|
+
node = parent;
|
|
35225
|
+
parent = node.parent;
|
|
35226
|
+
continue;
|
|
35227
|
+
}
|
|
35228
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
35229
|
+
node = parent;
|
|
35230
|
+
parent = node.parent;
|
|
35231
|
+
continue;
|
|
35232
|
+
}
|
|
35233
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
35234
|
+
const expressions = parent.expressions ?? [];
|
|
35235
|
+
if (expressions[expressions.length - 1] !== node) return false;
|
|
35236
|
+
node = parent;
|
|
35237
|
+
parent = node.parent;
|
|
35238
|
+
continue;
|
|
35239
|
+
}
|
|
35240
|
+
if (!isNodeOfType(parent, "ArrowFunctionExpression") || parent.body !== node) return !isResultDiscardedCall(node);
|
|
35241
|
+
const invocation = parent.parent;
|
|
35242
|
+
if (!isNodeOfType(invocation, "CallExpression") || !executesDuringRender(parent, scopes)) return true;
|
|
35243
|
+
if (invocation.arguments?.[0] === parent || invocation.arguments?.[1] === parent) {
|
|
35244
|
+
const callee = stripParenExpression(invocation.callee);
|
|
35245
|
+
return !(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" && invocation.arguments[0] === parent);
|
|
35246
|
+
}
|
|
35247
|
+
node = invocation;
|
|
35248
|
+
parent = node.parent;
|
|
35249
|
+
}
|
|
35250
|
+
return false;
|
|
35251
|
+
};
|
|
35252
|
+
const noPropCallbackInRender = defineRule({
|
|
35253
|
+
id: "no-prop-callback-in-render",
|
|
35254
|
+
title: "Prop callback invoked during render",
|
|
35255
|
+
severity: "error",
|
|
35256
|
+
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.",
|
|
35257
|
+
create: (context) => ({ CallExpression(node) {
|
|
35258
|
+
if (!isResultDiscardedCall(node)) return;
|
|
35259
|
+
if (isPreservedThroughConciseArrow(node, context.scopes)) return;
|
|
35260
|
+
if (!findRenderPhaseComponentOrHook(node, context.scopes)) return;
|
|
35261
|
+
const analysis = getProgramAnalysis(node);
|
|
35262
|
+
if (!analysis) return;
|
|
35263
|
+
const callee = stripParenExpression(node.callee);
|
|
35264
|
+
if (isFunctionLike$1(callee)) return;
|
|
35265
|
+
if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
|
|
35266
|
+
context.report({
|
|
35267
|
+
node,
|
|
35268
|
+
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."
|
|
35269
|
+
});
|
|
35270
|
+
} })
|
|
35271
|
+
});
|
|
35272
|
+
//#endregion
|
|
34818
35273
|
//#region src/plugin/rules/architecture/no-prop-types.ts
|
|
34819
35274
|
const PROP_TYPES_PROPERTY = "propTypes";
|
|
34820
35275
|
const isPropTypesKey = (key, computed) => {
|
|
@@ -35509,6 +35964,63 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
35509
35964
|
}
|
|
35510
35965
|
});
|
|
35511
35966
|
//#endregion
|
|
35967
|
+
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
35968
|
+
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
35969
|
+
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
35970
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
35971
|
+
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
35972
|
+
if (!symbol?.initializer) return null;
|
|
35973
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
35974
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
35975
|
+
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
35976
|
+
};
|
|
35977
|
+
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
35978
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
|
|
35979
|
+
const receiver = stripParenExpression(node.object);
|
|
35980
|
+
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
35981
|
+
};
|
|
35982
|
+
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
35983
|
+
if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
|
|
35984
|
+
let descendant = assignmentExpression;
|
|
35985
|
+
let ancestor = descendant.parent;
|
|
35986
|
+
while (ancestor) {
|
|
35987
|
+
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
|
|
35988
|
+
const { left, right } = ancestor.test;
|
|
35989
|
+
if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
|
|
35990
|
+
}
|
|
35991
|
+
descendant = ancestor;
|
|
35992
|
+
ancestor = descendant.parent;
|
|
35993
|
+
}
|
|
35994
|
+
return false;
|
|
35995
|
+
};
|
|
35996
|
+
const noRefCurrentInRender = defineRule({
|
|
35997
|
+
id: "no-ref-current-in-render",
|
|
35998
|
+
title: "Ref mutated during render",
|
|
35999
|
+
severity: "error",
|
|
36000
|
+
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.",
|
|
36001
|
+
create: (context) => {
|
|
36002
|
+
const report = (memberExpression) => {
|
|
36003
|
+
if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
|
|
36004
|
+
if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
|
|
36005
|
+
context.report({
|
|
36006
|
+
node: memberExpression,
|
|
36007
|
+
message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
|
|
36008
|
+
});
|
|
36009
|
+
};
|
|
36010
|
+
return {
|
|
36011
|
+
AssignmentExpression(node) {
|
|
36012
|
+
const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
|
|
36013
|
+
if (!refSymbol) return;
|
|
36014
|
+
if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
|
|
36015
|
+
report(node.left);
|
|
36016
|
+
},
|
|
36017
|
+
UpdateExpression(node) {
|
|
36018
|
+
report(node.argument);
|
|
36019
|
+
}
|
|
36020
|
+
};
|
|
36021
|
+
}
|
|
36022
|
+
});
|
|
36023
|
+
//#endregion
|
|
35512
36024
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
35513
36025
|
const isInsideComponentContext = (node) => {
|
|
35514
36026
|
let cursor = node.parent;
|
|
@@ -56899,6 +57411,18 @@ const reactDoctorRules = [
|
|
|
56899
57411
|
requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
|
|
56900
57412
|
}
|
|
56901
57413
|
},
|
|
57414
|
+
{
|
|
57415
|
+
key: "react-doctor/no-prop-callback-in-render",
|
|
57416
|
+
id: "no-prop-callback-in-render",
|
|
57417
|
+
source: "react-doctor",
|
|
57418
|
+
originallyExternal: false,
|
|
57419
|
+
rule: {
|
|
57420
|
+
...noPropCallbackInRender,
|
|
57421
|
+
framework: "global",
|
|
57422
|
+
category: "Bugs",
|
|
57423
|
+
requires: [...new Set(["react", ...noPropCallbackInRender.requires ?? []])]
|
|
57424
|
+
}
|
|
57425
|
+
},
|
|
56902
57426
|
{
|
|
56903
57427
|
key: "react-doctor/no-prop-types",
|
|
56904
57428
|
id: "no-prop-types",
|
|
@@ -56990,6 +57514,18 @@ const reactDoctorRules = [
|
|
|
56990
57514
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
56991
57515
|
}
|
|
56992
57516
|
},
|
|
57517
|
+
{
|
|
57518
|
+
key: "react-doctor/no-ref-current-in-render",
|
|
57519
|
+
id: "no-ref-current-in-render",
|
|
57520
|
+
source: "react-doctor",
|
|
57521
|
+
originallyExternal: false,
|
|
57522
|
+
rule: {
|
|
57523
|
+
...noRefCurrentInRender,
|
|
57524
|
+
framework: "global",
|
|
57525
|
+
category: "Bugs",
|
|
57526
|
+
requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
|
|
57527
|
+
}
|
|
57528
|
+
},
|
|
56993
57529
|
{
|
|
56994
57530
|
key: "react-doctor/no-render-in-render",
|
|
56995
57531
|
id: "no-render-in-render",
|