oxlint-plugin-react-doctor 0.7.4-dev.118f806 → 0.7.4-dev.2b5a18e
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 +138 -0
- package/dist/index.js +1470 -361
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9191,6 +9191,24 @@ const fileContainsReleaseForUsage = (usage, context) => {
|
|
|
9191
9191
|
});
|
|
9192
9192
|
return didFindRelease;
|
|
9193
9193
|
};
|
|
9194
|
+
const isUseSyncExternalStoreSubscribeFunction = (functionNode, context) => {
|
|
9195
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
9196
|
+
if (!bindingIdentifier) return false;
|
|
9197
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
9198
|
+
const isSubscribeBinding = (candidateBinding) => {
|
|
9199
|
+
const symbol = context.scopes.symbolFor(candidateBinding);
|
|
9200
|
+
if (!symbol || visitedSymbolIds.has(symbol.id) || symbol.references.length === 0) return false;
|
|
9201
|
+
visitedSymbolIds.add(symbol.id);
|
|
9202
|
+
return symbol.references.every((reference) => {
|
|
9203
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
9204
|
+
const referenceParent = referenceRoot.parent;
|
|
9205
|
+
if (isNodeOfType(referenceParent, "CallExpression") && referenceParent.arguments?.[0] === referenceRoot) return isReactApiCall(referenceParent, "useSyncExternalStore", context.scopes);
|
|
9206
|
+
const aliasDeclaration = referenceParent?.parent;
|
|
9207
|
+
return Boolean(isNodeOfType(referenceParent, "VariableDeclarator") && referenceParent.init === referenceRoot && isNodeOfType(referenceParent.id, "Identifier") && isNodeOfType(aliasDeclaration, "VariableDeclaration") && aliasDeclaration.kind === "const" && isSubscribeBinding(referenceParent.id));
|
|
9208
|
+
});
|
|
9209
|
+
};
|
|
9210
|
+
return isSubscribeBinding(bindingIdentifier);
|
|
9211
|
+
};
|
|
9194
9212
|
const doesResourceResultEscape = (resourceNode, allowConciseReturnEscape) => {
|
|
9195
9213
|
let currentNode = resourceNode;
|
|
9196
9214
|
let parentNode = currentNode.parent;
|
|
@@ -9212,6 +9230,8 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
9212
9230
|
if (!body) return null;
|
|
9213
9231
|
let leak = null;
|
|
9214
9232
|
const allowConciseReturnEscape = !isInlineRetainedHandlerFunction(retainedFunction, context);
|
|
9233
|
+
const isExternalStoreSubscribeFunction = isUseSyncExternalStoreSubscribeFunction(retainedFunction, context);
|
|
9234
|
+
const hasReleaseForUsage = (usage) => isExternalStoreSubscribeFunction ? effectHasCleanupForUsage(retainedFunction, usage, context) : fileContainsReleaseForUsage(usage, context);
|
|
9215
9235
|
walkAst(body, (child) => {
|
|
9216
9236
|
if (leak !== null) return false;
|
|
9217
9237
|
if (isFunctionLike$1(child)) return false;
|
|
@@ -9226,7 +9246,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
9226
9246
|
eventKey: null,
|
|
9227
9247
|
handlerKey: null
|
|
9228
9248
|
};
|
|
9229
|
-
if (!
|
|
9249
|
+
if (!hasReleaseForUsage(socketUsage)) {
|
|
9230
9250
|
leak = socketUsage;
|
|
9231
9251
|
return false;
|
|
9232
9252
|
}
|
|
@@ -9243,7 +9263,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
9243
9263
|
eventKey: null,
|
|
9244
9264
|
handlerKey: null
|
|
9245
9265
|
};
|
|
9246
|
-
if (!
|
|
9266
|
+
if (!hasReleaseForUsage(timerUsage)) {
|
|
9247
9267
|
leak = timerUsage;
|
|
9248
9268
|
return false;
|
|
9249
9269
|
}
|
|
@@ -9257,7 +9277,7 @@ const findRetainedFunctionLeak = (retainedFunction, context) => {
|
|
|
9257
9277
|
handleKey: findAssignedResourceKey(child, context),
|
|
9258
9278
|
...registrationDetails
|
|
9259
9279
|
};
|
|
9260
|
-
if (!hasSelfReleasingListenerOptions(child, context) && !
|
|
9280
|
+
if (!hasSelfReleasingListenerOptions(child, context) && !hasReleaseForUsage(subscriptionUsage)) leak = subscriptionUsage;
|
|
9261
9281
|
return false;
|
|
9262
9282
|
}
|
|
9263
9283
|
});
|
|
@@ -9948,13 +9968,18 @@ const unwrapExpression$3 = (node) => {
|
|
|
9948
9968
|
return current;
|
|
9949
9969
|
};
|
|
9950
9970
|
/**
|
|
9951
|
-
* Get the hook name from a
|
|
9952
|
-
*
|
|
9953
|
-
* `React.useFoo()` (MemberExpression).
|
|
9971
|
+
* Get the hook name from a direct, wrapped, namespaced, or immutable
|
|
9972
|
+
* React import alias call.
|
|
9954
9973
|
*/
|
|
9955
|
-
const getHookName = (callee) => {
|
|
9956
|
-
|
|
9957
|
-
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;
|
|
9958
9983
|
return null;
|
|
9959
9984
|
};
|
|
9960
9985
|
const FUNCTION_SCOPE_KINDS = new Set([
|
|
@@ -10139,7 +10164,7 @@ const STABLE_IDENTITY_WRAPPER_HOOK_NAMES = new Set([
|
|
|
10139
10164
|
* - primitive-literal local consts (the value never changes
|
|
10140
10165
|
* between renders unless the literal does)
|
|
10141
10166
|
*/
|
|
10142
|
-
const symbolHasStableHookOrigin = (symbol) => {
|
|
10167
|
+
const symbolHasStableHookOrigin = (symbol, scopes) => {
|
|
10143
10168
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
10144
10169
|
let declarator = symbol.declarationNode;
|
|
10145
10170
|
while (declarator && declarator.type !== "VariableDeclarator") declarator = declarator.parent ?? null;
|
|
@@ -10152,7 +10177,7 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
10152
10177
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
10153
10178
|
}
|
|
10154
10179
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
10155
|
-
const initializerHookName = getHookName(initializer.callee);
|
|
10180
|
+
const initializerHookName = getHookName(initializer.callee, scopes);
|
|
10156
10181
|
if (!initializerHookName) return false;
|
|
10157
10182
|
if (initializerHookName === "useRef") return true;
|
|
10158
10183
|
if (initializerHookName === "useEffectEvent") return true;
|
|
@@ -10187,7 +10212,7 @@ const symbolHasStableMemoizedOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
10187
10212
|
if (!declarator.init) return false;
|
|
10188
10213
|
const initializer = unwrapExpression$3(declarator.init);
|
|
10189
10214
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
10190
|
-
const initializerHookName = getHookName(initializer.callee);
|
|
10215
|
+
const initializerHookName = getHookName(initializer.callee, scopes);
|
|
10191
10216
|
if (!initializerHookName || !MEMOIZING_HOOK_NAMES.has(initializerHookName)) return false;
|
|
10192
10217
|
const depsArgument = initializer.arguments[1];
|
|
10193
10218
|
if (!depsArgument || !isAstNode(depsArgument)) return false;
|
|
@@ -10217,7 +10242,7 @@ const getObjectPropertyValue = (objectExpression, propertyName) => {
|
|
|
10217
10242
|
}
|
|
10218
10243
|
return null;
|
|
10219
10244
|
};
|
|
10220
|
-
const isStableRefContainerCapture = (symbol, depKey) => {
|
|
10245
|
+
const isStableRefContainerCapture = (symbol, depKey, scopes) => {
|
|
10221
10246
|
if (symbol.kind !== "const") return false;
|
|
10222
10247
|
if (!depKey.startsWith(`${symbol.name}.`)) return false;
|
|
10223
10248
|
if (symbol.references.some((reference) => reference.flag !== "read")) return false;
|
|
@@ -10231,7 +10256,7 @@ const isStableRefContainerCapture = (symbol, depKey) => {
|
|
|
10231
10256
|
if (!propertyValue) return false;
|
|
10232
10257
|
currentValue = propertyValue;
|
|
10233
10258
|
}
|
|
10234
|
-
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee) === "useRef";
|
|
10259
|
+
return isNodeOfType(currentValue, "CallExpression") && getHookName(currentValue.callee, scopes) === "useRef";
|
|
10235
10260
|
};
|
|
10236
10261
|
const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
10237
10262
|
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
@@ -10249,7 +10274,13 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
10249
10274
|
}
|
|
10250
10275
|
return true;
|
|
10251
10276
|
};
|
|
10252
|
-
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);
|
|
10253
10284
|
//#endregion
|
|
10254
10285
|
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
10255
10286
|
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
@@ -10437,10 +10468,18 @@ const collectCaptureDepKeys = (callback, scopes) => {
|
|
|
10437
10468
|
}
|
|
10438
10469
|
const depKey = computeDepKey(reference);
|
|
10439
10470
|
if (!depKey) continue;
|
|
10440
|
-
if (isStableRefContainerCapture(symbol, depKey)) {
|
|
10471
|
+
if (isStableRefContainerCapture(symbol, depKey, scopes)) {
|
|
10441
10472
|
stableCapturedNames.add(depKey);
|
|
10442
10473
|
continue;
|
|
10443
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
|
+
}
|
|
10444
10483
|
keys.add(depKey);
|
|
10445
10484
|
}
|
|
10446
10485
|
return {
|
|
@@ -10469,12 +10508,60 @@ const hasComputedMemberExpression = (node) => {
|
|
|
10469
10508
|
if (stripped.computed) return true;
|
|
10470
10509
|
return hasComputedMemberExpression(stripped.object);
|
|
10471
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
|
+
};
|
|
10472
10559
|
const isUseCallbackResultDep = (node, scopes) => {
|
|
10473
10560
|
const rootSymbol = getRootSymbol(node, scopes);
|
|
10474
10561
|
const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
|
|
10475
|
-
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee) === "useCallback");
|
|
10562
|
+
return Boolean(initializer && isNodeOfType(initializer, "CallExpression") && getHookName(initializer.callee, scopes) === "useCallback");
|
|
10476
10563
|
};
|
|
10477
|
-
const
|
|
10564
|
+
const isExtraReactiveDepAllowed = (node, scopes) => {
|
|
10478
10565
|
const rootIdentifier = getMemberRootIdentifier(node);
|
|
10479
10566
|
if (!rootIdentifier) return false;
|
|
10480
10567
|
const symbol = scopes.symbolFor(rootIdentifier);
|
|
@@ -10502,12 +10589,78 @@ const isUnstableInitializer = (node) => {
|
|
|
10502
10589
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
10503
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");
|
|
10504
10591
|
};
|
|
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
|
+
};
|
|
10505
10612
|
const hasDirectIdentifierDeclarator = (symbol) => isNodeOfType(symbol.declarationNode, "VariableDeclarator") && isNodeOfType(symbol.declarationNode.id, "Identifier") || isNodeOfType(symbol.declarationNode, "ClassDeclaration");
|
|
10506
10613
|
const isFunctionValueSymbol = (symbol) => getFunctionValueNode(symbol) !== null;
|
|
10507
|
-
const isStableSetterLikeSymbol = (symbol) => {
|
|
10508
|
-
if (!symbolHasStableHookOrigin(symbol)) return false;
|
|
10614
|
+
const isStableSetterLikeSymbol = (symbol, scopes) => {
|
|
10615
|
+
if (!symbolHasStableHookOrigin(symbol, scopes)) return false;
|
|
10509
10616
|
return symbol.name.startsWith("set") || symbol.name.startsWith("dispatch") || symbol.name.startsWith("startTransition");
|
|
10510
10617
|
};
|
|
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
|
+
};
|
|
10511
10664
|
const findStableSetterReference = (node, scopes) => {
|
|
10512
10665
|
let setterName = null;
|
|
10513
10666
|
const visit = (current) => {
|
|
@@ -10515,7 +10668,7 @@ const findStableSetterReference = (node, scopes) => {
|
|
|
10515
10668
|
if (current !== node && (isNodeOfType(current, "FunctionDeclaration") || isNodeOfType(current, "FunctionExpression") || isNodeOfType(current, "ArrowFunctionExpression"))) return;
|
|
10516
10669
|
if (isNodeOfType(current, "Identifier")) {
|
|
10517
10670
|
const symbol = scopes.referenceFor(current)?.resolvedSymbol;
|
|
10518
|
-
if (symbol && isStableSetterLikeSymbol(symbol)) {
|
|
10671
|
+
if (symbol && isStableSetterLikeSymbol(symbol, scopes) && !isGuardedStableSetterCall(current, symbol, scopes)) {
|
|
10519
10672
|
setterName = symbol.name;
|
|
10520
10673
|
return;
|
|
10521
10674
|
}
|
|
@@ -10631,11 +10784,11 @@ const hasRefCurrentAssignmentInComponent = (refSymbol) => {
|
|
|
10631
10784
|
}
|
|
10632
10785
|
return false;
|
|
10633
10786
|
};
|
|
10634
|
-
const isSeededDataRefSymbol = (refSymbol) => {
|
|
10787
|
+
const isSeededDataRefSymbol = (refSymbol, scopes) => {
|
|
10635
10788
|
if (!refSymbol) return false;
|
|
10636
10789
|
const initializer = refSymbol.initializer ? unwrapExpression$3(refSymbol.initializer) : null;
|
|
10637
10790
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
10638
|
-
if (getHookName(initializer.callee) !== "useRef") return false;
|
|
10791
|
+
if (getHookName(initializer.callee, scopes) !== "useRef") return false;
|
|
10639
10792
|
const firstArgument = initializer.arguments[0];
|
|
10640
10793
|
if (!firstArgument || !isAstNode(firstArgument)) return false;
|
|
10641
10794
|
const strippedArgument = unwrapExpression$3(firstArgument);
|
|
@@ -10675,7 +10828,9 @@ const isOuterFunctionScopeDep = (node, callback, scopes) => {
|
|
|
10675
10828
|
const componentOrHookScope = componentOrHookFunction ? scopes.ownScopeFor(componentOrHookFunction) : null;
|
|
10676
10829
|
return Boolean(componentOrHookScope && !isDescendantScope(symbol.scope, componentOrHookScope));
|
|
10677
10830
|
};
|
|
10678
|
-
const hasMemberCallForRoot = (node, rootName) => {
|
|
10831
|
+
const hasMemberCallForRoot = (node, rootName, scopes) => {
|
|
10832
|
+
const rootSymbol = closureCaptures(node, scopes).find((reference) => reference.resolvedSymbol?.name === rootName)?.resolvedSymbol ?? null;
|
|
10833
|
+
if (!rootSymbol) return false;
|
|
10679
10834
|
let didFindMemberCall = false;
|
|
10680
10835
|
const visit = (current) => {
|
|
10681
10836
|
if (didFindMemberCall) return;
|
|
@@ -10691,7 +10846,7 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
10691
10846
|
}
|
|
10692
10847
|
chainObject = unwrapExpression$3(chainObject.object);
|
|
10693
10848
|
}
|
|
10694
|
-
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
|
|
10849
|
+
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName && scopes.symbolFor(chainObject) === rootSymbol) {
|
|
10695
10850
|
didFindMemberCall = true;
|
|
10696
10851
|
return;
|
|
10697
10852
|
}
|
|
@@ -10709,9 +10864,11 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
10709
10864
|
visit(node);
|
|
10710
10865
|
return didFindMemberCall;
|
|
10711
10866
|
};
|
|
10712
|
-
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback) => {
|
|
10713
|
-
|
|
10714
|
-
if (
|
|
10867
|
+
const addAggregatePropsDependency = (captureKeys, declaredKeys, callback, scopes) => {
|
|
10868
|
+
const propsCaptureKeys = [...captureKeys].filter((captureKey) => captureKey.startsWith("props."));
|
|
10869
|
+
if (propsCaptureKeys.length < 2 || declaredKeys.has("props")) return;
|
|
10870
|
+
if (propsCaptureKeys.every((captureKey) => [...declaredKeys].some((declaredKey) => isMatchingDepOrPrefix(declaredKey, captureKey)))) return;
|
|
10871
|
+
if (hasMemberCallForRoot(callback, "props", scopes)) captureKeys.add("props");
|
|
10715
10872
|
};
|
|
10716
10873
|
const exhaustiveDeps = defineRule({
|
|
10717
10874
|
id: "exhaustive-deps",
|
|
@@ -10766,7 +10923,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10766
10923
|
};
|
|
10767
10924
|
const isAutoDependenciesHook = (hookName) => settings.experimental_autoDependenciesHooks.includes(hookName);
|
|
10768
10925
|
return { CallExpression(node) {
|
|
10769
|
-
const hookName = getHookName(node.callee);
|
|
10926
|
+
const hookName = getHookName(node.callee, context.scopes);
|
|
10770
10927
|
if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
|
|
10771
10928
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
10772
10929
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
@@ -10823,7 +10980,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10823
10980
|
if (outerAssignments.length > 0) return;
|
|
10824
10981
|
const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
|
|
10825
10982
|
const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
|
|
10826
|
-
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol)) context.report({
|
|
10983
|
+
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol) && !isSeededDataRefSymbol(refCurrentInCleanup.refSymbol, context.scopes)) context.report({
|
|
10827
10984
|
node: callbackToAnalyze,
|
|
10828
10985
|
message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
|
|
10829
10986
|
});
|
|
@@ -10922,7 +11079,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10922
11079
|
const fullChain = stringifyMemberChain(stripped);
|
|
10923
11080
|
if (fullChain && fullChain.endsWith(".current") && isNodeOfType(stripped, "MemberExpression") && isNodeOfType(stripped.object, "Identifier")) {
|
|
10924
11081
|
const refSymbol = context.scopes.symbolFor(stripped.object);
|
|
10925
|
-
if (refSymbol && symbolHasStableHookOrigin(refSymbol)) {
|
|
11082
|
+
if (refSymbol && symbolHasStableHookOrigin(refSymbol, context.scopes)) {
|
|
10926
11083
|
if (!didReportRefCurrentDep) {
|
|
10927
11084
|
context.report({
|
|
10928
11085
|
node: elementNode,
|
|
@@ -10960,7 +11117,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10960
11117
|
declaredKeys.add(key);
|
|
10961
11118
|
declaredKeyToReportNode.set(key, elementNode);
|
|
10962
11119
|
}
|
|
10963
|
-
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument);
|
|
11120
|
+
addAggregatePropsDependency(captureKeys, declaredKeys, callbackToAnalyze ?? callbackArgument, context.scopes);
|
|
10964
11121
|
const missingCaptureKeys = [];
|
|
10965
11122
|
for (const captureKey of captureKeys) {
|
|
10966
11123
|
let isCoveredByDeclared = false;
|
|
@@ -11046,7 +11203,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
11046
11203
|
if (stableCapturedNames.has(rootName) || stableCapturedNames.has(declaredKey)) continue;
|
|
11047
11204
|
if (outerFunctionCapturedNames.has(rootName)) continue;
|
|
11048
11205
|
const reportNode = declaredKeyToReportNode.get(declaredKey) ?? depsArgument;
|
|
11049
|
-
if (
|
|
11206
|
+
if (isExtraDepAllowedForHook(hookName, reportNode, context.scopes)) continue;
|
|
11050
11207
|
if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
|
|
11051
11208
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
11052
11209
|
if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
|
|
@@ -20988,6 +21145,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
20988
21145
|
continue;
|
|
20989
21146
|
}
|
|
20990
21147
|
if (isNodeOfType(statement, "ExportNamedDeclaration")) {
|
|
21148
|
+
if (statement.exportKind === "type") continue;
|
|
20991
21149
|
const declaration = statement.declaration;
|
|
20992
21150
|
if (declaration && isNodeOfType(declaration, "VariableDeclaration")) {
|
|
20993
21151
|
recordVariableDeclaration(declaration);
|
|
@@ -21002,6 +21160,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
21002
21160
|
}
|
|
21003
21161
|
for (const specifier of statement.specifiers ?? []) {
|
|
21004
21162
|
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
21163
|
+
if (specifier.exportKind === "type") continue;
|
|
21005
21164
|
const local = specifier.local;
|
|
21006
21165
|
const exported = specifier.exported;
|
|
21007
21166
|
if (!isNodeOfType(local, "Identifier")) continue;
|
|
@@ -21050,25 +21209,37 @@ const resolveImportedExportName = (importSpecifier) => {
|
|
|
21050
21209
|
if (isNodeOfType(importSpecifier, "ImportDefaultSpecifier")) return "default";
|
|
21051
21210
|
return null;
|
|
21052
21211
|
};
|
|
21053
|
-
const
|
|
21212
|
+
const findReExportTargetsForName = (programRoot, exportedName) => {
|
|
21054
21213
|
if (!isNodeOfType(programRoot, "Program")) return [];
|
|
21055
|
-
const
|
|
21214
|
+
const exportAllTargets = [];
|
|
21056
21215
|
for (const statement of programRoot.body ?? []) {
|
|
21057
21216
|
if (isNodeOfType(statement, "ExportNamedDeclaration") && statement.source) {
|
|
21217
|
+
if (statement.exportKind === "type") continue;
|
|
21058
21218
|
const sourceValue = statement.source.value;
|
|
21059
21219
|
if (typeof sourceValue !== "string") continue;
|
|
21060
21220
|
for (const specifier of statement.specifiers ?? []) {
|
|
21061
21221
|
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
21222
|
+
if (specifier.exportKind === "type") continue;
|
|
21062
21223
|
const exported = specifier.exported;
|
|
21063
|
-
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null)
|
|
21224
|
+
if ((isNodeOfType(exported, "Identifier") ? exported.name : isNodeOfType(exported, "Literal") && typeof exported.value === "string" ? exported.value : null) !== exportedName) continue;
|
|
21225
|
+
const local = specifier.local;
|
|
21226
|
+
const importedName = isNodeOfType(local, "Identifier") ? local.name : isNodeOfType(local, "Literal") && typeof local.value === "string" ? local.value : null;
|
|
21227
|
+
if (importedName) return [{
|
|
21228
|
+
importedName,
|
|
21229
|
+
source: sourceValue
|
|
21230
|
+
}];
|
|
21064
21231
|
}
|
|
21065
21232
|
}
|
|
21066
21233
|
if (isNodeOfType(statement, "ExportAllDeclaration") && statement.source) {
|
|
21234
|
+
if (statement.exportKind === "type" || statement.exported) continue;
|
|
21067
21235
|
const sourceValue = statement.source.value;
|
|
21068
|
-
if (typeof sourceValue === "string")
|
|
21236
|
+
if (typeof sourceValue === "string") exportAllTargets.push({
|
|
21237
|
+
importedName: exportedName,
|
|
21238
|
+
source: sourceValue
|
|
21239
|
+
});
|
|
21069
21240
|
}
|
|
21070
21241
|
}
|
|
21071
|
-
return
|
|
21242
|
+
return exportAllTargets;
|
|
21072
21243
|
};
|
|
21073
21244
|
//#endregion
|
|
21074
21245
|
//#region src/plugin/utils/attach-parent-references.ts
|
|
@@ -21157,139 +21328,6 @@ const parseSourceFile = (absoluteFilePath) => {
|
|
|
21157
21328
|
return parsedProgram;
|
|
21158
21329
|
};
|
|
21159
21330
|
//#endregion
|
|
21160
|
-
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
21161
|
-
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
21162
|
-
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
21163
|
-
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
21164
|
-
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
21165
|
-
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
21166
|
-
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
21167
|
-
const createNonBarrelInfo = () => ({
|
|
21168
|
-
isBarrel: false,
|
|
21169
|
-
exportsByName: /* @__PURE__ */ new Map(),
|
|
21170
|
-
starExportSources: []
|
|
21171
|
-
});
|
|
21172
|
-
const addImportedBinding = (importedBindings, binding) => {
|
|
21173
|
-
importedBindings.set(binding.localName, {
|
|
21174
|
-
...binding,
|
|
21175
|
-
didExport: false
|
|
21176
|
-
});
|
|
21177
|
-
};
|
|
21178
|
-
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
21179
|
-
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
21180
|
-
localName: specifier.exportedName,
|
|
21181
|
-
importedName: specifier.localName,
|
|
21182
|
-
source,
|
|
21183
|
-
isTypeOnly: specifier.isTypeOnly
|
|
21184
|
-
});
|
|
21185
|
-
};
|
|
21186
|
-
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
21187
|
-
const trimmedImportClause = importClause.trim();
|
|
21188
|
-
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
21189
|
-
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
21190
|
-
localName: namespaceMatch[1],
|
|
21191
|
-
importedName: "*",
|
|
21192
|
-
source,
|
|
21193
|
-
isTypeOnly: declarationIsTypeOnly
|
|
21194
|
-
});
|
|
21195
|
-
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
21196
|
-
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
21197
|
-
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
21198
|
-
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
21199
|
-
localName: defaultImportName,
|
|
21200
|
-
importedName: "default",
|
|
21201
|
-
source,
|
|
21202
|
-
isTypeOnly: declarationIsTypeOnly
|
|
21203
|
-
});
|
|
21204
|
-
};
|
|
21205
|
-
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
21206
|
-
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
21207
|
-
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
21208
|
-
return "";
|
|
21209
|
-
});
|
|
21210
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
21211
|
-
const isTypeOnly = Boolean(typeKeyword);
|
|
21212
|
-
if (namespaceExportName) {
|
|
21213
|
-
exportsByName.set(namespaceExportName, {
|
|
21214
|
-
exportedName: namespaceExportName,
|
|
21215
|
-
importedName: "*",
|
|
21216
|
-
source,
|
|
21217
|
-
isTypeOnly
|
|
21218
|
-
});
|
|
21219
|
-
return "";
|
|
21220
|
-
}
|
|
21221
|
-
if (specifiersText) {
|
|
21222
|
-
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
21223
|
-
exportedName: specifier.exportedName,
|
|
21224
|
-
importedName: specifier.localName,
|
|
21225
|
-
source,
|
|
21226
|
-
isTypeOnly: specifier.isTypeOnly
|
|
21227
|
-
});
|
|
21228
|
-
return "";
|
|
21229
|
-
}
|
|
21230
|
-
starExportSources.push(source);
|
|
21231
|
-
return "";
|
|
21232
|
-
});
|
|
21233
|
-
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
|
|
21234
|
-
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
21235
|
-
const importedBinding = importedBindings.get(specifier.localName);
|
|
21236
|
-
if (!importedBinding) return _match;
|
|
21237
|
-
importedBinding.didExport = true;
|
|
21238
|
-
exportsByName.set(specifier.exportedName, {
|
|
21239
|
-
exportedName: specifier.exportedName,
|
|
21240
|
-
importedName: importedBinding.importedName,
|
|
21241
|
-
source: importedBinding.source,
|
|
21242
|
-
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
21243
|
-
});
|
|
21244
|
-
}
|
|
21245
|
-
return "";
|
|
21246
|
-
});
|
|
21247
|
-
return withoutKnownDeclarations;
|
|
21248
|
-
};
|
|
21249
|
-
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
21250
|
-
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
21251
|
-
return false;
|
|
21252
|
-
};
|
|
21253
|
-
const classifyBarrelModule = (sourceText) => {
|
|
21254
|
-
const strippedSource = stripJsComments(sourceText).trim();
|
|
21255
|
-
if (!strippedSource) return createNonBarrelInfo();
|
|
21256
|
-
const importedBindings = /* @__PURE__ */ new Map();
|
|
21257
|
-
const exportsByName = /* @__PURE__ */ new Map();
|
|
21258
|
-
const starExportSources = [];
|
|
21259
|
-
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
21260
|
-
return {
|
|
21261
|
-
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
21262
|
-
exportsByName,
|
|
21263
|
-
starExportSources
|
|
21264
|
-
};
|
|
21265
|
-
};
|
|
21266
|
-
const getBarrelIndexModuleInfo = (filePath) => {
|
|
21267
|
-
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
21268
|
-
recordContentProbe(filePath);
|
|
21269
|
-
let fileStat;
|
|
21270
|
-
try {
|
|
21271
|
-
fileStat = fs.statSync(filePath);
|
|
21272
|
-
} catch {
|
|
21273
|
-
fileStat = null;
|
|
21274
|
-
}
|
|
21275
|
-
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
21276
|
-
if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
|
|
21277
|
-
if (fileStat === null) return createNonBarrelInfo();
|
|
21278
|
-
let moduleInfo = createNonBarrelInfo();
|
|
21279
|
-
try {
|
|
21280
|
-
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
21281
|
-
} catch {
|
|
21282
|
-
moduleInfo = createNonBarrelInfo();
|
|
21283
|
-
}
|
|
21284
|
-
barrelIndexModuleInfoCache.set(filePath, {
|
|
21285
|
-
mtimeMs: fileStat.mtimeMs,
|
|
21286
|
-
size: fileStat.size,
|
|
21287
|
-
moduleInfo
|
|
21288
|
-
});
|
|
21289
|
-
return moduleInfo;
|
|
21290
|
-
};
|
|
21291
|
-
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
21292
|
-
//#endregion
|
|
21293
21331
|
//#region src/plugin/utils/resolve-relative-import-path.ts
|
|
21294
21332
|
const MODULE_FILE_EXTENSIONS = [
|
|
21295
21333
|
".ts",
|
|
@@ -21406,35 +21444,6 @@ const resolveModuleFileFromAbsolutePath = (importPath) => {
|
|
|
21406
21444
|
};
|
|
21407
21445
|
const resolveRelativeImportPath = (filename, source) => resolveModuleFileFromAbsolutePath(path.resolve(path.dirname(filename), source));
|
|
21408
21446
|
//#endregion
|
|
21409
|
-
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
21410
|
-
const getUniqueFilePath = (filePaths) => {
|
|
21411
|
-
const uniqueFilePaths = new Set(filePaths);
|
|
21412
|
-
if (uniqueFilePaths.size !== 1) return null;
|
|
21413
|
-
const [filePath] = uniqueFilePaths;
|
|
21414
|
-
return filePath ?? null;
|
|
21415
|
-
};
|
|
21416
|
-
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
21417
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
21418
|
-
if (!resolvedTargetPath) return null;
|
|
21419
|
-
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
21420
|
-
if (nestedTargetPath) return nestedTargetPath;
|
|
21421
|
-
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
21422
|
-
};
|
|
21423
|
-
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
21424
|
-
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
21425
|
-
visitedFilePaths.add(barrelFilePath);
|
|
21426
|
-
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
21427
|
-
if (!moduleInfo.isBarrel) return null;
|
|
21428
|
-
const target = moduleInfo.exportsByName.get(exportedName);
|
|
21429
|
-
if (target) {
|
|
21430
|
-
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
21431
|
-
if (!resolvedTargetPath) return null;
|
|
21432
|
-
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
21433
|
-
}
|
|
21434
|
-
if (exportedName === "default") return null;
|
|
21435
|
-
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
21436
|
-
};
|
|
21437
|
-
//#endregion
|
|
21438
21447
|
//#region src/plugin/utils/resolve-tsconfig-alias.ts
|
|
21439
21448
|
const TSCONFIG_FILE_NAMES = ["tsconfig.json", "jsconfig.json"];
|
|
21440
21449
|
const isObjectRecord = (value) => typeof value === "object" && value !== null;
|
|
@@ -21613,25 +21622,29 @@ const resolveTsconfigAliasPath = (fromFilename, source) => {
|
|
|
21613
21622
|
};
|
|
21614
21623
|
//#endregion
|
|
21615
21624
|
//#region src/plugin/utils/resolve-module-path.ts
|
|
21616
|
-
const resolveModulePath = (fromFilename, source) =>
|
|
21625
|
+
const resolveModulePath = (fromFilename, source) => {
|
|
21626
|
+
if (path.isAbsolute(source)) return null;
|
|
21627
|
+
return source.startsWith(".") ? resolveRelativeImportPath(fromFilename, source) : resolveTsconfigAliasPath(fromFilename, source);
|
|
21628
|
+
};
|
|
21617
21629
|
//#endregion
|
|
21618
21630
|
//#region src/plugin/utils/resolve-cross-file-function-export.ts
|
|
21619
21631
|
const resolveFunctionExportInFile = (filePath, exportedName, visitedFilePaths) => {
|
|
21620
21632
|
if (visitedFilePaths.size >= 4) return null;
|
|
21621
21633
|
if (visitedFilePaths.has(filePath)) return null;
|
|
21622
21634
|
visitedFilePaths.add(filePath);
|
|
21623
|
-
const
|
|
21624
|
-
const programRoot = parseSourceFile(actualFilePath);
|
|
21635
|
+
const programRoot = parseSourceFile(filePath);
|
|
21625
21636
|
if (!programRoot) return null;
|
|
21626
21637
|
const exported = findExportedFunctionBody(programRoot, exportedName);
|
|
21627
21638
|
if (exported) return exported;
|
|
21628
|
-
|
|
21629
|
-
|
|
21639
|
+
const resolvedCandidates = /* @__PURE__ */ new Set();
|
|
21640
|
+
for (const target of findReExportTargetsForName(programRoot, exportedName)) {
|
|
21641
|
+
const nextFilePath = resolveModulePath(filePath, target.source);
|
|
21630
21642
|
if (!nextFilePath) continue;
|
|
21631
|
-
const resolved = resolveFunctionExportInFile(nextFilePath,
|
|
21632
|
-
if (resolved)
|
|
21643
|
+
const resolved = resolveFunctionExportInFile(nextFilePath, target.importedName, new Set(visitedFilePaths));
|
|
21644
|
+
if (resolved) resolvedCandidates.add(resolved);
|
|
21633
21645
|
}
|
|
21634
|
-
return null;
|
|
21646
|
+
if (resolvedCandidates.size !== 1) return null;
|
|
21647
|
+
return resolvedCandidates.values().next().value ?? null;
|
|
21635
21648
|
};
|
|
21636
21649
|
const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
21637
21650
|
const resolvedFilePath = resolveModulePath(fromFilename, source);
|
|
@@ -22416,6 +22429,26 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
22416
22429
|
const importDeclaration = declarationNode.parent;
|
|
22417
22430
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
22418
22431
|
}));
|
|
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
|
+
};
|
|
22419
22452
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
22420
22453
|
if (!node) return false;
|
|
22421
22454
|
if (isNodeOfType(node, "Identifier")) {
|
|
@@ -22814,6 +22847,46 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
|
|
|
22814
22847
|
"parseInt"
|
|
22815
22848
|
]);
|
|
22816
22849
|
const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
|
|
22850
|
+
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
|
|
22851
|
+
"isRawJSON",
|
|
22852
|
+
"parse",
|
|
22853
|
+
"rawJSON",
|
|
22854
|
+
"stringify"
|
|
22855
|
+
])], ["Math", new Set([
|
|
22856
|
+
"abs",
|
|
22857
|
+
"acos",
|
|
22858
|
+
"acosh",
|
|
22859
|
+
"asin",
|
|
22860
|
+
"asinh",
|
|
22861
|
+
"atan",
|
|
22862
|
+
"atan2",
|
|
22863
|
+
"atanh",
|
|
22864
|
+
"cbrt",
|
|
22865
|
+
"ceil",
|
|
22866
|
+
"clz32",
|
|
22867
|
+
"cos",
|
|
22868
|
+
"cosh",
|
|
22869
|
+
"exp",
|
|
22870
|
+
"floor",
|
|
22871
|
+
"fround",
|
|
22872
|
+
"hypot",
|
|
22873
|
+
"imul",
|
|
22874
|
+
"log",
|
|
22875
|
+
"log10",
|
|
22876
|
+
"log1p",
|
|
22877
|
+
"log2",
|
|
22878
|
+
"max",
|
|
22879
|
+
"min",
|
|
22880
|
+
"pow",
|
|
22881
|
+
"round",
|
|
22882
|
+
"sign",
|
|
22883
|
+
"sin",
|
|
22884
|
+
"sinh",
|
|
22885
|
+
"sqrt",
|
|
22886
|
+
"tan",
|
|
22887
|
+
"tanh",
|
|
22888
|
+
"trunc"
|
|
22889
|
+
])]]);
|
|
22817
22890
|
const PURE_MEMBER_TRANSFORM_NAMES = new Set([
|
|
22818
22891
|
"concat",
|
|
22819
22892
|
"filter",
|
|
@@ -22860,6 +22933,42 @@ const getParameterBindingIdentity = (analysis, functionNode, parameter) => {
|
|
|
22860
22933
|
}
|
|
22861
22934
|
return parameter;
|
|
22862
22935
|
};
|
|
22936
|
+
const isAsyncOrGeneratorFunction = (functionNode) => Boolean(functionNode.async === true || functionNode.generator === true);
|
|
22937
|
+
const isModuleFunction = (functionNode) => {
|
|
22938
|
+
let ancestor = functionNode.parent;
|
|
22939
|
+
while (ancestor) {
|
|
22940
|
+
if (isFunctionLike$1(ancestor)) return false;
|
|
22941
|
+
if (isNodeOfType(ancestor, "Program")) return true;
|
|
22942
|
+
ancestor = ancestor.parent;
|
|
22943
|
+
}
|
|
22944
|
+
return false;
|
|
22945
|
+
};
|
|
22946
|
+
const getFunctionBindingNames = (functionNode) => {
|
|
22947
|
+
const names = /* @__PURE__ */ new Set();
|
|
22948
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) names.add(functionNode.id.name);
|
|
22949
|
+
const parent = functionNode.parent;
|
|
22950
|
+
if (parent && isNodeOfType(parent, "VariableDeclarator") && isNodeOfType(parent.id, "Identifier")) names.add(parent.id.name);
|
|
22951
|
+
return names;
|
|
22952
|
+
};
|
|
22953
|
+
const collectModuleBindingNames = (functionNode) => {
|
|
22954
|
+
let program = functionNode.parent;
|
|
22955
|
+
while (program && !isNodeOfType(program, "Program")) program = program.parent;
|
|
22956
|
+
const bindingNames = /* @__PURE__ */ new Set();
|
|
22957
|
+
if (!program || !isNodeOfType(program, "Program")) return bindingNames;
|
|
22958
|
+
for (const statement of program.body ?? []) {
|
|
22959
|
+
if (isNodeOfType(statement, "ImportDeclaration")) {
|
|
22960
|
+
for (const specifier of statement.specifiers ?? []) if (isNodeOfType(specifier.local, "Identifier")) bindingNames.add(specifier.local.name);
|
|
22961
|
+
continue;
|
|
22962
|
+
}
|
|
22963
|
+
const declaration = isNodeOfType(statement, "ExportNamedDeclaration") || isNodeOfType(statement, "ExportDefaultDeclaration") ? statement.declaration : statement;
|
|
22964
|
+
if (isNodeOfType(declaration, "VariableDeclaration")) {
|
|
22965
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, bindingNames);
|
|
22966
|
+
continue;
|
|
22967
|
+
}
|
|
22968
|
+
if ((isNodeOfType(declaration, "FunctionDeclaration") || isNodeOfType(declaration, "ClassDeclaration")) && isNodeOfType(declaration.id, "Identifier")) bindingNames.add(declaration.id.name);
|
|
22969
|
+
}
|
|
22970
|
+
return bindingNames;
|
|
22971
|
+
};
|
|
22863
22972
|
const buildSubstitutions = (analysis, functionNode, argumentExpressions, parentFrame) => {
|
|
22864
22973
|
const substitutions = /* @__PURE__ */ new Map();
|
|
22865
22974
|
const parameters = getFunctionParameters(functionNode);
|
|
@@ -22990,7 +23099,7 @@ const collectIntroducedBindings = (analysis, functionNode) => {
|
|
|
22990
23099
|
for (const parameter of getFunctionParameters(functionNode)) if (isNodeOfType(parameter, "Identifier")) introducedBindings.add(getParameterBindingIdentity(analysis, functionNode, parameter));
|
|
22991
23100
|
return introducedBindings;
|
|
22992
23101
|
};
|
|
22993
|
-
const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
23102
|
+
const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilename) => {
|
|
22994
23103
|
const effectFunction = getEffectFn(analysis, effectNode);
|
|
22995
23104
|
if (!effectFunction || !isFunctionLike$1(effectFunction) || effectFunction.async === true) return [];
|
|
22996
23105
|
const invokedFunctionEvidence = collectEffectInvokedFunctions(effectFunction);
|
|
@@ -22999,7 +23108,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
|
22999
23108
|
invocation: null,
|
|
23000
23109
|
isDeferred: false,
|
|
23001
23110
|
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23002
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
23111
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
23112
|
+
currentFilename
|
|
23003
23113
|
};
|
|
23004
23114
|
const frames = [rootFrame];
|
|
23005
23115
|
walkAst(effectFunction, (child) => {
|
|
@@ -23020,7 +23130,8 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode) => {
|
|
|
23020
23130
|
invocation: child,
|
|
23021
23131
|
isDeferred,
|
|
23022
23132
|
introducedBindings,
|
|
23023
|
-
substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame)
|
|
23133
|
+
substitutions: buildSubstitutions(analysis, callable, argumentsForCallable, rootFrame),
|
|
23134
|
+
currentFilename
|
|
23024
23135
|
});
|
|
23025
23136
|
};
|
|
23026
23137
|
if (isFunctionLike$1(callee)) {
|
|
@@ -23073,6 +23184,174 @@ const isOpaqueHookCall = (callExpression) => {
|
|
|
23073
23184
|
const calleeName = getCallCalleeName$1(callExpression);
|
|
23074
23185
|
return Boolean(calleeName && /^use[A-Z0-9]/.test(calleeName) && calleeName !== "useMemo" && calleeName !== "useCallback" && calleeName !== "useEffectEvent");
|
|
23075
23186
|
};
|
|
23187
|
+
const analyzeHelperStatements = (statements, environment, usedParameterIndices, analyzeExpression) => {
|
|
23188
|
+
let canContinue = true;
|
|
23189
|
+
for (const statement of statements) {
|
|
23190
|
+
if (!canContinue) {
|
|
23191
|
+
if (!isNodeOfType(statement, "EmptyStatement")) return {
|
|
23192
|
+
canContinue: false,
|
|
23193
|
+
isValid: false
|
|
23194
|
+
};
|
|
23195
|
+
continue;
|
|
23196
|
+
}
|
|
23197
|
+
if (isNodeOfType(statement, "EmptyStatement")) continue;
|
|
23198
|
+
if (isNodeOfType(statement, "ReturnStatement")) {
|
|
23199
|
+
if (!statement.argument || !analyzeExpression(statement.argument, environment, usedParameterIndices)) return {
|
|
23200
|
+
canContinue: false,
|
|
23201
|
+
isValid: false
|
|
23202
|
+
};
|
|
23203
|
+
canContinue = false;
|
|
23204
|
+
continue;
|
|
23205
|
+
}
|
|
23206
|
+
if (isNodeOfType(statement, "BlockStatement")) {
|
|
23207
|
+
const blockSummary = analyzeHelperStatements(statement.body ?? [], environment, usedParameterIndices, analyzeExpression);
|
|
23208
|
+
if (!blockSummary.isValid) return blockSummary;
|
|
23209
|
+
canContinue = blockSummary.canContinue;
|
|
23210
|
+
continue;
|
|
23211
|
+
}
|
|
23212
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
23213
|
+
if (!analyzeExpression(statement.test, environment, usedParameterIndices)) return {
|
|
23214
|
+
canContinue: false,
|
|
23215
|
+
isValid: false
|
|
23216
|
+
};
|
|
23217
|
+
const consequentSummary = analyzeHelperStatements([statement.consequent], environment, usedParameterIndices, analyzeExpression);
|
|
23218
|
+
if (!consequentSummary.isValid) return consequentSummary;
|
|
23219
|
+
const alternateSummary = statement.alternate ? analyzeHelperStatements([statement.alternate], environment, usedParameterIndices, analyzeExpression) : {
|
|
23220
|
+
canContinue: true,
|
|
23221
|
+
isValid: true
|
|
23222
|
+
};
|
|
23223
|
+
if (!alternateSummary.isValid) return alternateSummary;
|
|
23224
|
+
canContinue = consequentSummary.canContinue || alternateSummary.canContinue;
|
|
23225
|
+
continue;
|
|
23226
|
+
}
|
|
23227
|
+
return {
|
|
23228
|
+
canContinue: false,
|
|
23229
|
+
isValid: false
|
|
23230
|
+
};
|
|
23231
|
+
}
|
|
23232
|
+
return {
|
|
23233
|
+
canContinue,
|
|
23234
|
+
isValid: true
|
|
23235
|
+
};
|
|
23236
|
+
};
|
|
23237
|
+
const analyzeHelperExpression = (expression, environment, usedParameterIndices) => {
|
|
23238
|
+
const node = stripParenExpression(expression);
|
|
23239
|
+
if (isNodeOfType(node, "Literal") || isNodeOfType(node, "TemplateElement")) return true;
|
|
23240
|
+
if (isNodeOfType(node, "Identifier")) {
|
|
23241
|
+
const parameterIndex = environment.parameterIndices.get(node.name);
|
|
23242
|
+
if (parameterIndex !== void 0) {
|
|
23243
|
+
if (parameterIndex !== null) usedParameterIndices.add(parameterIndex);
|
|
23244
|
+
return true;
|
|
23245
|
+
}
|
|
23246
|
+
return node.name === "undefined" || node.name === "NaN" || node.name === "Infinity";
|
|
23247
|
+
}
|
|
23248
|
+
if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => !element || analyzeHelperExpression(element, environment, usedParameterIndices));
|
|
23249
|
+
if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => {
|
|
23250
|
+
if (isNodeOfType(property, "SpreadElement")) return analyzeHelperExpression(property.argument, environment, usedParameterIndices);
|
|
23251
|
+
if (!isNodeOfType(property, "Property") || property.kind !== "init" || property.method === true) return false;
|
|
23252
|
+
if (property.computed && !analyzeHelperExpression(property.key, environment, usedParameterIndices)) return false;
|
|
23253
|
+
return analyzeHelperExpression(property.value, environment, usedParameterIndices);
|
|
23254
|
+
});
|
|
23255
|
+
if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every((templateExpression) => analyzeHelperExpression(templateExpression, environment, usedParameterIndices));
|
|
23256
|
+
if (isNodeOfType(node, "UnaryExpression")) return node.operator !== "delete" && analyzeHelperExpression(node.argument, environment, usedParameterIndices);
|
|
23257
|
+
if (isNodeOfType(node, "BinaryExpression") || isNodeOfType(node, "LogicalExpression")) return analyzeHelperExpression(node.left, environment, usedParameterIndices) && analyzeHelperExpression(node.right, environment, usedParameterIndices);
|
|
23258
|
+
if (isNodeOfType(node, "ConditionalExpression")) return analyzeHelperExpression(node.test, environment, usedParameterIndices) && analyzeHelperExpression(node.consequent, environment, usedParameterIndices) && analyzeHelperExpression(node.alternate, environment, usedParameterIndices);
|
|
23259
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
23260
|
+
if (!analyzeHelperExpression(node.object, environment, usedParameterIndices)) return false;
|
|
23261
|
+
return !node.computed || analyzeHelperExpression(node.property, environment, usedParameterIndices);
|
|
23262
|
+
}
|
|
23263
|
+
if (isFunctionLike$1(node)) {
|
|
23264
|
+
if (isAsyncOrGeneratorFunction(node)) return false;
|
|
23265
|
+
const callbackParameterIndices = new Map(environment.parameterIndices);
|
|
23266
|
+
for (const parameter of getFunctionParameters(node)) {
|
|
23267
|
+
if (!isNodeOfType(parameter, "Identifier")) return false;
|
|
23268
|
+
callbackParameterIndices.set(parameter.name, null);
|
|
23269
|
+
}
|
|
23270
|
+
const callbackEnvironment = {
|
|
23271
|
+
parameterIndices: callbackParameterIndices,
|
|
23272
|
+
recursiveNames: environment.recursiveNames,
|
|
23273
|
+
shadowedGlobalNames: environment.shadowedGlobalNames
|
|
23274
|
+
};
|
|
23275
|
+
if (!isNodeOfType(node.body, "BlockStatement")) return analyzeHelperExpression(node.body, callbackEnvironment, usedParameterIndices);
|
|
23276
|
+
const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
|
|
23277
|
+
return callbackSummary.isValid && !callbackSummary.canContinue;
|
|
23278
|
+
}
|
|
23279
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
23280
|
+
const callee = stripParenExpression(node.callee);
|
|
23281
|
+
const calleeRoot = getMemberRoot(callee);
|
|
23282
|
+
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && !environment.parameterIndices.has(callee.name) && !environment.recursiveNames.has(callee.name) && !environment.shadowedGlobalNames.has(callee.name);
|
|
23283
|
+
const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
|
|
23284
|
+
const namespaceMemberName = getStaticMemberName(callee);
|
|
23285
|
+
const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
|
|
23286
|
+
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
23287
|
+
if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
|
|
23288
|
+
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
|
|
23289
|
+
return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
|
|
23290
|
+
}
|
|
23291
|
+
if (isNodeOfType(node, "SpreadElement")) return analyzeHelperExpression(node.argument, environment, usedParameterIndices);
|
|
23292
|
+
return false;
|
|
23293
|
+
};
|
|
23294
|
+
const helperSummaryCache = /* @__PURE__ */ new WeakMap();
|
|
23295
|
+
const summarizeHelperReturn = (functionNode) => {
|
|
23296
|
+
if (!isFunctionLike$1(functionNode) || !isModuleFunction(functionNode)) return null;
|
|
23297
|
+
if (helperSummaryCache.has(functionNode)) return helperSummaryCache.get(functionNode) ?? null;
|
|
23298
|
+
if (isAsyncOrGeneratorFunction(functionNode)) {
|
|
23299
|
+
helperSummaryCache.set(functionNode, null);
|
|
23300
|
+
return null;
|
|
23301
|
+
}
|
|
23302
|
+
const parameterIndices = /* @__PURE__ */ new Map();
|
|
23303
|
+
const parameters = getFunctionParameters(functionNode);
|
|
23304
|
+
for (let parameterIndex = 0; parameterIndex < parameters.length; parameterIndex += 1) {
|
|
23305
|
+
const parameter = parameters[parameterIndex];
|
|
23306
|
+
if (!parameter || !isNodeOfType(parameter, "Identifier") || parameterIndices.has(parameter.name)) {
|
|
23307
|
+
helperSummaryCache.set(functionNode, null);
|
|
23308
|
+
return null;
|
|
23309
|
+
}
|
|
23310
|
+
parameterIndices.set(parameter.name, parameterIndex);
|
|
23311
|
+
}
|
|
23312
|
+
const environment = {
|
|
23313
|
+
parameterIndices,
|
|
23314
|
+
recursiveNames: getFunctionBindingNames(functionNode),
|
|
23315
|
+
shadowedGlobalNames: collectModuleBindingNames(functionNode)
|
|
23316
|
+
};
|
|
23317
|
+
const usedParameterIndices = /* @__PURE__ */ new Set();
|
|
23318
|
+
if (!isNodeOfType(functionNode.body, "BlockStatement")) {
|
|
23319
|
+
if (!analyzeHelperExpression(functionNode.body, environment, usedParameterIndices)) {
|
|
23320
|
+
helperSummaryCache.set(functionNode, null);
|
|
23321
|
+
return null;
|
|
23322
|
+
}
|
|
23323
|
+
} else {
|
|
23324
|
+
const controlFlowSummary = analyzeHelperStatements(functionNode.body.body ?? [], environment, usedParameterIndices, analyzeHelperExpression);
|
|
23325
|
+
if (!controlFlowSummary.isValid || controlFlowSummary.canContinue) {
|
|
23326
|
+
helperSummaryCache.set(functionNode, null);
|
|
23327
|
+
return null;
|
|
23328
|
+
}
|
|
23329
|
+
}
|
|
23330
|
+
const summary = { usedParameterIndices };
|
|
23331
|
+
helperSummaryCache.set(functionNode, summary);
|
|
23332
|
+
return summary;
|
|
23333
|
+
};
|
|
23334
|
+
const resolveValueHelperFunction = (analysis, callee, currentFilename) => {
|
|
23335
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
23336
|
+
const reference = getRef(analysis, callee);
|
|
23337
|
+
if (!reference?.resolved) return null;
|
|
23338
|
+
const importDefinition = reference.resolved.defs.find((definition) => definition.type === "ImportBinding");
|
|
23339
|
+
if (importDefinition) {
|
|
23340
|
+
if (!currentFilename) return null;
|
|
23341
|
+
const specifier = importDefinition.node;
|
|
23342
|
+
if (!isNodeOfType(specifier, "ImportSpecifier") && !isNodeOfType(specifier, "ImportDefaultSpecifier")) return null;
|
|
23343
|
+
const importDeclaration = specifier.parent;
|
|
23344
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return null;
|
|
23345
|
+
if (importDeclaration.importKind === "type" || isNodeOfType(specifier, "ImportSpecifier") && specifier.importKind === "type") return null;
|
|
23346
|
+
const source = importDeclaration.source?.value;
|
|
23347
|
+
if (typeof source !== "string") return null;
|
|
23348
|
+
const exportedName = resolveImportedExportName(specifier);
|
|
23349
|
+
if (!exportedName) return null;
|
|
23350
|
+
return resolveCrossFileFunctionExport(currentFilename, source, exportedName);
|
|
23351
|
+
}
|
|
23352
|
+
const callable = resolveWrappedCallable(analysis, callee);
|
|
23353
|
+
return callable && isModuleFunction(callable) ? callable : null;
|
|
23354
|
+
};
|
|
23076
23355
|
const isLocallyConstructedObjectMember = (reference, memberExpression) => isNodeOfType(memberExpression, "MemberExpression") && reference.resolved?.defs.some((definition) => {
|
|
23077
23356
|
const definitionNode = definition.node;
|
|
23078
23357
|
return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
|
|
@@ -23158,38 +23437,55 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23158
23437
|
const calleeRoot = getMemberRoot(callee);
|
|
23159
23438
|
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(calleeRoot, "Identifier") && PURE_GLOBAL_NAMESPACE_NAMES.has(calleeRoot.name) && getIdentifierBindingIdentity(analysis, calleeRoot) === null;
|
|
23160
23439
|
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
23161
|
-
if (isPureMemberTransform)
|
|
23162
|
-
|
|
23163
|
-
|
|
23164
|
-
|
|
23165
|
-
|
|
23166
|
-
|
|
23167
|
-
if (isNodeOfType(callee, "MemberExpression")) {
|
|
23168
|
-
evidence.hasUnknownSource = true;
|
|
23440
|
+
if (isPureGlobalCall || isPureMemberTransform) {
|
|
23441
|
+
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
23442
|
+
for (const argument of node.arguments ?? []) {
|
|
23443
|
+
if (isFunctionLike$1(argument)) continue;
|
|
23444
|
+
mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
23445
|
+
}
|
|
23169
23446
|
return evidence;
|
|
23170
23447
|
}
|
|
23171
23448
|
if (remainingCallFrames <= 0) {
|
|
23172
23449
|
evidence.hasUnknownSource = true;
|
|
23173
23450
|
return evidence;
|
|
23174
23451
|
}
|
|
23175
|
-
const
|
|
23176
|
-
if (
|
|
23177
|
-
|
|
23452
|
+
const localHelperFunction = resolveWrappedCallable(analysis, callee);
|
|
23453
|
+
if (localHelperFunction && !isModuleFunction(localHelperFunction)) {
|
|
23454
|
+
if (isAsyncOrGeneratorFunction(localHelperFunction) || functionInvokesItself(analysis, localHelperFunction)) {
|
|
23455
|
+
evidence.hasUnknownSource = true;
|
|
23456
|
+
return evidence;
|
|
23457
|
+
}
|
|
23458
|
+
const localHelperFrame = {
|
|
23459
|
+
functionNode: localHelperFunction,
|
|
23460
|
+
invocation: node,
|
|
23461
|
+
isDeferred: false,
|
|
23462
|
+
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23463
|
+
substitutions: buildSubstitutions(analysis, localHelperFunction, node.arguments ?? [], frame),
|
|
23464
|
+
currentFilename: frame.currentFilename
|
|
23465
|
+
};
|
|
23466
|
+
const returnedExpressions = getReturnedExpressions(localHelperFunction);
|
|
23467
|
+
if (returnedExpressions.length === 0) {
|
|
23468
|
+
evidence.hasUnknownSource = true;
|
|
23469
|
+
return evidence;
|
|
23470
|
+
}
|
|
23471
|
+
for (const returnedExpression of returnedExpressions) mergeEvidence(evidence, collectValueEvidence(analysis, returnedExpression, localHelperFrame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
23178
23472
|
return evidence;
|
|
23179
23473
|
}
|
|
23180
|
-
const
|
|
23181
|
-
|
|
23182
|
-
|
|
23183
|
-
isDeferred: false,
|
|
23184
|
-
introducedBindings: /* @__PURE__ */ new Set(),
|
|
23185
|
-
substitutions: buildSubstitutions(analysis, callable, node.arguments ?? [], frame)
|
|
23186
|
-
};
|
|
23187
|
-
const returnedExpressions = getReturnedExpressions(callable);
|
|
23188
|
-
if (returnedExpressions.length === 0) {
|
|
23474
|
+
const helperFunction = resolveValueHelperFunction(analysis, callee, frame.currentFilename);
|
|
23475
|
+
const helperSummary = helperFunction ? summarizeHelperReturn(helperFunction) : null;
|
|
23476
|
+
if (!helperSummary) {
|
|
23189
23477
|
evidence.hasUnknownSource = true;
|
|
23190
23478
|
return evidence;
|
|
23191
23479
|
}
|
|
23192
|
-
|
|
23480
|
+
const argumentsForHelper = node.arguments ?? [];
|
|
23481
|
+
for (const parameterIndex of helperSummary.usedParameterIndices) {
|
|
23482
|
+
const argument = argumentsForHelper[parameterIndex];
|
|
23483
|
+
if (!argument) {
|
|
23484
|
+
evidence.hasUnknownSource = true;
|
|
23485
|
+
return evidence;
|
|
23486
|
+
}
|
|
23487
|
+
mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames - 1, new Set(visitedBindings)));
|
|
23488
|
+
}
|
|
23193
23489
|
return evidence;
|
|
23194
23490
|
}
|
|
23195
23491
|
if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
|
|
@@ -23207,6 +23503,20 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23207
23503
|
}
|
|
23208
23504
|
return evidence;
|
|
23209
23505
|
};
|
|
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
|
+
};
|
|
23210
23520
|
const findStateSetterReference = (analysis, callExpression) => {
|
|
23211
23521
|
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
23212
23522
|
const callee = stripParenExpression(callExpression.callee);
|
|
@@ -23269,8 +23579,8 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
|
|
|
23269
23579
|
}
|
|
23270
23580
|
return false;
|
|
23271
23581
|
};
|
|
23272
|
-
const collectEffectStateWriteFacts = (analysis, effectNode) => {
|
|
23273
|
-
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode);
|
|
23582
|
+
const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
|
|
23583
|
+
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
|
|
23274
23584
|
if (frames.length === 0) return [];
|
|
23275
23585
|
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
23276
23586
|
const cleanupManagedStateDeclarators = /* @__PURE__ */ new Set();
|
|
@@ -23290,7 +23600,8 @@ const collectEffectStateWriteFacts = (analysis, effectNode) => {
|
|
|
23290
23600
|
invocation: callExpression,
|
|
23291
23601
|
isDeferred: frame.isDeferred,
|
|
23292
23602
|
introducedBindings: collectIntroducedBindings(analysis, writtenValue),
|
|
23293
|
-
substitutions: /* @__PURE__ */ new Map()
|
|
23603
|
+
substitutions: /* @__PURE__ */ new Map(),
|
|
23604
|
+
currentFilename
|
|
23294
23605
|
};
|
|
23295
23606
|
valueEvidence = emptyEvidence();
|
|
23296
23607
|
const returnedExpressions = getReturnedExpressions(writtenValue);
|
|
@@ -23339,7 +23650,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
23339
23650
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
23340
23651
|
if (!dependencyReferences) return;
|
|
23341
23652
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
23342
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
23653
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
23343
23654
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
23344
23655
|
context.report({
|
|
23345
23656
|
node: fact.callExpression,
|
|
@@ -24600,6 +24911,139 @@ const createRelativeImportSource = (filename, targetFilePath) => {
|
|
|
24600
24911
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
24601
24912
|
};
|
|
24602
24913
|
//#endregion
|
|
24914
|
+
//#region src/plugin/utils/is-barrel-index-module.ts
|
|
24915
|
+
const INDEX_MODULE_FILE_PATTERN = /^index\.(?:[cm]?[jt]sx?|mjs)$/;
|
|
24916
|
+
const BINDING_IMPORT_DECLARATION_PATTERN = /^\s*import\s+(type\s+)?(?!["'])([^;]*?)\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24917
|
+
const BARREL_REEXPORT_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?(?:\*(?:\s+as\s+([\w$]+))?|\{([\s\S]*?)\})\s+from\s+["']([^"']+)["']\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24918
|
+
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN = /^\s*export\s+(type\s+)?\{([\s\S]*?)\}\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
24919
|
+
const barrelIndexModuleInfoCache = /* @__PURE__ */ new Map();
|
|
24920
|
+
const isIndexModuleFilePath = (filePath) => INDEX_MODULE_FILE_PATTERN.test(path.basename(filePath));
|
|
24921
|
+
const createNonBarrelInfo = () => ({
|
|
24922
|
+
isBarrel: false,
|
|
24923
|
+
exportsByName: /* @__PURE__ */ new Map(),
|
|
24924
|
+
starExportSources: []
|
|
24925
|
+
});
|
|
24926
|
+
const addImportedBinding = (importedBindings, binding) => {
|
|
24927
|
+
importedBindings.set(binding.localName, {
|
|
24928
|
+
...binding,
|
|
24929
|
+
didExport: false
|
|
24930
|
+
});
|
|
24931
|
+
};
|
|
24932
|
+
const collectNamedImportBindings = (namedSpecifiersText, source, declarationIsTypeOnly, importedBindings) => {
|
|
24933
|
+
for (const specifier of parseExportSpecifiers(namedSpecifiersText, declarationIsTypeOnly)) addImportedBinding(importedBindings, {
|
|
24934
|
+
localName: specifier.exportedName,
|
|
24935
|
+
importedName: specifier.localName,
|
|
24936
|
+
source,
|
|
24937
|
+
isTypeOnly: specifier.isTypeOnly
|
|
24938
|
+
});
|
|
24939
|
+
};
|
|
24940
|
+
const collectImportBindings = (importClause, source, declarationIsTypeOnly, importedBindings) => {
|
|
24941
|
+
const trimmedImportClause = importClause.trim();
|
|
24942
|
+
const namespaceMatch = trimmedImportClause.match(/(?:^|,\s*)\*\s+as\s+([\w$]+)/);
|
|
24943
|
+
if (namespaceMatch?.[1]) addImportedBinding(importedBindings, {
|
|
24944
|
+
localName: namespaceMatch[1],
|
|
24945
|
+
importedName: "*",
|
|
24946
|
+
source,
|
|
24947
|
+
isTypeOnly: declarationIsTypeOnly
|
|
24948
|
+
});
|
|
24949
|
+
const namedImportMatch = trimmedImportClause.match(/\{([\s\S]*?)\}/);
|
|
24950
|
+
if (namedImportMatch?.[1]) collectNamedImportBindings(namedImportMatch[1], source, declarationIsTypeOnly, importedBindings);
|
|
24951
|
+
const defaultImportName = trimmedImportClause.split(",")[0]?.trim();
|
|
24952
|
+
if (defaultImportName && !defaultImportName.startsWith("{") && !defaultImportName.startsWith("*")) addImportedBinding(importedBindings, {
|
|
24953
|
+
localName: defaultImportName,
|
|
24954
|
+
importedName: "default",
|
|
24955
|
+
source,
|
|
24956
|
+
isTypeOnly: declarationIsTypeOnly
|
|
24957
|
+
});
|
|
24958
|
+
};
|
|
24959
|
+
const replaceKnownDeclarations = (sourceText, importedBindings, exportsByName, starExportSources) => {
|
|
24960
|
+
let withoutKnownDeclarations = sourceText.replace(BINDING_IMPORT_DECLARATION_PATTERN, (_match, typeKeyword, importClause, source) => {
|
|
24961
|
+
collectImportBindings(importClause, source, Boolean(typeKeyword), importedBindings);
|
|
24962
|
+
return "";
|
|
24963
|
+
});
|
|
24964
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(BARREL_REEXPORT_DECLARATION_PATTERN, (_match, typeKeyword, namespaceExportName, specifiersText, source) => {
|
|
24965
|
+
const isTypeOnly = Boolean(typeKeyword);
|
|
24966
|
+
if (namespaceExportName) {
|
|
24967
|
+
exportsByName.set(namespaceExportName, {
|
|
24968
|
+
exportedName: namespaceExportName,
|
|
24969
|
+
importedName: "*",
|
|
24970
|
+
source,
|
|
24971
|
+
isTypeOnly
|
|
24972
|
+
});
|
|
24973
|
+
return "";
|
|
24974
|
+
}
|
|
24975
|
+
if (specifiersText) {
|
|
24976
|
+
for (const specifier of parseExportSpecifiers(specifiersText, isTypeOnly)) exportsByName.set(specifier.exportedName, {
|
|
24977
|
+
exportedName: specifier.exportedName,
|
|
24978
|
+
importedName: specifier.localName,
|
|
24979
|
+
source,
|
|
24980
|
+
isTypeOnly: specifier.isTypeOnly
|
|
24981
|
+
});
|
|
24982
|
+
return "";
|
|
24983
|
+
}
|
|
24984
|
+
starExportSources.push(source);
|
|
24985
|
+
return "";
|
|
24986
|
+
});
|
|
24987
|
+
withoutKnownDeclarations = withoutKnownDeclarations.replace(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN, (_match, typeKeyword, specifiersText) => {
|
|
24988
|
+
for (const specifier of parseExportSpecifiers(specifiersText, Boolean(typeKeyword))) {
|
|
24989
|
+
const importedBinding = importedBindings.get(specifier.localName);
|
|
24990
|
+
if (!importedBinding) return _match;
|
|
24991
|
+
importedBinding.didExport = true;
|
|
24992
|
+
exportsByName.set(specifier.exportedName, {
|
|
24993
|
+
exportedName: specifier.exportedName,
|
|
24994
|
+
importedName: importedBinding.importedName,
|
|
24995
|
+
source: importedBinding.source,
|
|
24996
|
+
isTypeOnly: specifier.isTypeOnly || importedBinding.isTypeOnly
|
|
24997
|
+
});
|
|
24998
|
+
}
|
|
24999
|
+
return "";
|
|
25000
|
+
});
|
|
25001
|
+
return withoutKnownDeclarations;
|
|
25002
|
+
};
|
|
25003
|
+
const hasUnexportedRuntimeImport = (importedBindings) => {
|
|
25004
|
+
for (const binding of importedBindings.values()) if (!binding.isTypeOnly && !binding.didExport) return true;
|
|
25005
|
+
return false;
|
|
25006
|
+
};
|
|
25007
|
+
const classifyBarrelModule = (sourceText) => {
|
|
25008
|
+
const strippedSource = stripJsComments(sourceText).trim();
|
|
25009
|
+
if (!strippedSource) return createNonBarrelInfo();
|
|
25010
|
+
const importedBindings = /* @__PURE__ */ new Map();
|
|
25011
|
+
const exportsByName = /* @__PURE__ */ new Map();
|
|
25012
|
+
const starExportSources = [];
|
|
25013
|
+
if (replaceKnownDeclarations(strippedSource, importedBindings, exportsByName, starExportSources).trim() || hasUnexportedRuntimeImport(importedBindings)) return createNonBarrelInfo();
|
|
25014
|
+
return {
|
|
25015
|
+
isBarrel: exportsByName.size > 0 || starExportSources.length > 0,
|
|
25016
|
+
exportsByName,
|
|
25017
|
+
starExportSources
|
|
25018
|
+
};
|
|
25019
|
+
};
|
|
25020
|
+
const getBarrelIndexModuleInfo = (filePath) => {
|
|
25021
|
+
if (!isIndexModuleFilePath(filePath)) return createNonBarrelInfo();
|
|
25022
|
+
recordContentProbe(filePath);
|
|
25023
|
+
let fileStat;
|
|
25024
|
+
try {
|
|
25025
|
+
fileStat = fs.statSync(filePath);
|
|
25026
|
+
} catch {
|
|
25027
|
+
fileStat = null;
|
|
25028
|
+
}
|
|
25029
|
+
const cachedResult = barrelIndexModuleInfoCache.get(filePath);
|
|
25030
|
+
if (cachedResult !== void 0 && fileStat !== null && cachedResult.mtimeMs === fileStat.mtimeMs && cachedResult.size === fileStat.size) return cachedResult.moduleInfo;
|
|
25031
|
+
if (fileStat === null) return createNonBarrelInfo();
|
|
25032
|
+
let moduleInfo = createNonBarrelInfo();
|
|
25033
|
+
try {
|
|
25034
|
+
moduleInfo = classifyBarrelModule(fs.readFileSync(filePath, "utf8"));
|
|
25035
|
+
} catch {
|
|
25036
|
+
moduleInfo = createNonBarrelInfo();
|
|
25037
|
+
}
|
|
25038
|
+
barrelIndexModuleInfoCache.set(filePath, {
|
|
25039
|
+
mtimeMs: fileStat.mtimeMs,
|
|
25040
|
+
size: fileStat.size,
|
|
25041
|
+
moduleInfo
|
|
25042
|
+
});
|
|
25043
|
+
return moduleInfo;
|
|
25044
|
+
};
|
|
25045
|
+
const isBarrelIndexModule = (filePath) => getBarrelIndexModuleInfo(filePath).isBarrel;
|
|
25046
|
+
//#endregion
|
|
24603
25047
|
//#region src/react-native-dependency-names.ts
|
|
24604
25048
|
const EXPO_MANAGED_DEPENDENCY_NAMES = new Set([
|
|
24605
25049
|
"expo",
|
|
@@ -24802,6 +25246,35 @@ const classifyReactNativeFileTarget = (context) => {
|
|
|
24802
25246
|
};
|
|
24803
25247
|
const isReactNativeFileActive = (context) => classifyReactNativeFileTarget(context) !== "web";
|
|
24804
25248
|
//#endregion
|
|
25249
|
+
//#region src/plugin/utils/resolve-barrel-export-file-path.ts
|
|
25250
|
+
const getUniqueFilePath = (filePaths) => {
|
|
25251
|
+
const uniqueFilePaths = new Set(filePaths);
|
|
25252
|
+
if (uniqueFilePaths.size !== 1) return null;
|
|
25253
|
+
const [filePath] = uniqueFilePaths;
|
|
25254
|
+
return filePath ?? null;
|
|
25255
|
+
};
|
|
25256
|
+
const resolveStarExportFilePath = (barrelFilePath, exportedName, source, visitedFilePaths) => {
|
|
25257
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, source);
|
|
25258
|
+
if (!resolvedTargetPath) return null;
|
|
25259
|
+
const nestedTargetPath = resolveBarrelExportFilePath(resolvedTargetPath, exportedName, new Set(visitedFilePaths));
|
|
25260
|
+
if (nestedTargetPath) return nestedTargetPath;
|
|
25261
|
+
return doesModuleExportName(resolvedTargetPath, exportedName) ? resolvedTargetPath : null;
|
|
25262
|
+
};
|
|
25263
|
+
const resolveBarrelExportFilePath = (barrelFilePath, exportedName, visitedFilePaths = /* @__PURE__ */ new Set()) => {
|
|
25264
|
+
if (visitedFilePaths.has(barrelFilePath)) return null;
|
|
25265
|
+
visitedFilePaths.add(barrelFilePath);
|
|
25266
|
+
const moduleInfo = getBarrelIndexModuleInfo(barrelFilePath);
|
|
25267
|
+
if (!moduleInfo.isBarrel) return null;
|
|
25268
|
+
const target = moduleInfo.exportsByName.get(exportedName);
|
|
25269
|
+
if (target) {
|
|
25270
|
+
const resolvedTargetPath = resolveRelativeImportPath(barrelFilePath, target.source);
|
|
25271
|
+
if (!resolvedTargetPath) return null;
|
|
25272
|
+
return resolveBarrelExportFilePath(resolvedTargetPath, target.importedName, visitedFilePaths) ?? resolvedTargetPath;
|
|
25273
|
+
}
|
|
25274
|
+
if (exportedName === "default") return null;
|
|
25275
|
+
return getUniqueFilePath(moduleInfo.starExportSources.map((source) => resolveStarExportFilePath(barrelFilePath, exportedName, source, visitedFilePaths)).filter((filePath) => Boolean(filePath)));
|
|
25276
|
+
};
|
|
25277
|
+
//#endregion
|
|
24805
25278
|
//#region src/plugin/rules/bundle-size/no-barrel-import.ts
|
|
24806
25279
|
const TYPE_DECLARATION_FILE_PATTERN = /\.d\.[cm]?ts$/;
|
|
24807
25280
|
const SERVER_ONLY_FILE_PATTERN = /\.server\.[cm]?[jt]sx?$/;
|
|
@@ -26096,57 +26569,6 @@ const noDefaultProps = defineRule({
|
|
|
26096
26569
|
} })
|
|
26097
26570
|
});
|
|
26098
26571
|
//#endregion
|
|
26099
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state.ts
|
|
26100
|
-
const getStateName$1 = (stateDeclarator) => {
|
|
26101
|
-
if (!isNodeOfType(stateDeclarator, "VariableDeclarator")) return "<state>";
|
|
26102
|
-
if (!isNodeOfType(stateDeclarator.id, "ArrayPattern")) return "<state>";
|
|
26103
|
-
const stateBinding = stateDeclarator.id.elements?.[0];
|
|
26104
|
-
if (stateBinding && isNodeOfType(stateBinding, "Identifier")) return stateBinding.name;
|
|
26105
|
-
const setterBinding = stateDeclarator.id.elements?.[1];
|
|
26106
|
-
if (!setterBinding || !isNodeOfType(setterBinding, "Identifier")) return "<state>";
|
|
26107
|
-
if (!setterBinding.name.startsWith("set") || setterBinding.name.length <= 3) return setterBinding.name;
|
|
26108
|
-
return setterBinding.name[3].toLowerCase() + setterBinding.name.slice(4);
|
|
26109
|
-
};
|
|
26110
|
-
const noDerivedState = defineRule({
|
|
26111
|
-
id: "no-derived-state",
|
|
26112
|
-
title: "Derived value copied into state",
|
|
26113
|
-
severity: "warn",
|
|
26114
|
-
tags: ["test-noise"],
|
|
26115
|
-
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",
|
|
26116
|
-
create: (context) => ({ CallExpression(node) {
|
|
26117
|
-
if (!isUseEffect(node)) return;
|
|
26118
|
-
const analysis = getProgramAnalysis(node);
|
|
26119
|
-
if (!analysis) return;
|
|
26120
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
26121
|
-
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
26122
|
-
const stateName = getStateName$1(fact.stateDeclarator);
|
|
26123
|
-
context.report({
|
|
26124
|
-
node: fact.callExpression,
|
|
26125
|
-
message: `Storing "${stateName}" in state when you can derive it from other values costs an extra render.`
|
|
26126
|
-
});
|
|
26127
|
-
}
|
|
26128
|
-
} })
|
|
26129
|
-
});
|
|
26130
|
-
//#endregion
|
|
26131
|
-
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
26132
|
-
const noDerivedStateEffect = defineRule({
|
|
26133
|
-
id: "no-derived-state-effect",
|
|
26134
|
-
title: "Derived state stored in an effect",
|
|
26135
|
-
severity: "warn",
|
|
26136
|
-
tags: ["test-noise"],
|
|
26137
|
-
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",
|
|
26138
|
-
create: (context) => ({ CallExpression(node) {
|
|
26139
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
26140
|
-
const analysis = getProgramAnalysis(node);
|
|
26141
|
-
if (!analysis) return;
|
|
26142
|
-
if (!collectEffectStateWriteFacts(analysis, node).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
26143
|
-
context.report({
|
|
26144
|
-
node,
|
|
26145
|
-
message: "You pay an extra render for state you can derive from other values."
|
|
26146
|
-
});
|
|
26147
|
-
} })
|
|
26148
|
-
});
|
|
26149
|
-
//#endregion
|
|
26150
26572
|
//#region src/plugin/utils/is-component-function.ts
|
|
26151
26573
|
const isFunctionAssignedToComponent = (functionNode) => {
|
|
26152
26574
|
let cursor = functionNode.parent ?? null;
|
|
@@ -26280,6 +26702,236 @@ const createComponentPropStackTracker = (callbacks) => {
|
|
|
26280
26702
|
};
|
|
26281
26703
|
};
|
|
26282
26704
|
//#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
|
|
26283
26935
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
26284
26936
|
const isInitialOnlyPropName = (propName) => {
|
|
26285
26937
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -29643,28 +30295,26 @@ const noGiantComponent = defineRule({
|
|
|
29643
30295
|
const lineCount = bodyNode.loc.end.line - bodyNode.loc.start.line + 1;
|
|
29644
30296
|
return lineCount > 300 ? lineCount : null;
|
|
29645
30297
|
};
|
|
29646
|
-
const reportOversizedComponent = (nameNode, componentName
|
|
30298
|
+
const reportOversizedComponent = (nameNode, componentName) => {
|
|
29647
30299
|
context.report({
|
|
29648
30300
|
node: nameNode,
|
|
29649
|
-
message: `Component "${componentName}" is
|
|
30301
|
+
message: `Component "${componentName}" is over 300 lines long, which is hard to read & change. Split it into a few smaller components.`
|
|
29650
30302
|
});
|
|
29651
30303
|
};
|
|
29652
30304
|
return {
|
|
29653
30305
|
FunctionDeclaration(node) {
|
|
29654
30306
|
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
29655
|
-
|
|
29656
|
-
if (lineCount === null) return;
|
|
30307
|
+
if (getOversizedComponentLineCount(node) === null) return;
|
|
29657
30308
|
if (!functionContainsReactRenderOutput(node, context.scopes)) return;
|
|
29658
|
-
reportOversizedComponent(node.id, node.id.name
|
|
30309
|
+
reportOversizedComponent(node.id, node.id.name);
|
|
29659
30310
|
},
|
|
29660
30311
|
VariableDeclarator(node) {
|
|
29661
30312
|
if (!isNodeOfType(node.id, "Identifier") || !isUppercaseName(node.id.name)) return;
|
|
29662
30313
|
const functionNode = unwrapReactHocFunction(node.init);
|
|
29663
30314
|
if (!functionNode) return;
|
|
29664
|
-
|
|
29665
|
-
if (lineCount === null) return;
|
|
30315
|
+
if (getOversizedComponentLineCount(functionNode) === null) return;
|
|
29666
30316
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
29667
|
-
reportOversizedComponent(node.id, node.id.name
|
|
30317
|
+
reportOversizedComponent(node.id, node.id.name);
|
|
29668
30318
|
}
|
|
29669
30319
|
};
|
|
29670
30320
|
}
|
|
@@ -30301,6 +30951,131 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
30301
30951
|
} })
|
|
30302
30952
|
});
|
|
30303
30953
|
//#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
|
|
30304
31079
|
//#region src/plugin/rules/correctness/no-indeterminate-attribute.ts
|
|
30305
31080
|
const MESSAGE$21 = "The `indeterminate` HTML attribute does not set a checkbox's visual state. Assign the `HTMLInputElement.indeterminate` DOM property instead.";
|
|
30306
31081
|
const REACT_USE_REF_OPTIONS = {
|
|
@@ -30467,7 +31242,7 @@ const noInitializeState = defineRule({
|
|
|
30467
31242
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
30468
31243
|
const analysis = getProgramAnalysis(node);
|
|
30469
31244
|
if (!analysis) return;
|
|
30470
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node)) {
|
|
31245
|
+
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
30471
31246
|
if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
|
|
30472
31247
|
const stateName = getStateName(fact.stateDeclarator);
|
|
30473
31248
|
context.report({
|
|
@@ -33469,6 +34244,166 @@ const isDirectParentCallbackRef = (analysis, ref) => {
|
|
|
33469
34244
|
return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
|
|
33470
34245
|
}));
|
|
33471
34246
|
};
|
|
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
|
+
};
|
|
33472
34407
|
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
33473
34408
|
const node = def.node;
|
|
33474
34409
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
@@ -33524,71 +34459,79 @@ const noPassDataToParent = defineRule({
|
|
|
33524
34459
|
severity: "warn",
|
|
33525
34460
|
tags: ["test-noise"],
|
|
33526
34461
|
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",
|
|
33527
|
-
create: (context) =>
|
|
33528
|
-
|
|
33529
|
-
|
|
33530
|
-
|
|
33531
|
-
|
|
33532
|
-
|
|
33533
|
-
|
|
33534
|
-
|
|
33535
|
-
|
|
33536
|
-
|
|
33537
|
-
const
|
|
33538
|
-
if (!
|
|
33539
|
-
|
|
33540
|
-
if (!
|
|
33541
|
-
const
|
|
33542
|
-
|
|
33543
|
-
|
|
33544
|
-
|
|
33545
|
-
if (
|
|
33546
|
-
|
|
33547
|
-
|
|
33548
|
-
|
|
33549
|
-
|
|
33550
|
-
|
|
33551
|
-
|
|
33552
|
-
|
|
33553
|
-
|
|
33554
|
-
|
|
33555
|
-
|
|
33556
|
-
|
|
33557
|
-
|
|
33558
|
-
|
|
33559
|
-
|
|
33560
|
-
|
|
33561
|
-
|
|
33562
|
-
|
|
33563
|
-
|
|
33564
|
-
|
|
33565
|
-
|
|
33566
|
-
|
|
33567
|
-
|
|
33568
|
-
|
|
33569
|
-
|
|
33570
|
-
|
|
33571
|
-
|
|
33572
|
-
|
|
33573
|
-
|
|
33574
|
-
|
|
33575
|
-
|
|
33576
|
-
|
|
33577
|
-
|
|
33578
|
-
if (
|
|
33579
|
-
if (
|
|
33580
|
-
|
|
33581
|
-
|
|
33582
|
-
|
|
33583
|
-
|
|
33584
|
-
|
|
33585
|
-
|
|
33586
|
-
|
|
33587
|
-
|
|
33588
|
-
|
|
33589
|
-
|
|
33590
|
-
|
|
33591
|
-
|
|
34462
|
+
create: (context) => {
|
|
34463
|
+
const isReactUseRefCall = (node) => isReactApiCall(node, "useRef", context.scopes, {
|
|
34464
|
+
allowGlobalReactNamespace: true,
|
|
34465
|
+
allowUnboundBareCalls: true
|
|
34466
|
+
});
|
|
34467
|
+
return { CallExpression(node) {
|
|
34468
|
+
if (!isUseEffect(node)) return;
|
|
34469
|
+
const analysis = getProgramAnalysis(node);
|
|
34470
|
+
if (!analysis) return;
|
|
34471
|
+
if (hasCleanup(analysis, node)) return;
|
|
34472
|
+
const effectFnRefs = getEffectFnRefs(analysis, node);
|
|
34473
|
+
if (!effectFnRefs) return;
|
|
34474
|
+
const effectFn = getEffectFn(analysis, node);
|
|
34475
|
+
if (!effectFn) return;
|
|
34476
|
+
for (const ref of effectFnRefs) {
|
|
34477
|
+
const callExpr = getCallExpr(ref);
|
|
34478
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
34479
|
+
const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall);
|
|
34480
|
+
if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
|
|
34481
|
+
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
34482
|
+
const calleeNode = unwrapChainExpression$1(callExpr.callee);
|
|
34483
|
+
const identifier = ref.identifier;
|
|
34484
|
+
if (callbackRefProvenance) {
|
|
34485
|
+
if ([...callbackRefProvenance.callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
|
|
34486
|
+
} else if (calleeNode === identifier) {
|
|
34487
|
+
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
34488
|
+
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
34489
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
34490
|
+
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
34491
|
+
if (isCustomHookParameter(ref)) continue;
|
|
34492
|
+
} else continue;
|
|
34493
|
+
const methodName = getCallMethodName(calleeNode);
|
|
34494
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
34495
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
34496
|
+
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
34497
|
+
if (!callbackRefProvenance && isNamespacedApiCallee(calleeNode)) continue;
|
|
34498
|
+
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) ?? ""));
|
|
34499
|
+
const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
|
|
34500
|
+
const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
|
|
34501
|
+
if (isFunctionLike$1(argument)) {
|
|
34502
|
+
if (!isSetterNamedCallee) return [];
|
|
34503
|
+
return getFunctionalUpdaterDataRefs(analysis, argument);
|
|
34504
|
+
}
|
|
34505
|
+
if (isHandlerBagArgument(analysis, argument)) return [];
|
|
34506
|
+
if (isParentWiredHookResultArgument(analysis, argument)) return [];
|
|
34507
|
+
if (isNodeOfType(argument, "Identifier")) {
|
|
34508
|
+
const argumentRef = getRef(analysis, argument);
|
|
34509
|
+
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
34510
|
+
}
|
|
34511
|
+
return getDownstreamRefs(analysis, argument);
|
|
34512
|
+
}).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
34513
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
34514
|
+
if (!argsUpstreamRefs.some((argRef) => {
|
|
34515
|
+
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
34516
|
+
if (isProp(analysis, argRef)) return false;
|
|
34517
|
+
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
34518
|
+
if (isRefCurrent(argRef)) return false;
|
|
34519
|
+
if (isConstant(argRef)) return false;
|
|
34520
|
+
if (isParentWiredHookResultRef(analysis, argRef)) return false;
|
|
34521
|
+
if (isParentWiredHookCalleeRef(analysis, argRef)) return false;
|
|
34522
|
+
if (resolvesToFunctionBinding(argRef)) return false;
|
|
34523
|
+
const argIdentifier = argRef.identifier;
|
|
34524
|
+
if (isImportBindingRef(argRef) && !isCalleePosition(argIdentifier)) return false;
|
|
34525
|
+
if (isNodeOfType(argIdentifier, "Identifier") && argIdentifier.name === "undefined") return false;
|
|
34526
|
+
return true;
|
|
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
|
+
}
|
|
33592
34535
|
});
|
|
33593
34536
|
//#endregion
|
|
33594
34537
|
//#region src/plugin/utils/is-call-result-consumed-as-argument.ts
|
|
@@ -34142,6 +35085,66 @@ const noPropCallbackInEffect = defineRule({
|
|
|
34142
35085
|
}
|
|
34143
35086
|
});
|
|
34144
35087
|
//#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
|
|
34145
35148
|
//#region src/plugin/rules/architecture/no-prop-types.ts
|
|
34146
35149
|
const PROP_TYPES_PROPERTY = "propTypes";
|
|
34147
35150
|
const isPropTypesKey = (key, computed) => {
|
|
@@ -34836,6 +35839,63 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
34836
35839
|
}
|
|
34837
35840
|
});
|
|
34838
35841
|
//#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
|
|
34839
35899
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
34840
35900
|
const isInsideComponentContext = (node) => {
|
|
34841
35901
|
let cursor = node.parent;
|
|
@@ -55798,6 +56858,18 @@ const reactDoctorRules = [
|
|
|
55798
56858
|
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
55799
56859
|
}
|
|
55800
56860
|
},
|
|
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
|
+
},
|
|
55801
56873
|
{
|
|
55802
56874
|
key: "react-doctor/no-indeterminate-attribute",
|
|
55803
56875
|
id: "no-indeterminate-attribute",
|
|
@@ -56214,6 +57286,18 @@ const reactDoctorRules = [
|
|
|
56214
57286
|
requires: [...new Set(["react", ...noPropCallbackInEffect.requires ?? []])]
|
|
56215
57287
|
}
|
|
56216
57288
|
},
|
|
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
|
+
},
|
|
56217
57301
|
{
|
|
56218
57302
|
key: "react-doctor/no-prop-types",
|
|
56219
57303
|
id: "no-prop-types",
|
|
@@ -56305,6 +57389,18 @@ const reactDoctorRules = [
|
|
|
56305
57389
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
56306
57390
|
}
|
|
56307
57391
|
},
|
|
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
|
+
},
|
|
56308
57404
|
{
|
|
56309
57405
|
key: "react-doctor/no-render-in-render",
|
|
56310
57406
|
id: "no-render-in-render",
|
|
@@ -58897,6 +59993,11 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
58897
59993
|
"no-indeterminate-attribute",
|
|
58898
59994
|
"no-locale-format-in-render",
|
|
58899
59995
|
"no-match-media-in-state-initializer",
|
|
59996
|
+
"no-adjust-state-on-prop-change",
|
|
59997
|
+
"no-derived-state",
|
|
59998
|
+
"no-derived-state-effect",
|
|
59999
|
+
"no-event-handler",
|
|
60000
|
+
"no-initialize-state",
|
|
58900
60001
|
"no-mutating-reducer-state",
|
|
58901
60002
|
"no-unguarded-browser-global-in-render-or-hook-init",
|
|
58902
60003
|
"prefer-dynamic-import",
|
|
@@ -58950,6 +60051,9 @@ const collectNextjsSearchParamsDependencies = ({ absoluteFilePath, staticImports
|
|
|
58950
60051
|
if (hasAncestorSuspenseLayout(absoluteFilePath)) return;
|
|
58951
60052
|
for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
|
|
58952
60053
|
};
|
|
60054
|
+
const collectEffectValueHelperDependencies = ({ absoluteFilePath, staticImports }) => {
|
|
60055
|
+
for (const entry of flattenImportEntries(staticImports)) resolveCrossFileFunctionExport(absoluteFilePath, entry.source, entry.exportedName);
|
|
60056
|
+
};
|
|
58953
60057
|
const collectMutatingReducerDependencies = ({ absoluteFilePath, sourceText, staticImports, program }) => {
|
|
58954
60058
|
const namedUseReducerLocals = /* @__PURE__ */ new Set();
|
|
58955
60059
|
const reactObjectLocals = /* @__PURE__ */ new Set();
|
|
@@ -59036,6 +60140,11 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
59036
60140
|
["no-indeterminate-attribute", collectNearestManifestDependencies],
|
|
59037
60141
|
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
59038
60142
|
["no-match-media-in-state-initializer", collectNearestManifestDependencies],
|
|
60143
|
+
["no-adjust-state-on-prop-change", collectEffectValueHelperDependencies],
|
|
60144
|
+
["no-derived-state", collectEffectValueHelperDependencies],
|
|
60145
|
+
["no-derived-state-effect", collectEffectValueHelperDependencies],
|
|
60146
|
+
["no-event-handler", collectEffectValueHelperDependencies],
|
|
60147
|
+
["no-initialize-state", collectEffectValueHelperDependencies],
|
|
59039
60148
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
59040
60149
|
["no-unguarded-browser-global-in-render-or-hook-init", collectNearestManifestDependencies],
|
|
59041
60150
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|