oxlint-plugin-react-doctor 0.7.4-dev.4a5d9bb → 0.7.4-dev.593824d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +46 -0
- package/dist/index.js +742 -146
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -4445,6 +4445,29 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
4445
4445
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
4446
4446
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
4447
4447
|
};
|
|
4448
|
+
}, {
|
|
4449
|
+
readonly key: "react-doctor/no-impure-state-updater";
|
|
4450
|
+
readonly id: "no-impure-state-updater";
|
|
4451
|
+
readonly source: "react-doctor";
|
|
4452
|
+
readonly originallyExternal: false;
|
|
4453
|
+
readonly rule: {
|
|
4454
|
+
readonly framework: "global";
|
|
4455
|
+
readonly category: "Bugs";
|
|
4456
|
+
readonly requires: readonly Capability[];
|
|
4457
|
+
readonly id: string;
|
|
4458
|
+
readonly title?: string;
|
|
4459
|
+
readonly severity: RuleSeverity;
|
|
4460
|
+
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
4461
|
+
readonly tags?: ReadonlyArray<string>;
|
|
4462
|
+
readonly matchByOccurrence?: boolean;
|
|
4463
|
+
readonly defaultEnabled?: boolean;
|
|
4464
|
+
readonly lifecycle?: "retired";
|
|
4465
|
+
readonly scan?: FileScan;
|
|
4466
|
+
readonly committedFilesOnly?: boolean;
|
|
4467
|
+
readonly recommendation?: string;
|
|
4468
|
+
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
4469
|
+
readonly create: (context: RuleContext) => RuleVisitors;
|
|
4470
|
+
};
|
|
4448
4471
|
}, {
|
|
4449
4472
|
readonly key: "react-doctor/no-indeterminate-attribute";
|
|
4450
4473
|
readonly id: "no-indeterminate-attribute";
|
|
@@ -13642,6 +13665,29 @@ declare const RULES: readonly [{
|
|
|
13642
13665
|
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
13643
13666
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
13644
13667
|
};
|
|
13668
|
+
}, {
|
|
13669
|
+
readonly key: "react-doctor/no-impure-state-updater";
|
|
13670
|
+
readonly id: "no-impure-state-updater";
|
|
13671
|
+
readonly source: "react-doctor";
|
|
13672
|
+
readonly originallyExternal: false;
|
|
13673
|
+
readonly rule: {
|
|
13674
|
+
readonly framework: "global";
|
|
13675
|
+
readonly category: "Bugs";
|
|
13676
|
+
readonly requires: readonly Capability[];
|
|
13677
|
+
readonly id: string;
|
|
13678
|
+
readonly title?: string;
|
|
13679
|
+
readonly severity: RuleSeverity;
|
|
13680
|
+
readonly disabledWhen?: ReadonlyArray<Capability>;
|
|
13681
|
+
readonly tags?: ReadonlyArray<string>;
|
|
13682
|
+
readonly matchByOccurrence?: boolean;
|
|
13683
|
+
readonly defaultEnabled?: boolean;
|
|
13684
|
+
readonly lifecycle?: "retired";
|
|
13685
|
+
readonly scan?: FileScan;
|
|
13686
|
+
readonly committedFilesOnly?: boolean;
|
|
13687
|
+
readonly recommendation?: string;
|
|
13688
|
+
readonly recommendationFor?: (hasCapability: CapabilityQuery) => string | undefined;
|
|
13689
|
+
readonly create: (context: RuleContext) => RuleVisitors;
|
|
13690
|
+
};
|
|
13645
13691
|
}, {
|
|
13646
13692
|
readonly key: "react-doctor/no-indeterminate-attribute";
|
|
13647
13693
|
readonly id: "no-indeterminate-attribute";
|
package/dist/index.js
CHANGED
|
@@ -9968,13 +9968,18 @@ const unwrapExpression$3 = (node) => {
|
|
|
9968
9968
|
return current;
|
|
9969
9969
|
};
|
|
9970
9970
|
/**
|
|
9971
|
-
* Get the hook name from a
|
|
9972
|
-
*
|
|
9973
|
-
* `React.useFoo()` (MemberExpression).
|
|
9971
|
+
* Get the hook name from a direct, wrapped, namespaced, or immutable
|
|
9972
|
+
* React import alias call.
|
|
9974
9973
|
*/
|
|
9975
|
-
const getHookName = (callee) => {
|
|
9976
|
-
|
|
9977
|
-
if (isNodeOfType(
|
|
9974
|
+
const getHookName = (callee, scopes) => {
|
|
9975
|
+
const strippedCallee = unwrapExpression$3(callee);
|
|
9976
|
+
if (isNodeOfType(strippedCallee, "Identifier")) {
|
|
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;
|
|
9978
9983
|
return null;
|
|
9979
9984
|
};
|
|
9980
9985
|
const FUNCTION_SCOPE_KINDS = new Set([
|
|
@@ -10159,7 +10164,7 @@ const STABLE_IDENTITY_WRAPPER_HOOK_NAMES = new Set([
|
|
|
10159
10164
|
* - primitive-literal local consts (the value never changes
|
|
10160
10165
|
* between renders unless the literal does)
|
|
10161
10166
|
*/
|
|
10162
|
-
const symbolHasStableHookOrigin = (symbol) => {
|
|
10167
|
+
const symbolHasStableHookOrigin = (symbol, scopes) => {
|
|
10163
10168
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
10164
10169
|
let declarator = symbol.declarationNode;
|
|
10165
10170
|
while (declarator && declarator.type !== "VariableDeclarator") declarator = declarator.parent ?? null;
|
|
@@ -10172,7 +10177,7 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
10172
10177
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
10173
10178
|
}
|
|
10174
10179
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
10175
|
-
const initializerHookName = getHookName(initializer.callee);
|
|
10180
|
+
const initializerHookName = getHookName(initializer.callee, scopes);
|
|
10176
10181
|
if (!initializerHookName) return false;
|
|
10177
10182
|
if (initializerHookName === "useRef") return true;
|
|
10178
10183
|
if (initializerHookName === "useEffectEvent") return true;
|
|
@@ -10207,7 +10212,7 @@ const symbolHasStableMemoizedOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
10207
10212
|
if (!declarator.init) return false;
|
|
10208
10213
|
const initializer = unwrapExpression$3(declarator.init);
|
|
10209
10214
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
10210
|
-
const initializerHookName = getHookName(initializer.callee);
|
|
10215
|
+
const initializerHookName = getHookName(initializer.callee, scopes);
|
|
10211
10216
|
if (!initializerHookName || !MEMOIZING_HOOK_NAMES.has(initializerHookName)) return false;
|
|
10212
10217
|
const depsArgument = initializer.arguments[1];
|
|
10213
10218
|
if (!depsArgument || !isAstNode(depsArgument)) return false;
|
|
@@ -10237,7 +10242,7 @@ const getObjectPropertyValue = (objectExpression, propertyName) => {
|
|
|
10237
10242
|
}
|
|
10238
10243
|
return null;
|
|
10239
10244
|
};
|
|
10240
|
-
const isStableRefContainerCapture = (symbol, depKey) => {
|
|
10245
|
+
const isStableRefContainerCapture = (symbol, depKey, scopes) => {
|
|
10241
10246
|
if (symbol.kind !== "const") return false;
|
|
10242
10247
|
if (!depKey.startsWith(`${symbol.name}.`)) return false;
|
|
10243
10248
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
@@ -10251,7 +10256,7 @@ const isStableRefContainerCapture = (symbol, depKey) => {
|
|
|
10251
10256
|
if (!propertyValue) return false;
|
|
10252
10257
|
currentValue = propertyValue;
|
|
10253
10258
|
}
|
|
10254
|
-
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee) === "useRef";
|
|
10259
|
+
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee, scopes) === "useRef";
|
|
10255
10260
|
};
|
|
10256
10261
|
const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
10257
10262
|
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
@@ -10269,7 +10274,13 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
10269
10274
|
}
|
|
10270
10275
|
return true;
|
|
10271
10276
|
};
|
|
10272
|
-
const
|
|
10277
|
+
const symbolHasStableImportedAlias = (symbol, scopes) => {
|
|
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);
|
|
10273
10284
|
//#endregion
|
|
10274
10285
|
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
10275
10286
|
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
@@ -10457,10 +10468,18 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
10457
10468
|
}
|
|
10458
10469
|
const depKey = computeDepKey(reference);
|
|
10459
10470
|
if (!depKey) continue;
|
|
10460
|
-
if (isStableRefContainerCapture(symbol, depKey)) {
|
|
10471
|
+
if (isStableRefContainerCapture(symbol, depKey, scopes)) {
|
|
10461
10472
|
stableCapturedNames.add(depKey);
|
|
10462
10473
|
continue;
|
|
10463
10474
|
}
|
|
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
|
+
}
|
|
10464
10483
|
keys.add(depKey);
|
|
10465
10484
|
}
|
|
10466
10485
|
return {
|
|
@@ -10489,12 +10508,60 @@ const hasComputedMemberExpression = (node) => {
|
|
|
10489
10508
|
if (stripped.computed) return true;
|
|
10490
10509
|
return hasComputedMemberExpression(stripped.object);
|
|
10491
10510
|
};
|
|
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
|
+
};
|
|
10492
10559
|
const isUseCallbackResultDep = (node, scopes) => {
|
|
10493
10560
|
const rootSymbol = getRootSymbol(node, scopes);
|
|
10494
10561
|
const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
|
|
10495
|
-
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee) === "useCallback");
|
|
10562
|
+
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee, scopes) === "useCallback");
|
|
10496
10563
|
};
|
|
10497
|
-
const
|
|
10564
|
+
const isExtraReactiveDepAllowed = (node, scopes) => {
|
|
10498
10565
|
const rootIdentifier = getMemberRootIdentifier(node);
|
|
10499
10566
|
if (!rootIdentifier) return false;
|
|
10500
10567
|
const symbol = scopes.symbolFor(rootIdentifier);
|
|
@@ -10522,10 +10589,17 @@ const isUnstableInitializer = (node) => {
|
|
|
10522
10589
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
10523
10590
|
return isNodeOfType(stripped, "ObjectExpression") || isNodeOfType(stripped, "ArrayExpression") || isNodeOfType(stripped, "ClassExpression") || isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "JSXElement") || isNodeOfType(stripped, "JSXFragment") || isNodeOfType(stripped, "AssignmentExpression") || isNodeOfType(stripped, "NewExpression");
|
|
10524
10591
|
};
|
|
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
|
+
};
|
|
10525
10599
|
const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
|
|
10526
10600
|
const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
|
|
10527
|
-
const isStableSetterLikeSymbol = (symbol) => {
|
|
10528
|
-
if (!symbolHasStableHookOrigin(symbol)) return false;
|
|
10601
|
+
const isStableSetterLikeSymbol = (symbol, scopes) => {
|
|
10602
|
+
if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
|
|
10529
10603
|
return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
|
|
10530
10604
|
};
|
|
10531
10605
|
const findStableSetterReference = (node, scopes) => {
|
|
@@ -10535,7 +10609,7 @@ const findStableSetterReference = (node, scopes) => {
|
|
|
10535
10609
|
if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
|
|
10536
10610
|
if (isNodeOfType(current, "Identifier")) {
|
|
10537
10611
|
const symbol = scopes.referenceFor(current)?.resolvedSymbol;
|
|
10538
|
-
if (symbol && isStableSetterLikeSymbol(symbol)) {
|
|
10612
|
+
if (symbol && isStableSetterLikeSymbol(symbol, scopes)) {
|
|
10539
10613
|
setterName = symbol.name;
|
|
10540
10614
|
return;
|
|
10541
10615
|
}
|
|
@@ -10651,11 +10725,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
|
|
|
10651
10725
|
}
|
|
10652
10726
|
return false;
|
|
10653
10727
|
};
|
|
10654
|
-
const isSeededDataRefSymbol = (refSymbol) => {
|
|
10728
|
+
const isSeededDataRefSymbol = (refSymbol, scopes) => {
|
|
10655
10729
|
if (!refSymbol) return false;
|
|
10656
10730
|
const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
|
|
10657
10731
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
10658
|
-
if (getHookName(initializer.callee) !== "useRef") return false;
|
|
10732
|
+
if (getHookName(initializer.callee, scopes) !== "useRef") return false;
|
|
10659
10733
|
const firstArgument = initializer.arguments[0];
|
|
10660
10734
|
if (!firstArgument || !isAstNode(firstArgument)) return false;
|
|
10661
10735
|
const strippedArgument = unwrapExpression$3(firstArgument);
|
|
@@ -10695,7 +10769,9 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
|
|
|
10695
10769
|
const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
|
|
10696
10770
|
return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
|
|
10697
10771
|
};
|
|
10698
|
-
const hasMemberCallForRoot = (node, rootName) => {
|
|
10772
|
+
const hasMemberCallForRoot = (node, rootName, scopes) => {
|
|
10773
|
+
const rootSymbol = closureCaptures(node, scopes).find((reference) => reference.resolvedSymbol?.name === rootName)?.resolvedSymbol ?? null;
|
|
10774
|
+
if (!rootSymbol) return false;
|
|
10699
10775
|
let didFindMemberCall = false;
|
|
10700
10776
|
const visit = (current) => {
|
|
10701
10777
|
if (didFindMemberCall) return;
|
|
@@ -10711,7 +10787,7 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
10711
10787
|
}
|
|
10712
10788
|
chainObject = unwrapExpression$3(chainObject.object);
|
|
10713
10789
|
}
|
|
10714
|
-
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
|
|
10790
|
+
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName && scopes.symbolFor(chainObject) === rootSymbol) {
|
|
10715
10791
|
didFindMemberCall = true;
|
|
10716
10792
|
return;
|
|
10717
10793
|
}
|
|
@@ -10729,9 +10805,11 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
10729
10805
|
visit(node);
|
|
10730
10806
|
return didFindMemberCall;
|
|
10731
10807
|
};
|
|
10732
|
-
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback) => {
|
|
10733
|
-
|
|
10734
|
-
if (
|
|
10808
|
+
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback, scopes) => {
|
|
10809
|
+
const propsCaptureKeys = [...captureKeys].filter((captureKey) => captureKey.startsWith("props."));
|
|
10810
|
+
if (propsCaptureKeys.length < 2 || declaredKeys.has("props")) return;
|
|
10811
|
+
if (propsCaptureKeys.every((captureKey) => [...declaredKeys].some((declaredKey) => isMatchingDepOrPrefix(declaredKey, captureKey)))) return;
|
|
10812
|
+
if (hasMemberCallForRoot(callback, "props", scopes)) captureKeys.add("props");
|
|
10735
10813
|
};
|
|
10736
10814
|
const exhaustiveDeps = defineRule({
|
|
10737
10815
|
id: "exhaustive-deps",
|
|
@@ -10786,7 +10864,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10786
10864
|
};
|
|
10787
10865
|
const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
|
|
10788
10866
|
return { CallExpression(node) {
|
|
10789
|
-
const hookName = getHookName(node.callee);
|
|
10867
|
+
const hookName = getHookName(node.callee, context.scopes);
|
|
10790
10868
|
if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
|
|
10791
10869
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
10792
10870
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
@@ -10843,7 +10921,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10843
10921
|
if (outerAssignments.length > 0) return;
|
|
10844
10922
|
const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
|
|
10845
10923
|
const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
|
|
10846
|
-
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol)) context.report({
|
|
10924
|
+
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol, context.scopes)) context.report({
|
|
10847
10925
|
node: callbackToAnalyze,
|
|
10848
10926
|
message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
|
|
10849
10927
|
});
|
|
@@ -10942,7 +11020,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10942
11020
|
const fullChain = stringifyMemberChain(stripped);
|
|
10943
11021
|
if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
|
|
10944
11022
|
const refSymbol = context.scopes.symbolFor(stripped.object);
|
|
10945
|
-
if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
|
|
11023
|
+
if (refSymbol && symbolHasStableHookOrigin(refSymbol, context.scopes)) {
|
|
10946
11024
|
if (!didReportRefCurrentDep) {
|
|
10947
11025
|
context.report({
|
|
10948
11026
|
node: elementNode,
|
|
@@ -10980,7 +11058,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10980
11058
|
declaredKeys.add(key);
|
|
10981
11059
|
declaredKeyToReportNode.set(key, elementNode);
|
|
10982
11060
|
}
|
|
10983
|
-
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
|
|
11061
|
+
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
|
|
10984
11062
|
const missingCaptureKeys = [];
|
|
10985
11063
|
for (const captureKey of captureKeys) {
|
|
10986
11064
|
let isCoveredByDeclared = false;
|
|
@@ -11066,7 +11144,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11066
11144
|
if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
|
|
11067
11145
|
if (outerFunctionCapturedNames.has(rootName)) continue;
|
|
11068
11146
|
const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
|
|
11069
|
-
if (
|
|
11147
|
+
if (isExtraDepAllowedForHook(hookName, reportNode, context.scopes)) continue;
|
|
11070
11148
|
if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
|
|
11071
11149
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
11072
11150
|
if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
|
|
@@ -22292,6 +22370,26 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
22292
22370
|
const importDeclaration = declarationNode.parent;
|
|
22293
22371
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22294
22372
|
}));
|
|
22373
|
+
const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
|
|
22374
|
+
if (def.type !== "ImportBinding") return false;
|
|
22375
|
+
const declarationNode = def.node;
|
|
22376
|
+
if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
|
|
22377
|
+
const importDeclaration = declarationNode.parent;
|
|
22378
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22379
|
+
}));
|
|
22380
|
+
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
22381
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
22382
|
+
const callee = stripParenExpression(declarator.init.callee);
|
|
22383
|
+
if (isNodeOfType(callee, "Identifier")) {
|
|
22384
|
+
const reference = getRef(analysis, callee);
|
|
22385
|
+
if (!reference?.resolved) return callee.name === hookName;
|
|
22386
|
+
return isReactNamedImportReference(reference, hookName);
|
|
22387
|
+
}
|
|
22388
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
22389
|
+
const namespaceReference = getRef(analysis, callee.object);
|
|
22390
|
+
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
22391
|
+
return isReactNamespaceImportReference(namespaceReference);
|
|
22392
|
+
};
|
|
22295
22393
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
22296
22394
|
if (!node) return false;
|
|
22297
22395
|
if (isNodeOfType(node, "Identifier")) {
|
|
@@ -23346,6 +23444,20 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23346
23444
|
}
|
|
23347
23445
|
return evidence;
|
|
23348
23446
|
};
|
|
23447
|
+
const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
|
|
23448
|
+
const evidence = collectValueEvidence(analysis, expression, {
|
|
23449
|
+
functionNode: componentFunction,
|
|
23450
|
+
invocation: null,
|
|
23451
|
+
isDeferred: false,
|
|
23452
|
+
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23453
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
23454
|
+
currentFilename
|
|
23455
|
+
}, 1);
|
|
23456
|
+
return {
|
|
23457
|
+
sourceReferences: evidence.sourceReferences,
|
|
23458
|
+
isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
|
|
23459
|
+
};
|
|
23460
|
+
};
|
|
23349
23461
|
const findStateSetterReference = (analysis, callExpression) => {
|
|
23350
23462
|
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
23351
23463
|
const callee = stripParenExpression(callExpression.callee);
|
|
@@ -26398,57 +26510,6 @@ const noDefaultProps = defineRule({
|
|
|
26398
26510
|
} })
|
|
26399
26511
|
});
|
|
26400
26512
|
//#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
|
|
26452
26513
|
//#region src/plugin/utils/is-component-function.ts
|
|
26453
26514
|
const isFunctionAssignedToComponent = (functionNode) => {
|
|
26454
26515
|
let cursor = functionNode.parent ?? null;
|
|
@@ -26582,6 +26643,236 @@ const createComponentPropStackTracker = (callbacks) => {
|
|
|
26582
26643
|
};
|
|
26583
26644
|
};
|
|
26584
26645
|
//#endregion
|
|
26646
|
+
//#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
|
|
26647
|
+
const findReferenceDeclarator = (reference) => {
|
|
26648
|
+
for (const definition of reference.resolved?.defs ?? []) {
|
|
26649
|
+
const definitionNode = definition.node;
|
|
26650
|
+
if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
|
|
26651
|
+
}
|
|
26652
|
+
return null;
|
|
26653
|
+
};
|
|
26654
|
+
const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
26655
|
+
const node = stripParenExpression(expression);
|
|
26656
|
+
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26657
|
+
const reference = getRef(analysis, node);
|
|
26658
|
+
if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
|
|
26659
|
+
if (isProp(analysis, reference)) {
|
|
26660
|
+
if (isWholePropsObjectReference(analysis, reference)) return null;
|
|
26661
|
+
return { bindingIdentity: reference.resolved };
|
|
26662
|
+
}
|
|
26663
|
+
if (isState(analysis, reference)) {
|
|
26664
|
+
const stateDeclarator = getUseStateDecl(analysis, reference);
|
|
26665
|
+
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
|
|
26666
|
+
return { bindingIdentity: reference.resolved };
|
|
26667
|
+
}
|
|
26668
|
+
if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
|
|
26669
|
+
const declarator = findReferenceDeclarator(reference);
|
|
26670
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
26671
|
+
const nextVisitedBindings = new Set(visitedBindings);
|
|
26672
|
+
nextVisitedBindings.add(reference.resolved);
|
|
26673
|
+
return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
|
|
26674
|
+
};
|
|
26675
|
+
const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
|
|
26676
|
+
const getDirectBranchStatements = (branch) => {
|
|
26677
|
+
if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
|
|
26678
|
+
return [branch];
|
|
26679
|
+
};
|
|
26680
|
+
const getDirectCallExpression = (statement) => {
|
|
26681
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26682
|
+
const expression = stripParenExpression(statement.expression);
|
|
26683
|
+
return isNodeOfType(expression, "CallExpression") ? expression : null;
|
|
26684
|
+
};
|
|
26685
|
+
const getDirectAssignmentExpression = (statement) => {
|
|
26686
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26687
|
+
const expression = stripParenExpression(statement.expression);
|
|
26688
|
+
return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
|
|
26689
|
+
};
|
|
26690
|
+
const findDirectStateSetterReference = (analysis, callExpression) => {
|
|
26691
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
26692
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
26693
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
26694
|
+
const reference = getRef(analysis, callee);
|
|
26695
|
+
return reference && isStateSetter(analysis, reference) ? reference : null;
|
|
26696
|
+
};
|
|
26697
|
+
const getStaticRefCurrentReference = (analysis, expression) => {
|
|
26698
|
+
const node = stripParenExpression(expression);
|
|
26699
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
26700
|
+
const reference = getRef(analysis, node.object);
|
|
26701
|
+
if (!reference) return null;
|
|
26702
|
+
const declarator = findReferenceDeclarator(reference);
|
|
26703
|
+
return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
|
|
26704
|
+
};
|
|
26705
|
+
const getStateTracker = (analysis, expression) => {
|
|
26706
|
+
const node = stripParenExpression(expression);
|
|
26707
|
+
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26708
|
+
const reference = getRef(analysis, node);
|
|
26709
|
+
if (!reference || !isState(analysis, reference)) return null;
|
|
26710
|
+
const declarator = getUseStateDecl(analysis, reference);
|
|
26711
|
+
if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
|
|
26712
|
+
return {
|
|
26713
|
+
kind: "state",
|
|
26714
|
+
declarator
|
|
26715
|
+
};
|
|
26716
|
+
};
|
|
26717
|
+
const getRefTracker = (analysis, expression) => {
|
|
26718
|
+
const reference = getStaticRefCurrentReference(analysis, expression);
|
|
26719
|
+
return reference ? {
|
|
26720
|
+
kind: "ref",
|
|
26721
|
+
reference
|
|
26722
|
+
} : null;
|
|
26723
|
+
};
|
|
26724
|
+
const getTrackerInitializer = (tracker) => {
|
|
26725
|
+
const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
|
|
26726
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
|
|
26727
|
+
const initializer = declarator.init.arguments?.[0];
|
|
26728
|
+
return initializer ? initializer : null;
|
|
26729
|
+
};
|
|
26730
|
+
const isTrackerSynchronized = (analysis, guard) => {
|
|
26731
|
+
for (const statement of guard.statements) {
|
|
26732
|
+
if (guard.tracker.kind === "state") {
|
|
26733
|
+
const callExpression = getDirectCallExpression(statement);
|
|
26734
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26735
|
+
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26736
|
+
if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
|
|
26737
|
+
const writtenValue = callExpression.arguments?.[0];
|
|
26738
|
+
if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26739
|
+
continue;
|
|
26740
|
+
}
|
|
26741
|
+
const assignmentExpression = getDirectAssignmentExpression(statement);
|
|
26742
|
+
if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
|
|
26743
|
+
if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
|
|
26744
|
+
if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26745
|
+
}
|
|
26746
|
+
return false;
|
|
26747
|
+
};
|
|
26748
|
+
const parseRenderTrackerGuard = (analysis, statement) => {
|
|
26749
|
+
if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
|
|
26750
|
+
const left = statement.test.left;
|
|
26751
|
+
const right = statement.test.right;
|
|
26752
|
+
const candidates = [{
|
|
26753
|
+
sourceExpression: left,
|
|
26754
|
+
trackerExpression: right
|
|
26755
|
+
}, {
|
|
26756
|
+
sourceExpression: right,
|
|
26757
|
+
trackerExpression: left
|
|
26758
|
+
}];
|
|
26759
|
+
for (const candidate of candidates) {
|
|
26760
|
+
const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
|
|
26761
|
+
if (!source) continue;
|
|
26762
|
+
const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
|
|
26763
|
+
if (!tracker) continue;
|
|
26764
|
+
const trackerInitializer = getTrackerInitializer(tracker);
|
|
26765
|
+
if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
|
|
26766
|
+
const guard = {
|
|
26767
|
+
source,
|
|
26768
|
+
tracker,
|
|
26769
|
+
statements: getDirectBranchStatements(statement.consequent)
|
|
26770
|
+
};
|
|
26771
|
+
return isTrackerSynchronized(analysis, guard) ? guard : null;
|
|
26772
|
+
}
|
|
26773
|
+
return null;
|
|
26774
|
+
};
|
|
26775
|
+
const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
|
|
26776
|
+
if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
|
|
26777
|
+
const references = setterReference.resolved.references.filter((reference) => !reference.init);
|
|
26778
|
+
if (references.length !== 1) return false;
|
|
26779
|
+
return references[0].identifier === callExpression.callee;
|
|
26780
|
+
};
|
|
26781
|
+
const getStateInitializer = (stateDeclarator) => {
|
|
26782
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
|
|
26783
|
+
const initializer = stateDeclarator.init.arguments?.[0];
|
|
26784
|
+
return initializer ? initializer : null;
|
|
26785
|
+
};
|
|
26786
|
+
const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
|
|
26787
|
+
if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
|
|
26788
|
+
const componentFunction = componentBody.parent;
|
|
26789
|
+
const facts = [];
|
|
26790
|
+
for (const statement of componentBody.body ?? []) {
|
|
26791
|
+
const guard = parseRenderTrackerGuard(analysis, statement);
|
|
26792
|
+
if (!guard) continue;
|
|
26793
|
+
for (const branchStatement of guard.statements) {
|
|
26794
|
+
const callExpression = getDirectCallExpression(branchStatement);
|
|
26795
|
+
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26796
|
+
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26797
|
+
if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
|
|
26798
|
+
const stateDeclarator = getUseStateDecl(analysis, setterReference);
|
|
26799
|
+
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
|
|
26800
|
+
const writtenValue = callExpression.arguments?.[0];
|
|
26801
|
+
const initializer = getStateInitializer(stateDeclarator);
|
|
26802
|
+
if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
|
|
26803
|
+
facts.push({
|
|
26804
|
+
callExpression,
|
|
26805
|
+
stateDeclarator
|
|
26806
|
+
});
|
|
26807
|
+
}
|
|
26808
|
+
}
|
|
26809
|
+
return facts;
|
|
26810
|
+
};
|
|
26811
|
+
//#endregion
|
|
26812
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
26813
|
+
const getStateName$1 = (stateDeclarator) => {
|
|
26814
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
26815
|
+
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
26816
|
+
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
26817
|
+
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
26818
|
+
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
26819
|
+
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
26820
|
+
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
26821
|
+
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
26822
|
+
};
|
|
26823
|
+
const noDerivedState = defineRule({
|
|
26824
|
+
id: "no-derived-state",
|
|
26825
|
+
title: "Derived value copied into state",
|
|
26826
|
+
severity: "warn",
|
|
26827
|
+
tags: ["test-noise"],
|
|
26828
|
+
recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into state and synchronizing it during render or through an effect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
|
|
26829
|
+
create: (context) => {
|
|
26830
|
+
const reportStateWrite = (callExpression, stateDeclarator) => {
|
|
26831
|
+
const stateName = getStateName$1(stateDeclarator);
|
|
26832
|
+
context.report({
|
|
26833
|
+
node: callExpression,
|
|
26834
|
+
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
26835
|
+
});
|
|
26836
|
+
};
|
|
26837
|
+
return {
|
|
26838
|
+
...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
|
|
26839
|
+
if (!componentBody) return;
|
|
26840
|
+
const analysis = getProgramAnalysis(componentBody);
|
|
26841
|
+
if (!analysis) return;
|
|
26842
|
+
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26843
|
+
} }).visitors,
|
|
26844
|
+
CallExpression(node) {
|
|
26845
|
+
if (!isUseEffect(node)) return;
|
|
26846
|
+
const analysis = getProgramAnalysis(node);
|
|
26847
|
+
if (!analysis) return;
|
|
26848
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
26849
|
+
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
26850
|
+
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26851
|
+
}
|
|
26852
|
+
}
|
|
26853
|
+
};
|
|
26854
|
+
}
|
|
26855
|
+
});
|
|
26856
|
+
//#endregion
|
|
26857
|
+
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
26858
|
+
const noDerivedStateEffect = defineRule({
|
|
26859
|
+
id: "no-derived-state-effect",
|
|
26860
|
+
title: "Derived state stored in an effect",
|
|
26861
|
+
severity: "warn",
|
|
26862
|
+
tags: ["test-noise"],
|
|
26863
|
+
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
26864
|
+
create: (context) => ({ CallExpression(node) {
|
|
26865
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
26866
|
+
const analysis = getProgramAnalysis(node);
|
|
26867
|
+
if (!analysis) return;
|
|
26868
|
+
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
26869
|
+
context.report({
|
|
26870
|
+
node,
|
|
26871
|
+
message: "You pay an extra render for state you can derive from other values."
|
|
26872
|
+
});
|
|
26873
|
+
} })
|
|
26874
|
+
});
|
|
26875
|
+
//#endregion
|
|
26585
26876
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
26586
26877
|
const isInitialOnlyPropName = (propName) => {
|
|
26587
26878
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -30603,6 +30894,131 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
30603
30894
|
} })
|
|
30604
30895
|
});
|
|
30605
30896
|
//#endregion
|
|
30897
|
+
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
30898
|
+
const TIMER_FUNCTION_NAMES = new Set([
|
|
30899
|
+
"cancelAnimationFrame",
|
|
30900
|
+
"clearInterval",
|
|
30901
|
+
"clearTimeout",
|
|
30902
|
+
"queueMicrotask",
|
|
30903
|
+
"requestAnimationFrame",
|
|
30904
|
+
"setInterval",
|
|
30905
|
+
"setTimeout"
|
|
30906
|
+
]);
|
|
30907
|
+
const STORAGE_MUTATION_METHOD_NAMES = new Set([
|
|
30908
|
+
"clear",
|
|
30909
|
+
"removeItem",
|
|
30910
|
+
"setItem"
|
|
30911
|
+
]);
|
|
30912
|
+
const STORAGE_RECEIVER_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
30913
|
+
const EXTERNAL_READ_METHOD_NAMES = new Set(["getBoundingClientRect", "getClientRects"]);
|
|
30914
|
+
const NOTIFICATION_RECEIVER_NAMES = new Set([
|
|
30915
|
+
"message",
|
|
30916
|
+
"notification",
|
|
30917
|
+
"toast"
|
|
30918
|
+
]);
|
|
30919
|
+
const NOTIFICATION_METHOD_NAMES = new Set([
|
|
30920
|
+
"error",
|
|
30921
|
+
"info",
|
|
30922
|
+
"loading",
|
|
30923
|
+
"open",
|
|
30924
|
+
"show",
|
|
30925
|
+
"success",
|
|
30926
|
+
"warning"
|
|
30927
|
+
]);
|
|
30928
|
+
const getMemberCall = (node) => {
|
|
30929
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
30930
|
+
if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
|
|
30931
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
30932
|
+
return {
|
|
30933
|
+
methodName: node.callee.property.name,
|
|
30934
|
+
receiver: stripParenExpression(node.callee.object)
|
|
30935
|
+
};
|
|
30936
|
+
};
|
|
30937
|
+
const isNotificationReceiver = (receiver, scopes) => {
|
|
30938
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
30939
|
+
if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
|
|
30940
|
+
const symbol = scopes.symbolFor(receiver);
|
|
30941
|
+
if (symbol?.kind === "import") return true;
|
|
30942
|
+
if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
|
|
30943
|
+
const callee = symbol.initializer.callee;
|
|
30944
|
+
return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
|
|
30945
|
+
};
|
|
30946
|
+
const getKnownImpureCall = (callExpression, scopes) => {
|
|
30947
|
+
if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
|
|
30948
|
+
const memberCall = getMemberCall(callExpression);
|
|
30949
|
+
if (!memberCall) return null;
|
|
30950
|
+
const { methodName, receiver } = memberCall;
|
|
30951
|
+
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
|
|
30952
|
+
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
|
|
30953
|
+
if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
|
|
30954
|
+
return null;
|
|
30955
|
+
};
|
|
30956
|
+
const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
|
|
30957
|
+
let rootIdentifier = null;
|
|
30958
|
+
if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
|
|
30959
|
+
else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
|
|
30960
|
+
if (!rootIdentifier) return null;
|
|
30961
|
+
const updaterScope = scopes.ownScopeFor(updater);
|
|
30962
|
+
if (!updaterScope) return null;
|
|
30963
|
+
const symbol = scopes.symbolFor(rootIdentifier);
|
|
30964
|
+
if (!symbol) return `the external value "${rootIdentifier.name}"`;
|
|
30965
|
+
if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
|
|
30966
|
+
return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
|
|
30967
|
+
};
|
|
30968
|
+
const findImpureUpdaterOperation = (updater, scopes) => {
|
|
30969
|
+
const analysis = getProgramAnalysis(updater);
|
|
30970
|
+
let operation = null;
|
|
30971
|
+
walkAst(updater, (child) => {
|
|
30972
|
+
if (operation) return false;
|
|
30973
|
+
if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
|
|
30974
|
+
if (isNodeOfType(child, "CallExpression")) {
|
|
30975
|
+
if (isNodeOfType(child.callee, "Identifier") && analysis) {
|
|
30976
|
+
const calleeReference = getRef(analysis, child.callee);
|
|
30977
|
+
if (calleeReference && isStateSetterCall(analysis, calleeReference)) {
|
|
30978
|
+
operation = `the nested state update "${child.callee.name}()"`;
|
|
30979
|
+
return false;
|
|
30980
|
+
}
|
|
30981
|
+
}
|
|
30982
|
+
const impureCall = getKnownImpureCall(child, scopes);
|
|
30983
|
+
if (impureCall) {
|
|
30984
|
+
operation = impureCall;
|
|
30985
|
+
return false;
|
|
30986
|
+
}
|
|
30987
|
+
}
|
|
30988
|
+
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
30989
|
+
operation = getExternalAssignmentDescription(child.left, updater, scopes);
|
|
30990
|
+
if (operation) return false;
|
|
30991
|
+
}
|
|
30992
|
+
if (isNodeOfType(child, "UpdateExpression")) {
|
|
30993
|
+
operation = getExternalAssignmentDescription(child.argument, updater, scopes);
|
|
30994
|
+
if (operation) return false;
|
|
30995
|
+
}
|
|
30996
|
+
});
|
|
30997
|
+
return operation;
|
|
30998
|
+
};
|
|
30999
|
+
const noImpureStateUpdater = defineRule({
|
|
31000
|
+
id: "no-impure-state-updater",
|
|
31001
|
+
title: "State updater has side effects",
|
|
31002
|
+
severity: "error",
|
|
31003
|
+
recommendation: "Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.",
|
|
31004
|
+
create: (context) => ({ CallExpression(node) {
|
|
31005
|
+
const updater = node.arguments?.[0];
|
|
31006
|
+
if (!updater || !isFunctionLike$1(updater)) return;
|
|
31007
|
+
const analysis = getProgramAnalysis(node);
|
|
31008
|
+
if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
|
|
31009
|
+
const calleeReference = getRef(analysis, node.callee);
|
|
31010
|
+
if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
|
|
31011
|
+
const stateDeclarator = getUseStateDecl(analysis, calleeReference);
|
|
31012
|
+
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
|
|
31013
|
+
const operation = findImpureUpdaterOperation(updater, context.scopes);
|
|
31014
|
+
if (!operation) return;
|
|
31015
|
+
context.report({
|
|
31016
|
+
node: updater,
|
|
31017
|
+
message: `This state updater performs ${operation}. React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.`
|
|
31018
|
+
});
|
|
31019
|
+
} })
|
|
31020
|
+
});
|
|
31021
|
+
//#endregion
|
|
30606
31022
|
//#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
|
|
30607
31023
|
const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
|
|
30608
31024
|
const REACT_USE_REF_OPTIONS = {
|
|
@@ -33771,6 +34187,166 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
33771
34187
|
return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
|
|
33772
34188
|
}));
|
|
33773
34189
|
};
|
|
34190
|
+
const getDeclarationKind = (declarator) => {
|
|
34191
|
+
const declaration = declarator.parent;
|
|
34192
|
+
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
34193
|
+
};
|
|
34194
|
+
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
34195
|
+
const getDestructuredPropertyName = (bindingIdentifier) => {
|
|
34196
|
+
const property = bindingIdentifier.parent;
|
|
34197
|
+
if (!property || !isNodeOfType(property, "Property")) return null;
|
|
34198
|
+
if (property.computed) return isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? String(property.key.value) : null;
|
|
34199
|
+
return isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
34200
|
+
};
|
|
34201
|
+
const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
|
|
34202
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
34203
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
34204
|
+
const callbackReference = getRef(analysis, unwrappedExpression);
|
|
34205
|
+
const callbackVariable = callbackReference?.resolved;
|
|
34206
|
+
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable)) return null;
|
|
34207
|
+
if (isProp(analysis, callbackReference)) {
|
|
34208
|
+
if (isWholePropsObjectReference(analysis, callbackReference)) return null;
|
|
34209
|
+
const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
|
|
34210
|
+
return (bindingIdentifier && getDestructuredPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
|
|
34211
|
+
}
|
|
34212
|
+
if (hasMutableBindingWrite(callbackReference)) return null;
|
|
34213
|
+
const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
34214
|
+
if (definitions.length !== 1) return null;
|
|
34215
|
+
const declarator = definitions[0];
|
|
34216
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || getDeclarationKind(declarator) !== "const" || !declarator.init) return null;
|
|
34217
|
+
visitedVariables.add(callbackVariable);
|
|
34218
|
+
if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
34219
|
+
const bindingIdentifier = callbackVariable.defs[0]?.name;
|
|
34220
|
+
const propertyName = bindingIdentifier ? getDestructuredPropertyName(bindingIdentifier) : null;
|
|
34221
|
+
const propsReference = getDownstreamRefs(analysis, declarator.init).find((candidateReference) => isWholePropsObjectReference(analysis, candidateReference));
|
|
34222
|
+
return propertyName && propsReference ? propertyName : null;
|
|
34223
|
+
}
|
|
34224
|
+
if (!isNodeOfType(declarator.id, "Identifier")) return null;
|
|
34225
|
+
return getParentCallbackPropName(analysis, declarator.init, visitedVariables);
|
|
34226
|
+
}
|
|
34227
|
+
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
|
|
34228
|
+
const callbackName = getStaticMemberPropertyName(unwrappedExpression);
|
|
34229
|
+
if (!callbackName) return null;
|
|
34230
|
+
const receiver = stripParenExpression(unwrappedExpression.object);
|
|
34231
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
34232
|
+
const receiverReference = getRef(analysis, receiver);
|
|
34233
|
+
if (!receiverReference || !isWholePropsObjectReference(analysis, receiverReference)) return null;
|
|
34234
|
+
return callbackName;
|
|
34235
|
+
};
|
|
34236
|
+
const isNullishRefInitializer = (initializer) => {
|
|
34237
|
+
if (!initializer) return true;
|
|
34238
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
34239
|
+
if (isNodeOfType(unwrappedInitializer, "Literal")) return unwrappedInitializer.value === null;
|
|
34240
|
+
if (!isNodeOfType(unwrappedInitializer, "Identifier")) return false;
|
|
34241
|
+
return unwrappedInitializer.name === "undefined";
|
|
34242
|
+
};
|
|
34243
|
+
const getDirectComponentBodyStatement = (node, componentBody) => {
|
|
34244
|
+
let current = node;
|
|
34245
|
+
while (current?.parent && current.parent !== componentBody) current = current.parent;
|
|
34246
|
+
return current?.parent === componentBody ? current : null;
|
|
34247
|
+
};
|
|
34248
|
+
const getVariableForDeclarator = (analysis, declarator) => {
|
|
34249
|
+
for (const scope of analysis.scopeManager.scopes) {
|
|
34250
|
+
const variable = scope.variables.find((candidateVariable) => candidateVariable.defs.some((definition) => definition.node === declarator));
|
|
34251
|
+
if (variable) return variable;
|
|
34252
|
+
}
|
|
34253
|
+
return null;
|
|
34254
|
+
};
|
|
34255
|
+
const getRefMember = (identifier) => {
|
|
34256
|
+
const receiver = findTransparentExpressionRoot(identifier);
|
|
34257
|
+
const member = receiver.parent;
|
|
34258
|
+
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== receiver) return null;
|
|
34259
|
+
return member;
|
|
34260
|
+
};
|
|
34261
|
+
const getRefMemberAssignment = (identifier) => {
|
|
34262
|
+
const member = getRefMember(identifier);
|
|
34263
|
+
if (!member) return null;
|
|
34264
|
+
const memberRoot = findTransparentExpressionRoot(member);
|
|
34265
|
+
const assignment = memberRoot.parent;
|
|
34266
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== memberRoot) return null;
|
|
34267
|
+
return assignment;
|
|
34268
|
+
};
|
|
34269
|
+
const getRefAliasDeclarator = (identifier) => {
|
|
34270
|
+
const initializer = findTransparentExpressionRoot(identifier);
|
|
34271
|
+
const declarator = initializer.parent;
|
|
34272
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.id, "Identifier") || getDeclarationKind(declarator) !== "const") return null;
|
|
34273
|
+
return declarator;
|
|
34274
|
+
};
|
|
34275
|
+
const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
34276
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
34277
|
+
const receiverReference = getRef(analysis, receiver);
|
|
34278
|
+
if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
|
|
34279
|
+
const variables = /* @__PURE__ */ new Set();
|
|
34280
|
+
let currentVariable = receiverReference.resolved;
|
|
34281
|
+
let refCall = null;
|
|
34282
|
+
while (currentVariable && !variables.has(currentVariable)) {
|
|
34283
|
+
variables.add(currentVariable);
|
|
34284
|
+
const definitions = currentVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
34285
|
+
if (definitions.length !== 1) return null;
|
|
34286
|
+
const declarator = definitions[0];
|
|
34287
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return null;
|
|
34288
|
+
if (isNodeOfType(declarator.init, "CallExpression") && isReactUseRefCall(declarator.init)) {
|
|
34289
|
+
refCall = declarator.init;
|
|
34290
|
+
break;
|
|
34291
|
+
}
|
|
34292
|
+
if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
|
|
34293
|
+
const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
|
|
34294
|
+
if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
|
|
34295
|
+
currentVariable = upstreamReference.resolved;
|
|
34296
|
+
}
|
|
34297
|
+
if (!refCall) return null;
|
|
34298
|
+
const pendingVariables = [...variables];
|
|
34299
|
+
while (pendingVariables.length > 0) {
|
|
34300
|
+
const variable = pendingVariables.pop();
|
|
34301
|
+
if (!variable) continue;
|
|
34302
|
+
for (const candidateReference of variable.references) {
|
|
34303
|
+
const aliasDeclarator = getRefAliasDeclarator(candidateReference.identifier);
|
|
34304
|
+
if (!aliasDeclarator) continue;
|
|
34305
|
+
const aliasVariable = getVariableForDeclarator(analysis, aliasDeclarator);
|
|
34306
|
+
if (!aliasVariable || variables.has(aliasVariable)) continue;
|
|
34307
|
+
if (aliasVariable.references.some((aliasReference) => aliasReference.isWrite() && !aliasReference.init)) return null;
|
|
34308
|
+
variables.add(aliasVariable);
|
|
34309
|
+
pendingVariables.push(aliasVariable);
|
|
34310
|
+
}
|
|
34311
|
+
}
|
|
34312
|
+
return {
|
|
34313
|
+
refCall,
|
|
34314
|
+
variables
|
|
34315
|
+
};
|
|
34316
|
+
};
|
|
34317
|
+
const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
|
|
34318
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
34319
|
+
if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
|
|
34320
|
+
const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
|
|
34321
|
+
if (!bindingProvenance) return null;
|
|
34322
|
+
const { refCall, variables } = bindingProvenance;
|
|
34323
|
+
const componentFunction = findEnclosingFunction(effectCall);
|
|
34324
|
+
if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
34325
|
+
if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
|
|
34326
|
+
const callbackPropNames = /* @__PURE__ */ new Set();
|
|
34327
|
+
const initializer = refCall.arguments?.[0];
|
|
34328
|
+
const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
|
|
34329
|
+
if (initializerCallbackName) callbackPropNames.add(initializerCallbackName);
|
|
34330
|
+
else if (!isNullishRefInitializer(initializer)) return null;
|
|
34331
|
+
for (const variable of variables) for (const candidateReference of variable.references) {
|
|
34332
|
+
if (candidateReference.init) continue;
|
|
34333
|
+
if (candidateReference.isWrite()) return null;
|
|
34334
|
+
const identifier = candidateReference.identifier;
|
|
34335
|
+
if (getRefAliasDeclarator(identifier)) continue;
|
|
34336
|
+
const member = getRefMember(identifier);
|
|
34337
|
+
if (!member || getStaticMemberPropertyName(member) !== "current") return null;
|
|
34338
|
+
const memberParent = findTransparentExpressionRoot(member).parent;
|
|
34339
|
+
if (memberParent && (isNodeOfType(memberParent, "UpdateExpression") || isNodeOfType(memberParent, "UnaryExpression") && memberParent.operator === "delete")) return null;
|
|
34340
|
+
const assignment = getRefMemberAssignment(identifier);
|
|
34341
|
+
if (!assignment) continue;
|
|
34342
|
+
const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
|
|
34343
|
+
if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
|
|
34344
|
+
const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
|
|
34345
|
+
if (!assignedCallbackName) return null;
|
|
34346
|
+
callbackPropNames.add(assignedCallbackName);
|
|
34347
|
+
}
|
|
34348
|
+
return callbackPropNames.size > 0 ? { callbackPropNames } : null;
|
|
34349
|
+
};
|
|
33774
34350
|
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
33775
34351
|
const node = def.node;
|
|
33776
34352
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
@@ -33826,71 +34402,79 @@ const noPassDataToParent = defineRule({
|
|
|
33826
34402
|
severity: "warn",
|
|
33827
34403
|
tags: ["test-noise"],
|
|
33828
34404
|
recommendation: "Fetch the data in the parent and pass it down as a prop (or return it from the hook), instead of handing it back up through a prop callback in a useEffect. See https://react.dev/learn/you-might-not-need-an-effect#passing-data-to-the-parent",
|
|
33829
|
-
create: (context) =>
|
|
33830
|
-
|
|
33831
|
-
|
|
33832
|
-
|
|
33833
|
-
|
|
33834
|
-
|
|
33835
|
-
|
|
33836
|
-
|
|
33837
|
-
|
|
33838
|
-
|
|
33839
|
-
const
|
|
33840
|
-
if (!
|
|
33841
|
-
|
|
33842
|
-
if (!
|
|
33843
|
-
const
|
|
33844
|
-
|
|
33845
|
-
|
|
33846
|
-
|
|
33847
|
-
if (
|
|
33848
|
-
|
|
33849
|
-
|
|
33850
|
-
|
|
33851
|
-
|
|
33852
|
-
|
|
33853
|
-
|
|
33854
|
-
|
|
33855
|
-
|
|
33856
|
-
|
|
33857
|
-
|
|
33858
|
-
|
|
33859
|
-
|
|
33860
|
-
|
|
33861
|
-
|
|
33862
|
-
|
|
33863
|
-
|
|
33864
|
-
|
|
33865
|
-
|
|
33866
|
-
|
|
33867
|
-
|
|
33868
|
-
|
|
33869
|
-
|
|
33870
|
-
|
|
33871
|
-
|
|
33872
|
-
|
|
33873
|
-
|
|
33874
|
-
|
|
33875
|
-
|
|
33876
|
-
|
|
33877
|
-
|
|
33878
|
-
|
|
33879
|
-
|
|
33880
|
-
if (
|
|
33881
|
-
if (
|
|
33882
|
-
|
|
33883
|
-
|
|
33884
|
-
|
|
33885
|
-
|
|
33886
|
-
|
|
33887
|
-
|
|
33888
|
-
|
|
33889
|
-
|
|
33890
|
-
|
|
33891
|
-
|
|
33892
|
-
|
|
33893
|
-
|
|
34405
|
+
create: (context) => {
|
|
34406
|
+
const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
|
|
34407
|
+
allowGlobalReactNamespace: true,
|
|
34408
|
+
allowUnboundBareCalls: true
|
|
34409
|
+
});
|
|
34410
|
+
return { CallExpression(node) {
|
|
34411
|
+
if (!isUseEffect(node)) return;
|
|
34412
|
+
const analysis = getProgramAnalysis(node);
|
|
34413
|
+
if (!analysis) return;
|
|
34414
|
+
if (hasCleanup(analysis, node)) return;
|
|
34415
|
+
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
34416
|
+
if (!effectFnRefs) return;
|
|
34417
|
+
const effectFn = getEffectFn(analysis, node);
|
|
34418
|
+
if (!effectFn) return;
|
|
34419
|
+
for (const ref of effectFnRefs) {
|
|
34420
|
+
const callExpr = getCallExpr(ref);
|
|
34421
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
34422
|
+
const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall);
|
|
34423
|
+
if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
|
|
34424
|
+
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
34425
|
+
const calleeNode = unwrapChainExpression$1(callExpr.callee);
|
|
34426
|
+
const identifier = ref.identifier;
|
|
34427
|
+
if (callbackRefProvenance) {
|
|
34428
|
+
if ([...callbackRefProvenance.callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
|
|
34429
|
+
} else if (calleeNode === identifier) {
|
|
34430
|
+
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
34431
|
+
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
34432
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
34433
|
+
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
34434
|
+
if (isCustomHookParameter(ref)) continue;
|
|
34435
|
+
} else continue;
|
|
34436
|
+
const methodName = getCallMethodName(calleeNode);
|
|
34437
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
34438
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
34439
|
+
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
34440
|
+
if (!callbackRefProvenance && isNamespacedApiCallee(calleeNode)) continue;
|
|
34441
|
+
const isSetterNamedCallee = callbackRefProvenance ? [...callbackRefProvenance.callbackPropNames].every((callbackPropName) => SETTER_NAMED_PROP_PATTERN.test(callbackPropName)) : Boolean((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) && SETTER_NAMED_PROP_PATTERN.test((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) ?? ""));
|
|
34442
|
+
const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
|
|
34443
|
+
const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
|
|
34444
|
+
if (isFunctionLike$1(argument)) {
|
|
34445
|
+
if (!isSetterNamedCallee) return [];
|
|
34446
|
+
return getFunctionalUpdaterDataRefs(analysis, argument);
|
|
34447
|
+
}
|
|
34448
|
+
if (isHandlerBagArgument(analysis, argument)) return [];
|
|
34449
|
+
if (isParentWiredHookResultArgument(analysis, argument)) return [];
|
|
34450
|
+
if (isNodeOfType(argument, "Identifier")) {
|
|
34451
|
+
const argumentRef = getRef(analysis, argument);
|
|
34452
|
+
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
34453
|
+
}
|
|
34454
|
+
return getDownstreamRefs(analysis, argument);
|
|
34455
|
+
}).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
34456
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
34457
|
+
if (!argsUpstreamRefs.some((argRef) => {
|
|
34458
|
+
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
34459
|
+
if (isProp(analysis, argRef)) return false;
|
|
34460
|
+
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
34461
|
+
if (isRefCurrent(argRef)) return false;
|
|
34462
|
+
if (isConstant(argRef)) return false;
|
|
34463
|
+
if (isParentWiredHookResultRef(analysis, argRef)) return false;
|
|
34464
|
+
if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
|
|
34465
|
+
if (resolvesToFunctionBinding(argRef)) return false;
|
|
34466
|
+
const argIdentifier = argRef.identifier;
|
|
34467
|
+
if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
|
|
34468
|
+
if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
|
|
34469
|
+
return true;
|
|
34470
|
+
})) continue;
|
|
34471
|
+
context.report({
|
|
34472
|
+
node: callExpr,
|
|
34473
|
+
message: "Handing data back to a parent from a useEffect costs your users an extra render."
|
|
34474
|
+
});
|
|
34475
|
+
}
|
|
34476
|
+
} };
|
|
34477
|
+
}
|
|
33894
34478
|
});
|
|
33895
34479
|
//#endregion
|
|
33896
34480
|
//#region src/plugin/utils/is-call-result-consumed-as-argument.ts
|
|
@@ -56100,6 +56684,18 @@ const reactDoctorRules = [
|
|
|
56100
56684
|
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
56101
56685
|
}
|
|
56102
56686
|
},
|
|
56687
|
+
{
|
|
56688
|
+
key: "react-doctor/no-impure-state-updater",
|
|
56689
|
+
id: "no-impure-state-updater",
|
|
56690
|
+
source: "react-doctor",
|
|
56691
|
+
originallyExternal: false,
|
|
56692
|
+
rule: {
|
|
56693
|
+
...noImpureStateUpdater,
|
|
56694
|
+
framework: "global",
|
|
56695
|
+
category: "Bugs",
|
|
56696
|
+
requires: [...new Set(["react", ...noImpureStateUpdater.requires ?? []])]
|
|
56697
|
+
}
|
|
56698
|
+
},
|
|
56103
56699
|
{
|
|
56104
56700
|
key: "react-doctor/no-indeterminate-attribute",
|
|
56105
56701
|
id: "no-indeterminate-attribute",
|