oxlint-plugin-react-doctor 0.7.2-dev.08b768b → 0.7.2-dev.11e9c87
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.js +155 -350
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -532,12 +532,6 @@ const TIMER_AND_SCHEDULER_DIRECT_CALLEE_NAMES = new Set([
|
|
|
532
532
|
]);
|
|
533
533
|
const TIMER_CALLEE_NAMES_REQUIRING_CLEANUP = new Set(["setInterval", "setTimeout"]);
|
|
534
534
|
const TIMER_CLEANUP_CALLEE_NAMES = new Set(["clearInterval", "clearTimeout"]);
|
|
535
|
-
const SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP = new Set([
|
|
536
|
-
"WebSocket",
|
|
537
|
-
"EventSource",
|
|
538
|
-
"BroadcastChannel",
|
|
539
|
-
"RTCPeerConnection"
|
|
540
|
-
]);
|
|
541
535
|
const MUTABLE_GLOBAL_ROOTS = new Set([
|
|
542
536
|
"location",
|
|
543
537
|
"window",
|
|
@@ -693,10 +687,7 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
|
|
|
693
687
|
"unwatch",
|
|
694
688
|
"unlisten",
|
|
695
689
|
"unsub",
|
|
696
|
-
"abort"
|
|
697
|
-
"disconnect",
|
|
698
|
-
"unobserve",
|
|
699
|
-
"close"
|
|
690
|
+
"abort"
|
|
700
691
|
]);
|
|
701
692
|
const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
|
|
702
693
|
"remove",
|
|
@@ -7657,95 +7648,6 @@ const displayName = defineRule({
|
|
|
7657
7648
|
}
|
|
7658
7649
|
});
|
|
7659
7650
|
//#endregion
|
|
7660
|
-
//#region src/plugin/utils/is-react-hook-name.ts
|
|
7661
|
-
const isReactHookName = (name) => {
|
|
7662
|
-
if (!name.startsWith("use")) return false;
|
|
7663
|
-
if (name.length === 3) return true;
|
|
7664
|
-
const fourthCharacter = name.charCodeAt(3);
|
|
7665
|
-
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
7666
|
-
};
|
|
7667
|
-
//#endregion
|
|
7668
|
-
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
7669
|
-
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
7670
|
-
//#endregion
|
|
7671
|
-
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
7672
|
-
const hocWrapperCalleeName = (callee) => {
|
|
7673
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
7674
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
7675
|
-
return null;
|
|
7676
|
-
};
|
|
7677
|
-
const displayNameFromFunctionBinding = (functionNode) => {
|
|
7678
|
-
let current = functionNode;
|
|
7679
|
-
for (;;) {
|
|
7680
|
-
const parent = current.parent;
|
|
7681
|
-
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
7682
|
-
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
7683
|
-
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
7684
|
-
current = parent;
|
|
7685
|
-
continue;
|
|
7686
|
-
}
|
|
7687
|
-
}
|
|
7688
|
-
break;
|
|
7689
|
-
}
|
|
7690
|
-
const binding = current.parent;
|
|
7691
|
-
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
7692
|
-
return null;
|
|
7693
|
-
};
|
|
7694
|
-
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
7695
|
-
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
7696
|
-
return displayNameFromFunctionBinding(functionNode);
|
|
7697
|
-
};
|
|
7698
|
-
//#endregion
|
|
7699
|
-
//#region src/plugin/utils/find-enclosing-function.ts
|
|
7700
|
-
const findEnclosingFunction = (node) => {
|
|
7701
|
-
let cursor = node.parent;
|
|
7702
|
-
while (cursor) {
|
|
7703
|
-
if (isFunctionLike$1(cursor)) return cursor;
|
|
7704
|
-
cursor = cursor.parent ?? null;
|
|
7705
|
-
}
|
|
7706
|
-
return null;
|
|
7707
|
-
};
|
|
7708
|
-
//#endregion
|
|
7709
|
-
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
7710
|
-
const enclosingComponentOrHookName = (node) => {
|
|
7711
|
-
const functionNode = findEnclosingFunction(node);
|
|
7712
|
-
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
7713
|
-
};
|
|
7714
|
-
//#endregion
|
|
7715
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
7716
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
7717
|
-
let node = callExpression;
|
|
7718
|
-
let parent = node.parent;
|
|
7719
|
-
while (parent) {
|
|
7720
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
7721
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
7722
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
7723
|
-
node = parent;
|
|
7724
|
-
parent = node.parent;
|
|
7725
|
-
continue;
|
|
7726
|
-
}
|
|
7727
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
7728
|
-
node = parent;
|
|
7729
|
-
parent = node.parent;
|
|
7730
|
-
continue;
|
|
7731
|
-
}
|
|
7732
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
7733
|
-
node = parent;
|
|
7734
|
-
parent = node.parent;
|
|
7735
|
-
continue;
|
|
7736
|
-
}
|
|
7737
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
7738
|
-
const expressions = parent.expressions ?? [];
|
|
7739
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
7740
|
-
node = parent;
|
|
7741
|
-
parent = node.parent;
|
|
7742
|
-
continue;
|
|
7743
|
-
}
|
|
7744
|
-
return false;
|
|
7745
|
-
}
|
|
7746
|
-
return false;
|
|
7747
|
-
};
|
|
7748
|
-
//#endregion
|
|
7749
7651
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
7750
7652
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
7751
7653
|
if (!node || typeof node !== "object") return;
|
|
@@ -7897,17 +7799,6 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
7897
7799
|
};
|
|
7898
7800
|
//#endregion
|
|
7899
7801
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
7900
|
-
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
7901
|
-
const RESOURCE_NOUN_BY_KIND = {
|
|
7902
|
-
subscribe: "subscription",
|
|
7903
|
-
timer: "timer",
|
|
7904
|
-
socket: "connection"
|
|
7905
|
-
};
|
|
7906
|
-
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
7907
|
-
const isSubscribeOrObserveCall = (node) => {
|
|
7908
|
-
if (isSubscribeLikeCallExpression(node)) return true;
|
|
7909
|
-
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
7910
|
-
};
|
|
7911
7802
|
const findSubscribeLikeUsages = (callback) => {
|
|
7912
7803
|
const usages = [];
|
|
7913
7804
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
@@ -7919,14 +7810,6 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7919
7810
|
}
|
|
7920
7811
|
walkAst(callback, (child) => {
|
|
7921
7812
|
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
7922
|
-
if (isSocketConstruction(child)) {
|
|
7923
|
-
usages.push({
|
|
7924
|
-
kind: "socket",
|
|
7925
|
-
node: child,
|
|
7926
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
7927
|
-
});
|
|
7928
|
-
return;
|
|
7929
|
-
}
|
|
7930
7813
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7931
7814
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
7932
7815
|
usages.push({
|
|
@@ -7936,7 +7819,7 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7936
7819
|
});
|
|
7937
7820
|
return;
|
|
7938
7821
|
}
|
|
7939
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") &&
|
|
7822
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
7940
7823
|
kind: "subscribe",
|
|
7941
7824
|
node: child,
|
|
7942
7825
|
resourceName: child.callee.property.name
|
|
@@ -7959,12 +7842,7 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7959
7842
|
const bindingName = declarator.id.name;
|
|
7960
7843
|
bindings.effectScopeVariableNames.add(bindingName);
|
|
7961
7844
|
const init = declarator.init;
|
|
7962
|
-
if (!init) continue;
|
|
7963
|
-
if (isSocketConstruction(init)) {
|
|
7964
|
-
bindings.subscriptionNames.add(bindingName);
|
|
7965
|
-
continue;
|
|
7966
|
-
}
|
|
7967
|
-
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
7845
|
+
if (!init || !isNodeOfType(init, "CallExpression")) continue;
|
|
7968
7846
|
if (isSubscribeLikeCallExpression(init)) {
|
|
7969
7847
|
bindings.subscriptionNames.add(bindingName);
|
|
7970
7848
|
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
@@ -8016,168 +7894,26 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
8016
7894
|
});
|
|
8017
7895
|
return didFindCleanupReturn;
|
|
8018
7896
|
};
|
|
8019
|
-
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8020
|
-
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some((property) => isNodeOfType(property, "Property") && isNodeOfType(property.key, "Identifier") && (property.key.name === "signal" || property.key.name === "once" && isNodeOfType(property.value, "Literal") && property.value.value === true)));
|
|
8021
|
-
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8022
|
-
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8023
|
-
["addListener", new Set([
|
|
8024
|
-
"removeListener",
|
|
8025
|
-
"off",
|
|
8026
|
-
"abort"
|
|
8027
|
-
])],
|
|
8028
|
-
["on", new Set([
|
|
8029
|
-
"off",
|
|
8030
|
-
"removeListener",
|
|
8031
|
-
"on"
|
|
8032
|
-
])],
|
|
8033
|
-
["subscribe", new Set(["unsubscribe", "unsub"])],
|
|
8034
|
-
["sub", new Set(["unsub", "unsubscribe"])],
|
|
8035
|
-
["watch", new Set(["unwatch", "close"])],
|
|
8036
|
-
["listen", new Set(["unlisten", "close"])],
|
|
8037
|
-
[OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
|
|
8038
|
-
]);
|
|
8039
|
-
const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
8040
|
-
"cleanup",
|
|
8041
|
-
"dispose",
|
|
8042
|
-
"destroy",
|
|
8043
|
-
"teardown"
|
|
8044
|
-
]);
|
|
8045
|
-
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8046
|
-
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8047
|
-
const getReleaseVerbName = (node) => {
|
|
8048
|
-
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8049
|
-
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8050
|
-
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8051
|
-
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8052
|
-
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8053
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
8054
|
-
return null;
|
|
8055
|
-
};
|
|
8056
|
-
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8057
|
-
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8058
|
-
let didFindPairedRelease = false;
|
|
8059
|
-
walkAst(body, (child) => {
|
|
8060
|
-
if (didFindPairedRelease) return false;
|
|
8061
|
-
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8062
|
-
const releaseVerbName = getReleaseVerbName(child);
|
|
8063
|
-
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8064
|
-
didFindPairedRelease = true;
|
|
8065
|
-
return false;
|
|
8066
|
-
}
|
|
8067
|
-
});
|
|
8068
|
-
return didFindPairedRelease;
|
|
8069
|
-
};
|
|
8070
|
-
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8071
|
-
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8072
|
-
let programNode = anyNode;
|
|
8073
|
-
while (programNode.parent) programNode = programNode.parent;
|
|
8074
|
-
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8075
|
-
if (cached) return cached;
|
|
8076
|
-
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8077
|
-
walkAst(programNode, (child) => {
|
|
8078
|
-
const releaseVerbName = getReleaseVerbName(child);
|
|
8079
|
-
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8080
|
-
});
|
|
8081
|
-
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8082
|
-
return releaseVerbNames;
|
|
8083
|
-
};
|
|
8084
|
-
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8085
|
-
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8086
|
-
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8087
|
-
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8088
|
-
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
8089
|
-
return false;
|
|
8090
|
-
};
|
|
8091
|
-
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
8092
|
-
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8093
|
-
const body = retainedFunction.body;
|
|
8094
|
-
if (!body) return null;
|
|
8095
|
-
let leak = null;
|
|
8096
|
-
walkAst(body, (child) => {
|
|
8097
|
-
if (leak !== null) return false;
|
|
8098
|
-
if (isFunctionLike$1(child)) return false;
|
|
8099
|
-
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8100
|
-
leak = {
|
|
8101
|
-
kind: "socket",
|
|
8102
|
-
node: child,
|
|
8103
|
-
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8104
|
-
};
|
|
8105
|
-
return false;
|
|
8106
|
-
}
|
|
8107
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8108
|
-
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8109
|
-
leak = {
|
|
8110
|
-
kind: "timer",
|
|
8111
|
-
node: child,
|
|
8112
|
-
resourceName: "setInterval"
|
|
8113
|
-
};
|
|
8114
|
-
return false;
|
|
8115
|
-
}
|
|
8116
|
-
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8117
|
-
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8118
|
-
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
8119
|
-
kind: "subscribe",
|
|
8120
|
-
node: child,
|
|
8121
|
-
resourceName: registrationVerbName
|
|
8122
|
-
};
|
|
8123
|
-
return false;
|
|
8124
|
-
}
|
|
8125
|
-
});
|
|
8126
|
-
return leak;
|
|
8127
|
-
};
|
|
8128
|
-
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
8129
|
-
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
8130
|
-
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
8131
|
-
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8132
|
-
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8133
|
-
};
|
|
8134
7897
|
const effectNeedsCleanup = defineRule({
|
|
8135
7898
|
id: "effect-needs-cleanup",
|
|
8136
7899
|
title: "Effect subscription or timer never cleaned up",
|
|
8137
7900
|
severity: "error",
|
|
8138
7901
|
tags: ["test-noise"],
|
|
8139
|
-
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers,
|
|
8140
|
-
create: (context) => {
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
8155
|
-
return;
|
|
8156
|
-
}
|
|
8157
|
-
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
8158
|
-
const callback = getEffectCallback(node);
|
|
8159
|
-
if (!callback) return;
|
|
8160
|
-
const usages = findSubscribeLikeUsages(callback);
|
|
8161
|
-
if (usages.length === 0) return;
|
|
8162
|
-
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8163
|
-
const firstUsage = usages[0];
|
|
8164
|
-
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
8165
|
-
context.report({
|
|
8166
|
-
node,
|
|
8167
|
-
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8168
|
-
});
|
|
8169
|
-
},
|
|
8170
|
-
FunctionDeclaration(node) {
|
|
8171
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8172
|
-
},
|
|
8173
|
-
ArrowFunctionExpression(node) {
|
|
8174
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8175
|
-
},
|
|
8176
|
-
FunctionExpression(node) {
|
|
8177
|
-
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8178
|
-
}
|
|
8179
|
-
};
|
|
8180
|
-
}
|
|
7902
|
+
recommendation: "Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
7903
|
+
create: (context) => ({ CallExpression(node) {
|
|
7904
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
7905
|
+
const callback = getEffectCallback(node);
|
|
7906
|
+
if (!callback) return;
|
|
7907
|
+
const usages = findSubscribeLikeUsages(callback);
|
|
7908
|
+
if (usages.length === 0) return;
|
|
7909
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
7910
|
+
const firstUsage = usages[0];
|
|
7911
|
+
const resourceKind = firstUsage.kind === "timer" ? "timer" : "subscription";
|
|
7912
|
+
context.report({
|
|
7913
|
+
node,
|
|
7914
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceKind} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
7915
|
+
});
|
|
7916
|
+
} })
|
|
8181
7917
|
});
|
|
8182
7918
|
//#endregion
|
|
8183
7919
|
//#region src/plugin/semantic/scope-analysis.ts
|
|
@@ -8736,6 +8472,17 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
8736
8472
|
return computedCaptures;
|
|
8737
8473
|
};
|
|
8738
8474
|
//#endregion
|
|
8475
|
+
//#region src/plugin/utils/is-react-hook-name.ts
|
|
8476
|
+
const isReactHookName = (name) => {
|
|
8477
|
+
if (!name.startsWith("use")) return false;
|
|
8478
|
+
if (name.length === 3) return true;
|
|
8479
|
+
const fourthCharacter = name.charCodeAt(3);
|
|
8480
|
+
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
8481
|
+
};
|
|
8482
|
+
//#endregion
|
|
8483
|
+
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
8484
|
+
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
8485
|
+
//#endregion
|
|
8739
8486
|
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
8740
8487
|
const reactHocCalleeName = (callee) => {
|
|
8741
8488
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -11550,6 +11297,16 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11550
11297
|
return names;
|
|
11551
11298
|
};
|
|
11552
11299
|
//#endregion
|
|
11300
|
+
//#region src/plugin/utils/find-enclosing-function.ts
|
|
11301
|
+
const findEnclosingFunction = (node) => {
|
|
11302
|
+
let cursor = node.parent;
|
|
11303
|
+
while (cursor) {
|
|
11304
|
+
if (isFunctionLike$1(cursor)) return cursor;
|
|
11305
|
+
cursor = cursor.parent ?? null;
|
|
11306
|
+
}
|
|
11307
|
+
return null;
|
|
11308
|
+
};
|
|
11309
|
+
//#endregion
|
|
11553
11310
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11554
11311
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11555
11312
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -21354,7 +21111,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21354
21111
|
const noAdjustStateOnPropChange = defineRule({
|
|
21355
21112
|
id: "no-adjust-state-on-prop-change",
|
|
21356
21113
|
title: "State synced to a prop inside an effect",
|
|
21357
|
-
severity: "
|
|
21114
|
+
severity: "error",
|
|
21358
21115
|
tags: ["test-noise"],
|
|
21359
21116
|
recommendation: "Adjust the state inline during render with a `prev`-prop comparison (`if (prop !== prevProp) { setPrevProp(prop); setX(...); }`), or refactor to remove the duplicated state. Routing the adjustment through a useEffect forces an extra render with a stale UI between the two commits. See https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes",
|
|
21360
21117
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -23722,6 +23479,40 @@ const noCloneElement = defineRule({
|
|
|
23722
23479
|
} })
|
|
23723
23480
|
});
|
|
23724
23481
|
//#endregion
|
|
23482
|
+
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
23483
|
+
const hocWrapperCalleeName = (callee) => {
|
|
23484
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
23485
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
23486
|
+
return null;
|
|
23487
|
+
};
|
|
23488
|
+
const displayNameFromFunctionBinding = (functionNode) => {
|
|
23489
|
+
let current = functionNode;
|
|
23490
|
+
for (;;) {
|
|
23491
|
+
const parent = current.parent;
|
|
23492
|
+
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
23493
|
+
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
23494
|
+
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
23495
|
+
current = parent;
|
|
23496
|
+
continue;
|
|
23497
|
+
}
|
|
23498
|
+
}
|
|
23499
|
+
break;
|
|
23500
|
+
}
|
|
23501
|
+
const binding = current.parent;
|
|
23502
|
+
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
23503
|
+
return null;
|
|
23504
|
+
};
|
|
23505
|
+
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
23506
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
23507
|
+
return displayNameFromFunctionBinding(functionNode);
|
|
23508
|
+
};
|
|
23509
|
+
//#endregion
|
|
23510
|
+
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
23511
|
+
const enclosingComponentOrHookName = (node) => {
|
|
23512
|
+
const functionNode = findEnclosingFunction(node);
|
|
23513
|
+
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
23514
|
+
};
|
|
23515
|
+
//#endregion
|
|
23725
23516
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23726
23517
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23727
23518
|
const CONTEXT_MODULES = [
|
|
@@ -24735,26 +24526,6 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24735
24526
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24736
24527
|
}
|
|
24737
24528
|
};
|
|
24738
|
-
const flattenGuardedStatements = (statements) => {
|
|
24739
|
-
const flattened = [];
|
|
24740
|
-
for (const statement of statements) {
|
|
24741
|
-
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24742
|
-
flattened.push(statement);
|
|
24743
|
-
continue;
|
|
24744
|
-
}
|
|
24745
|
-
if (isNodeOfType(statement, "IfStatement")) {
|
|
24746
|
-
for (const branch of [statement.consequent, statement.alternate]) {
|
|
24747
|
-
if (!branch) continue;
|
|
24748
|
-
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
24749
|
-
if (flattenedBranch === null) return null;
|
|
24750
|
-
flattened.push(...flattenedBranch);
|
|
24751
|
-
}
|
|
24752
|
-
continue;
|
|
24753
|
-
}
|
|
24754
|
-
return null;
|
|
24755
|
-
}
|
|
24756
|
-
return flattened;
|
|
24757
|
-
};
|
|
24758
24529
|
const noDerivedStateEffect = defineRule({
|
|
24759
24530
|
id: "no-derived-state-effect",
|
|
24760
24531
|
title: "Derived state stored in an effect",
|
|
@@ -24786,8 +24557,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24786
24557
|
}
|
|
24787
24558
|
}
|
|
24788
24559
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24789
|
-
const statements =
|
|
24790
|
-
if (statements
|
|
24560
|
+
const statements = getCallbackStatements(callback);
|
|
24561
|
+
if (statements.length === 0) return;
|
|
24791
24562
|
if (!statements.every((statement) => {
|
|
24792
24563
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24793
24564
|
const expression = statement.expression;
|
|
@@ -29266,7 +29037,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
29266
29037
|
const noJsxElementType = defineRule({
|
|
29267
29038
|
id: "no-jsx-element-type",
|
|
29268
29039
|
title: "No JSX.Element",
|
|
29269
|
-
severity: "
|
|
29040
|
+
severity: "error",
|
|
29270
29041
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
29271
29042
|
create: (context) => {
|
|
29272
29043
|
let isJsxImported = false;
|
|
@@ -30308,6 +30079,40 @@ const noMutableInDeps = defineRule({
|
|
|
30308
30079
|
}
|
|
30309
30080
|
});
|
|
30310
30081
|
//#endregion
|
|
30082
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
30083
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
30084
|
+
let node = callExpression;
|
|
30085
|
+
let parent = node.parent;
|
|
30086
|
+
while (parent) {
|
|
30087
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
30088
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
30089
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
30090
|
+
node = parent;
|
|
30091
|
+
parent = node.parent;
|
|
30092
|
+
continue;
|
|
30093
|
+
}
|
|
30094
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
30095
|
+
node = parent;
|
|
30096
|
+
parent = node.parent;
|
|
30097
|
+
continue;
|
|
30098
|
+
}
|
|
30099
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
30100
|
+
node = parent;
|
|
30101
|
+
parent = node.parent;
|
|
30102
|
+
continue;
|
|
30103
|
+
}
|
|
30104
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
30105
|
+
const expressions = parent.expressions ?? [];
|
|
30106
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
30107
|
+
node = parent;
|
|
30108
|
+
parent = node.parent;
|
|
30109
|
+
continue;
|
|
30110
|
+
}
|
|
30111
|
+
return false;
|
|
30112
|
+
}
|
|
30113
|
+
return false;
|
|
30114
|
+
};
|
|
30115
|
+
//#endregion
|
|
30311
30116
|
//#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
|
|
30312
30117
|
const LODASH_MUTATOR_NAMES = new Set([
|
|
30313
30118
|
"set",
|
|
@@ -30749,7 +30554,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
30749
30554
|
id: "no-nested-component-definition",
|
|
30750
30555
|
title: "Component defined inside another component",
|
|
30751
30556
|
tags: ["test-noise", "react-jsx-only"],
|
|
30752
|
-
severity: "
|
|
30557
|
+
severity: "error",
|
|
30753
30558
|
category: "Correctness",
|
|
30754
30559
|
recommendation: "Move it to module scope or a separate file so React does not recreate the component and erase its state on every parent render.",
|
|
30755
30560
|
create: (context) => {
|
|
@@ -32641,7 +32446,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
32641
32446
|
});
|
|
32642
32447
|
//#endregion
|
|
32643
32448
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
32644
|
-
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
|
|
32449
|
+
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."], ["useContext", "useContext is replaced by `use()` in React 19, which reads context inside ifs & loops too, so switch to `import { use } from 'react'`."]]);
|
|
32645
32450
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
32646
32451
|
if (!rawFilename) return false;
|
|
32647
32452
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -32679,7 +32484,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
32679
32484
|
requires: ["react:19"],
|
|
32680
32485
|
tags: ["test-noise", "migration-hint"],
|
|
32681
32486
|
severity: "warn",
|
|
32682
|
-
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
|
|
32487
|
+
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Replace `useContext(X)` with `use(X)`. Only runs on React 19+ projects.",
|
|
32683
32488
|
create: (context) => {
|
|
32684
32489
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
32685
32490
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -33003,11 +32808,34 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
33003
32808
|
});
|
|
33004
32809
|
//#endregion
|
|
33005
32810
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
32811
|
+
const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32812
|
+
if (!symbol || visitedSymbols.has(symbol)) return false;
|
|
32813
|
+
visitedSymbols.add(symbol);
|
|
32814
|
+
if (isComponentParameterSymbol(symbol)) return true;
|
|
32815
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
32816
|
+
const source = symbol.initializer;
|
|
32817
|
+
if (!source) return false;
|
|
32818
|
+
return initializerRootsInProps(source, scopes, visitedSymbols);
|
|
32819
|
+
};
|
|
32820
|
+
const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32821
|
+
if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
|
|
32822
|
+
if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
|
|
32823
|
+
return rootsInProps(node, scopes, visitedSymbols);
|
|
32824
|
+
};
|
|
32825
|
+
const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32826
|
+
let current = node;
|
|
32827
|
+
while (isNodeOfType(current, "MemberExpression")) {
|
|
32828
|
+
if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
|
|
32829
|
+
current = current.object;
|
|
32830
|
+
}
|
|
32831
|
+
if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
|
|
32832
|
+
return false;
|
|
32833
|
+
};
|
|
33006
32834
|
const isInsideComponentContext = (node) => {
|
|
33007
32835
|
let cursor = node.parent;
|
|
33008
32836
|
while (cursor) {
|
|
32837
|
+
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
33009
32838
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33010
|
-
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
33011
32839
|
cursor = cursor.parent ?? null;
|
|
33012
32840
|
}
|
|
33013
32841
|
return false;
|
|
@@ -33020,21 +32848,21 @@ const functionBodyOf = (node) => {
|
|
|
33020
32848
|
const containsHookCall = (body) => {
|
|
33021
32849
|
let found = false;
|
|
33022
32850
|
walkAst(body, (child) => {
|
|
33023
|
-
if (found) return
|
|
33024
|
-
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32851
|
+
if (found) return;
|
|
33025
32852
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
33026
32853
|
const name = getCalleeName$2(child);
|
|
33027
32854
|
if (name && isReactHookName(name)) found = true;
|
|
33028
32855
|
});
|
|
33029
32856
|
return found;
|
|
33030
32857
|
};
|
|
33031
|
-
const
|
|
32858
|
+
const isModuleScopeHookFreeHelper = (symbol) => {
|
|
33032
32859
|
if (!symbol) return false;
|
|
33033
32860
|
const declaration = symbol.declarationNode;
|
|
33034
32861
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
33035
32862
|
const body = functionBodyOf(declaration);
|
|
33036
32863
|
if (!body) return false;
|
|
33037
|
-
|
|
32864
|
+
if (findEnclosingFunction(declaration) !== null) return false;
|
|
32865
|
+
return !containsHookCall(body);
|
|
33038
32866
|
};
|
|
33039
32867
|
const noRenderInRender = defineRule({
|
|
33040
32868
|
id: "no-render-in-render",
|
|
@@ -33045,11 +32873,18 @@ const noRenderInRender = defineRule({
|
|
|
33045
32873
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
33046
32874
|
const expression = node.expression;
|
|
33047
32875
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
33048
|
-
|
|
33049
|
-
|
|
33050
|
-
if (
|
|
32876
|
+
let calleeName = null;
|
|
32877
|
+
if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
|
|
32878
|
+
else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
|
|
32879
|
+
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33051
32880
|
if (!isInsideComponentContext(node)) return;
|
|
33052
|
-
if (
|
|
32881
|
+
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
32882
|
+
const calleeSymbol = context.scopes.symbolFor(expression.callee);
|
|
32883
|
+
if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
|
|
32884
|
+
if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
|
|
32885
|
+
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
32886
|
+
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
32887
|
+
}
|
|
33053
32888
|
context.report({
|
|
33054
32889
|
node: expression,
|
|
33055
32890
|
message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
|
|
@@ -36398,28 +36233,6 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36398
36233
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36399
36234
|
return true;
|
|
36400
36235
|
};
|
|
36401
|
-
const isTrivialContainerLiteral = (node) => {
|
|
36402
|
-
if (!node) return false;
|
|
36403
|
-
const innerExpression = stripParenExpression(node);
|
|
36404
|
-
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
36405
|
-
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
36406
|
-
return false;
|
|
36407
|
-
};
|
|
36408
|
-
const isNonEscapingRead = (identifier) => {
|
|
36409
|
-
const parentNode = identifier.parent;
|
|
36410
|
-
return isNodeOfType(parentNode, "MemberExpression") && parentNode.object === identifier;
|
|
36411
|
-
};
|
|
36412
|
-
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
36413
|
-
const parentNode = memoCallNode.parent;
|
|
36414
|
-
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
36415
|
-
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoCallNode) return false;
|
|
36416
|
-
const bindingTarget = parentNode.id;
|
|
36417
|
-
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
36418
|
-
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
36419
|
-
const symbol = scopes.symbolFor(bindingTarget);
|
|
36420
|
-
if (!symbol) return false;
|
|
36421
|
-
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
36422
|
-
};
|
|
36423
36236
|
const noUsememoSimpleExpression = defineRule({
|
|
36424
36237
|
id: "no-usememo-simple-expression",
|
|
36425
36238
|
title: "useMemo on a cheap value",
|
|
@@ -36441,17 +36254,9 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36441
36254
|
let returnExpression = null;
|
|
36442
36255
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36443
36256
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36444
|
-
if (
|
|
36445
|
-
if (isTriviallyCheapExpression(returnExpression)) {
|
|
36446
|
-
context.report({
|
|
36447
|
-
node,
|
|
36448
|
-
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
36449
|
-
});
|
|
36450
|
-
return;
|
|
36451
|
-
}
|
|
36452
|
-
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36257
|
+
if (returnExpression && isTriviallyCheapExpression(returnExpression)) context.report({
|
|
36453
36258
|
node,
|
|
36454
|
-
message: "This
|
|
36259
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
36455
36260
|
});
|
|
36456
36261
|
} })
|
|
36457
36262
|
});
|