oxlint-plugin-react-doctor 0.7.4-dev.593824d → 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 +343 -20
- 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;
|
|
@@ -22752,7 +22860,7 @@ const readsPostMountValueThroughLocals = (root, effectFn, options = {}, visitedL
|
|
|
22752
22860
|
};
|
|
22753
22861
|
//#endregion
|
|
22754
22862
|
//#region src/plugin/rules/state-and-effects/utils/collect-effect-state-write-facts.ts
|
|
22755
|
-
const SYNCHRONOUS_ITERATOR_METHOD_NAMES = new Set([
|
|
22863
|
+
const SYNCHRONOUS_ITERATOR_METHOD_NAMES$1 = new Set([
|
|
22756
22864
|
"every",
|
|
22757
22865
|
"filter",
|
|
22758
22866
|
"find",
|
|
@@ -23060,7 +23168,7 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilena
|
|
|
23060
23168
|
const calleeName = getCallCalleeName$1(child);
|
|
23061
23169
|
const memberName = getStaticMemberName(callee);
|
|
23062
23170
|
const isDeferringCall = calleeName !== null && DEFERRING_CALLEE_NAMES.has(calleeName) || memberName !== null && DEFERRING_MEMBER_NAMES.has(memberName);
|
|
23063
|
-
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");
|
|
23064
23172
|
const enqueueFrame = (callableNode, argumentsForCallable, isDeferred, introducedBindings, allowWithoutInvocationEvidence = false) => {
|
|
23065
23173
|
const callable = resolveWrappedCallable(analysis, callableNode);
|
|
23066
23174
|
if (!callable || callable.async === true || callable === effectFunction) return;
|
|
@@ -23297,10 +23405,15 @@ const isLocallyConstructedObjectMember = (reference, memberExpression) => isNode
|
|
|
23297
23405
|
const definitionNode = definition.node;
|
|
23298
23406
|
return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
|
|
23299
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
|
+
};
|
|
23300
23412
|
const collectValueEvidence = (analysis, expression, frame, remainingCallFrames, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
23301
23413
|
const node = stripParenExpression(expression);
|
|
23302
23414
|
const evidence = emptyEvidence();
|
|
23303
|
-
|
|
23415
|
+
const useRefDeclarator = getUseRefDeclarator(analysis, node);
|
|
23416
|
+
if (!useRefDeclarator && (readsPostMountValue(node) || readsPostMountValueThroughLocals(node, frame.functionNode))) {
|
|
23304
23417
|
evidence.readsExternalValue = true;
|
|
23305
23418
|
return evidence;
|
|
23306
23419
|
}
|
|
@@ -23342,7 +23455,11 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23342
23455
|
evidence.hasUnknownSource = true;
|
|
23343
23456
|
return evidence;
|
|
23344
23457
|
}
|
|
23345
|
-
|
|
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);
|
|
23346
23463
|
evidence.hasUnknownSource = true;
|
|
23347
23464
|
return evidence;
|
|
23348
23465
|
}
|
|
@@ -23358,6 +23475,40 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23358
23475
|
return collectValueEvidence(analysis, initializer.init, frame, remainingCallFrames, visitedBindings);
|
|
23359
23476
|
}
|
|
23360
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
|
+
}
|
|
23361
23512
|
if (isNodeOfType(node.object, "Identifier")) {
|
|
23362
23513
|
const objectReference = getRef(analysis, node.object);
|
|
23363
23514
|
if (objectReference && isLocallyConstructedObjectMember(objectReference, node)) {
|
|
@@ -29136,6 +29287,18 @@ const MUTATING_COLLECTION_METHOD_NAMES = new Set([
|
|
|
29136
29287
|
"set",
|
|
29137
29288
|
"add"
|
|
29138
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
|
+
]);
|
|
29139
29302
|
const getMemberRootBindingName = (node, scope) => {
|
|
29140
29303
|
let currentNode = node;
|
|
29141
29304
|
while (isNodeOfType(currentNode, "MemberExpression")) currentNode = currentNode.object;
|
|
@@ -29189,6 +29352,26 @@ const addMutatingCollectionCallDependencies = (graph, expression, scope, eventHa
|
|
|
29189
29352
|
addDependencyNames(dependencyNames, controlDependencyNames);
|
|
29190
29353
|
addDependencies(graph, receiverRootName, dependencyNames);
|
|
29191
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
|
+
};
|
|
29192
29375
|
const collectExpressionDependencies = (graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames) => {
|
|
29193
29376
|
if (isNodeOfType(expression, "AssignmentExpression")) {
|
|
29194
29377
|
addAssignmentExpressionDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
|
|
@@ -29196,6 +29379,7 @@ const collectExpressionDependencies = (graph, expression, scope, eventHandlerRef
|
|
|
29196
29379
|
}
|
|
29197
29380
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
29198
29381
|
addMutatingCollectionCallDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
|
|
29382
|
+
addIteratorMutationDependencies(graph, expression, scope, eventHandlerReferenceNames, controlDependencyNames);
|
|
29199
29383
|
return;
|
|
29200
29384
|
}
|
|
29201
29385
|
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
@@ -30236,28 +30420,26 @@ const noGiantComponent = defineRule({
|
|
|
30236
30420
|
const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
|
|
30237
30421
|
return lineCount > 300 ? lineCount : null;
|
|
30238
30422
|
};
|
|
30239
|
-
const reportOversizedComponent = (nameNode, componentName
|
|
30423
|
+
const reportOversizedComponent = (nameNode, componentName) => {
|
|
30240
30424
|
context.report({
|
|
30241
30425
|
node: nameNode,
|
|
30242
|
-
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.`
|
|
30243
30427
|
});
|
|
30244
30428
|
};
|
|
30245
30429
|
return {
|
|
30246
30430
|
FunctionDeclaration(node) {
|
|
30247
30431
|
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
30248
|
-
|
|
30249
|
-
if (lineCount === null) return;
|
|
30432
|
+
if (getOversizedComponentLineCount(node) === null) return;
|
|
30250
30433
|
if (!functionContainsReactRenderOutput(node, context.scopes)) return;
|
|
30251
|
-
reportOversizedComponent(node.id, node.id.name
|
|
30434
|
+
reportOversizedComponent(node.id, node.id.name);
|
|
30252
30435
|
},
|
|
30253
30436
|
VariableDeclarator(node) {
|
|
30254
30437
|
if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
|
|
30255
30438
|
const functionNode = unwrapReactHocFunction(node.init);
|
|
30256
30439
|
if (!functionNode) return;
|
|
30257
|
-
|
|
30258
|
-
if (lineCount === null) return;
|
|
30440
|
+
if (getOversizedComponentLineCount(functionNode) === null) return;
|
|
30259
30441
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
30260
|
-
reportOversizedComponent(node.id, node.id.name
|
|
30442
|
+
reportOversizedComponent(node.id, node.id.name);
|
|
30261
30443
|
}
|
|
30262
30444
|
};
|
|
30263
30445
|
}
|
|
@@ -35028,6 +35210,66 @@ const noPropCallbackInEffect = defineRule({
|
|
|
35028
35210
|
}
|
|
35029
35211
|
});
|
|
35030
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
|
|
35031
35273
|
//#region src/plugin/rules/architecture/no-prop-types.ts
|
|
35032
35274
|
const PROP_TYPES_PROPERTY = "propTypes";
|
|
35033
35275
|
const isPropTypesKey = (key, computed) => {
|
|
@@ -35722,6 +35964,63 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
35722
35964
|
}
|
|
35723
35965
|
});
|
|
35724
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
|
|
35725
36024
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
35726
36025
|
const isInsideComponentContext = (node) => {
|
|
35727
36026
|
let cursor = node.parent;
|
|
@@ -57112,6 +57411,18 @@ const reactDoctorRules = [
|
|
|
57112
57411
|
requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
|
|
57113
57412
|
}
|
|
57114
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
|
+
},
|
|
57115
57426
|
{
|
|
57116
57427
|
key: "react-doctor/no-prop-types",
|
|
57117
57428
|
id: "no-prop-types",
|
|
@@ -57203,6 +57514,18 @@ const reactDoctorRules = [
|
|
|
57203
57514
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
57204
57515
|
}
|
|
57205
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
|
+
},
|
|
57206
57529
|
{
|
|
57207
57530
|
key: "react-doctor/no-render-in-render",
|
|
57208
57531
|
id: "no-render-in-render",
|