oxlint-plugin-react-doctor 0.7.4-dev.2b5a18e → 0.7.4-dev.4a5d9bb
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 -138
- package/dist/index.js +154 -948
- package/package.json +1 -1
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,78 +10522,12 @@ 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 isPotentiallyFreshComparedValue = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
10593
|
-
const candidate = unwrapExpression$3(node);
|
|
10594
|
-
if (isUnstableInitializer(candidate)) return true;
|
|
10595
|
-
if (isNodeOfType(candidate, "ConditionalExpression")) return isPotentiallyFreshComparedValue(candidate.consequent, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.alternate, scopes, visitedSymbolIds);
|
|
10596
|
-
if (isNodeOfType(candidate, "LogicalExpression")) return isPotentiallyFreshComparedValue(candidate.left, scopes, visitedSymbolIds) || isPotentiallyFreshComparedValue(candidate.right, scopes, visitedSymbolIds);
|
|
10597
|
-
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
10598
|
-
const symbol = scopes.symbolFor(candidate);
|
|
10599
|
-
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
10600
|
-
if (symbol.kind === "let" || symbol.kind === "var") return true;
|
|
10601
|
-
if (symbol.kind !== "const" || !symbol.initializer) return false;
|
|
10602
|
-
visitedSymbolIds.add(symbol.id);
|
|
10603
|
-
return isPotentiallyFreshComparedValue(symbol.initializer, scopes, visitedSymbolIds);
|
|
10604
|
-
};
|
|
10605
|
-
const isExtraDepAllowedForHook = (hookName, node, scopes) => {
|
|
10606
|
-
if (!isExtraReactiveDepAllowed(node, scopes)) return false;
|
|
10607
|
-
if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName)) return true;
|
|
10608
|
-
if (hookName !== "useMemo") return false;
|
|
10609
|
-
const rootSymbol = getRootSymbol(node, scopes);
|
|
10610
|
-
return Boolean(rootSymbol && !symbolHasStableValue(rootSymbol, scopes) && !isUnstableInitializer(rootSymbol.initializer));
|
|
10611
|
-
};
|
|
10612
10525
|
const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
|
|
10613
10526
|
const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
|
|
10614
|
-
const isStableSetterLikeSymbol = (symbol
|
|
10615
|
-
if (!symbolHasStableHookOrigin(symbol
|
|
10527
|
+
const isStableSetterLikeSymbol = (symbol) => {
|
|
10528
|
+
if (!symbolHasStableHookOrigin(symbol)) return false;
|
|
10616
10529
|
return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
|
|
10617
10530
|
};
|
|
10618
|
-
const isConvergingFunctionalUpdater = (node, scopes) => {
|
|
10619
|
-
const updater = unwrapExpression$3(node);
|
|
10620
|
-
if (!isNodeOfType(updater, "ArrowFunctionExpression") && !isNodeOfType(updater, "FunctionExpression")) return false;
|
|
10621
|
-
const previousValueParameter = updater.params?.[0];
|
|
10622
|
-
if (!previousValueParameter || !isNodeOfType(previousValueParameter, "Identifier")) return false;
|
|
10623
|
-
let returnedExpression = updater.body;
|
|
10624
|
-
if (isNodeOfType(updater.body, "BlockStatement")) {
|
|
10625
|
-
returnedExpression = null;
|
|
10626
|
-
for (const statement of updater.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) {
|
|
10627
|
-
returnedExpression = statement.argument;
|
|
10628
|
-
break;
|
|
10629
|
-
}
|
|
10630
|
-
}
|
|
10631
|
-
const conditional = returnedExpression ? unwrapExpression$3(returnedExpression) : null;
|
|
10632
|
-
if (!conditional || !isNodeOfType(conditional, "ConditionalExpression")) return false;
|
|
10633
|
-
const test = unwrapExpression$3(conditional.test);
|
|
10634
|
-
if (!isNodeOfType(test, "BinaryExpression") || ![
|
|
10635
|
-
"===",
|
|
10636
|
-
"!==",
|
|
10637
|
-
"==",
|
|
10638
|
-
"!="
|
|
10639
|
-
].includes(test.operator)) return false;
|
|
10640
|
-
const isPreviousValue = (expression) => {
|
|
10641
|
-
const candidate = unwrapExpression$3(expression);
|
|
10642
|
-
return isNodeOfType(candidate, "Identifier") && candidate.name === previousValueParameter.name;
|
|
10643
|
-
};
|
|
10644
|
-
let comparedValue = null;
|
|
10645
|
-
if (isPreviousValue(test.left)) comparedValue = test.right;
|
|
10646
|
-
else if (isPreviousValue(test.right)) comparedValue = test.left;
|
|
10647
|
-
if (!comparedValue) return false;
|
|
10648
|
-
if (isPotentiallyFreshComparedValue(comparedValue, scopes)) return false;
|
|
10649
|
-
const isSameComparedValue = (expression) => {
|
|
10650
|
-
const candidate = unwrapExpression$3(expression);
|
|
10651
|
-
const compared = unwrapExpression$3(comparedValue);
|
|
10652
|
-
if (isNodeOfType(candidate, "Identifier") && isNodeOfType(compared, "Identifier")) return candidate.name === compared.name;
|
|
10653
|
-
if (isNodeOfType(candidate, "Literal") && isNodeOfType(compared, "Literal")) return candidate.value === compared.value;
|
|
10654
|
-
return false;
|
|
10655
|
-
};
|
|
10656
|
-
return test.operator === "===" || test.operator === "==" ? isPreviousValue(conditional.consequent) && isSameComparedValue(conditional.alternate) : isSameComparedValue(conditional.consequent) && isPreviousValue(conditional.alternate);
|
|
10657
|
-
};
|
|
10658
|
-
const isGuardedStableSetterCall = (identifier, symbol, scopes) => {
|
|
10659
|
-
const parent = identifier.parent;
|
|
10660
|
-
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier || !isStableSetterLikeSymbol(symbol, scopes)) return false;
|
|
10661
|
-
const writtenValue = parent.arguments?.[0];
|
|
10662
|
-
return Boolean(writtenValue && isConvergingFunctionalUpdater(writtenValue, scopes));
|
|
10663
|
-
};
|
|
10664
10531
|
const findStableSetterReference = (node, scopes) => {
|
|
10665
10532
|
let setterName = null;
|
|
10666
10533
|
const visit = (current) => {
|
|
@@ -10668,7 +10535,7 @@ const findStableSetterReference = (node, scopes) => {
|
|
|
10668
10535
|
if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
|
|
10669
10536
|
if (isNodeOfType(current, "Identifier")) {
|
|
10670
10537
|
const symbol = scopes.referenceFor(current)?.resolvedSymbol;
|
|
10671
|
-
if (symbol && isStableSetterLikeSymbol(symbol
|
|
10538
|
+
if (symbol && isStableSetterLikeSymbol(symbol)) {
|
|
10672
10539
|
setterName = symbol.name;
|
|
10673
10540
|
return;
|
|
10674
10541
|
}
|
|
@@ -10784,11 +10651,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
|
|
|
10784
10651
|
}
|
|
10785
10652
|
return false;
|
|
10786
10653
|
};
|
|
10787
|
-
const isSeededDataRefSymbol = (refSymbol
|
|
10654
|
+
const isSeededDataRefSymbol = (refSymbol) => {
|
|
10788
10655
|
if (!refSymbol) return false;
|
|
10789
10656
|
const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
|
|
10790
10657
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
10791
|
-
if (getHookName(initializer.callee
|
|
10658
|
+
if (getHookName(initializer.callee) !== "useRef") return false;
|
|
10792
10659
|
const firstArgument = initializer.arguments[0];
|
|
10793
10660
|
if (!firstArgument || !isAstNode(firstArgument)) return false;
|
|
10794
10661
|
const strippedArgument = unwrapExpression$3(firstArgument);
|
|
@@ -10828,9 +10695,7 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
|
|
|
10828
10695
|
const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
|
|
10829
10696
|
return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
|
|
10830
10697
|
};
|
|
10831
|
-
const hasMemberCallForRoot = (node, rootName
|
|
10832
|
-
const rootSymbol = closureCaptures(node, scopes).find((reference) => reference.resolvedSymbol?.name === rootName)?.resolvedSymbol ?? null;
|
|
10833
|
-
if (!rootSymbol) return false;
|
|
10698
|
+
const hasMemberCallForRoot = (node, rootName) => {
|
|
10834
10699
|
let didFindMemberCall = false;
|
|
10835
10700
|
const visit = (current) => {
|
|
10836
10701
|
if (didFindMemberCall) return;
|
|
@@ -10846,7 +10711,7 @@ const hasMemberCallForRoot = (node, rootName, scopes) => {
|
|
|
10846
10711
|
}
|
|
10847
10712
|
chainObject = unwrapExpression$3(chainObject.object);
|
|
10848
10713
|
}
|
|
10849
|
-
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName
|
|
10714
|
+
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
|
|
10850
10715
|
didFindMemberCall = true;
|
|
10851
10716
|
return;
|
|
10852
10717
|
}
|
|
@@ -10864,11 +10729,9 @@ const hasMemberCallForRoot = (node, rootName, scopes) => {
|
|
|
10864
10729
|
visit(node);
|
|
10865
10730
|
return didFindMemberCall;
|
|
10866
10731
|
};
|
|
10867
|
-
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback
|
|
10868
|
-
|
|
10869
|
-
if (
|
|
10870
|
-
if (propsCaptureKeys.every((captureKey) => [...declaredKeys].some((declaredKey) => isMatchingDepOrPrefix(declaredKey, captureKey)))) return;
|
|
10871
|
-
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");
|
|
10872
10735
|
};
|
|
10873
10736
|
const exhaustiveDeps = defineRule({
|
|
10874
10737
|
id: "exhaustive-deps",
|
|
@@ -10923,7 +10786,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10923
10786
|
};
|
|
10924
10787
|
const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
|
|
10925
10788
|
return { CallExpression(node) {
|
|
10926
|
-
const hookName = getHookName(node.callee
|
|
10789
|
+
const hookName = getHookName(node.callee);
|
|
10927
10790
|
if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
|
|
10928
10791
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
10929
10792
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
@@ -10980,7 +10843,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10980
10843
|
if (outerAssignments.length > 0) return;
|
|
10981
10844
|
const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
|
|
10982
10845
|
const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
|
|
10983
|
-
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({
|
|
10984
10847
|
node: callbackToAnalyze,
|
|
10985
10848
|
message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
|
|
10986
10849
|
});
|
|
@@ -11079,7 +10942,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11079
10942
|
const fullChain = stringifyMemberChain(stripped);
|
|
11080
10943
|
if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
|
|
11081
10944
|
const refSymbol = context.scopes.symbolFor(stripped.object);
|
|
11082
|
-
if (refSymbol && symbolHasStableHookOrigin(refSymbol
|
|
10945
|
+
if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
|
|
11083
10946
|
if (!didReportRefCurrentDep) {
|
|
11084
10947
|
context.report({
|
|
11085
10948
|
node: elementNode,
|
|
@@ -11117,7 +10980,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11117
10980
|
declaredKeys.add(key);
|
|
11118
10981
|
declaredKeyToReportNode.set(key, elementNode);
|
|
11119
10982
|
}
|
|
11120
|
-
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument
|
|
10983
|
+
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
|
|
11121
10984
|
const missingCaptureKeys = [];
|
|
11122
10985
|
for (const captureKey of captureKeys) {
|
|
11123
10986
|
let isCoveredByDeclared = false;
|
|
@@ -11203,7 +11066,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11203
11066
|
if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
|
|
11204
11067
|
if (outerFunctionCapturedNames.has(rootName)) continue;
|
|
11205
11068
|
const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
|
|
11206
|
-
if (
|
|
11069
|
+
if (EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) && isExtraEffectDepAllowed(reportNode, context.scopes)) continue;
|
|
11207
11070
|
if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
|
|
11208
11071
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
11209
11072
|
if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
|
|
@@ -22429,26 +22292,6 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
22429
22292
|
const importDeclaration = declarationNode.parent;
|
|
22430
22293
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22431
22294
|
}));
|
|
22432
|
-
const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.some((def) => {
|
|
22433
|
-
if (def.type !== "ImportBinding") return false;
|
|
22434
|
-
const declarationNode = def.node;
|
|
22435
|
-
if (!isNodeOfType(declarationNode, "ImportNamespaceSpecifier") && !isNodeOfType(declarationNode, "ImportDefaultSpecifier")) return false;
|
|
22436
|
-
const importDeclaration = declarationNode.parent;
|
|
22437
|
-
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22438
|
-
}));
|
|
22439
|
-
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
22440
|
-
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
22441
|
-
const callee = stripParenExpression(declarator.init.callee);
|
|
22442
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
22443
|
-
const reference = getRef(analysis, callee);
|
|
22444
|
-
if (!reference?.resolved) return callee.name === hookName;
|
|
22445
|
-
return isReactNamedImportReference(reference, hookName);
|
|
22446
|
-
}
|
|
22447
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
22448
|
-
const namespaceReference = getRef(analysis, callee.object);
|
|
22449
|
-
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
22450
|
-
return isReactNamespaceImportReference(namespaceReference);
|
|
22451
|
-
};
|
|
22452
22295
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
22453
22296
|
if (!node) return false;
|
|
22454
22297
|
if (isNodeOfType(node, "Identifier")) {
|
|
@@ -23503,20 +23346,6 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23503
23346
|
}
|
|
23504
23347
|
return evidence;
|
|
23505
23348
|
};
|
|
23506
|
-
const collectRenderValueEvidence = (analysis, expression, componentFunction, currentFilename) => {
|
|
23507
|
-
const evidence = collectValueEvidence(analysis, expression, {
|
|
23508
|
-
functionNode: componentFunction,
|
|
23509
|
-
invocation: null,
|
|
23510
|
-
isDeferred: false,
|
|
23511
|
-
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23512
|
-
substitutions: /* @__PURE__ */ new Map(),
|
|
23513
|
-
currentFilename
|
|
23514
|
-
}, 1);
|
|
23515
|
-
return {
|
|
23516
|
-
sourceReferences: evidence.sourceReferences,
|
|
23517
|
-
isExclusivelyRenderKnown: evidence.sourceReferences.size > 0 && !evidence.hasUnknownSource && !evidence.hasDeferredIntroducedValue && !evidence.readsExternalValue
|
|
23518
|
-
};
|
|
23519
|
-
};
|
|
23520
23349
|
const findStateSetterReference = (analysis, callExpression) => {
|
|
23521
23350
|
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
23522
23351
|
const callee = stripParenExpression(callExpression.callee);
|
|
@@ -26569,6 +26398,57 @@ const noDefaultProps = defineRule({
|
|
|
26569
26398
|
} })
|
|
26570
26399
|
});
|
|
26571
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
|
|
26572
26452
|
//#region src/plugin/utils/is-component-function.ts
|
|
26573
26453
|
const isFunctionAssignedToComponent = (functionNode) => {
|
|
26574
26454
|
let cursor = functionNode.parent ?? null;
|
|
@@ -26702,236 +26582,6 @@ const createComponentPropStackTracker = (callbacks) => {
|
|
|
26702
26582
|
};
|
|
26703
26583
|
};
|
|
26704
26584
|
//#endregion
|
|
26705
|
-
//#region src/plugin/rules/state-and-effects/utils/collect-render-state-write-facts.ts
|
|
26706
|
-
const findReferenceDeclarator = (reference) => {
|
|
26707
|
-
for (const definition of reference.resolved?.defs ?? []) {
|
|
26708
|
-
const definitionNode = definition.node;
|
|
26709
|
-
if (isNodeOfType(definitionNode, "VariableDeclarator")) return definitionNode;
|
|
26710
|
-
}
|
|
26711
|
-
return null;
|
|
26712
|
-
};
|
|
26713
|
-
const resolveSimpleRenderSource = (analysis, expression, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
26714
|
-
const node = stripParenExpression(expression);
|
|
26715
|
-
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26716
|
-
const reference = getRef(analysis, node);
|
|
26717
|
-
if (!reference?.resolved || visitedBindings.has(reference.resolved)) return null;
|
|
26718
|
-
if (isProp(analysis, reference)) {
|
|
26719
|
-
if (isWholePropsObjectReference(analysis, reference)) return null;
|
|
26720
|
-
return { bindingIdentity: reference.resolved };
|
|
26721
|
-
}
|
|
26722
|
-
if (isState(analysis, reference)) {
|
|
26723
|
-
const stateDeclarator = getUseStateDecl(analysis, reference);
|
|
26724
|
-
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || isExternallyDrivenState(analysis, reference)) return null;
|
|
26725
|
-
return { bindingIdentity: reference.resolved };
|
|
26726
|
-
}
|
|
26727
|
-
if (reference.resolved.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init)) return null;
|
|
26728
|
-
const declarator = findReferenceDeclarator(reference);
|
|
26729
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
26730
|
-
const nextVisitedBindings = new Set(visitedBindings);
|
|
26731
|
-
nextVisitedBindings.add(reference.resolved);
|
|
26732
|
-
return resolveSimpleRenderSource(analysis, declarator.init, nextVisitedBindings);
|
|
26733
|
-
};
|
|
26734
|
-
const doesEvidenceMatchSource = (evidence, source) => evidence.isExclusivelyRenderKnown && evidence.sourceReferences.size > 0 && [...evidence.sourceReferences].every((sourceReference) => sourceReference.resolved === source.bindingIdentity);
|
|
26735
|
-
const getDirectBranchStatements = (branch) => {
|
|
26736
|
-
if (isNodeOfType(branch, "BlockStatement")) return branch.body ?? [];
|
|
26737
|
-
return [branch];
|
|
26738
|
-
};
|
|
26739
|
-
const getDirectCallExpression = (statement) => {
|
|
26740
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26741
|
-
const expression = stripParenExpression(statement.expression);
|
|
26742
|
-
return isNodeOfType(expression, "CallExpression") ? expression : null;
|
|
26743
|
-
};
|
|
26744
|
-
const getDirectAssignmentExpression = (statement) => {
|
|
26745
|
-
if (!isNodeOfType(statement, "ExpressionStatement")) return null;
|
|
26746
|
-
const expression = stripParenExpression(statement.expression);
|
|
26747
|
-
return isNodeOfType(expression, "AssignmentExpression") && expression.operator === "=" ? expression : null;
|
|
26748
|
-
};
|
|
26749
|
-
const findDirectStateSetterReference = (analysis, callExpression) => {
|
|
26750
|
-
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
26751
|
-
const callee = stripParenExpression(callExpression.callee);
|
|
26752
|
-
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
26753
|
-
const reference = getRef(analysis, callee);
|
|
26754
|
-
return reference && isStateSetter(analysis, reference) ? reference : null;
|
|
26755
|
-
};
|
|
26756
|
-
const getStaticRefCurrentReference = (analysis, expression) => {
|
|
26757
|
-
const node = stripParenExpression(expression);
|
|
26758
|
-
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.object, "Identifier") || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
26759
|
-
const reference = getRef(analysis, node.object);
|
|
26760
|
-
if (!reference) return null;
|
|
26761
|
-
const declarator = findReferenceDeclarator(reference);
|
|
26762
|
-
return declarator && isGenuineReactHookDeclarator(analysis, declarator, "useRef") ? reference : null;
|
|
26763
|
-
};
|
|
26764
|
-
const getStateTracker = (analysis, expression) => {
|
|
26765
|
-
const node = stripParenExpression(expression);
|
|
26766
|
-
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26767
|
-
const reference = getRef(analysis, node);
|
|
26768
|
-
if (!reference || !isState(analysis, reference)) return null;
|
|
26769
|
-
const declarator = getUseStateDecl(analysis, reference);
|
|
26770
|
-
if (!declarator || !isGenuineReactHookDeclarator(analysis, declarator, "useState")) return null;
|
|
26771
|
-
return {
|
|
26772
|
-
kind: "state",
|
|
26773
|
-
declarator
|
|
26774
|
-
};
|
|
26775
|
-
};
|
|
26776
|
-
const getRefTracker = (analysis, expression) => {
|
|
26777
|
-
const reference = getStaticRefCurrentReference(analysis, expression);
|
|
26778
|
-
return reference ? {
|
|
26779
|
-
kind: "ref",
|
|
26780
|
-
reference
|
|
26781
|
-
} : null;
|
|
26782
|
-
};
|
|
26783
|
-
const getTrackerInitializer = (tracker) => {
|
|
26784
|
-
const declarator = tracker.kind === "state" ? tracker.declarator : findReferenceDeclarator(tracker.reference);
|
|
26785
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return null;
|
|
26786
|
-
const initializer = declarator.init.arguments?.[0];
|
|
26787
|
-
return initializer ? initializer : null;
|
|
26788
|
-
};
|
|
26789
|
-
const isTrackerSynchronized = (analysis, guard) => {
|
|
26790
|
-
for (const statement of guard.statements) {
|
|
26791
|
-
if (guard.tracker.kind === "state") {
|
|
26792
|
-
const callExpression = getDirectCallExpression(statement);
|
|
26793
|
-
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26794
|
-
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26795
|
-
if (!setterReference || getUseStateDecl(analysis, setterReference) !== guard.tracker.declarator || (callExpression.arguments ?? []).length !== 1) continue;
|
|
26796
|
-
const writtenValue = callExpression.arguments?.[0];
|
|
26797
|
-
if (writtenValue && resolveSimpleRenderSource(analysis, writtenValue)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26798
|
-
continue;
|
|
26799
|
-
}
|
|
26800
|
-
const assignmentExpression = getDirectAssignmentExpression(statement);
|
|
26801
|
-
if (!assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression")) continue;
|
|
26802
|
-
if (getStaticRefCurrentReference(analysis, assignmentExpression.left)?.resolved !== guard.tracker.reference.resolved) continue;
|
|
26803
|
-
if (resolveSimpleRenderSource(analysis, assignmentExpression.right)?.bindingIdentity === guard.source.bindingIdentity) return true;
|
|
26804
|
-
}
|
|
26805
|
-
return false;
|
|
26806
|
-
};
|
|
26807
|
-
const parseRenderTrackerGuard = (analysis, statement) => {
|
|
26808
|
-
if (!isNodeOfType(statement, "IfStatement") || statement.alternate || !isNodeOfType(statement.test, "BinaryExpression") || statement.test.operator !== "!==" || !isNodeOfType(statement.consequent, "BlockStatement")) return null;
|
|
26809
|
-
const left = statement.test.left;
|
|
26810
|
-
const right = statement.test.right;
|
|
26811
|
-
const candidates = [{
|
|
26812
|
-
sourceExpression: left,
|
|
26813
|
-
trackerExpression: right
|
|
26814
|
-
}, {
|
|
26815
|
-
sourceExpression: right,
|
|
26816
|
-
trackerExpression: left
|
|
26817
|
-
}];
|
|
26818
|
-
for (const candidate of candidates) {
|
|
26819
|
-
const source = resolveSimpleRenderSource(analysis, candidate.sourceExpression);
|
|
26820
|
-
if (!source) continue;
|
|
26821
|
-
const tracker = getStateTracker(analysis, candidate.trackerExpression) ?? getRefTracker(analysis, candidate.trackerExpression);
|
|
26822
|
-
if (!tracker) continue;
|
|
26823
|
-
const trackerInitializer = getTrackerInitializer(tracker);
|
|
26824
|
-
if (!trackerInitializer || resolveSimpleRenderSource(analysis, trackerInitializer)?.bindingIdentity !== source.bindingIdentity) continue;
|
|
26825
|
-
const guard = {
|
|
26826
|
-
source,
|
|
26827
|
-
tracker,
|
|
26828
|
-
statements: getDirectBranchStatements(statement.consequent)
|
|
26829
|
-
};
|
|
26830
|
-
return isTrackerSynchronized(analysis, guard) ? guard : null;
|
|
26831
|
-
}
|
|
26832
|
-
return null;
|
|
26833
|
-
};
|
|
26834
|
-
const isExclusiveDestinationSetterReference = (setterReference, callExpression) => {
|
|
26835
|
-
if (!setterReference.resolved || !isNodeOfType(callExpression, "CallExpression")) return false;
|
|
26836
|
-
const references = setterReference.resolved.references.filter((reference) => !reference.init);
|
|
26837
|
-
if (references.length !== 1) return false;
|
|
26838
|
-
return references[0].identifier === callExpression.callee;
|
|
26839
|
-
};
|
|
26840
|
-
const getStateInitializer = (stateDeclarator) => {
|
|
26841
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression")) return null;
|
|
26842
|
-
const initializer = stateDeclarator.init.arguments?.[0];
|
|
26843
|
-
return initializer ? initializer : null;
|
|
26844
|
-
};
|
|
26845
|
-
const collectRenderStateWriteFacts = (analysis, componentBody, currentFilename) => {
|
|
26846
|
-
if (!isNodeOfType(componentBody, "BlockStatement") || !componentBody.parent || !isFunctionLike$1(componentBody.parent)) return [];
|
|
26847
|
-
const componentFunction = componentBody.parent;
|
|
26848
|
-
const facts = [];
|
|
26849
|
-
for (const statement of componentBody.body ?? []) {
|
|
26850
|
-
const guard = parseRenderTrackerGuard(analysis, statement);
|
|
26851
|
-
if (!guard) continue;
|
|
26852
|
-
for (const branchStatement of guard.statements) {
|
|
26853
|
-
const callExpression = getDirectCallExpression(branchStatement);
|
|
26854
|
-
if (!callExpression || !isNodeOfType(callExpression, "CallExpression")) continue;
|
|
26855
|
-
const setterReference = findDirectStateSetterReference(analysis, callExpression);
|
|
26856
|
-
if (!setterReference || (callExpression.arguments ?? []).length !== 1 || !isExclusiveDestinationSetterReference(setterReference, callExpression)) continue;
|
|
26857
|
-
const stateDeclarator = getUseStateDecl(analysis, setterReference);
|
|
26858
|
-
if (!stateDeclarator || !isGenuineReactHookDeclarator(analysis, stateDeclarator, "useState") || guard.tracker.kind === "state" && stateDeclarator === guard.tracker.declarator) continue;
|
|
26859
|
-
const writtenValue = callExpression.arguments?.[0];
|
|
26860
|
-
const initializer = getStateInitializer(stateDeclarator);
|
|
26861
|
-
if (!writtenValue || !initializer || isFunctionLike$1(writtenValue) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, writtenValue, componentFunction, currentFilename), guard.source) || !doesEvidenceMatchSource(collectRenderValueEvidence(analysis, initializer, componentFunction, currentFilename), guard.source)) continue;
|
|
26862
|
-
facts.push({
|
|
26863
|
-
callExpression,
|
|
26864
|
-
stateDeclarator
|
|
26865
|
-
});
|
|
26866
|
-
}
|
|
26867
|
-
}
|
|
26868
|
-
return facts;
|
|
26869
|
-
};
|
|
26870
|
-
//#endregion
|
|
26871
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
26872
|
-
const getStateName$1 = (stateDeclarator) => {
|
|
26873
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
26874
|
-
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
26875
|
-
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
26876
|
-
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
26877
|
-
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
26878
|
-
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
26879
|
-
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
26880
|
-
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
26881
|
-
};
|
|
26882
|
-
const noDerivedState = defineRule({
|
|
26883
|
-
id: "no-derived-state",
|
|
26884
|
-
title: "Derived value copied into state",
|
|
26885
|
-
severity: "warn",
|
|
26886
|
-
tags: ["test-noise"],
|
|
26887
|
-
recommendation: "Work out the value while rendering (or with useMemo if it's expensive) instead of copying it into state and synchronizing it during render or through an effect. See https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state",
|
|
26888
|
-
create: (context) => {
|
|
26889
|
-
const reportStateWrite = (callExpression, stateDeclarator) => {
|
|
26890
|
-
const stateName = getStateName$1(stateDeclarator);
|
|
26891
|
-
context.report({
|
|
26892
|
-
node: callExpression,
|
|
26893
|
-
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
26894
|
-
});
|
|
26895
|
-
};
|
|
26896
|
-
return {
|
|
26897
|
-
...createComponentPropStackTracker({ onComponentEnter: (componentBody) => {
|
|
26898
|
-
if (!componentBody) return;
|
|
26899
|
-
const analysis = getProgramAnalysis(componentBody);
|
|
26900
|
-
if (!analysis) return;
|
|
26901
|
-
for (const fact of collectRenderStateWriteFacts(analysis, componentBody, context.filename)) reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26902
|
-
} }).visitors,
|
|
26903
|
-
CallExpression(node) {
|
|
26904
|
-
if (!isUseEffect(node)) return;
|
|
26905
|
-
const analysis = getProgramAnalysis(node);
|
|
26906
|
-
if (!analysis) return;
|
|
26907
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
26908
|
-
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
26909
|
-
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
26910
|
-
}
|
|
26911
|
-
}
|
|
26912
|
-
};
|
|
26913
|
-
}
|
|
26914
|
-
});
|
|
26915
|
-
//#endregion
|
|
26916
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
26917
|
-
const noDerivedStateEffect = defineRule({
|
|
26918
|
-
id: "no-derived-state-effect",
|
|
26919
|
-
title: "Derived state stored in an effect",
|
|
26920
|
-
severity: "warn",
|
|
26921
|
-
tags: ["test-noise"],
|
|
26922
|
-
recommendation: "Work out derived values while rendering: `const x = fn(dep)`. To reset a component's state when a prop changes, give it a key prop: `<Component key={prop} />`. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
26923
|
-
create: (context) => ({ CallExpression(node) {
|
|
26924
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
26925
|
-
const analysis = getProgramAnalysis(node);
|
|
26926
|
-
if (!analysis) return;
|
|
26927
|
-
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
26928
|
-
context.report({
|
|
26929
|
-
node,
|
|
26930
|
-
message: "You pay an extra render for state you can derive from other values."
|
|
26931
|
-
});
|
|
26932
|
-
} })
|
|
26933
|
-
});
|
|
26934
|
-
//#endregion
|
|
26935
26585
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
26936
26586
|
const isInitialOnlyPropName = (propName) => {
|
|
26937
26587
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -30295,26 +29945,28 @@ const noGiantComponent = defineRule({
|
|
|
30295
29945
|
const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
|
|
30296
29946
|
return lineCount > 300 ? lineCount : null;
|
|
30297
29947
|
};
|
|
30298
|
-
const reportOversizedComponent = (nameNode, componentName) => {
|
|
29948
|
+
const reportOversizedComponent = (nameNode, componentName, lineCount) => {
|
|
30299
29949
|
context.report({
|
|
30300
29950
|
node: nameNode,
|
|
30301
|
-
message: `Component "${componentName}" is
|
|
29951
|
+
message: `Component "${componentName}" is ${lineCount} lines long, which is hard to read & change. Split it into a few smaller components.`
|
|
30302
29952
|
});
|
|
30303
29953
|
};
|
|
30304
29954
|
return {
|
|
30305
29955
|
FunctionDeclaration(node) {
|
|
30306
29956
|
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
30307
|
-
|
|
29957
|
+
const lineCount = getOversizedComponentLineCount(node);
|
|
29958
|
+
if (lineCount === null) return;
|
|
30308
29959
|
if (!functionContainsReactRenderOutput(node, context.scopes)) return;
|
|
30309
|
-
reportOversizedComponent(node.id, node.id.name);
|
|
29960
|
+
reportOversizedComponent(node.id, node.id.name, lineCount);
|
|
30310
29961
|
},
|
|
30311
29962
|
VariableDeclarator(node) {
|
|
30312
29963
|
if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
|
|
30313
29964
|
const functionNode = unwrapReactHocFunction(node.init);
|
|
30314
29965
|
if (!functionNode) return;
|
|
30315
|
-
|
|
29966
|
+
const lineCount = getOversizedComponentLineCount(functionNode);
|
|
29967
|
+
if (lineCount === null) return;
|
|
30316
29968
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
30317
|
-
reportOversizedComponent(node.id, node.id.name);
|
|
29969
|
+
reportOversizedComponent(node.id, node.id.name, lineCount);
|
|
30318
29970
|
}
|
|
30319
29971
|
};
|
|
30320
29972
|
}
|
|
@@ -30951,131 +30603,6 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
30951
30603
|
} })
|
|
30952
30604
|
});
|
|
30953
30605
|
//#endregion
|
|
30954
|
-
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
30955
|
-
const TIMER_FUNCTION_NAMES = new Set([
|
|
30956
|
-
"cancelAnimationFrame",
|
|
30957
|
-
"clearInterval",
|
|
30958
|
-
"clearTimeout",
|
|
30959
|
-
"queueMicrotask",
|
|
30960
|
-
"requestAnimationFrame",
|
|
30961
|
-
"setInterval",
|
|
30962
|
-
"setTimeout"
|
|
30963
|
-
]);
|
|
30964
|
-
const STORAGE_MUTATION_METHOD_NAMES = new Set([
|
|
30965
|
-
"clear",
|
|
30966
|
-
"removeItem",
|
|
30967
|
-
"setItem"
|
|
30968
|
-
]);
|
|
30969
|
-
const STORAGE_RECEIVER_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
30970
|
-
const EXTERNAL_READ_METHOD_NAMES = new Set(["getBoundingClientRect", "getClientRects"]);
|
|
30971
|
-
const NOTIFICATION_RECEIVER_NAMES = new Set([
|
|
30972
|
-
"message",
|
|
30973
|
-
"notification",
|
|
30974
|
-
"toast"
|
|
30975
|
-
]);
|
|
30976
|
-
const NOTIFICATION_METHOD_NAMES = new Set([
|
|
30977
|
-
"error",
|
|
30978
|
-
"info",
|
|
30979
|
-
"loading",
|
|
30980
|
-
"open",
|
|
30981
|
-
"show",
|
|
30982
|
-
"success",
|
|
30983
|
-
"warning"
|
|
30984
|
-
]);
|
|
30985
|
-
const getMemberCall = (node) => {
|
|
30986
|
-
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
30987
|
-
if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
|
|
30988
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
30989
|
-
return {
|
|
30990
|
-
methodName: node.callee.property.name,
|
|
30991
|
-
receiver: stripParenExpression(node.callee.object)
|
|
30992
|
-
};
|
|
30993
|
-
};
|
|
30994
|
-
const isNotificationReceiver = (receiver, scopes) => {
|
|
30995
|
-
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
30996
|
-
if (!NOTIFICATION_RECEIVER_NAMES.has(receiver.name)) return false;
|
|
30997
|
-
const symbol = scopes.symbolFor(receiver);
|
|
30998
|
-
if (symbol?.kind === "import") return true;
|
|
30999
|
-
if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
|
|
31000
|
-
const callee = symbol.initializer.callee;
|
|
31001
|
-
return isNodeOfType(callee, "Identifier") && /^use(?:Message|Notification|Toast)$/.test(callee.name);
|
|
31002
|
-
};
|
|
31003
|
-
const getKnownImpureCall = (callExpression, scopes) => {
|
|
31004
|
-
if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
|
|
31005
|
-
const memberCall = getMemberCall(callExpression);
|
|
31006
|
-
if (!memberCall) return null;
|
|
31007
|
-
const { methodName, receiver } = memberCall;
|
|
31008
|
-
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && STORAGE_RECEIVER_NAMES.has(receiver.name) && scopes.isGlobalReference(receiver)) return `${receiver.name}.${methodName}()`;
|
|
31009
|
-
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
|
|
31010
|
-
if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNodeOfType(receiver, "Identifier") && isNotificationReceiver(receiver, scopes)) return `${receiver.name}.${methodName}()`;
|
|
31011
|
-
return null;
|
|
31012
|
-
};
|
|
31013
|
-
const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
|
|
31014
|
-
let rootIdentifier = null;
|
|
31015
|
-
if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
|
|
31016
|
-
else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
|
|
31017
|
-
if (!rootIdentifier) return null;
|
|
31018
|
-
const updaterScope = scopes.ownScopeFor(updater);
|
|
31019
|
-
if (!updaterScope) return null;
|
|
31020
|
-
const symbol = scopes.symbolFor(rootIdentifier);
|
|
31021
|
-
if (!symbol) return `the external value "${rootIdentifier.name}"`;
|
|
31022
|
-
if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
|
|
31023
|
-
return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
|
|
31024
|
-
};
|
|
31025
|
-
const findImpureUpdaterOperation = (updater, scopes) => {
|
|
31026
|
-
const analysis = getProgramAnalysis(updater);
|
|
31027
|
-
let operation = null;
|
|
31028
|
-
walkAst(updater, (child) => {
|
|
31029
|
-
if (operation) return false;
|
|
31030
|
-
if (child !== updater && isFunctionLike$1(child) && !executesDuringRender(child, scopes)) return false;
|
|
31031
|
-
if (isNodeOfType(child, "CallExpression")) {
|
|
31032
|
-
if (isNodeOfType(child.callee, "Identifier") && analysis) {
|
|
31033
|
-
const calleeReference = getRef(analysis, child.callee);
|
|
31034
|
-
if (calleeReference && isStateSetterCall(analysis, calleeReference)) {
|
|
31035
|
-
operation = `the nested state update "${child.callee.name}()"`;
|
|
31036
|
-
return false;
|
|
31037
|
-
}
|
|
31038
|
-
}
|
|
31039
|
-
const impureCall = getKnownImpureCall(child, scopes);
|
|
31040
|
-
if (impureCall) {
|
|
31041
|
-
operation = impureCall;
|
|
31042
|
-
return false;
|
|
31043
|
-
}
|
|
31044
|
-
}
|
|
31045
|
-
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
31046
|
-
operation = getExternalAssignmentDescription(child.left, updater, scopes);
|
|
31047
|
-
if (operation) return false;
|
|
31048
|
-
}
|
|
31049
|
-
if (isNodeOfType(child, "UpdateExpression")) {
|
|
31050
|
-
operation = getExternalAssignmentDescription(child.argument, updater, scopes);
|
|
31051
|
-
if (operation) return false;
|
|
31052
|
-
}
|
|
31053
|
-
});
|
|
31054
|
-
return operation;
|
|
31055
|
-
};
|
|
31056
|
-
const noImpureStateUpdater = defineRule({
|
|
31057
|
-
id: "no-impure-state-updater",
|
|
31058
|
-
title: "State updater has side effects",
|
|
31059
|
-
severity: "error",
|
|
31060
|
-
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.",
|
|
31061
|
-
create: (context) => ({ CallExpression(node) {
|
|
31062
|
-
const updater = node.arguments?.[0];
|
|
31063
|
-
if (!updater || !isFunctionLike$1(updater)) return;
|
|
31064
|
-
const analysis = getProgramAnalysis(node);
|
|
31065
|
-
if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
|
|
31066
|
-
const calleeReference = getRef(analysis, node.callee);
|
|
31067
|
-
if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
|
|
31068
|
-
const stateDeclarator = getUseStateDecl(analysis, calleeReference);
|
|
31069
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
|
|
31070
|
-
const operation = findImpureUpdaterOperation(updater, context.scopes);
|
|
31071
|
-
if (!operation) return;
|
|
31072
|
-
context.report({
|
|
31073
|
-
node: updater,
|
|
31074
|
-
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.`
|
|
31075
|
-
});
|
|
31076
|
-
} })
|
|
31077
|
-
});
|
|
31078
|
-
//#endregion
|
|
31079
30606
|
//#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
|
|
31080
30607
|
const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
|
|
31081
30608
|
const REACT_USE_REF_OPTIONS = {
|
|
@@ -34244,166 +33771,6 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
34244
33771
|
return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
|
|
34245
33772
|
}));
|
|
34246
33773
|
};
|
|
34247
|
-
const getDeclarationKind = (declarator) => {
|
|
34248
|
-
const declaration = declarator.parent;
|
|
34249
|
-
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
34250
|
-
};
|
|
34251
|
-
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
34252
|
-
const getDestructuredPropertyName = (bindingIdentifier) => {
|
|
34253
|
-
const property = bindingIdentifier.parent;
|
|
34254
|
-
if (!property || !isNodeOfType(property, "Property")) return null;
|
|
34255
|
-
if (property.computed) return isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" ? String(property.key.value) : null;
|
|
34256
|
-
return isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
34257
|
-
};
|
|
34258
|
-
const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
|
|
34259
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
34260
|
-
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
34261
|
-
const callbackReference = getRef(analysis, unwrappedExpression);
|
|
34262
|
-
const callbackVariable = callbackReference?.resolved;
|
|
34263
|
-
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable)) return null;
|
|
34264
|
-
if (isProp(analysis, callbackReference)) {
|
|
34265
|
-
if (isWholePropsObjectReference(analysis, callbackReference)) return null;
|
|
34266
|
-
const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
|
|
34267
|
-
return (bindingIdentifier && getDestructuredPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
|
|
34268
|
-
}
|
|
34269
|
-
if (hasMutableBindingWrite(callbackReference)) return null;
|
|
34270
|
-
const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
34271
|
-
if (definitions.length !== 1) return null;
|
|
34272
|
-
const declarator = definitions[0];
|
|
34273
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || getDeclarationKind(declarator) !== "const" || !declarator.init) return null;
|
|
34274
|
-
visitedVariables.add(callbackVariable);
|
|
34275
|
-
if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
34276
|
-
const bindingIdentifier = callbackVariable.defs[0]?.name;
|
|
34277
|
-
const propertyName = bindingIdentifier ? getDestructuredPropertyName(bindingIdentifier) : null;
|
|
34278
|
-
const propsReference = getDownstreamRefs(analysis, declarator.init).find((candidateReference) => isWholePropsObjectReference(analysis, candidateReference));
|
|
34279
|
-
return propertyName && propsReference ? propertyName : null;
|
|
34280
|
-
}
|
|
34281
|
-
if (!isNodeOfType(declarator.id, "Identifier")) return null;
|
|
34282
|
-
return getParentCallbackPropName(analysis, declarator.init, visitedVariables);
|
|
34283
|
-
}
|
|
34284
|
-
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
|
|
34285
|
-
const callbackName = getStaticMemberPropertyName(unwrappedExpression);
|
|
34286
|
-
if (!callbackName) return null;
|
|
34287
|
-
const receiver = stripParenExpression(unwrappedExpression.object);
|
|
34288
|
-
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
34289
|
-
const receiverReference = getRef(analysis, receiver);
|
|
34290
|
-
if (!receiverReference || !isWholePropsObjectReference(analysis, receiverReference)) return null;
|
|
34291
|
-
return callbackName;
|
|
34292
|
-
};
|
|
34293
|
-
const isNullishRefInitializer = (initializer) => {
|
|
34294
|
-
if (!initializer) return true;
|
|
34295
|
-
const unwrappedInitializer = stripParenExpression(initializer);
|
|
34296
|
-
if (isNodeOfType(unwrappedInitializer, "Literal")) return unwrappedInitializer.value === null;
|
|
34297
|
-
if (!isNodeOfType(unwrappedInitializer, "Identifier")) return false;
|
|
34298
|
-
return unwrappedInitializer.name === "undefined";
|
|
34299
|
-
};
|
|
34300
|
-
const getDirectComponentBodyStatement = (node, componentBody) => {
|
|
34301
|
-
let current = node;
|
|
34302
|
-
while (current?.parent && current.parent !== componentBody) current = current.parent;
|
|
34303
|
-
return current?.parent === componentBody ? current : null;
|
|
34304
|
-
};
|
|
34305
|
-
const getVariableForDeclarator = (analysis, declarator) => {
|
|
34306
|
-
for (const scope of analysis.scopeManager.scopes) {
|
|
34307
|
-
const variable = scope.variables.find((candidateVariable) => candidateVariable.defs.some((definition) => definition.node === declarator));
|
|
34308
|
-
if (variable) return variable;
|
|
34309
|
-
}
|
|
34310
|
-
return null;
|
|
34311
|
-
};
|
|
34312
|
-
const getRefMember = (identifier) => {
|
|
34313
|
-
const receiver = findTransparentExpressionRoot(identifier);
|
|
34314
|
-
const member = receiver.parent;
|
|
34315
|
-
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== receiver) return null;
|
|
34316
|
-
return member;
|
|
34317
|
-
};
|
|
34318
|
-
const getRefMemberAssignment = (identifier) => {
|
|
34319
|
-
const member = getRefMember(identifier);
|
|
34320
|
-
if (!member) return null;
|
|
34321
|
-
const memberRoot = findTransparentExpressionRoot(member);
|
|
34322
|
-
const assignment = memberRoot.parent;
|
|
34323
|
-
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== memberRoot) return null;
|
|
34324
|
-
return assignment;
|
|
34325
|
-
};
|
|
34326
|
-
const getRefAliasDeclarator = (identifier) => {
|
|
34327
|
-
const initializer = findTransparentExpressionRoot(identifier);
|
|
34328
|
-
const declarator = initializer.parent;
|
|
34329
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || declarator.init !== initializer || !isNodeOfType(declarator.id, "Identifier") || getDeclarationKind(declarator) !== "const") return null;
|
|
34330
|
-
return declarator;
|
|
34331
|
-
};
|
|
34332
|
-
const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
34333
|
-
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
34334
|
-
const receiverReference = getRef(analysis, receiver);
|
|
34335
|
-
if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
|
|
34336
|
-
const variables = /* @__PURE__ */ new Set();
|
|
34337
|
-
let currentVariable = receiverReference.resolved;
|
|
34338
|
-
let refCall = null;
|
|
34339
|
-
while (currentVariable && !variables.has(currentVariable)) {
|
|
34340
|
-
variables.add(currentVariable);
|
|
34341
|
-
const definitions = currentVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
34342
|
-
if (definitions.length !== 1) return null;
|
|
34343
|
-
const declarator = definitions[0];
|
|
34344
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) return null;
|
|
34345
|
-
if (isNodeOfType(declarator.init, "CallExpression") && isReactUseRefCall(declarator.init)) {
|
|
34346
|
-
refCall = declarator.init;
|
|
34347
|
-
break;
|
|
34348
|
-
}
|
|
34349
|
-
if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
|
|
34350
|
-
const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
|
|
34351
|
-
if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
|
|
34352
|
-
currentVariable = upstreamReference.resolved;
|
|
34353
|
-
}
|
|
34354
|
-
if (!refCall) return null;
|
|
34355
|
-
const pendingVariables = [...variables];
|
|
34356
|
-
while (pendingVariables.length > 0) {
|
|
34357
|
-
const variable = pendingVariables.pop();
|
|
34358
|
-
if (!variable) continue;
|
|
34359
|
-
for (const candidateReference of variable.references) {
|
|
34360
|
-
const aliasDeclarator = getRefAliasDeclarator(candidateReference.identifier);
|
|
34361
|
-
if (!aliasDeclarator) continue;
|
|
34362
|
-
const aliasVariable = getVariableForDeclarator(analysis, aliasDeclarator);
|
|
34363
|
-
if (!aliasVariable || variables.has(aliasVariable)) continue;
|
|
34364
|
-
if (aliasVariable.references.some((aliasReference) => aliasReference.isWrite() && !aliasReference.init)) return null;
|
|
34365
|
-
variables.add(aliasVariable);
|
|
34366
|
-
pendingVariables.push(aliasVariable);
|
|
34367
|
-
}
|
|
34368
|
-
}
|
|
34369
|
-
return {
|
|
34370
|
-
refCall,
|
|
34371
|
-
variables
|
|
34372
|
-
};
|
|
34373
|
-
};
|
|
34374
|
-
const getCallbackRefProvenance = (analysis, effectCall, callExpression, isReactUseRefCall) => {
|
|
34375
|
-
const callee = stripParenExpression(callExpression.callee);
|
|
34376
|
-
if (!isNodeOfType(callee, "MemberExpression") || getStaticMemberPropertyName(callee) !== "current") return null;
|
|
34377
|
-
const bindingProvenance = getRefBindingProvenance(analysis, stripParenExpression(callee.object), isReactUseRefCall);
|
|
34378
|
-
if (!bindingProvenance) return null;
|
|
34379
|
-
const { refCall, variables } = bindingProvenance;
|
|
34380
|
-
const componentFunction = findEnclosingFunction(effectCall);
|
|
34381
|
-
if (!componentFunction || !isFunctionLike$1(componentFunction) || !isComponentFunction$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
34382
|
-
if (!getDirectComponentBodyStatement(effectCall, componentFunction.body)) return null;
|
|
34383
|
-
const callbackPropNames = /* @__PURE__ */ new Set();
|
|
34384
|
-
const initializer = refCall.arguments?.[0];
|
|
34385
|
-
const initializerCallbackName = initializer ? getParentCallbackPropName(analysis, initializer) : null;
|
|
34386
|
-
if (initializerCallbackName) callbackPropNames.add(initializerCallbackName);
|
|
34387
|
-
else if (!isNullishRefInitializer(initializer)) return null;
|
|
34388
|
-
for (const variable of variables) for (const candidateReference of variable.references) {
|
|
34389
|
-
if (candidateReference.init) continue;
|
|
34390
|
-
if (candidateReference.isWrite()) return null;
|
|
34391
|
-
const identifier = candidateReference.identifier;
|
|
34392
|
-
if (getRefAliasDeclarator(identifier)) continue;
|
|
34393
|
-
const member = getRefMember(identifier);
|
|
34394
|
-
if (!member || getStaticMemberPropertyName(member) !== "current") return null;
|
|
34395
|
-
const memberParent = findTransparentExpressionRoot(member).parent;
|
|
34396
|
-
if (memberParent && (isNodeOfType(memberParent, "UpdateExpression") || isNodeOfType(memberParent, "UnaryExpression") && memberParent.operator === "delete")) return null;
|
|
34397
|
-
const assignment = getRefMemberAssignment(identifier);
|
|
34398
|
-
if (!assignment) continue;
|
|
34399
|
-
const assignmentStatement = findTransparentExpressionRoot(assignment).parent;
|
|
34400
|
-
if (assignment.operator !== "=" || !assignmentStatement || !isNodeOfType(assignmentStatement, "ExpressionStatement") || assignmentStatement.parent !== componentFunction.body) return null;
|
|
34401
|
-
const assignedCallbackName = getParentCallbackPropName(analysis, assignment.right);
|
|
34402
|
-
if (!assignedCallbackName) return null;
|
|
34403
|
-
callbackPropNames.add(assignedCallbackName);
|
|
34404
|
-
}
|
|
34405
|
-
return callbackPropNames.size > 0 ? { callbackPropNames } : null;
|
|
34406
|
-
};
|
|
34407
33774
|
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
34408
33775
|
const node = def.node;
|
|
34409
33776
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
@@ -34459,79 +33826,71 @@ const noPassDataToParent = defineRule({
|
|
|
34459
33826
|
severity: "warn",
|
|
34460
33827
|
tags: ["test-noise"],
|
|
34461
33828
|
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",
|
|
34462
|
-
create: (context) => {
|
|
34463
|
-
|
|
34464
|
-
|
|
34465
|
-
|
|
34466
|
-
|
|
34467
|
-
|
|
34468
|
-
|
|
34469
|
-
|
|
34470
|
-
|
|
34471
|
-
|
|
34472
|
-
const
|
|
34473
|
-
if (!
|
|
34474
|
-
|
|
34475
|
-
if (!effectFn)
|
|
34476
|
-
|
|
34477
|
-
|
|
34478
|
-
|
|
34479
|
-
|
|
34480
|
-
if (
|
|
34481
|
-
|
|
34482
|
-
|
|
34483
|
-
|
|
34484
|
-
|
|
34485
|
-
|
|
34486
|
-
|
|
34487
|
-
|
|
34488
|
-
|
|
34489
|
-
|
|
34490
|
-
|
|
34491
|
-
|
|
34492
|
-
|
|
34493
|
-
|
|
34494
|
-
|
|
34495
|
-
|
|
34496
|
-
|
|
34497
|
-
|
|
34498
|
-
|
|
34499
|
-
|
|
34500
|
-
|
|
34501
|
-
|
|
34502
|
-
|
|
34503
|
-
|
|
34504
|
-
|
|
34505
|
-
|
|
34506
|
-
|
|
34507
|
-
|
|
34508
|
-
|
|
34509
|
-
|
|
34510
|
-
|
|
34511
|
-
|
|
34512
|
-
|
|
34513
|
-
if (
|
|
34514
|
-
if (
|
|
34515
|
-
|
|
34516
|
-
|
|
34517
|
-
|
|
34518
|
-
|
|
34519
|
-
|
|
34520
|
-
|
|
34521
|
-
|
|
34522
|
-
|
|
34523
|
-
|
|
34524
|
-
|
|
34525
|
-
|
|
34526
|
-
|
|
34527
|
-
})) continue;
|
|
34528
|
-
context.report({
|
|
34529
|
-
node: callExpr,
|
|
34530
|
-
message: "Handing data back to a parent from a useEffect costs your users an extra render."
|
|
34531
|
-
});
|
|
34532
|
-
}
|
|
34533
|
-
} };
|
|
34534
|
-
}
|
|
33829
|
+
create: (context) => ({ CallExpression(node) {
|
|
33830
|
+
if (!isUseEffect(node)) return;
|
|
33831
|
+
const analysis = getProgramAnalysis(node);
|
|
33832
|
+
if (!analysis) return;
|
|
33833
|
+
if (hasCleanup(analysis, node)) return;
|
|
33834
|
+
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
33835
|
+
if (!effectFnRefs) return;
|
|
33836
|
+
const effectFn = getEffectFn(analysis, node);
|
|
33837
|
+
if (!effectFn) return;
|
|
33838
|
+
for (const ref of effectFnRefs) {
|
|
33839
|
+
const callExpr = getCallExpr(ref);
|
|
33840
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
33841
|
+
if (isRefCall(analysis, ref)) continue;
|
|
33842
|
+
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
33843
|
+
const calleeNode = unwrapChainExpression$1(callExpr.callee);
|
|
33844
|
+
const identifier = ref.identifier;
|
|
33845
|
+
if (calleeNode === identifier) {
|
|
33846
|
+
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
33847
|
+
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
33848
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
33849
|
+
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
33850
|
+
if (isCustomHookParameter(ref)) continue;
|
|
33851
|
+
} else continue;
|
|
33852
|
+
const methodName = getCallMethodName(calleeNode);
|
|
33853
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
33854
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
33855
|
+
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
33856
|
+
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
33857
|
+
const calleeName = isNodeOfType(identifier, "Identifier") ? identifier.name : methodName;
|
|
33858
|
+
const isSetterNamedCallee = Boolean(calleeName && SETTER_NAMED_PROP_PATTERN.test(calleeName));
|
|
33859
|
+
const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
|
|
33860
|
+
const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
|
|
33861
|
+
if (isFunctionLike$1(argument)) {
|
|
33862
|
+
if (!isSetterNamedCallee) return [];
|
|
33863
|
+
return getFunctionalUpdaterDataRefs(analysis, argument);
|
|
33864
|
+
}
|
|
33865
|
+
if (isHandlerBagArgument(analysis, argument)) return [];
|
|
33866
|
+
if (isParentWiredHookResultArgument(analysis, argument)) return [];
|
|
33867
|
+
if (isNodeOfType(argument, "Identifier")) {
|
|
33868
|
+
const argumentRef = getRef(analysis, argument);
|
|
33869
|
+
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
33870
|
+
}
|
|
33871
|
+
return getDownstreamRefs(analysis, argument);
|
|
33872
|
+
}).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
33873
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
33874
|
+
if (!argsUpstreamRefs.some((argRef) => {
|
|
33875
|
+
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
33876
|
+
if (isProp(analysis, argRef)) return false;
|
|
33877
|
+
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
33878
|
+
if (isRefCurrent(argRef)) return false;
|
|
33879
|
+
if (isConstant(argRef)) return false;
|
|
33880
|
+
if (isParentWiredHookResultRef(analysis, argRef)) return false;
|
|
33881
|
+
if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
|
|
33882
|
+
if (resolvesToFunctionBinding(argRef)) return false;
|
|
33883
|
+
const argIdentifier = argRef.identifier;
|
|
33884
|
+
if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
|
|
33885
|
+
if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
|
|
33886
|
+
return true;
|
|
33887
|
+
})) continue;
|
|
33888
|
+
context.report({
|
|
33889
|
+
node: callExpr,
|
|
33890
|
+
message: "Handing data back to a parent from a useEffect costs your users an extra render."
|
|
33891
|
+
});
|
|
33892
|
+
}
|
|
33893
|
+
} })
|
|
34535
33894
|
});
|
|
34536
33895
|
//#endregion
|
|
34537
33896
|
//#region src/plugin/utils/is-call-result-consumed-as-argument.ts
|
|
@@ -35085,66 +34444,6 @@ const noPropCallbackInEffect = defineRule({
|
|
|
35085
34444
|
}
|
|
35086
34445
|
});
|
|
35087
34446
|
//#endregion
|
|
35088
|
-
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-render.ts
|
|
35089
|
-
const isPreservedThroughConciseArrow = (callExpression, scopes) => {
|
|
35090
|
-
let node = callExpression;
|
|
35091
|
-
let parent = node.parent;
|
|
35092
|
-
while (parent) {
|
|
35093
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
35094
|
-
node = parent;
|
|
35095
|
-
parent = node.parent;
|
|
35096
|
-
continue;
|
|
35097
|
-
}
|
|
35098
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
35099
|
-
node = parent;
|
|
35100
|
-
parent = node.parent;
|
|
35101
|
-
continue;
|
|
35102
|
-
}
|
|
35103
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
35104
|
-
node = parent;
|
|
35105
|
-
parent = node.parent;
|
|
35106
|
-
continue;
|
|
35107
|
-
}
|
|
35108
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
35109
|
-
const expressions = parent.expressions ?? [];
|
|
35110
|
-
if (expressions[expressions.length - 1] !== node) return false;
|
|
35111
|
-
node = parent;
|
|
35112
|
-
parent = node.parent;
|
|
35113
|
-
continue;
|
|
35114
|
-
}
|
|
35115
|
-
if (!isNodeOfType(parent, "ArrowFunctionExpression") || parent.body !== node) return !isResultDiscardedCall(node);
|
|
35116
|
-
const invocation = parent.parent;
|
|
35117
|
-
if (!isNodeOfType(invocation, "CallExpression") || !executesDuringRender(parent, scopes)) return true;
|
|
35118
|
-
if (invocation.arguments?.[0] === parent || invocation.arguments?.[1] === parent) {
|
|
35119
|
-
const callee = stripParenExpression(invocation.callee);
|
|
35120
|
-
return !(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "forEach" && invocation.arguments[0] === parent);
|
|
35121
|
-
}
|
|
35122
|
-
node = invocation;
|
|
35123
|
-
parent = node.parent;
|
|
35124
|
-
}
|
|
35125
|
-
return false;
|
|
35126
|
-
};
|
|
35127
|
-
const noPropCallbackInRender = defineRule({
|
|
35128
|
-
id: "no-prop-callback-in-render",
|
|
35129
|
-
title: "Prop callback invoked during render",
|
|
35130
|
-
severity: "error",
|
|
35131
|
-
recommendation: "Invoke the callback from the event or asynchronous operation that produced the value, or from an effect when synchronizing with an external system. Render must stay pure because React can replay or discard it.",
|
|
35132
|
-
create: (context) => ({ CallExpression(node) {
|
|
35133
|
-
if (!isResultDiscardedCall(node)) return;
|
|
35134
|
-
if (isPreservedThroughConciseArrow(node, context.scopes)) return;
|
|
35135
|
-
if (!findRenderPhaseComponentOrHook(node, context.scopes)) return;
|
|
35136
|
-
const analysis = getProgramAnalysis(node);
|
|
35137
|
-
if (!analysis) return;
|
|
35138
|
-
const callee = stripParenExpression(node.callee);
|
|
35139
|
-
if (isFunctionLike$1(callee)) return;
|
|
35140
|
-
if (!getDownstreamRefs(analysis, callee).some((reference) => isPropCallbackInvocationRef(analysis, reference))) return;
|
|
35141
|
-
context.report({
|
|
35142
|
-
node,
|
|
35143
|
-
message: "This prop callback runs during render. React can replay or discard render work, so the callback can fire more than once or for UI that never commits."
|
|
35144
|
-
});
|
|
35145
|
-
} })
|
|
35146
|
-
});
|
|
35147
|
-
//#endregion
|
|
35148
34447
|
//#region src/plugin/rules/architecture/no-prop-types.ts
|
|
35149
34448
|
const PROP_TYPES_PROPERTY = "propTypes";
|
|
35150
34449
|
const isPropTypesKey = (key, computed) => {
|
|
@@ -35839,63 +35138,6 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
35839
35138
|
}
|
|
35840
35139
|
});
|
|
35841
35140
|
//#endregion
|
|
35842
|
-
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
35843
|
-
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
35844
|
-
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
35845
|
-
if (!isNodeOfType(memberExpression, "MemberExpression") || memberExpression.computed || !isNodeOfType(memberExpression.property, "Identifier") || memberExpression.property.name !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
35846
|
-
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
35847
|
-
if (!symbol?.initializer) return null;
|
|
35848
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
35849
|
-
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
35850
|
-
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
35851
|
-
};
|
|
35852
|
-
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
35853
|
-
if (!isNodeOfType(node, "MemberExpression") || node.computed || !isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return false;
|
|
35854
|
-
const receiver = stripParenExpression(node.object);
|
|
35855
|
-
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
35856
|
-
};
|
|
35857
|
-
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
35858
|
-
if (assignmentExpression.operator !== "=" || !isNodeOfType(assignmentExpression.right, "NewExpression")) return false;
|
|
35859
|
-
let descendant = assignmentExpression;
|
|
35860
|
-
let ancestor = descendant.parent;
|
|
35861
|
-
while (ancestor) {
|
|
35862
|
-
if (isNodeOfType(ancestor, "IfStatement") && ancestor.consequent === descendant && isNodeOfType(ancestor.test, "BinaryExpression") && (ancestor.test.operator === "===" || ancestor.test.operator === "==")) {
|
|
35863
|
-
const { left, right } = ancestor.test;
|
|
35864
|
-
if (isSameRefCurrentMember(left, refSymbol, scopes) && isNodeOfType(right, "Literal") && right.value === null || isSameRefCurrentMember(right, refSymbol, scopes) && isNodeOfType(left, "Literal") && left.value === null) return true;
|
|
35865
|
-
}
|
|
35866
|
-
descendant = ancestor;
|
|
35867
|
-
ancestor = descendant.parent;
|
|
35868
|
-
}
|
|
35869
|
-
return false;
|
|
35870
|
-
};
|
|
35871
|
-
const noRefCurrentInRender = defineRule({
|
|
35872
|
-
id: "no-ref-current-in-render",
|
|
35873
|
-
title: "Ref mutated during render",
|
|
35874
|
-
severity: "error",
|
|
35875
|
-
recommendation: "Move ref writes into an event handler or effect. Render must stay pure because React can replay or discard it. The predictable null-guarded lazy initialization pattern remains supported.",
|
|
35876
|
-
create: (context) => {
|
|
35877
|
-
const report = (memberExpression) => {
|
|
35878
|
-
if (!resolveReactRefSymbol(memberExpression, context.scopes)) return;
|
|
35879
|
-
if (!findRenderPhaseComponentOrHook(memberExpression, context.scopes)) return;
|
|
35880
|
-
context.report({
|
|
35881
|
-
node: memberExpression,
|
|
35882
|
-
message: "This ref is mutated during render. React can replay or discard render work, so the mutation can leak from UI that never commits."
|
|
35883
|
-
});
|
|
35884
|
-
};
|
|
35885
|
-
return {
|
|
35886
|
-
AssignmentExpression(node) {
|
|
35887
|
-
const refSymbol = resolveReactRefSymbol(node.left, context.scopes);
|
|
35888
|
-
if (!refSymbol) return;
|
|
35889
|
-
if (isDocumentedLazyInitialization(node, refSymbol, context.scopes)) return;
|
|
35890
|
-
report(node.left);
|
|
35891
|
-
},
|
|
35892
|
-
UpdateExpression(node) {
|
|
35893
|
-
report(node.argument);
|
|
35894
|
-
}
|
|
35895
|
-
};
|
|
35896
|
-
}
|
|
35897
|
-
});
|
|
35898
|
-
//#endregion
|
|
35899
35141
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
35900
35142
|
const isInsideComponentContext = (node) => {
|
|
35901
35143
|
let cursor = node.parent;
|
|
@@ -56858,18 +56100,6 @@ const reactDoctorRules = [
|
|
|
56858
56100
|
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
56859
56101
|
}
|
|
56860
56102
|
},
|
|
56861
|
-
{
|
|
56862
|
-
key: "react-doctor/no-impure-state-updater",
|
|
56863
|
-
id: "no-impure-state-updater",
|
|
56864
|
-
source: "react-doctor",
|
|
56865
|
-
originallyExternal: false,
|
|
56866
|
-
rule: {
|
|
56867
|
-
...noImpureStateUpdater,
|
|
56868
|
-
framework: "global",
|
|
56869
|
-
category: "Bugs",
|
|
56870
|
-
requires: [...new Set(["react", ...noImpureStateUpdater.requires ?? []])]
|
|
56871
|
-
}
|
|
56872
|
-
},
|
|
56873
56103
|
{
|
|
56874
56104
|
key: "react-doctor/no-indeterminate-attribute",
|
|
56875
56105
|
id: "no-indeterminate-attribute",
|
|
@@ -57286,18 +56516,6 @@ const reactDoctorRules = [
|
|
|
57286
56516
|
requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
|
|
57287
56517
|
}
|
|
57288
56518
|
},
|
|
57289
|
-
{
|
|
57290
|
-
key: "react-doctor/no-prop-callback-in-render",
|
|
57291
|
-
id: "no-prop-callback-in-render",
|
|
57292
|
-
source: "react-doctor",
|
|
57293
|
-
originallyExternal: false,
|
|
57294
|
-
rule: {
|
|
57295
|
-
...noPropCallbackInRender,
|
|
57296
|
-
framework: "global",
|
|
57297
|
-
category: "Bugs",
|
|
57298
|
-
requires: [...new Set(["react", ...noPropCallbackInRender.requires ?? []])]
|
|
57299
|
-
}
|
|
57300
|
-
},
|
|
57301
56519
|
{
|
|
57302
56520
|
key: "react-doctor/no-prop-types",
|
|
57303
56521
|
id: "no-prop-types",
|
|
@@ -57389,18 +56607,6 @@ const reactDoctorRules = [
|
|
|
57389
56607
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
57390
56608
|
}
|
|
57391
56609
|
},
|
|
57392
|
-
{
|
|
57393
|
-
key: "react-doctor/no-ref-current-in-render",
|
|
57394
|
-
id: "no-ref-current-in-render",
|
|
57395
|
-
source: "react-doctor",
|
|
57396
|
-
originallyExternal: false,
|
|
57397
|
-
rule: {
|
|
57398
|
-
...noRefCurrentInRender,
|
|
57399
|
-
framework: "global",
|
|
57400
|
-
category: "Bugs",
|
|
57401
|
-
requires: [...new Set(["react", ...noRefCurrentInRender.requires ?? []])]
|
|
57402
|
-
}
|
|
57403
|
-
},
|
|
57404
56610
|
{
|
|
57405
56611
|
key: "react-doctor/no-render-in-render",
|
|
57406
56612
|
id: "no-render-in-render",
|