oxlint-plugin-react-doctor 0.7.4-dev.7bbb792 → 0.7.4-dev.98005f2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -46
- package/dist/index.js +81 -441
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -5480,29 +5480,6 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
5480
5480
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5481
5481
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5482
5482
|
};
|
|
5483
|
-
}, {
|
|
5484
|
-
readonly key: "react-doctor/no-ref-current-in-render";
|
|
5485
|
-
readonly id: "no-ref-current-in-render";
|
|
5486
|
-
readonly source: "react-doctor";
|
|
5487
|
-
readonly originallyExternal: false;
|
|
5488
|
-
readonly rule: {
|
|
5489
|
-
readonly framework: "global";
|
|
5490
|
-
readonly category: "Bugs";
|
|
5491
|
-
readonly requires: readonly Capability[];
|
|
5492
|
-
readonly id: string;
|
|
5493
|
-
readonly title?: string;
|
|
5494
|
-
readonly severity: RuleSeverity;
|
|
5495
|
-
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
5496
|
-
readonly tags?: ReadonlyArray<string>;
|
|
5497
|
-
readonly matchByOccurrence?: boolean;
|
|
5498
|
-
readonly defaultEnabled?: boolean;
|
|
5499
|
-
readonly lifecycle?: "retired";
|
|
5500
|
-
readonly scan?: FileScan;
|
|
5501
|
-
readonly committedFilesOnly?: boolean;
|
|
5502
|
-
readonly recommendation?: string;
|
|
5503
|
-
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
5504
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
5505
|
-
};
|
|
5506
5483
|
}, {
|
|
5507
5484
|
readonly key: "react-doctor/no-render-in-render";
|
|
5508
5485
|
readonly id: "no-render-in-render";
|
|
@@ -14723,29 +14700,6 @@ declare const RULES: readonly [{
|
|
|
14723
14700
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14724
14701
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14725
14702
|
};
|
|
14726
|
-
}, {
|
|
14727
|
-
readonly key: "react-doctor/no-ref-current-in-render";
|
|
14728
|
-
readonly id: "no-ref-current-in-render";
|
|
14729
|
-
readonly source: "react-doctor";
|
|
14730
|
-
readonly originallyExternal: false;
|
|
14731
|
-
readonly rule: {
|
|
14732
|
-
readonly framework: "global";
|
|
14733
|
-
readonly category: "Bugs";
|
|
14734
|
-
readonly requires: readonly Capability[];
|
|
14735
|
-
readonly id: string;
|
|
14736
|
-
readonly title?: string;
|
|
14737
|
-
readonly severity: RuleSeverity;
|
|
14738
|
-
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
14739
|
-
readonly tags?: ReadonlyArray<string>;
|
|
14740
|
-
readonly matchByOccurrence?: boolean;
|
|
14741
|
-
readonly defaultEnabled?: boolean;
|
|
14742
|
-
readonly lifecycle?: "retired";
|
|
14743
|
-
readonly scan?: FileScan;
|
|
14744
|
-
readonly committedFilesOnly?: boolean;
|
|
14745
|
-
readonly recommendation?: string;
|
|
14746
|
-
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
14747
|
-
readonly create: (context: RuleContext) => RuleVisitors;
|
|
14748
|
-
};
|
|
14749
14703
|
}, {
|
|
14750
14704
|
readonly key: "react-doctor/no-render-in-render";
|
|
14751
14705
|
readonly id: "no-render-in-render";
|
package/dist/index.js
CHANGED
|
@@ -9968,18 +9968,13 @@ const unwrapExpression$3 = (node) => {
|
|
|
9968
9968
|
return current;
|
|
9969
9969
|
};
|
|
9970
9970
|
/**
|
|
9971
|
-
* Get the hook name from a
|
|
9972
|
-
*
|
|
9971
|
+
* Get the hook name from a call expression's callee, regardless of
|
|
9972
|
+
* whether the hook is called as `useFoo()` (Identifier) or
|
|
9973
|
+
* `React.useFoo()` (MemberExpression).
|
|
9973
9974
|
*/
|
|
9974
|
-
const getHookName = (callee
|
|
9975
|
-
|
|
9976
|
-
if (isNodeOfType(
|
|
9977
|
-
const resolvedSymbol = scopes ? resolveConstIdentifierAlias(strippedCallee, scopes) : null;
|
|
9978
|
-
const importDeclaration = resolvedSymbol?.declarationNode.parent;
|
|
9979
|
-
if (resolvedSymbol?.kind === "import" && importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "react") return getImportedName(resolvedSymbol.declarationNode) ?? strippedCallee.name;
|
|
9980
|
-
return strippedCallee.name;
|
|
9981
|
-
}
|
|
9982
|
-
if (isNodeOfType(strippedCallee, "MemberExpression") && !strippedCallee.computed && isNodeOfType(strippedCallee.property, "Identifier")) return strippedCallee.property.name;
|
|
9975
|
+
const getHookName = (callee) => {
|
|
9976
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
9977
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9983
9978
|
return null;
|
|
9984
9979
|
};
|
|
9985
9980
|
const FUNCTION_SCOPE_KINDS = new Set([
|
|
@@ -10164,7 +10159,7 @@ const STABLE_IDENTITY_WRAPPER_HOOK_NAMES = new Set([
|
|
|
10164
10159
|
* - primitive-literal local consts (the value never changes
|
|
10165
10160
|
* between renders unless the literal does)
|
|
10166
10161
|
*/
|
|
10167
|
-
const symbolHasStableHookOrigin = (symbol
|
|
10162
|
+
const symbolHasStableHookOrigin = (symbol) => {
|
|
10168
10163
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
10169
10164
|
let declarator = symbol.declarationNode;
|
|
10170
10165
|
while (declarator && declarator.type !== "VariableDeclarator") declarator = declarator.parent ?? null;
|
|
@@ -10177,7 +10172,7 @@ const symbolHasStableHookOrigin = (symbol, scopes) => {
|
|
|
10177
10172
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
10178
10173
|
}
|
|
10179
10174
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
10180
|
-
const initializerHookName = getHookName(initializer.callee
|
|
10175
|
+
const initializerHookName = getHookName(initializer.callee);
|
|
10181
10176
|
if (!initializerHookName) return false;
|
|
10182
10177
|
if (initializerHookName === "useRef") return true;
|
|
10183
10178
|
if (initializerHookName === "useEffectEvent") return true;
|
|
@@ -10212,7 +10207,7 @@ const symbolHasStableMemoizedOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
10212
10207
|
if (!declarator.init) return false;
|
|
10213
10208
|
const initializer = unwrapExpression$3(declarator.init);
|
|
10214
10209
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
10215
|
-
const initializerHookName = getHookName(initializer.callee
|
|
10210
|
+
const initializerHookName = getHookName(initializer.callee);
|
|
10216
10211
|
if (!initializerHookName || !MEMOIZING_HOOK_NAMES.has(initializerHookName)) return false;
|
|
10217
10212
|
const depsArgument = initializer.arguments[1];
|
|
10218
10213
|
if (!depsArgument || !isAstNode(depsArgument)) return false;
|
|
@@ -10242,7 +10237,7 @@ const getObjectPropertyValue = (objectExpression, propertyName) => {
|
|
|
10242
10237
|
}
|
|
10243
10238
|
return null;
|
|
10244
10239
|
};
|
|
10245
|
-
const isStableRefContainerCapture = (symbol, depKey
|
|
10240
|
+
const isStableRefContainerCapture = (symbol, depKey) => {
|
|
10246
10241
|
if (symbol.kind !== "const") return false;
|
|
10247
10242
|
if (!depKey.startsWith(`${symbol.name}.`)) return false;
|
|
10248
10243
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
@@ -10256,7 +10251,7 @@ const isStableRefContainerCapture = (symbol, depKey, scopes) => {
|
|
|
10256
10251
|
if (!propertyValue) return false;
|
|
10257
10252
|
currentValue = propertyValue;
|
|
10258
10253
|
}
|
|
10259
|
-
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee
|
|
10254
|
+
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee) === "useRef";
|
|
10260
10255
|
};
|
|
10261
10256
|
const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
10262
10257
|
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
@@ -10274,13 +10269,7 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
10274
10269
|
}
|
|
10275
10270
|
return true;
|
|
10276
10271
|
};
|
|
10277
|
-
const
|
|
10278
|
-
if (symbol.kind !== "const") return false;
|
|
10279
|
-
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
10280
|
-
const resolvedSymbol = resolveConstIdentifierAlias(symbol.bindingIdentifier, scopes);
|
|
10281
|
-
return resolvedSymbol !== null && resolvedSymbol !== symbol && resolvedSymbol.kind === "import";
|
|
10282
|
-
};
|
|
10283
|
-
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol, scopes) || symbolHasStableImportedAlias(symbol, scopes) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
10272
|
+
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
10284
10273
|
//#endregion
|
|
10285
10274
|
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
10286
10275
|
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
@@ -10468,18 +10457,10 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
10468
10457
|
}
|
|
10469
10458
|
const depKey = computeDepKey(reference);
|
|
10470
10459
|
if (!depKey) continue;
|
|
10471
|
-
if (isStableRefContainerCapture(symbol, depKey
|
|
10460
|
+
if (isStableRefContainerCapture(symbol, depKey)) {
|
|
10472
10461
|
stableCapturedNames.add(depKey);
|
|
10473
10462
|
continue;
|
|
10474
10463
|
}
|
|
10475
|
-
if (depKey === symbol.name) {
|
|
10476
|
-
const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
10477
|
-
if (identitySourceKeys) {
|
|
10478
|
-
if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
|
|
10479
|
-
for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
|
|
10480
|
-
continue;
|
|
10481
|
-
}
|
|
10482
|
-
}
|
|
10483
10464
|
keys.add(depKey);
|
|
10484
10465
|
}
|
|
10485
10466
|
return {
|
|
@@ -10508,60 +10489,12 @@ const hasComputedMemberExpression = (node) => {
|
|
|
10508
10489
|
if (stripped.computed) return true;
|
|
10509
10490
|
return hasComputedMemberExpression(stripped.object);
|
|
10510
10491
|
};
|
|
10511
|
-
const mergeIdentitySourceKeys = (expressions, scopes, visitedSymbolIds) => {
|
|
10512
|
-
const identitySourceKeys = /* @__PURE__ */ new Set();
|
|
10513
|
-
for (const expression of expressions) {
|
|
10514
|
-
const expressionSourceKeys = resolveIdentitySourceKeysFromExpression(expression, scopes, visitedSymbolIds);
|
|
10515
|
-
if (!expressionSourceKeys) return null;
|
|
10516
|
-
for (const expressionSourceKey of expressionSourceKeys) identitySourceKeys.add(expressionSourceKey);
|
|
10517
|
-
}
|
|
10518
|
-
return identitySourceKeys;
|
|
10519
|
-
};
|
|
10520
|
-
const resolveIdentitySourceKeysFromExpression = (expression, scopes, visitedSymbolIds) => {
|
|
10521
|
-
const stripped = unwrapExpression$3(expression);
|
|
10522
|
-
if (isNodeOfType(stripped, "Literal") && (stripped.value === null || typeof stripped.value === "string" || typeof stripped.value === "number" || typeof stripped.value === "boolean") || isNodeOfType(stripped, "TemplateLiteral") && getStaticTemplateLiteralValue(stripped) !== null) return /* @__PURE__ */ new Set();
|
|
10523
|
-
if (isNodeOfType(stripped, "Identifier")) {
|
|
10524
|
-
const sourceSymbol = scopes.symbolFor(stripped);
|
|
10525
|
-
if (!sourceSymbol) return null;
|
|
10526
|
-
if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
10527
|
-
if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((reference) => reference.flag === "read")) {
|
|
10528
|
-
if (visitedSymbolIds.has(sourceSymbol.id)) return null;
|
|
10529
|
-
visitedSymbolIds.add(sourceSymbol.id);
|
|
10530
|
-
const sourceKeys = resolveIdentitySourceKeysFromExpression(sourceSymbol.initializer, scopes, visitedSymbolIds);
|
|
10531
|
-
visitedSymbolIds.delete(sourceSymbol.id);
|
|
10532
|
-
if (sourceKeys) return sourceKeys;
|
|
10533
|
-
}
|
|
10534
|
-
return new Set([sourceSymbol.name]);
|
|
10535
|
-
}
|
|
10536
|
-
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
10537
|
-
if (hasComputedMemberExpression(stripped)) return null;
|
|
10538
|
-
const sourceKey = stringifyMemberChain(stripped);
|
|
10539
|
-
const rootIdentifier = getMemberRootIdentifier(stripped);
|
|
10540
|
-
const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
10541
|
-
if (!sourceKey || !rootSymbol) return null;
|
|
10542
|
-
if (isOutsideAllFunctions(rootSymbol)) return /* @__PURE__ */ new Set();
|
|
10543
|
-
if (isStableRefContainerCapture(rootSymbol, sourceKey, scopes)) return /* @__PURE__ */ new Set();
|
|
10544
|
-
if (symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
10545
|
-
return new Set([sourceKey]);
|
|
10546
|
-
}
|
|
10547
|
-
if (isNodeOfType(stripped, "LogicalExpression")) return mergeIdentitySourceKeys([stripped.left, stripped.right], scopes, visitedSymbolIds);
|
|
10548
|
-
if (isNodeOfType(stripped, "ConditionalExpression")) return mergeIdentitySourceKeys([
|
|
10549
|
-
stripped.test,
|
|
10550
|
-
stripped.consequent,
|
|
10551
|
-
stripped.alternate
|
|
10552
|
-
], scopes, visitedSymbolIds);
|
|
10553
|
-
return null;
|
|
10554
|
-
};
|
|
10555
|
-
const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
|
|
10556
|
-
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
10557
|
-
return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
|
|
10558
|
-
};
|
|
10559
10492
|
const isUseCallbackResultDep = (node, scopes) => {
|
|
10560
10493
|
const rootSymbol = getRootSymbol(node, scopes);
|
|
10561
10494
|
const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
|
|
10562
|
-
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee
|
|
10495
|
+
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee) === "useCallback");
|
|
10563
10496
|
};
|
|
10564
|
-
const
|
|
10497
|
+
const isExtraEffectDepAllowed = (node, scopes) => {
|
|
10565
10498
|
const rootIdentifier = getMemberRootIdentifier(node);
|
|
10566
10499
|
if (!rootIdentifier) return false;
|
|
10567
10500
|
const symbol = scopes.symbolFor(rootIdentifier);
|
|
@@ -10589,17 +10522,10 @@ const isUnstableInitializer = (node) => {
|
|
|
10589
10522
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
10590
10523
|
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
10524
|
};
|
|
10592
|
-
const isExtraDepAllowedForHook = (hookName, node, scopes) => {
|
|
10593
|
-
if (!isExtraReactiveDepAllowed(node, scopes)) return false;
|
|
10594
|
-
if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
|
|
10595
|
-
if (hookName !== "useMemo") return false;
|
|
10596
|
-
const rootSymbol = getRootSymbol(node, scopes);
|
|
10597
|
-
return Boolean(rootSymbol && !symbolHasStableValue(rootSymbol, scopes) && !isUnstableInitializer(rootSymbol.initializer));
|
|
10598
|
-
};
|
|
10599
10525
|
const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
|
|
10600
10526
|
const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
|
|
10601
|
-
const isStableSetterLikeSymbol = (symbol
|
|
10602
|
-
if (!symbolHasStableHookOrigin(symbol
|
|
10527
|
+
const isStableSetterLikeSymbol = (symbol) => {
|
|
10528
|
+
if (!symbolHasStableHookOrigin(symbol)) return false;
|
|
10603
10529
|
return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
|
|
10604
10530
|
};
|
|
10605
10531
|
const findStableSetterReference = (node, scopes) => {
|
|
@@ -10609,7 +10535,7 @@ const findStableSetterReference = (node, scopes) => {
|
|
|
10609
10535
|
if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
|
|
10610
10536
|
if (isNodeOfType(current, "Identifier")) {
|
|
10611
10537
|
const symbol = scopes.referenceFor(current)?.resolvedSymbol;
|
|
10612
|
-
if (symbol && isStableSetterLikeSymbol(symbol
|
|
10538
|
+
if (symbol && isStableSetterLikeSymbol(symbol)) {
|
|
10613
10539
|
setterName = symbol.name;
|
|
10614
10540
|
return;
|
|
10615
10541
|
}
|
|
@@ -10725,11 +10651,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
|
|
|
10725
10651
|
}
|
|
10726
10652
|
return false;
|
|
10727
10653
|
};
|
|
10728
|
-
const isSeededDataRefSymbol = (refSymbol
|
|
10654
|
+
const isSeededDataRefSymbol = (refSymbol) => {
|
|
10729
10655
|
if (!refSymbol) return false;
|
|
10730
10656
|
const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
|
|
10731
10657
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
10732
|
-
if (getHookName(initializer.callee
|
|
10658
|
+
if (getHookName(initializer.callee) !== "useRef") return false;
|
|
10733
10659
|
const firstArgument = initializer.arguments[0];
|
|
10734
10660
|
if (!firstArgument || !isAstNode(firstArgument)) return false;
|
|
10735
10661
|
const strippedArgument = unwrapExpression$3(firstArgument);
|
|
@@ -10769,9 +10695,7 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
|
|
|
10769
10695
|
const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
|
|
10770
10696
|
return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
|
|
10771
10697
|
};
|
|
10772
|
-
const hasMemberCallForRoot = (node, rootName
|
|
10773
|
-
const rootSymbol = closureCaptures(node, scopes).find((reference) => reference.resolvedSymbol?.name === rootName)?.resolvedSymbol ?? null;
|
|
10774
|
-
if (!rootSymbol) return false;
|
|
10698
|
+
const hasMemberCallForRoot = (node, rootName) => {
|
|
10775
10699
|
let didFindMemberCall = false;
|
|
10776
10700
|
const visit = (current) => {
|
|
10777
10701
|
if (didFindMemberCall) return;
|
|
@@ -10787,7 +10711,7 @@ const hasMemberCallForRoot = (node, rootName, scopes) => {
|
|
|
10787
10711
|
}
|
|
10788
10712
|
chainObject = unwrapExpression$3(chainObject.object);
|
|
10789
10713
|
}
|
|
10790
|
-
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName
|
|
10714
|
+
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
|
|
10791
10715
|
didFindMemberCall = true;
|
|
10792
10716
|
return;
|
|
10793
10717
|
}
|
|
@@ -10805,11 +10729,9 @@ const hasMemberCallForRoot = (node, rootName, scopes) => {
|
|
|
10805
10729
|
visit(node);
|
|
10806
10730
|
return didFindMemberCall;
|
|
10807
10731
|
};
|
|
10808
|
-
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback
|
|
10809
|
-
|
|
10810
|
-
if (
|
|
10811
|
-
if (propsCaptureKeys.every((captureKey) => [...declaredKeys].some((declaredKey) => isMatchingDepOrPrefix(declaredKey, captureKey)))) return;
|
|
10812
|
-
if (hasMemberCallForRoot(callback, "props", scopes)) captureKeys.add("props");
|
|
10732
|
+
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback) => {
|
|
10733
|
+
if ([...captureKeys].filter((captureKey) => captureKey.startsWith("props.")).length < 2 || declaredKeys.has("props")) return;
|
|
10734
|
+
if (hasMemberCallForRoot(callback, "props")) captureKeys.add("props");
|
|
10813
10735
|
};
|
|
10814
10736
|
const exhaustiveDeps = defineRule({
|
|
10815
10737
|
id: "exhaustive-deps",
|
|
@@ -10864,7 +10786,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10864
10786
|
};
|
|
10865
10787
|
const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
|
|
10866
10788
|
return { CallExpression(node) {
|
|
10867
|
-
const hookName = getHookName(node.callee
|
|
10789
|
+
const hookName = getHookName(node.callee);
|
|
10868
10790
|
if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
|
|
10869
10791
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
10870
10792
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
@@ -10921,7 +10843,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10921
10843
|
if (outerAssignments.length > 0) return;
|
|
10922
10844
|
const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
|
|
10923
10845
|
const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
|
|
10924
|
-
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol
|
|
10846
|
+
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol)) context.report({
|
|
10925
10847
|
node: callbackToAnalyze,
|
|
10926
10848
|
message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
|
|
10927
10849
|
});
|
|
@@ -11020,7 +10942,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11020
10942
|
const fullChain = stringifyMemberChain(stripped);
|
|
11021
10943
|
if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
|
|
11022
10944
|
const refSymbol = context.scopes.symbolFor(stripped.object);
|
|
11023
|
-
if (refSymbol && symbolHasStableHookOrigin(refSymbol
|
|
10945
|
+
if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
|
|
11024
10946
|
if (!didReportRefCurrentDep) {
|
|
11025
10947
|
context.report({
|
|
11026
10948
|
node: elementNode,
|
|
@@ -11058,7 +10980,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11058
10980
|
declaredKeys.add(key);
|
|
11059
10981
|
declaredKeyToReportNode.set(key, elementNode);
|
|
11060
10982
|
}
|
|
11061
|
-
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument
|
|
10983
|
+
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
|
|
11062
10984
|
const missingCaptureKeys = [];
|
|
11063
10985
|
for (const captureKey of captureKeys) {
|
|
11064
10986
|
let isCoveredByDeclared = false;
|
|
@@ -11144,7 +11066,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11144
11066
|
if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
|
|
11145
11067
|
if (outerFunctionCapturedNames.has(rootName)) continue;
|
|
11146
11068
|
const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
|
|
11147
|
-
if (
|
|
11069
|
+
if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) && isExtraEffectDepAllowed(reportNode, context.scopes)) continue;
|
|
11148
11070
|
if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
|
|
11149
11071
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
11150
11072
|
if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
|
|
@@ -22370,26 +22292,6 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
22370
22292
|
const importDeclaration = declarationNode.parent;
|
|
22371
22293
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22372
22294
|
}));
|
|
22373
|
-
const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
|
|
22374
|
-
if (def.type !== "ImportBinding") return false;
|
|
22375
|
-
const declarationNode = def.node;
|
|
22376
|
-
if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
|
|
22377
|
-
const importDeclaration = declarationNode.parent;
|
|
22378
|
-
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22379
|
-
}));
|
|
22380
|
-
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
22381
|
-
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
22382
|
-
const callee = stripParenExpression(declarator.init.callee);
|
|
22383
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
22384
|
-
const reference = getRef(analysis, callee);
|
|
22385
|
-
if (!reference?.resolved) return callee.name === hookName;
|
|
22386
|
-
return isReactNamedImportReference(reference, hookName);
|
|
22387
|
-
}
|
|
22388
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
22389
|
-
const namespaceReference = getRef(analysis, callee.object);
|
|
22390
|
-
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
22391
|
-
return isReactNamespaceImportReference(namespaceReference);
|
|
22392
|
-
};
|
|
22393
22295
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
22394
22296
|
if (!node) return false;
|
|
22395
22297
|
if (isNodeOfType(node, "Identifier")) {
|
|
@@ -23444,20 +23346,6 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23444
23346
|
}
|
|
23445
23347
|
return evidence;
|
|
23446
23348
|
};
|
|
23447
|
-
const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
|
|
23448
|
-
const evidence = collectValueEvidence(analysis, expression, {
|
|
23449
|
-
functionNode: componentFunction,
|
|
23450
|
-
invocation: null,
|
|
23451
|
-
isDeferred: false,
|
|
23452
|
-
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23453
|
-
substitutions: /* @__PURE__ */ new Map(),
|
|
23454
|
-
currentFilename
|
|
23455
|
-
}, 1);
|
|
23456
|
-
return {
|
|
23457
|
-
sourceReferences: evidence.sourceReferences,
|
|
23458
|
-
isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
|
|
23459
|
-
};
|
|
23460
|
-
};
|
|
23461
23349
|
const findStateSetterReference = (analysis, callExpression) => {
|
|
23462
23350
|
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
23463
23351
|
const callee = stripParenExpression(callExpression.callee);
|
|
@@ -26510,6 +26398,57 @@ const noDefaultProps = defineRule({
|
|
|
26510
26398
|
} })
|
|
26511
26399
|
});
|
|
26512
26400
|
//#endregion
|
|
26401
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
26402
|
+
const getStateName$1 = (stateDeclarator) => {
|
|
26403
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
26404
|
+
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
26405
|
+
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
26406
|
+
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
26407
|
+
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
26408
|
+
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
26409
|
+
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
26410
|
+
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
26411
|
+
};
|
|
26412
|
+
const noDerivedState = defineRule({
|
|
26413
|
+
id: "no-derived-state",
|
|
26414
|
+
title: "Derived value copied into state",
|
|
26415
|
+
severity: "warn",
|
|
26416
|
+
tags: ["test-noise"],
|
|
26417
|
+
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",
|
|
26418
|
+
create: (context) => ({ CallExpression(node) {
|
|
26419
|
+
if (!isUseEffect(node)) return;
|
|
26420
|
+
const analysis = getProgramAnalysis(node);
|
|
26421
|
+
if (!analysis) return;
|
|
26422
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
26423
|
+
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
26424
|
+
const stateName = getStateName$1(fact.stateDeclarator);
|
|
26425
|
+
context.report({
|
|
26426
|
+
node: fact.callExpression,
|
|
26427
|
+
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
26428
|
+
});
|
|
26429
|
+
}
|
|
26430
|
+
} })
|
|
26431
|
+
});
|
|
26432
|
+
//#endregion
|
|
26433
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
26434
|
+
const noDerivedStateEffect = defineRule({
|
|
26435
|
+
id: "no-derived-state-effect",
|
|
26436
|
+
title: "Derived state stored in an effect",
|
|
26437
|
+
severity: "warn",
|
|
26438
|
+
tags: ["test-noise"],
|
|
26439
|
+
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",
|
|
26440
|
+
create: (context) => ({ CallExpression(node) {
|
|
26441
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
26442
|
+
const analysis = getProgramAnalysis(node);
|
|
26443
|
+
if (!analysis) return;
|
|
26444
|
+
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
26445
|
+
context.report({
|
|
26446
|
+
node,
|
|
26447
|
+
message: "You pay an extra render for state you can derive from other values."
|
|
26448
|
+
});
|
|
26449
|
+
} })
|
|
26450
|
+
});
|
|
26451
|
+
//#endregion
|
|
26513
26452
|
//#region src/plugin/utils/is-component-function.ts
|
|
26514
26453
|
const isFunctionAssignedToComponent = (functionNode) => {
|
|
26515
26454
|
let cursor = functionNode.parent ?? null;
|
|
@@ -26643,236 +26582,6 @@ const createComponentPropStackTracker = (callbacks) => {
|
|
|
26643
26582
|
};
|
|
26644
26583
|
};
|
|
26645
26584
|
//#endregion
|
|
26646
|
-
//#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
|
|
26647
|
-
const findReferenceDeclarator = (reference) => {
|
|
26648
|
-
for (const definition of reference.resolved?.defs ?? []) {
|
|
26649
|
-
const definitionNode = definition.node;
|
|
26650
|
-
if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
|
|
26651
|
-
}
|
|
26652
|
-
return null;
|
|
26653
|
-
};
|
|
26654
|
-
const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
26655
|
-
const node = stripParenExpression(expression);
|
|
26656
|
-
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26657
|
-
const reference = getRef(analysis, node);
|
|
26658
|
-
if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
|
|
26659
|
-
if (isProp(analysis, reference)) {
|
|
26660
|
-
if (isWholePropsObjectReference(analysis, reference)) return null;
|
|
26661
|
-
return { bindingIdentity: reference.resolved };
|
|
26662
|
-
}
|
|
26663
|
-
if (isState(analysis, reference)) {
|
|
26664
|
-
const stateDeclarator = getUseStateDecl(analysis, reference);
|
|
26665
|
-
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
|
|
26666
|
-
return { bindingIdentity: reference.resolved };
|
|
26667
|
-
}
|
|
26668
|
-
if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
|
|
26669
|
-
const declarator = findReferenceDeclarator(reference);
|
|
26670
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
26671
|
-
const nextVisitedBindings = new Set(visitedBindings);
|
|
26672
|
-
nextVisitedBindings.add(reference.resolved);
|
|
26673
|
-
return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
|
|
26674
|
-
};
|
|
26675
|
-
const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
|
|
26676
|
-
const getDirectBranchStatements = (branch) => {
|
|
26677
|
-
if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
|
|
26678
|
-
return [branch];
|
|
26679
|
-
};
|
|
26680
|
-
const getDirectCallExpression = (statement) => {
|
|
26681
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26682
|
-
const expression = stripParenExpression(statement.expression);
|
|
26683
|
-
return isNodeOfType(expression, "CallExpression") ? expression : null;
|
|
26684
|
-
};
|
|
26685
|
-
const getDirectAssignmentExpression = (statement) => {
|
|
26686
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26687
|
-
const expression = stripParenExpression(statement.expression);
|
|
26688
|
-
return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
|
|
26689
|
-
};
|
|
26690
|
-
const findDirectStateSetterReference = (analysis, callExpression) => {
|
|
26691
|
-
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
26692
|
-
const callee = stripParenExpression(callExpression.callee);
|
|
26693
|
-
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
26694
|
-
const reference = getRef(analysis, callee);
|
|
26695
|
-
return reference && isStateSetter(analysis, reference) ? reference : null;
|
|
26696
|
-
};
|
|
26697
|
-
const getStaticRefCurrentReference = (analysis, expression) => {
|
|
26698
|
-
const node = stripParenExpression(expression);
|
|
26699
|
-
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
26700
|
-
const reference = getRef(analysis, node.object);
|
|
26701
|
-
if (!reference) return null;
|
|
26702
|
-
const declarator = findReferenceDeclarator(reference);
|
|
26703
|
-
return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
|
|
26704
|
-
};
|
|
26705
|
-
const getStateTracker = (analysis, expression) => {
|
|
26706
|
-
const node = stripParenExpression(expression);
|
|
26707
|
-
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26708
|
-
const reference = getRef(analysis, node);
|
|
26709
|
-
if (!reference || !isState(analysis, reference)) return null;
|
|
26710
|
-
const declarator = getUseStateDecl(analysis, reference);
|
|
26711
|
-
if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
|
|
26712
|
-
return {
|
|
26713
|
-
kind: "state",
|
|
26714
|
-
declarator
|
|
26715
|
-
};
|
|
26716
|
-
};
|
|
26717
|
-
const getRefTracker = (analysis, expression) => {
|
|
26718
|
-
const reference = getStaticRefCurrentReference(analysis, expression);
|
|
26719
|
-
return reference ? {
|
|
26720
|
-
kind: "ref",
|
|
26721
|
-
reference
|
|
26722
|
-
} : null;
|
|
26723
|
-
};
|
|
26724
|
-
const getTrackerInitializer = (tracker) => {
|
|
26725
|
-
const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
|
|
26726
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
|
|
26727
|
-
const initializer = declarator.init.arguments?.[0];
|
|
26728
|
-
return initializer ? initializer : null;
|
|
26729
|
-
};
|
|
26730
|
-
const isTrackerSynchronized = (analysis, guard) => {
|
|
26731
|
-
for (const statement of guard.statements) {
|
|
26732
|
-
if (guard.tracker.kind === "state") {
|
|
26733
|
-
const callExpression = getDirectCallExpression(statement);
|
|
26734
|
-
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26735
|
-
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26736
|
-
if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
|
|
26737
|
-
const writtenValue = callExpression.arguments?.[0];
|
|
26738
|
-
if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26739
|
-
continue;
|
|
26740
|
-
}
|
|
26741
|
-
const assignmentExpression = getDirectAssignmentExpression(statement);
|
|
26742
|
-
if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
|
|
26743
|
-
if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
|
|
26744
|
-
if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26745
|
-
}
|
|
26746
|
-
return false;
|
|
26747
|
-
};
|
|
26748
|
-
const parseRenderTrackerGuard = (analysis, statement) => {
|
|
26749
|
-
if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
|
|
26750
|
-
const left = statement.test.left;
|
|
26751
|
-
const right = statement.test.right;
|
|
26752
|
-
const candidates = [{
|
|
26753
|
-
sourceExpression: left,
|
|
26754
|
-
trackerExpression: right
|
|
26755
|
-
}, {
|
|
26756
|
-
sourceExpression: right,
|
|
26757
|
-
trackerExpression: left
|
|
26758
|
-
}];
|
|
26759
|
-
for (const candidate of candidates) {
|
|
26760
|
-
const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
|
|
26761
|
-
if (!source) continue;
|
|
26762
|
-
const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
|
|
26763
|
-
if (!tracker) continue;
|
|
26764
|
-
const trackerInitializer = getTrackerInitializer(tracker);
|
|
26765
|
-
if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
|
|
26766
|
-
const guard = {
|
|
26767
|
-
source,
|
|
26768
|
-
tracker,
|
|
26769
|
-
statements: getDirectBranchStatements(statement.consequent)
|
|
26770
|
-
};
|
|
26771
|
-
return isTrackerSynchronized(analysis, guard) ? guard : null;
|
|
26772
|
-
}
|
|
26773
|
-
return null;
|
|
26774
|
-
};
|
|
26775
|
-
const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
|
|
26776
|
-
if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
|
|
26777
|
-
const references = setterReference.resolved.references.filter((reference) => !reference.init);
|
|
26778
|
-
if (references.length !== 1) return false;
|
|
26779
|
-
return references[0].identifier === callExpression.callee;
|
|
26780
|
-
};
|
|
26781
|
-
const getStateInitializer = (stateDeclarator) => {
|
|
26782
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
|
|
26783
|
-
const initializer = stateDeclarator.init.arguments?.[0];
|
|
26784
|
-
return initializer ? initializer : null;
|
|
26785
|
-
};
|
|
26786
|
-
const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
|
|
26787
|
-
if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
|
|
26788
|
-
const componentFunction = componentBody.parent;
|
|
26789
|
-
const facts = [];
|
|
26790
|
-
for (const statement of componentBody.body ?? []) {
|
|
26791
|
-
const guard = parseRenderTrackerGuard(analysis, statement);
|
|
26792
|
-
if (!guard) continue;
|
|
26793
|
-
for (const branchStatement of guard.statements) {
|
|
26794
|
-
const callExpression = getDirectCallExpression(branchStatement);
|
|
26795
|
-
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26796
|
-
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26797
|
-
if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
|
|
26798
|
-
const stateDeclarator = getUseStateDecl(analysis, setterReference);
|
|
26799
|
-
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
|
|
26800
|
-
const writtenValue = callExpression.arguments?.[0];
|
|
26801
|
-
const initializer = getStateInitializer(stateDeclarator);
|
|
26802
|
-
if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
|
|
26803
|
-
facts.push({
|
|
26804
|
-
callExpression,
|
|
26805
|
-
stateDeclarator
|
|
26806
|
-
});
|
|
26807
|
-
}
|
|
26808
|
-
}
|
|
26809
|
-
return facts;
|
|
26810
|
-
};
|
|
26811
|
-
//#endregion
|
|
26812
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
26813
|
-
const getStateName$1 = (stateDeclarator) => {
|
|
26814
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
26815
|
-
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
26816
|
-
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
26817
|
-
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
26818
|
-
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
26819
|
-
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
26820
|
-
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
26821
|
-
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
26822
|
-
};
|
|
26823
|
-
const noDerivedState = defineRule({
|
|
26824
|
-
id: "no-derived-state",
|
|
26825
|
-
title: "Derived value copied into state",
|
|
26826
|
-
severity: "warn",
|
|
26827
|
-
tags: ["test-noise"],
|
|
26828
|
-
recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into state and synchronizing it during render or through an effect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
|
|
26829
|
-
create: (context) => {
|
|
26830
|
-
const reportStateWrite = (callExpression, stateDeclarator) => {
|
|
26831
|
-
const stateName = getStateName$1(stateDeclarator);
|
|
26832
|
-
context.report({
|
|
26833
|
-
node: callExpression,
|
|
26834
|
-
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
26835
|
-
});
|
|
26836
|
-
};
|
|
26837
|
-
return {
|
|
26838
|
-
...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
|
|
26839
|
-
if (!componentBody) return;
|
|
26840
|
-
const analysis = getProgramAnalysis(componentBody);
|
|
26841
|
-
if (!analysis) return;
|
|
26842
|
-
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26843
|
-
} }).visitors,
|
|
26844
|
-
CallExpression(node) {
|
|
26845
|
-
if (!isUseEffect(node)) return;
|
|
26846
|
-
const analysis = getProgramAnalysis(node);
|
|
26847
|
-
if (!analysis) return;
|
|
26848
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
26849
|
-
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
26850
|
-
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26851
|
-
}
|
|
26852
|
-
}
|
|
26853
|
-
};
|
|
26854
|
-
}
|
|
26855
|
-
});
|
|
26856
|
-
//#endregion
|
|
26857
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
26858
|
-
const noDerivedStateEffect = defineRule({
|
|
26859
|
-
id: "no-derived-state-effect",
|
|
26860
|
-
title: "Derived state stored in an effect",
|
|
26861
|
-
severity: "warn",
|
|
26862
|
-
tags: ["test-noise"],
|
|
26863
|
-
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
26864
|
-
create: (context) => ({ CallExpression(node) {
|
|
26865
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
26866
|
-
const analysis = getProgramAnalysis(node);
|
|
26867
|
-
if (!analysis) return;
|
|
26868
|
-
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
26869
|
-
context.report({
|
|
26870
|
-
node,
|
|
26871
|
-
message: "You pay an extra render for state you can derive from other values."
|
|
26872
|
-
});
|
|
26873
|
-
} })
|
|
26874
|
-
});
|
|
26875
|
-
//#endregion
|
|
26876
26585
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
26877
26586
|
const isInitialOnlyPropName = (propName) => {
|
|
26878
26587
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -35722,63 +35431,6 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
35722
35431
|
}
|
|
35723
35432
|
});
|
|
35724
35433
|
//#endregion
|
|
35725
|
-
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
35726
|
-
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
35727
|
-
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
35728
|
-
if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
35729
|
-
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
35730
|
-
if (!symbol?.initializer) return null;
|
|
35731
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
35732
|
-
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
35733
|
-
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
35734
|
-
};
|
|
35735
|
-
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
35736
|
-
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
|
|
35737
|
-
const receiver = stripParenExpression(node.object);
|
|
35738
|
-
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
35739
|
-
};
|
|
35740
|
-
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
35741
|
-
if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
|
|
35742
|
-
let descendant = assignmentExpression;
|
|
35743
|
-
let ancestor = descendant.parent;
|
|
35744
|
-
while (ancestor) {
|
|
35745
|
-
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
|
|
35746
|
-
const { left, right } = ancestor.test;
|
|
35747
|
-
if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
|
|
35748
|
-
}
|
|
35749
|
-
descendant = ancestor;
|
|
35750
|
-
ancestor = descendant.parent;
|
|
35751
|
-
}
|
|
35752
|
-
return false;
|
|
35753
|
-
};
|
|
35754
|
-
const noRefCurrentInRender = defineRule({
|
|
35755
|
-
id: "no-ref-current-in-render",
|
|
35756
|
-
title: "Ref mutated during render",
|
|
35757
|
-
severity: "error",
|
|
35758
|
-
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.",
|
|
35759
|
-
create: (context) => {
|
|
35760
|
-
const report = (memberExpression) => {
|
|
35761
|
-
if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
|
|
35762
|
-
if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
|
|
35763
|
-
context.report({
|
|
35764
|
-
node: memberExpression,
|
|
35765
|
-
message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
|
|
35766
|
-
});
|
|
35767
|
-
};
|
|
35768
|
-
return {
|
|
35769
|
-
AssignmentExpression(node) {
|
|
35770
|
-
const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
|
|
35771
|
-
if (!refSymbol) return;
|
|
35772
|
-
if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
|
|
35773
|
-
report(node.left);
|
|
35774
|
-
},
|
|
35775
|
-
UpdateExpression(node) {
|
|
35776
|
-
report(node.argument);
|
|
35777
|
-
}
|
|
35778
|
-
};
|
|
35779
|
-
}
|
|
35780
|
-
});
|
|
35781
|
-
//#endregion
|
|
35782
35434
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
35783
35435
|
const isInsideComponentContext = (node) => {
|
|
35784
35436
|
let cursor = node.parent;
|
|
@@ -57260,18 +56912,6 @@ const reactDoctorRules = [
|
|
|
57260
56912
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
57261
56913
|
}
|
|
57262
56914
|
},
|
|
57263
|
-
{
|
|
57264
|
-
key: "react-doctor/no-ref-current-in-render",
|
|
57265
|
-
id: "no-ref-current-in-render",
|
|
57266
|
-
source: "react-doctor",
|
|
57267
|
-
originallyExternal: false,
|
|
57268
|
-
rule: {
|
|
57269
|
-
...noRefCurrentInRender,
|
|
57270
|
-
framework: "global",
|
|
57271
|
-
category: "Bugs",
|
|
57272
|
-
requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
|
|
57273
|
-
}
|
|
57274
|
-
},
|
|
57275
56915
|
{
|
|
57276
56916
|
key: "react-doctor/no-render-in-render",
|
|
57277
56917
|
id: "no-render-in-render",
|