oxlint-plugin-react-doctor 0.7.2-dev.cb8f726 → 0.7.2-dev.f10f9ca
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +46 -0
- package/dist/index.js +1316 -429
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -532,6 +532,12 @@ 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
|
+
]);
|
|
535
541
|
const MUTABLE_GLOBAL_ROOTS = new Set([
|
|
536
542
|
"location",
|
|
537
543
|
"window",
|
|
@@ -687,7 +693,10 @@ const GLOBAL_RELEASE_METHOD_NAMES = new Set([
|
|
|
687
693
|
"unwatch",
|
|
688
694
|
"unlisten",
|
|
689
695
|
"unsub",
|
|
690
|
-
"abort"
|
|
696
|
+
"abort",
|
|
697
|
+
"disconnect",
|
|
698
|
+
"unobserve",
|
|
699
|
+
"close"
|
|
691
700
|
]);
|
|
692
701
|
const BOUND_RESOURCE_RELEASE_METHOD_NAMES = new Set([
|
|
693
702
|
"remove",
|
|
@@ -2268,6 +2277,7 @@ const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
|
|
|
2268
2277
|
if (remainingHops === 0) return null;
|
|
2269
2278
|
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
2270
2279
|
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
2280
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
2271
2281
|
return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
|
|
2272
2282
|
}
|
|
2273
2283
|
return null;
|
|
@@ -2307,7 +2317,8 @@ const isDirectChildOfLinkComponent = (openingElement) => {
|
|
|
2307
2317
|
const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
|
|
2308
2318
|
const isInvalidHref = (value, validHrefs) => {
|
|
2309
2319
|
if (validHrefs.has(value)) return false;
|
|
2310
|
-
|
|
2320
|
+
const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
|
|
2321
|
+
return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
|
|
2311
2322
|
};
|
|
2312
2323
|
const isNullishOrFragmentHref = (value) => {
|
|
2313
2324
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
@@ -5182,7 +5193,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
5182
5193
|
const callee = node.callee;
|
|
5183
5194
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
5184
5195
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
5185
|
-
if (!isWebStorageObject(callee.object)) return;
|
|
5196
|
+
if (!isWebStorageObject(stripParenExpression(callee.object))) return;
|
|
5186
5197
|
const keyArgument = node.arguments?.[0];
|
|
5187
5198
|
if (!keyArgument) return;
|
|
5188
5199
|
const keyString = resolveStaticKeyString(keyArgument);
|
|
@@ -6122,19 +6133,21 @@ const clickjackingRedirectRisk = defineRule({
|
|
|
6122
6133
|
})
|
|
6123
6134
|
});
|
|
6124
6135
|
//#endregion
|
|
6136
|
+
//#region src/plugin/utils/is-global-method-call.ts
|
|
6137
|
+
const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
6138
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6139
|
+
const callee = stripParenExpression(node.callee);
|
|
6140
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
6141
|
+
const receiver = stripParenExpression(callee.object);
|
|
6142
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6143
|
+
};
|
|
6144
|
+
//#endregion
|
|
6125
6145
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6126
6146
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6127
6147
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
6128
6148
|
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
6129
6149
|
const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
|
|
6130
|
-
const isJsonStringifyCall = (node) =>
|
|
6131
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6132
|
-
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
6133
|
-
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
6134
|
-
if (node.callee.object.name !== "JSON") return false;
|
|
6135
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
6136
|
-
return node.callee.property.name === "stringify";
|
|
6137
|
-
};
|
|
6150
|
+
const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
|
|
6138
6151
|
const resolveStringKey = (keyArg, context) => {
|
|
6139
6152
|
if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
|
|
6140
6153
|
if (!isNodeOfType(keyArg, "Identifier")) return null;
|
|
@@ -6153,8 +6166,9 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6153
6166
|
recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
|
|
6154
6167
|
create: (context) => ({ CallExpression(node) {
|
|
6155
6168
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6156
|
-
|
|
6157
|
-
if (!
|
|
6169
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6170
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6171
|
+
if (!STORAGE_OBJECTS.has(receiver.name)) return;
|
|
6158
6172
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6159
6173
|
if (node.callee.property.name !== "setItem") return;
|
|
6160
6174
|
const keyArg = node.arguments?.[0];
|
|
@@ -6167,7 +6181,7 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6167
6181
|
if (!isJsonStringifyCall(valueArg)) return;
|
|
6168
6182
|
context.report({
|
|
6169
6183
|
node: keyArg,
|
|
6170
|
-
message: `${
|
|
6184
|
+
message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
|
|
6171
6185
|
});
|
|
6172
6186
|
} })
|
|
6173
6187
|
});
|
|
@@ -7648,6 +7662,96 @@ const displayName = defineRule({
|
|
|
7648
7662
|
}
|
|
7649
7663
|
});
|
|
7650
7664
|
//#endregion
|
|
7665
|
+
//#region src/plugin/utils/is-react-hook-name.ts
|
|
7666
|
+
const isReactHookName = (name) => {
|
|
7667
|
+
if (!name.startsWith("use")) return false;
|
|
7668
|
+
if (name.length === 3) return true;
|
|
7669
|
+
const fourthCharacter = name.charCodeAt(3);
|
|
7670
|
+
return fourthCharacter >= 65 && fourthCharacter <= 90 || fourthCharacter >= 48 && fourthCharacter <= 57;
|
|
7671
|
+
};
|
|
7672
|
+
//#endregion
|
|
7673
|
+
//#region src/plugin/utils/is-react-component-or-hook-name.ts
|
|
7674
|
+
const isReactComponentOrHookName = (name) => isReactComponentName(name) || isReactHookName(name);
|
|
7675
|
+
//#endregion
|
|
7676
|
+
//#region src/plugin/utils/component-or-hook-display-name.ts
|
|
7677
|
+
const hocWrapperCalleeName = (callee) => {
|
|
7678
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
7679
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
7680
|
+
return null;
|
|
7681
|
+
};
|
|
7682
|
+
const displayNameFromFunctionBinding = (functionNode) => {
|
|
7683
|
+
let current = functionNode;
|
|
7684
|
+
for (;;) {
|
|
7685
|
+
const parent = current.parent;
|
|
7686
|
+
if (parent && isNodeOfType(parent, "CallExpression") && parent.arguments?.[0] === current) {
|
|
7687
|
+
const calleeName = hocWrapperCalleeName(parent.callee);
|
|
7688
|
+
if (calleeName && COMPONENT_HOC_WRAPPER_NAMES.has(calleeName)) {
|
|
7689
|
+
current = parent;
|
|
7690
|
+
continue;
|
|
7691
|
+
}
|
|
7692
|
+
}
|
|
7693
|
+
break;
|
|
7694
|
+
}
|
|
7695
|
+
const binding = current.parent;
|
|
7696
|
+
if (binding && isNodeOfType(binding, "VariableDeclarator") && isNodeOfType(binding.id, "Identifier") && binding.init === current) return isReactComponentOrHookName(binding.id.name) ? binding.id.name : null;
|
|
7697
|
+
return null;
|
|
7698
|
+
};
|
|
7699
|
+
const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
7700
|
+
if ((isNodeOfType(functionNode, "FunctionDeclaration") || isNodeOfType(functionNode, "FunctionExpression")) && functionNode.id) return isReactComponentOrHookName(functionNode.id.name) ? functionNode.id.name : null;
|
|
7701
|
+
return displayNameFromFunctionBinding(functionNode);
|
|
7702
|
+
};
|
|
7703
|
+
//#endregion
|
|
7704
|
+
//#region src/plugin/utils/find-enclosing-function.ts
|
|
7705
|
+
const findEnclosingFunction = (node) => {
|
|
7706
|
+
let cursor = node.parent;
|
|
7707
|
+
while (cursor) {
|
|
7708
|
+
if (isFunctionLike$1(cursor)) return cursor;
|
|
7709
|
+
cursor = cursor.parent ?? null;
|
|
7710
|
+
}
|
|
7711
|
+
return null;
|
|
7712
|
+
};
|
|
7713
|
+
//#endregion
|
|
7714
|
+
//#region src/plugin/utils/enclosing-component-or-hook-name.ts
|
|
7715
|
+
const enclosingComponentOrHookName = (node) => {
|
|
7716
|
+
const functionNode = findEnclosingFunction(node);
|
|
7717
|
+
return functionNode ? componentOrHookDisplayNameForFunction(functionNode) : null;
|
|
7718
|
+
};
|
|
7719
|
+
//#endregion
|
|
7720
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
7721
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
7722
|
+
let node = callExpression;
|
|
7723
|
+
let parent = node.parent;
|
|
7724
|
+
while (parent) {
|
|
7725
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
7726
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
7727
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
7728
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
7729
|
+
node = parent;
|
|
7730
|
+
parent = node.parent;
|
|
7731
|
+
continue;
|
|
7732
|
+
}
|
|
7733
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
7734
|
+
node = parent;
|
|
7735
|
+
parent = node.parent;
|
|
7736
|
+
continue;
|
|
7737
|
+
}
|
|
7738
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
7739
|
+
node = parent;
|
|
7740
|
+
parent = node.parent;
|
|
7741
|
+
continue;
|
|
7742
|
+
}
|
|
7743
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
7744
|
+
const expressions = parent.expressions ?? [];
|
|
7745
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
7746
|
+
node = parent;
|
|
7747
|
+
parent = node.parent;
|
|
7748
|
+
continue;
|
|
7749
|
+
}
|
|
7750
|
+
return false;
|
|
7751
|
+
}
|
|
7752
|
+
return false;
|
|
7753
|
+
};
|
|
7754
|
+
//#endregion
|
|
7651
7755
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
7652
7756
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
7653
7757
|
if (!node || typeof node !== "object") return;
|
|
@@ -7799,6 +7903,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
7799
7903
|
};
|
|
7800
7904
|
//#endregion
|
|
7801
7905
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
7906
|
+
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
7907
|
+
const RESOURCE_NOUN_BY_KIND = {
|
|
7908
|
+
subscribe: "subscription",
|
|
7909
|
+
timer: "timer",
|
|
7910
|
+
socket: "connection"
|
|
7911
|
+
};
|
|
7912
|
+
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
7913
|
+
const isSubscribeOrObserveCall = (node) => {
|
|
7914
|
+
if (isSubscribeLikeCallExpression(node)) return true;
|
|
7915
|
+
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
7916
|
+
};
|
|
7802
7917
|
const findSubscribeLikeUsages = (callback) => {
|
|
7803
7918
|
const usages = [];
|
|
7804
7919
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
@@ -7810,6 +7925,14 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7810
7925
|
}
|
|
7811
7926
|
walkAst(callback, (child) => {
|
|
7812
7927
|
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
7928
|
+
if (isSocketConstruction(child)) {
|
|
7929
|
+
usages.push({
|
|
7930
|
+
kind: "socket",
|
|
7931
|
+
node: child,
|
|
7932
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
7933
|
+
});
|
|
7934
|
+
return;
|
|
7935
|
+
}
|
|
7813
7936
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7814
7937
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
7815
7938
|
usages.push({
|
|
@@ -7819,7 +7942,7 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7819
7942
|
});
|
|
7820
7943
|
return;
|
|
7821
7944
|
}
|
|
7822
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
7945
|
+
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && (SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name) || child.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME)) usages.push({
|
|
7823
7946
|
kind: "subscribe",
|
|
7824
7947
|
node: child,
|
|
7825
7948
|
resourceName: child.callee.property.name
|
|
@@ -7835,6 +7958,13 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7835
7958
|
};
|
|
7836
7959
|
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
7837
7960
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
7961
|
+
walkAst(effectCallback.body, (child) => {
|
|
7962
|
+
if (!isSubscribeOrObserveCall(child)) return;
|
|
7963
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7964
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
7965
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
7966
|
+
bindings.subscriptionNames.add(child.callee.object.name);
|
|
7967
|
+
});
|
|
7838
7968
|
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
7839
7969
|
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
7840
7970
|
for (const declarator of child.declarations ?? []) {
|
|
@@ -7842,7 +7972,12 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7842
7972
|
const bindingName = declarator.id.name;
|
|
7843
7973
|
bindings.effectScopeVariableNames.add(bindingName);
|
|
7844
7974
|
const init = declarator.init;
|
|
7845
|
-
if (!init
|
|
7975
|
+
if (!init) continue;
|
|
7976
|
+
if (isSocketConstruction(init)) {
|
|
7977
|
+
bindings.subscriptionNames.add(bindingName);
|
|
7978
|
+
continue;
|
|
7979
|
+
}
|
|
7980
|
+
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
7846
7981
|
if (isSubscribeLikeCallExpression(init)) {
|
|
7847
7982
|
bindings.subscriptionNames.add(bindingName);
|
|
7848
7983
|
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
@@ -7872,6 +8007,22 @@ const getRangeStart = (node) => {
|
|
|
7872
8007
|
const rangeStart = node.range?.[0];
|
|
7873
8008
|
return typeof rangeStart === "number" ? rangeStart : null;
|
|
7874
8009
|
};
|
|
8010
|
+
const removeSynchronouslyReleasedUsages = (callback, usages) => {
|
|
8011
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8012
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8013
|
+
const releaseStarts = [];
|
|
8014
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8015
|
+
if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
|
|
8016
|
+
const releaseStart = getRangeStart(child);
|
|
8017
|
+
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8018
|
+
});
|
|
8019
|
+
if (releaseStarts.length === 0) return usages;
|
|
8020
|
+
return usages.filter((usage) => {
|
|
8021
|
+
const usageStart = getRangeStart(usage.node);
|
|
8022
|
+
if (usageStart === null) return true;
|
|
8023
|
+
return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
|
|
8024
|
+
});
|
|
8025
|
+
};
|
|
7875
8026
|
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
7876
8027
|
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
7877
8028
|
const returnStart = getRangeStart(returnStatement);
|
|
@@ -7894,26 +8045,177 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
7894
8045
|
});
|
|
7895
8046
|
return didFindCleanupReturn;
|
|
7896
8047
|
};
|
|
8048
|
+
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8049
|
+
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
8050
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
8051
|
+
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
8052
|
+
if (keyName === "signal") return true;
|
|
8053
|
+
if (keyName !== "once") return false;
|
|
8054
|
+
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
8055
|
+
};
|
|
8056
|
+
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
8057
|
+
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8058
|
+
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8059
|
+
["addListener", new Set([
|
|
8060
|
+
"removeListener",
|
|
8061
|
+
"off",
|
|
8062
|
+
"abort"
|
|
8063
|
+
])],
|
|
8064
|
+
["on", new Set([
|
|
8065
|
+
"off",
|
|
8066
|
+
"removeListener",
|
|
8067
|
+
"on"
|
|
8068
|
+
])],
|
|
8069
|
+
["subscribe", new Set(["unsubscribe", "unsub"])],
|
|
8070
|
+
["sub", new Set(["unsub", "unsubscribe"])],
|
|
8071
|
+
["watch", new Set(["unwatch", "close"])],
|
|
8072
|
+
["listen", new Set(["unlisten", "close"])],
|
|
8073
|
+
[OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
|
|
8074
|
+
]);
|
|
8075
|
+
const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
8076
|
+
"cleanup",
|
|
8077
|
+
"dispose",
|
|
8078
|
+
"destroy",
|
|
8079
|
+
"teardown"
|
|
8080
|
+
]);
|
|
8081
|
+
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8082
|
+
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8083
|
+
const getReleaseVerbName = (node) => {
|
|
8084
|
+
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8085
|
+
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8086
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8087
|
+
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8088
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8089
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
8090
|
+
return null;
|
|
8091
|
+
};
|
|
8092
|
+
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8093
|
+
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8094
|
+
let didFindPairedRelease = false;
|
|
8095
|
+
walkAst(body, (child) => {
|
|
8096
|
+
if (didFindPairedRelease) return false;
|
|
8097
|
+
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8098
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8099
|
+
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8100
|
+
didFindPairedRelease = true;
|
|
8101
|
+
return false;
|
|
8102
|
+
}
|
|
8103
|
+
});
|
|
8104
|
+
return didFindPairedRelease;
|
|
8105
|
+
};
|
|
8106
|
+
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8107
|
+
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8108
|
+
let programNode = anyNode;
|
|
8109
|
+
while (programNode.parent) programNode = programNode.parent;
|
|
8110
|
+
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8111
|
+
if (cached) return cached;
|
|
8112
|
+
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8113
|
+
walkAst(programNode, (child) => {
|
|
8114
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8115
|
+
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8116
|
+
});
|
|
8117
|
+
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8118
|
+
return releaseVerbNames;
|
|
8119
|
+
};
|
|
8120
|
+
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8121
|
+
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8122
|
+
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8123
|
+
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8124
|
+
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
8125
|
+
return false;
|
|
8126
|
+
};
|
|
8127
|
+
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
8128
|
+
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8129
|
+
const body = retainedFunction.body;
|
|
8130
|
+
if (!body) return null;
|
|
8131
|
+
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
8132
|
+
let leak = null;
|
|
8133
|
+
walkAst(body, (child) => {
|
|
8134
|
+
if (leak !== null) return false;
|
|
8135
|
+
if (isFunctionLike$1(child)) return false;
|
|
8136
|
+
if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
|
|
8137
|
+
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8138
|
+
leak = {
|
|
8139
|
+
kind: "socket",
|
|
8140
|
+
node: child,
|
|
8141
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8142
|
+
};
|
|
8143
|
+
return false;
|
|
8144
|
+
}
|
|
8145
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8146
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8147
|
+
leak = {
|
|
8148
|
+
kind: "timer",
|
|
8149
|
+
node: child,
|
|
8150
|
+
resourceName: "setInterval"
|
|
8151
|
+
};
|
|
8152
|
+
return false;
|
|
8153
|
+
}
|
|
8154
|
+
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8155
|
+
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8156
|
+
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
8157
|
+
kind: "subscribe",
|
|
8158
|
+
node: child,
|
|
8159
|
+
resourceName: registrationVerbName
|
|
8160
|
+
};
|
|
8161
|
+
return false;
|
|
8162
|
+
}
|
|
8163
|
+
});
|
|
8164
|
+
return leak;
|
|
8165
|
+
};
|
|
8166
|
+
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
8167
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
8168
|
+
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
8169
|
+
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8170
|
+
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8171
|
+
};
|
|
7897
8172
|
const effectNeedsCleanup = defineRule({
|
|
7898
8173
|
id: "effect-needs-cleanup",
|
|
7899
8174
|
title: "Effect subscription or timer never cleaned up",
|
|
7900
8175
|
severity: "error",
|
|
7901
8176
|
tags: ["test-noise"],
|
|
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) =>
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
8177
|
+
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, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",
|
|
8178
|
+
create: (context) => {
|
|
8179
|
+
const reportRetainedLeak = (retainedFunction) => {
|
|
8180
|
+
const leak = findRetainedFunctionLeak(retainedFunction);
|
|
8181
|
+
if (!leak) return;
|
|
8182
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
8183
|
+
context.report({
|
|
8184
|
+
node: leak.node,
|
|
8185
|
+
message: `\`${leak.resourceName}\` creates a ${resourceNoun} in a function that outlives the render, with no cleanup path. Store the handle and release it, or move this into a useEffect that returns cleanup, so it does not leak after unmount.`
|
|
8186
|
+
});
|
|
8187
|
+
};
|
|
8188
|
+
return {
|
|
8189
|
+
CallExpression(node) {
|
|
8190
|
+
if (isHookCall$2(node, "useCallback")) {
|
|
8191
|
+
const retainedCallback = getEffectCallback(node);
|
|
8192
|
+
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
8193
|
+
return;
|
|
8194
|
+
}
|
|
8195
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
8196
|
+
const callback = getEffectCallback(node);
|
|
8197
|
+
if (!callback) return;
|
|
8198
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
8199
|
+
if (usages.length === 0) return;
|
|
8200
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8201
|
+
const firstUsage = usages[0];
|
|
8202
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
8203
|
+
context.report({
|
|
8204
|
+
node,
|
|
8205
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8206
|
+
});
|
|
8207
|
+
},
|
|
8208
|
+
FunctionDeclaration(node) {
|
|
8209
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8210
|
+
},
|
|
8211
|
+
ArrowFunctionExpression(node) {
|
|
8212
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8213
|
+
},
|
|
8214
|
+
FunctionExpression(node) {
|
|
8215
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8216
|
+
}
|
|
8217
|
+
};
|
|
8218
|
+
}
|
|
7917
8219
|
});
|
|
7918
8220
|
//#endregion
|
|
7919
8221
|
//#region src/plugin/semantic/scope-analysis.ts
|
|
@@ -8472,17 +8774,6 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
8472
8774
|
return computedCaptures;
|
|
8473
8775
|
};
|
|
8474
8776
|
//#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
|
|
8486
8777
|
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
8487
8778
|
const reactHocCalleeName = (callee) => {
|
|
8488
8779
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -8696,8 +8987,8 @@ const isAstDescendant = (inner, outer) => {
|
|
|
8696
8987
|
* One cohesive concept: "given a captured symbol, is its value
|
|
8697
8988
|
* structurally stable across re-renders (and therefore unnecessary
|
|
8698
8989
|
* in a deps array)?". The rule reads `symbolHasStableValue` /
|
|
8699
|
-
* `symbolHasStableHookOrigin` / `
|
|
8700
|
-
*
|
|
8990
|
+
* `symbolHasStableHookOrigin` / `isRecursiveInitializerCapture` at
|
|
8991
|
+
* multiple sites — extracting
|
|
8701
8992
|
* them lets the rule body stay focused on the diff-the-captured-vs-
|
|
8702
8993
|
* declared logic.
|
|
8703
8994
|
*
|
|
@@ -8753,11 +9044,6 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
8753
9044
|
}
|
|
8754
9045
|
return false;
|
|
8755
9046
|
};
|
|
8756
|
-
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
8757
|
-
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
8758
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
8759
|
-
return getHookName(initializer.callee) === "useEffectEvent";
|
|
8760
|
-
};
|
|
8761
9047
|
const getFunctionValueNode = (symbol) => {
|
|
8762
9048
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
8763
9049
|
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
@@ -8843,6 +9129,37 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
8843
9129
|
};
|
|
8844
9130
|
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
8845
9131
|
//#endregion
|
|
9132
|
+
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
9133
|
+
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
9134
|
+
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
9135
|
+
if (importSource === null) return false;
|
|
9136
|
+
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
9137
|
+
};
|
|
9138
|
+
//#endregion
|
|
9139
|
+
//#region src/plugin/utils/is-non-react-effect-event-callee.ts
|
|
9140
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
9141
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
9142
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
9143
|
+
};
|
|
9144
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
9145
|
+
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
9146
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
9147
|
+
return false;
|
|
9148
|
+
};
|
|
9149
|
+
//#endregion
|
|
9150
|
+
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
9151
|
+
const getUseEffectEventCalleeName = (callee) => {
|
|
9152
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
9153
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9154
|
+
return null;
|
|
9155
|
+
};
|
|
9156
|
+
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
9157
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
9158
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
9159
|
+
if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
|
|
9160
|
+
return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
|
|
9161
|
+
};
|
|
9162
|
+
//#endregion
|
|
8846
9163
|
//#region src/plugin/rules/react-builtins/exhaustive-deps.ts
|
|
8847
9164
|
const HOOKS_REQUIRING_DEPS_MATCH = new Set([
|
|
8848
9165
|
"useEffect",
|
|
@@ -9493,7 +9810,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9493
9810
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
9494
9811
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
9495
9812
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
9496
|
-
if (depSymbol &&
|
|
9813
|
+
if (depSymbol && symbolHasReactUseEffectEventOrigin(depSymbol, context.scopes)) {
|
|
9497
9814
|
context.report({
|
|
9498
9815
|
node: elementNode,
|
|
9499
9816
|
message: buildEffectEventDepMessage()
|
|
@@ -9919,7 +10236,10 @@ const PRAGMA = "React";
|
|
|
9919
10236
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
9920
10237
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
9921
10238
|
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
9922
|
-
if (isNodeOfType(node.callee, "MemberExpression"))
|
|
10239
|
+
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
10240
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
10241
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
10242
|
+
}
|
|
9923
10243
|
return true;
|
|
9924
10244
|
};
|
|
9925
10245
|
//#endregion
|
|
@@ -11297,16 +11617,6 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11297
11617
|
return names;
|
|
11298
11618
|
};
|
|
11299
11619
|
//#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
|
|
11310
11620
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11311
11621
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11312
11622
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -12006,14 +12316,15 @@ const jsCacheStorage = defineRule({
|
|
|
12006
12316
|
"ArrowFunctionExpression:exit": exitFunctionScope,
|
|
12007
12317
|
CallExpression(node) {
|
|
12008
12318
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
12009
|
-
|
|
12319
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
12320
|
+
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
|
|
12010
12321
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
12011
12322
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
12012
12323
|
const storageKey = String(node.arguments[0].value);
|
|
12013
12324
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
12014
12325
|
storageReadCounts.set(storageKey, readCount);
|
|
12015
12326
|
if (readCount === 2) {
|
|
12016
|
-
const storageName =
|
|
12327
|
+
const storageName = receiver.name;
|
|
12017
12328
|
context.report({
|
|
12018
12329
|
node,
|
|
12019
12330
|
message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
|
|
@@ -12027,13 +12338,16 @@ const jsCacheStorage = defineRule({
|
|
|
12027
12338
|
//#region src/plugin/rules/js-performance/js-combine-iterations.ts
|
|
12028
12339
|
const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
|
|
12029
12340
|
const callee = callExpression.callee;
|
|
12030
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12341
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
12342
|
+
const receiver = stripParenExpression(callee.object);
|
|
12343
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12344
|
+
if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
12345
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
12346
|
+
return true;
|
|
12347
|
+
}
|
|
12348
|
+
return false;
|
|
12036
12349
|
}
|
|
12350
|
+
if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
|
|
12037
12351
|
return false;
|
|
12038
12352
|
};
|
|
12039
12353
|
const isChainPassThroughCall = (callExpression) => {
|
|
@@ -12045,10 +12359,7 @@ const isChainPassThroughCall = (callExpression) => {
|
|
|
12045
12359
|
const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
|
|
12046
12360
|
let cursor = receiverNode;
|
|
12047
12361
|
while (cursor) {
|
|
12048
|
-
|
|
12049
|
-
cursor = cursor.expression;
|
|
12050
|
-
continue;
|
|
12051
|
-
}
|
|
12362
|
+
cursor = stripParenExpression(cursor);
|
|
12052
12363
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12053
12364
|
if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
|
|
12054
12365
|
if (!isChainPassThroughCall(cursor)) return false;
|
|
@@ -12124,10 +12435,7 @@ const isStringSplitRootedChain = (receiverNode) => {
|
|
|
12124
12435
|
let hops = 0;
|
|
12125
12436
|
while (cursor && hops < 12) {
|
|
12126
12437
|
hops += 1;
|
|
12127
|
-
|
|
12128
|
-
cursor = cursor.expression;
|
|
12129
|
-
continue;
|
|
12130
|
-
}
|
|
12438
|
+
cursor = stripParenExpression(cursor);
|
|
12131
12439
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12132
12440
|
const callee = cursor.callee;
|
|
12133
12441
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
@@ -12151,10 +12459,7 @@ const isSmallLiteralArray = (node) => {
|
|
|
12151
12459
|
const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
|
|
12152
12460
|
let cursor = receiverNode;
|
|
12153
12461
|
while (cursor) {
|
|
12154
|
-
|
|
12155
|
-
cursor = cursor.expression;
|
|
12156
|
-
continue;
|
|
12157
|
-
}
|
|
12462
|
+
cursor = stripParenExpression(cursor);
|
|
12158
12463
|
if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
|
|
12159
12464
|
if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
|
|
12160
12465
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
@@ -12219,7 +12524,7 @@ const jsCombineIterations = defineRule({
|
|
|
12219
12524
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
12220
12525
|
const outerMethod = node.callee.property.name;
|
|
12221
12526
|
if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
|
|
12222
|
-
const innerCall = node.callee.object;
|
|
12527
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12223
12528
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12224
12529
|
const innerMethod = innerCall.callee.property.name;
|
|
12225
12530
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
@@ -12292,10 +12597,10 @@ const jsFlatmapFilter = defineRule({
|
|
|
12292
12597
|
if (!filterArgument) return;
|
|
12293
12598
|
const isIdentityArrow = isNodeOfType(filterArgument, "ArrowFunctionExpression") && filterArgument.params?.length === 1 && isNodeOfType(filterArgument.body, "Identifier") && isNodeOfType(filterArgument.params[0], "Identifier") && filterArgument.body.name === filterArgument.params[0].name;
|
|
12294
12599
|
if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
12295
|
-
const innerCall = node.callee.object;
|
|
12600
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12296
12601
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12297
12602
|
if (innerCall.callee.property.name !== "map") return;
|
|
12298
|
-
const receiver = innerCall.callee.object;
|
|
12603
|
+
const receiver = stripParenExpression(innerCall.callee.object);
|
|
12299
12604
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
12300
12605
|
const elements = receiver.elements ?? [];
|
|
12301
12606
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13555,7 +13860,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13555
13860
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13556
13861
|
create: (context) => ({ CallExpression(node) {
|
|
13557
13862
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
13558
|
-
const receiver = node.callee.object;
|
|
13863
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
13559
13864
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
|
|
13560
13865
|
const spreadArgument = receiver.elements[0].argument;
|
|
13561
13866
|
if (isFreshOrIteratorAllocation(spreadArgument)) return;
|
|
@@ -18592,7 +18897,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
|
|
|
18592
18897
|
};
|
|
18593
18898
|
const describeClientSideNavigation = (node) => {
|
|
18594
18899
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
|
|
18595
|
-
const
|
|
18900
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
18901
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
18596
18902
|
const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
18597
18903
|
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
|
|
18598
18904
|
}
|
|
@@ -19212,7 +19518,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
|
|
|
19212
19518
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
19213
19519
|
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
19214
19520
|
if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
|
|
19215
|
-
if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
|
|
19521
|
+
if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
|
|
19216
19522
|
return node.callee.property.name;
|
|
19217
19523
|
};
|
|
19218
19524
|
const isMutatingFetchCall = (node) => {
|
|
@@ -19226,7 +19532,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
|
|
|
19226
19532
|
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19227
19533
|
const { property, object } = node.callee;
|
|
19228
19534
|
if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
|
|
19229
|
-
if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
|
|
19535
|
+
if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
|
|
19230
19536
|
return true;
|
|
19231
19537
|
};
|
|
19232
19538
|
const getDbCallDescription = (node) => {
|
|
@@ -19234,7 +19540,8 @@ const getDbCallDescription = (node) => {
|
|
|
19234
19540
|
if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
|
|
19235
19541
|
if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
|
|
19236
19542
|
const methodName = node.callee.property.name;
|
|
19237
|
-
const
|
|
19543
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
19544
|
+
const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
19238
19545
|
return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
|
|
19239
19546
|
};
|
|
19240
19547
|
const findSideEffect = (node, options = {}) => {
|
|
@@ -20634,13 +20941,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
|
|
|
20634
20941
|
if (isNodeOfType(current, "CallExpression")) {
|
|
20635
20942
|
let node = ref.identifier;
|
|
20636
20943
|
let parent = node.parent;
|
|
20637
|
-
while (parent && isNodeOfType(parent, "MemberExpression")) {
|
|
20944
|
+
while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
|
|
20638
20945
|
node = parent;
|
|
20639
20946
|
parent = node.parent;
|
|
20640
20947
|
}
|
|
20641
20948
|
if (current.callee === node) return current;
|
|
20642
20949
|
}
|
|
20643
|
-
if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
|
|
20950
|
+
if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
|
|
20644
20951
|
return null;
|
|
20645
20952
|
};
|
|
20646
20953
|
const getArgsUpstreamRefs = (analysis, ref) => {
|
|
@@ -20775,7 +21082,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
20775
21082
|
const importDeclaration = declarationNode.parent;
|
|
20776
21083
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
20777
21084
|
}));
|
|
20778
|
-
const isHookCallee = (analysis, node, hookName) => {
|
|
21085
|
+
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
20779
21086
|
if (!node) return false;
|
|
20780
21087
|
if (isNodeOfType(node, "Identifier")) {
|
|
20781
21088
|
if (node.name === hookName) return true;
|
|
@@ -20784,15 +21091,19 @@ const isHookCallee = (analysis, node, hookName) => {
|
|
|
20784
21091
|
if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
20785
21092
|
return false;
|
|
20786
21093
|
}
|
|
20787
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
21094
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
21095
|
+
const receiver = stripParenExpression(node.object);
|
|
21096
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21097
|
+
}
|
|
20788
21098
|
return false;
|
|
20789
21099
|
};
|
|
20790
21100
|
const isUseEffect = (node) => {
|
|
20791
21101
|
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
20792
21102
|
const callee = node.callee;
|
|
20793
21103
|
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
20794
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
20795
|
-
|
|
21104
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
21105
|
+
const receiver = stripParenExpression(callee.object);
|
|
21106
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
20796
21107
|
};
|
|
20797
21108
|
const getEffectFn = (analysis, node) => {
|
|
20798
21109
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
@@ -20820,7 +21131,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20820
21131
|
const node = def.node;
|
|
20821
21132
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20822
21133
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20823
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21134
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20824
21135
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20825
21136
|
const elements = node.id.elements ?? [];
|
|
20826
21137
|
if (elements.length !== 1 && elements.length !== 2) return false;
|
|
@@ -20831,7 +21142,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
|
|
|
20831
21142
|
const node = def.node;
|
|
20832
21143
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20833
21144
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20834
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21145
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20835
21146
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20836
21147
|
const elements = node.id.elements ?? [];
|
|
20837
21148
|
if (elements.length !== 2) return false;
|
|
@@ -20878,7 +21189,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20878
21189
|
const node = def.node;
|
|
20879
21190
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20880
21191
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20881
|
-
return isHookCallee(analysis, node.init.callee, "useRef");
|
|
21192
|
+
return isHookCallee$1(analysis, node.init.callee, "useRef");
|
|
20882
21193
|
}));
|
|
20883
21194
|
const isRefCurrent = (ref) => {
|
|
20884
21195
|
const parent = ref.identifier.parent;
|
|
@@ -20891,11 +21202,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
|
|
|
20891
21202
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
20892
21203
|
const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
20893
21204
|
if (!isPropAlias(analysis, ref)) return false;
|
|
20894
|
-
|
|
20895
|
-
|
|
21205
|
+
let effectiveNode = ref.identifier;
|
|
21206
|
+
let parent = effectiveNode.parent;
|
|
21207
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
|
|
21208
|
+
effectiveNode = parent;
|
|
21209
|
+
parent = effectiveNode.parent;
|
|
21210
|
+
}
|
|
20896
21211
|
if (!parent) return false;
|
|
20897
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee ===
|
|
20898
|
-
if (isNodeOfType(parent, "MemberExpression") && parent.object ===
|
|
21212
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
|
|
21213
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
|
|
20899
21214
|
const memberParent = parent.parent;
|
|
20900
21215
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
20901
21216
|
if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
|
|
@@ -20906,7 +21221,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
20906
21221
|
};
|
|
20907
21222
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
20908
21223
|
const getUseStateDecl = (analysis, ref) => {
|
|
20909
|
-
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21224
|
+
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
|
|
20910
21225
|
while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
|
|
20911
21226
|
return node ?? null;
|
|
20912
21227
|
};
|
|
@@ -21111,7 +21426,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21111
21426
|
const noAdjustStateOnPropChange = defineRule({
|
|
21112
21427
|
id: "no-adjust-state-on-prop-change",
|
|
21113
21428
|
title: "State synced to a prop inside an effect",
|
|
21114
|
-
severity: "
|
|
21429
|
+
severity: "warn",
|
|
21115
21430
|
tags: ["test-noise"],
|
|
21116
21431
|
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",
|
|
21117
21432
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -21152,7 +21467,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
|
21152
21467
|
"summary",
|
|
21153
21468
|
"textarea"
|
|
21154
21469
|
]);
|
|
21470
|
+
const isStaticallyFalseBooleanAttribute = (attribute) => {
|
|
21471
|
+
const value = attribute.value;
|
|
21472
|
+
if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
21473
|
+
const expression = value.expression;
|
|
21474
|
+
return isNodeOfType(expression, "Literal") && expression.value === false;
|
|
21475
|
+
};
|
|
21476
|
+
const DISABLEABLE_TAGS = new Set([
|
|
21477
|
+
"button",
|
|
21478
|
+
"input",
|
|
21479
|
+
"select",
|
|
21480
|
+
"textarea"
|
|
21481
|
+
]);
|
|
21155
21482
|
const isNativelyFocusable = (tagName, openingElement) => {
|
|
21483
|
+
if (DISABLEABLE_TAGS.has(tagName)) {
|
|
21484
|
+
const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
|
|
21485
|
+
if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
|
|
21486
|
+
}
|
|
21156
21487
|
if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
|
|
21157
21488
|
switch (tagName) {
|
|
21158
21489
|
case "input": {
|
|
@@ -21166,7 +21497,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
|
|
|
21166
21497
|
case "a":
|
|
21167
21498
|
case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
|
|
21168
21499
|
case "audio":
|
|
21169
|
-
case "video":
|
|
21500
|
+
case "video": {
|
|
21501
|
+
const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
|
|
21502
|
+
return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
|
|
21503
|
+
}
|
|
21170
21504
|
default: return false;
|
|
21171
21505
|
}
|
|
21172
21506
|
};
|
|
@@ -22130,7 +22464,8 @@ const SECOND_INDEX_METHODS = new Set([
|
|
|
22130
22464
|
"some"
|
|
22131
22465
|
]);
|
|
22132
22466
|
const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
|
|
22133
|
-
const isPositionallyStableIterationReceiver = (
|
|
22467
|
+
const isPositionallyStableIterationReceiver = (receiverNode) => {
|
|
22468
|
+
const receiver = stripParenExpression(receiverNode);
|
|
22134
22469
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
22135
22470
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
|
|
22136
22471
|
const only = receiver.elements[0];
|
|
@@ -22141,17 +22476,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
|
|
|
22141
22476
|
}
|
|
22142
22477
|
if (!isNodeOfType(receiver, "CallExpression")) return false;
|
|
22143
22478
|
const callee = receiver.callee;
|
|
22144
|
-
if (
|
|
22479
|
+
if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
|
|
22145
22480
|
if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
|
|
22146
22481
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
|
|
22147
22482
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
|
|
22148
22483
|
return false;
|
|
22149
22484
|
};
|
|
22150
|
-
const isArrayFromMapperCallback = (parentCall, callback) =>
|
|
22151
|
-
if (parentCall.arguments[1] !== callback) return false;
|
|
22152
|
-
const callee = parentCall.callee;
|
|
22153
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
|
|
22154
|
-
};
|
|
22485
|
+
const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
|
|
22155
22486
|
const isArrayFromSourcePositionallyStable = (source) => {
|
|
22156
22487
|
if (isNodeOfType(source, "ObjectExpression")) {
|
|
22157
22488
|
for (const property of source.properties ?? []) {
|
|
@@ -22210,18 +22541,12 @@ const expressionUsesIndex = (expression, paramName) => {
|
|
|
22210
22541
|
return false;
|
|
22211
22542
|
}
|
|
22212
22543
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
22213
|
-
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
|
|
22544
|
+
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
|
|
22214
22545
|
if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
|
|
22215
22546
|
}
|
|
22216
22547
|
return false;
|
|
22217
22548
|
};
|
|
22218
|
-
const isReactCloneElement = (callExpression) =>
|
|
22219
|
-
const callee = callExpression.callee;
|
|
22220
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
22221
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
22222
|
-
if (callee.property.name !== "cloneElement") return false;
|
|
22223
|
-
return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
|
|
22224
|
-
};
|
|
22549
|
+
const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
|
|
22225
22550
|
const noArrayIndexKey = defineRule({
|
|
22226
22551
|
id: "no-array-index-key",
|
|
22227
22552
|
title: "Array index used as a key",
|
|
@@ -22945,7 +23270,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
22945
23270
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
22946
23271
|
return false;
|
|
22947
23272
|
};
|
|
22948
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
23273
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
22949
23274
|
"forEach",
|
|
22950
23275
|
"map",
|
|
22951
23276
|
"filter",
|
|
@@ -22966,30 +23291,51 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
22966
23291
|
if (parent.callee === functionNode) return true;
|
|
22967
23292
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
22968
23293
|
const callee = parent.callee;
|
|
22969
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
23294
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
|
|
22970
23295
|
};
|
|
22971
23296
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
22972
|
-
const
|
|
22973
|
-
|
|
22974
|
-
if (statement.alternate) return null;
|
|
22975
|
-
const consequent = statement.consequent;
|
|
22976
|
-
if (isTerminatingStatement(consequent)) return consequent;
|
|
22977
|
-
if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
|
|
22978
|
-
return null;
|
|
22979
|
-
};
|
|
22980
|
-
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23297
|
+
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
23298
|
+
const analyzeStatementSequence = (statements, context) => {
|
|
22981
23299
|
let fallThroughCount = 0;
|
|
22982
|
-
let
|
|
23300
|
+
let maxTerminatedCount = 0;
|
|
22983
23301
|
for (const statement of statements) {
|
|
22984
|
-
|
|
22985
|
-
|
|
22986
|
-
|
|
23302
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
23303
|
+
fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
|
|
23304
|
+
const thenSummary = analyzeBranchStatements(statement.consequent, context);
|
|
23305
|
+
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
23306
|
+
fallThroughCount: 0,
|
|
23307
|
+
maxTerminatedCount: 0,
|
|
23308
|
+
doAllPathsTerminate: false
|
|
23309
|
+
};
|
|
23310
|
+
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
23311
|
+
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
23312
|
+
fallThroughCount: 0,
|
|
23313
|
+
maxTerminatedCount,
|
|
23314
|
+
doAllPathsTerminate: true
|
|
23315
|
+
};
|
|
23316
|
+
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
23317
|
+
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
22987
23318
|
continue;
|
|
22988
23319
|
}
|
|
22989
|
-
if (isTerminatingStatement(statement))
|
|
23320
|
+
if (isTerminatingStatement(statement)) {
|
|
23321
|
+
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
23322
|
+
return {
|
|
23323
|
+
fallThroughCount: 0,
|
|
23324
|
+
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
23325
|
+
doAllPathsTerminate: true
|
|
23326
|
+
};
|
|
23327
|
+
}
|
|
22990
23328
|
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
22991
23329
|
}
|
|
22992
|
-
return
|
|
23330
|
+
return {
|
|
23331
|
+
fallThroughCount,
|
|
23332
|
+
maxTerminatedCount,
|
|
23333
|
+
doAllPathsTerminate: false
|
|
23334
|
+
};
|
|
23335
|
+
};
|
|
23336
|
+
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23337
|
+
const summary = analyzeStatementSequence(statements, context);
|
|
23338
|
+
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
22993
23339
|
};
|
|
22994
23340
|
const collectLocalHelperFunctions = (root) => {
|
|
22995
23341
|
const helpersByName = /* @__PURE__ */ new Map();
|
|
@@ -23120,7 +23466,9 @@ const isDevOnlyGuardedEffect = (callback) => {
|
|
|
23120
23466
|
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
23121
23467
|
const firstStatement = (body.body ?? [])[0];
|
|
23122
23468
|
if (!firstStatement) return false;
|
|
23123
|
-
if (!
|
|
23469
|
+
if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
|
|
23470
|
+
const consequent = firstStatement.consequent;
|
|
23471
|
+
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
23124
23472
|
return mentionsDevEnvFlag(firstStatement.test);
|
|
23125
23473
|
};
|
|
23126
23474
|
const noCascadingSetState = defineRule({
|
|
@@ -23479,40 +23827,6 @@ const noCloneElement = defineRule({
|
|
|
23479
23827
|
} })
|
|
23480
23828
|
});
|
|
23481
23829
|
//#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
|
|
23516
23830
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23517
23831
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23518
23832
|
const CONTEXT_MODULES = [
|
|
@@ -24483,11 +24797,21 @@ const isInitialOnlySetterCall = (callExpr) => {
|
|
|
24483
24797
|
return isInitialOnlyPropName(arg.name);
|
|
24484
24798
|
};
|
|
24485
24799
|
//#endregion
|
|
24800
|
+
//#region src/plugin/utils/is-no-op-statement.ts
|
|
24801
|
+
const isNoOpStatement = (statement) => {
|
|
24802
|
+
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
24803
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24804
|
+
const expression = stripParenExpression(statement.expression);
|
|
24805
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
24806
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
24807
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
24808
|
+
return false;
|
|
24809
|
+
};
|
|
24810
|
+
//#endregion
|
|
24486
24811
|
//#region src/plugin/utils/get-callback-statements.ts
|
|
24487
24812
|
const getCallbackStatements = (callback) => {
|
|
24488
24813
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
24489
|
-
|
|
24490
|
-
return callback.body ? [callback.body] : [];
|
|
24814
|
+
return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
|
|
24491
24815
|
};
|
|
24492
24816
|
//#endregion
|
|
24493
24817
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
@@ -24526,6 +24850,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24526
24850
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24527
24851
|
}
|
|
24528
24852
|
};
|
|
24853
|
+
const flattenGuardedStatements = (statements) => {
|
|
24854
|
+
const flattened = [];
|
|
24855
|
+
for (const statement of statements) {
|
|
24856
|
+
if (isNoOpStatement(statement)) continue;
|
|
24857
|
+
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24858
|
+
flattened.push(statement);
|
|
24859
|
+
continue;
|
|
24860
|
+
}
|
|
24861
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
24862
|
+
for (const branch of [statement.consequent, statement.alternate]) {
|
|
24863
|
+
if (!branch) continue;
|
|
24864
|
+
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
24865
|
+
if (flattenedBranch === null) return null;
|
|
24866
|
+
flattened.push(...flattenedBranch);
|
|
24867
|
+
}
|
|
24868
|
+
continue;
|
|
24869
|
+
}
|
|
24870
|
+
return null;
|
|
24871
|
+
}
|
|
24872
|
+
return flattened;
|
|
24873
|
+
};
|
|
24529
24874
|
const noDerivedStateEffect = defineRule({
|
|
24530
24875
|
id: "no-derived-state-effect",
|
|
24531
24876
|
title: "Derived state stored in an effect",
|
|
@@ -24557,8 +24902,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24557
24902
|
}
|
|
24558
24903
|
}
|
|
24559
24904
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24560
|
-
const statements = getCallbackStatements(callback);
|
|
24561
|
-
if (statements.length === 0) return;
|
|
24905
|
+
const statements = flattenGuardedStatements(getCallbackStatements(callback));
|
|
24906
|
+
if (statements === null || statements.length === 0) return;
|
|
24562
24907
|
if (!statements.every((statement) => {
|
|
24563
24908
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24564
24909
|
const expression = statement.expression;
|
|
@@ -25193,7 +25538,7 @@ const noDidMountSetState = defineRule({
|
|
|
25193
25538
|
const { mode } = resolveSettings$20(context.settings);
|
|
25194
25539
|
return { CallExpression(node) {
|
|
25195
25540
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25196
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25541
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25197
25542
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25198
25543
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25199
25544
|
if (isMountFlagArgument(node.arguments?.[0])) return;
|
|
@@ -25318,7 +25663,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
25318
25663
|
const { mode } = resolveSettings$19(context.settings);
|
|
25319
25664
|
return { CallExpression(node) {
|
|
25320
25665
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25321
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25666
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25322
25667
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25323
25668
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25324
25669
|
if (isInsideDiffGuard(node)) return;
|
|
@@ -25415,18 +25760,45 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
25415
25760
|
//#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
|
|
25416
25761
|
const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
|
|
25417
25762
|
const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
|
|
25763
|
+
const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
|
|
25764
|
+
const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
|
|
25765
|
+
"assign",
|
|
25766
|
+
"entries",
|
|
25767
|
+
"fromEntries",
|
|
25768
|
+
"keys",
|
|
25769
|
+
"values"
|
|
25770
|
+
]);
|
|
25771
|
+
const ARRAY_COPY_METHOD_NAMES = new Set([
|
|
25772
|
+
"map",
|
|
25773
|
+
"filter",
|
|
25774
|
+
"slice",
|
|
25775
|
+
"concat",
|
|
25776
|
+
"flat",
|
|
25777
|
+
"flatMap",
|
|
25778
|
+
"toSorted",
|
|
25779
|
+
"toReversed",
|
|
25780
|
+
"toSpliced",
|
|
25781
|
+
"with"
|
|
25782
|
+
]);
|
|
25418
25783
|
const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
25419
25784
|
const isPlainDataProducerCall = (expression) => {
|
|
25420
25785
|
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25421
25786
|
const callee = expression.callee;
|
|
25422
25787
|
if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
|
|
25423
25788
|
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
25424
|
-
if (isNodeOfType(callee.object, "Identifier")
|
|
25789
|
+
if (isNodeOfType(callee.object, "Identifier")) {
|
|
25790
|
+
if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
|
|
25791
|
+
if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
|
|
25792
|
+
if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
|
|
25793
|
+
}
|
|
25425
25794
|
return producesPlainStateValue(callee.object);
|
|
25426
25795
|
};
|
|
25796
|
+
const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
|
|
25797
|
+
const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
|
|
25427
25798
|
const producesPlainStateValue = (expression) => {
|
|
25428
25799
|
const unwrapped = stripParenExpression(expression);
|
|
25429
25800
|
if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
|
|
25801
|
+
if (isPlainDataNewExpression(unwrapped)) return true;
|
|
25430
25802
|
if (isNullOrUndefinedExpression(unwrapped)) return true;
|
|
25431
25803
|
if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
|
|
25432
25804
|
return isPlainDataProducerCall(unwrapped);
|
|
@@ -25441,6 +25813,38 @@ const initializerMarksPlainState = (initializerArgument) => {
|
|
|
25441
25813
|
}
|
|
25442
25814
|
return producesPlainStateValue(unwrapped);
|
|
25443
25815
|
};
|
|
25816
|
+
const producesOpaqueInstanceValue = (expression) => {
|
|
25817
|
+
if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
|
|
25818
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25819
|
+
const callee = expression.callee;
|
|
25820
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
25821
|
+
if (isPlainDataProducerCall(expression)) return false;
|
|
25822
|
+
if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
|
|
25823
|
+
return true;
|
|
25824
|
+
};
|
|
25825
|
+
const collectSetterValueObservations = (componentBody, setterNames) => {
|
|
25826
|
+
const plainFedSetterNames = /* @__PURE__ */ new Set();
|
|
25827
|
+
const opaqueFedSetterNames = /* @__PURE__ */ new Set();
|
|
25828
|
+
walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
|
|
25829
|
+
if (!isNodeOfType(node, "CallExpression")) return;
|
|
25830
|
+
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
25831
|
+
const setterName = node.callee.name;
|
|
25832
|
+
if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
|
|
25833
|
+
const argument = node.arguments?.[0];
|
|
25834
|
+
if (!argument) return;
|
|
25835
|
+
const unwrapped = stripParenExpression(argument);
|
|
25836
|
+
if (isNullOrUndefinedExpression(unwrapped)) return;
|
|
25837
|
+
if (producesPlainStateValue(unwrapped)) {
|
|
25838
|
+
plainFedSetterNames.add(setterName);
|
|
25839
|
+
return;
|
|
25840
|
+
}
|
|
25841
|
+
if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
|
|
25842
|
+
}, true);
|
|
25843
|
+
return {
|
|
25844
|
+
plainFedSetterNames,
|
|
25845
|
+
opaqueFedSetterNames
|
|
25846
|
+
};
|
|
25847
|
+
};
|
|
25444
25848
|
const collectCallbackRefSetterNames = (componentBody) => {
|
|
25445
25849
|
const callbackRefSetterNames = /* @__PURE__ */ new Set();
|
|
25446
25850
|
walkAst(componentBody, (node) => {
|
|
@@ -25510,11 +25914,15 @@ const noDirectStateMutation = defineRule({
|
|
|
25510
25914
|
if (bindings.length === 0) return;
|
|
25511
25915
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
25512
25916
|
const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
|
|
25917
|
+
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
25513
25918
|
const plainObjectStateValueNames = /* @__PURE__ */ new Set();
|
|
25514
25919
|
for (const binding of bindings) {
|
|
25515
25920
|
if (callbackRefSetterNames.has(binding.setterName)) continue;
|
|
25516
25921
|
if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
|
|
25517
|
-
|
|
25922
|
+
const initializerArgument = binding.declarator.init.arguments?.[0];
|
|
25923
|
+
if (!initializerMarksPlainState(initializerArgument)) continue;
|
|
25924
|
+
if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
|
|
25925
|
+
plainObjectStateValueNames.add(binding.valueName);
|
|
25518
25926
|
}
|
|
25519
25927
|
const visitMutationCandidate = (child, currentlyShadowed) => {
|
|
25520
25928
|
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
@@ -25639,9 +26047,10 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
25639
26047
|
create: (context) => ({ CallExpression(node) {
|
|
25640
26048
|
const callee = node.callee;
|
|
25641
26049
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
25642
|
-
|
|
26050
|
+
const receiver = stripParenExpression(callee.object);
|
|
26051
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25643
26052
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
25644
|
-
if (context.scopes.symbolFor(
|
|
26053
|
+
if (context.scopes.symbolFor(receiver) !== null) return;
|
|
25645
26054
|
if (!importsReactViewTransition(node)) return;
|
|
25646
26055
|
context.report({
|
|
25647
26056
|
node,
|
|
@@ -25661,7 +26070,8 @@ const noDocumentWrite = defineRule({
|
|
|
25661
26070
|
create: (context) => ({ CallExpression(node) {
|
|
25662
26071
|
const callee = node.callee;
|
|
25663
26072
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
25664
|
-
|
|
26073
|
+
const receiver = stripParenExpression(callee.object);
|
|
26074
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25665
26075
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
25666
26076
|
context.report({
|
|
25667
26077
|
node,
|
|
@@ -26293,13 +26703,6 @@ const noEffectEventHandler = defineRule({
|
|
|
26293
26703
|
}
|
|
26294
26704
|
});
|
|
26295
26705
|
//#endregion
|
|
26296
|
-
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
26297
|
-
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
26298
|
-
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
26299
|
-
if (importSource === null) return false;
|
|
26300
|
-
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
26301
|
-
};
|
|
26302
|
-
//#endregion
|
|
26303
26706
|
//#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
|
|
26304
26707
|
const createComponentBindingStackTracker = (callbacks) => {
|
|
26305
26708
|
const componentBindingStack = [];
|
|
@@ -26353,11 +26756,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
26353
26756
|
const initializer = declaratorNode.init;
|
|
26354
26757
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26355
26758
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26356
|
-
if (
|
|
26357
|
-
if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
|
|
26358
|
-
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26359
|
-
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26360
|
-
}
|
|
26759
|
+
if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
|
|
26361
26760
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26362
26761
|
} });
|
|
26363
26762
|
return {
|
|
@@ -28937,7 +29336,7 @@ const noIsMounted = defineRule({
|
|
|
28937
29336
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
28938
29337
|
create: (context) => ({ CallExpression(node) {
|
|
28939
29338
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
28940
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
29339
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
28941
29340
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
|
|
28942
29341
|
if (!getParentComponent(node)) return;
|
|
28943
29342
|
context.report({
|
|
@@ -28952,7 +29351,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
|
|
|
28952
29351
|
const isJsonMethodCall = (node, method) => {
|
|
28953
29352
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
28954
29353
|
const callee = node.callee;
|
|
28955
|
-
|
|
29354
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
29355
|
+
const receiver = stripParenExpression(callee.object);
|
|
29356
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
28956
29357
|
};
|
|
28957
29358
|
const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
|
|
28958
29359
|
const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
|
|
@@ -29037,7 +29438,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
29037
29438
|
const noJsxElementType = defineRule({
|
|
29038
29439
|
id: "no-jsx-element-type",
|
|
29039
29440
|
title: "No JSX.Element",
|
|
29040
|
-
severity: "
|
|
29441
|
+
severity: "warn",
|
|
29041
29442
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
29042
29443
|
create: (context) => {
|
|
29043
29444
|
let isJsxImported = false;
|
|
@@ -29363,6 +29764,378 @@ const noLegacyContextApi = defineRule({
|
|
|
29363
29764
|
}
|
|
29364
29765
|
});
|
|
29365
29766
|
//#endregion
|
|
29767
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
29768
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
29769
|
+
"map",
|
|
29770
|
+
"filter",
|
|
29771
|
+
"forEach",
|
|
29772
|
+
"flatMap",
|
|
29773
|
+
"reduce",
|
|
29774
|
+
"reduceRight",
|
|
29775
|
+
"some",
|
|
29776
|
+
"every",
|
|
29777
|
+
"find",
|
|
29778
|
+
"findIndex",
|
|
29779
|
+
"findLast",
|
|
29780
|
+
"findLastIndex",
|
|
29781
|
+
"sort",
|
|
29782
|
+
"toSorted"
|
|
29783
|
+
]);
|
|
29784
|
+
const executesDuringRender = (functionNode) => {
|
|
29785
|
+
const parent = functionNode.parent;
|
|
29786
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
29787
|
+
if (parent.callee === functionNode) return true;
|
|
29788
|
+
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
29789
|
+
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
|
|
29790
|
+
};
|
|
29791
|
+
//#endregion
|
|
29792
|
+
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
29793
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
29794
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
29795
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
29796
|
+
return false;
|
|
29797
|
+
};
|
|
29798
|
+
//#endregion
|
|
29799
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
29800
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
29801
|
+
let ancestor = bindingIdentifier.parent;
|
|
29802
|
+
while (ancestor) {
|
|
29803
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
29804
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
29805
|
+
ancestor = ancestor.parent ?? null;
|
|
29806
|
+
}
|
|
29807
|
+
return null;
|
|
29808
|
+
};
|
|
29809
|
+
//#endregion
|
|
29810
|
+
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
29811
|
+
const isFalsyLiteral = (node) => {
|
|
29812
|
+
if (!node) return true;
|
|
29813
|
+
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
29814
|
+
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
29815
|
+
};
|
|
29816
|
+
const isFalsyInitialStateBinding = (identifier) => {
|
|
29817
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
29818
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
29819
|
+
if (!binding) return false;
|
|
29820
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29821
|
+
if (!declarator?.init) return false;
|
|
29822
|
+
const init = stripParenExpression(declarator.init);
|
|
29823
|
+
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
29824
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
29825
|
+
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
29826
|
+
return isFalsyLiteral(init.arguments?.[0]);
|
|
29827
|
+
};
|
|
29828
|
+
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
29829
|
+
//#endregion
|
|
29830
|
+
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
29831
|
+
const isGatedByFalsyInitialState = (node) => {
|
|
29832
|
+
let cursor = node;
|
|
29833
|
+
let parent = node.parent;
|
|
29834
|
+
while (parent) {
|
|
29835
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
29836
|
+
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29837
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29838
|
+
cursor = parent;
|
|
29839
|
+
parent = parent.parent ?? null;
|
|
29840
|
+
}
|
|
29841
|
+
return false;
|
|
29842
|
+
};
|
|
29843
|
+
//#endregion
|
|
29844
|
+
//#region src/plugin/utils/references-client-only-flag.ts
|
|
29845
|
+
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
29846
|
+
const referencesClientOnlyFlag = (expression) => {
|
|
29847
|
+
const unwrapped = stripParenExpression(expression);
|
|
29848
|
+
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
29849
|
+
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
29850
|
+
const property = unwrapped.property;
|
|
29851
|
+
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
29852
|
+
}
|
|
29853
|
+
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
29854
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
29855
|
+
return false;
|
|
29856
|
+
};
|
|
29857
|
+
//#endregion
|
|
29858
|
+
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
29859
|
+
const isInsideClientOnlyGuard = (node) => {
|
|
29860
|
+
let cursor = node;
|
|
29861
|
+
let parent = node.parent;
|
|
29862
|
+
while (parent) {
|
|
29863
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
29864
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
29865
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
29866
|
+
cursor = parent;
|
|
29867
|
+
parent = parent.parent ?? null;
|
|
29868
|
+
}
|
|
29869
|
+
return false;
|
|
29870
|
+
};
|
|
29871
|
+
//#endregion
|
|
29872
|
+
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
29873
|
+
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
29874
|
+
"toLocaleString",
|
|
29875
|
+
"toLocaleDateString",
|
|
29876
|
+
"toLocaleTimeString"
|
|
29877
|
+
]);
|
|
29878
|
+
const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
|
|
29879
|
+
const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
|
|
29880
|
+
const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
29881
|
+
"format",
|
|
29882
|
+
"formatToParts",
|
|
29883
|
+
"formatRange"
|
|
29884
|
+
]);
|
|
29885
|
+
const isProvableDateExpression = (expression) => {
|
|
29886
|
+
if (!expression) return false;
|
|
29887
|
+
const unwrapped = stripParenExpression(expression);
|
|
29888
|
+
return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
|
|
29889
|
+
};
|
|
29890
|
+
const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
|
|
29891
|
+
const receiverNameLooksDateFlavored = (expression) => {
|
|
29892
|
+
if (!expression) return false;
|
|
29893
|
+
const unwrapped = stripParenExpression(expression);
|
|
29894
|
+
if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
|
|
29895
|
+
if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
|
|
29896
|
+
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
29897
|
+
return false;
|
|
29898
|
+
};
|
|
29899
|
+
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
29900
|
+
if (!objectExpression) return false;
|
|
29901
|
+
const unwrapped = stripParenExpression(objectExpression);
|
|
29902
|
+
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
29903
|
+
for (const property of unwrapped.properties ?? []) {
|
|
29904
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
29905
|
+
if (property.computed) continue;
|
|
29906
|
+
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
29907
|
+
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
29908
|
+
}
|
|
29909
|
+
return false;
|
|
29910
|
+
};
|
|
29911
|
+
const hasExplicitLocaleArgument = (argument) => {
|
|
29912
|
+
if (!argument) return false;
|
|
29913
|
+
const unwrapped = stripParenExpression(argument);
|
|
29914
|
+
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
29915
|
+
return true;
|
|
29916
|
+
};
|
|
29917
|
+
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
29918
|
+
const localeArgument = call.arguments?.[0];
|
|
29919
|
+
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
29920
|
+
const optionsArgument = call.arguments?.[1];
|
|
29921
|
+
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
29922
|
+
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
29923
|
+
};
|
|
29924
|
+
const matchLocaleMethodCall = (call) => {
|
|
29925
|
+
const callee = call.callee;
|
|
29926
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29927
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29928
|
+
const methodName = callee.property.name;
|
|
29929
|
+
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
29930
|
+
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
29931
|
+
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
29932
|
+
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
29933
|
+
return {
|
|
29934
|
+
node: call,
|
|
29935
|
+
display: `${methodName}()`
|
|
29936
|
+
};
|
|
29937
|
+
};
|
|
29938
|
+
const getIntlFormatterName = (expression) => {
|
|
29939
|
+
if (!expression) return null;
|
|
29940
|
+
const unwrapped = stripParenExpression(expression);
|
|
29941
|
+
if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
|
|
29942
|
+
const callee = unwrapped.callee;
|
|
29943
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29944
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
|
|
29945
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29946
|
+
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
29947
|
+
};
|
|
29948
|
+
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
29949
|
+
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
29950
|
+
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
29951
|
+
if (formatterName !== "DateTimeFormat") return true;
|
|
29952
|
+
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
29953
|
+
};
|
|
29954
|
+
const matchIntlFormatCall = (call) => {
|
|
29955
|
+
const callee = call.callee;
|
|
29956
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29957
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29958
|
+
if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
|
|
29959
|
+
let construction = stripParenExpression(callee.object);
|
|
29960
|
+
if (isNodeOfType(construction, "Identifier")) {
|
|
29961
|
+
const binding = findVariableInitializer(construction, construction.name);
|
|
29962
|
+
construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
|
|
29963
|
+
}
|
|
29964
|
+
if (!construction) return null;
|
|
29965
|
+
const formatterName = getIntlFormatterName(construction);
|
|
29966
|
+
if (!formatterName) return null;
|
|
29967
|
+
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
29968
|
+
return {
|
|
29969
|
+
node: call,
|
|
29970
|
+
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
29971
|
+
};
|
|
29972
|
+
};
|
|
29973
|
+
const isDeterministicInputDateConstruction = (expression) => {
|
|
29974
|
+
if (!expression) return false;
|
|
29975
|
+
const unwrapped = stripParenExpression(expression);
|
|
29976
|
+
if (!isNodeOfType(unwrapped, "NewExpression")) return false;
|
|
29977
|
+
if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
|
|
29978
|
+
return (unwrapped.arguments?.length ?? 0) > 0;
|
|
29979
|
+
};
|
|
29980
|
+
const matchDateDefaultStringification = (node) => {
|
|
29981
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
29982
|
+
const callee = node.callee;
|
|
29983
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
|
|
29984
|
+
node,
|
|
29985
|
+
display: "Date.prototype.toString()"
|
|
29986
|
+
};
|
|
29987
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
|
|
29988
|
+
node,
|
|
29989
|
+
display: "String(new Date(…))"
|
|
29990
|
+
};
|
|
29991
|
+
return null;
|
|
29992
|
+
}
|
|
29993
|
+
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
29994
|
+
for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
|
|
29995
|
+
node: expression,
|
|
29996
|
+
display: "`${new Date(…)}`"
|
|
29997
|
+
};
|
|
29998
|
+
}
|
|
29999
|
+
return null;
|
|
30000
|
+
};
|
|
30001
|
+
const findRenderPhaseComponentOrHook = (node) => {
|
|
30002
|
+
let functionNode = findEnclosingFunction(node);
|
|
30003
|
+
while (functionNode) {
|
|
30004
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
30005
|
+
if (!executesDuringRender(functionNode)) return null;
|
|
30006
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
30007
|
+
}
|
|
30008
|
+
return null;
|
|
30009
|
+
};
|
|
30010
|
+
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
30011
|
+
if (fileHasUseClientDirective) return true;
|
|
30012
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
30013
|
+
if (displayName && isReactHookName(displayName)) return true;
|
|
30014
|
+
let callsHook = false;
|
|
30015
|
+
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
30016
|
+
if (callsHook) return false;
|
|
30017
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
30018
|
+
callsHook = true;
|
|
30019
|
+
return false;
|
|
30020
|
+
}
|
|
30021
|
+
});
|
|
30022
|
+
return callsHook;
|
|
30023
|
+
};
|
|
30024
|
+
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
30025
|
+
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
30026
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
30027
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
30028
|
+
let cursor = node;
|
|
30029
|
+
while (cursor) {
|
|
30030
|
+
ancestors.add(cursor);
|
|
30031
|
+
cursor = cursor.parent ?? null;
|
|
30032
|
+
}
|
|
30033
|
+
for (const statement of body.body ?? []) {
|
|
30034
|
+
if (ancestors.has(statement)) return false;
|
|
30035
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30036
|
+
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
30037
|
+
let returnsEarly = false;
|
|
30038
|
+
walkAst(statement.consequent, (child) => {
|
|
30039
|
+
if (isFunctionLike$1(child)) return false;
|
|
30040
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
30041
|
+
returnsEarly = true;
|
|
30042
|
+
return false;
|
|
30043
|
+
}
|
|
30044
|
+
});
|
|
30045
|
+
if (returnsEarly) return true;
|
|
30046
|
+
}
|
|
30047
|
+
return false;
|
|
30048
|
+
};
|
|
30049
|
+
const findEnclosingJsxOpeningElement = (node) => {
|
|
30050
|
+
let cursor = node.parent;
|
|
30051
|
+
while (cursor) {
|
|
30052
|
+
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30053
|
+
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30054
|
+
cursor = cursor.parent ?? null;
|
|
30055
|
+
}
|
|
30056
|
+
return null;
|
|
30057
|
+
};
|
|
30058
|
+
const noLocaleFormatInRender = defineRule({
|
|
30059
|
+
id: "no-locale-format-in-render",
|
|
30060
|
+
title: "Locale/timezone formatting during render",
|
|
30061
|
+
severity: "warn",
|
|
30062
|
+
category: "Correctness",
|
|
30063
|
+
disabledWhen: [
|
|
30064
|
+
"vite",
|
|
30065
|
+
"cra",
|
|
30066
|
+
"expo",
|
|
30067
|
+
"react-native",
|
|
30068
|
+
"unknown"
|
|
30069
|
+
],
|
|
30070
|
+
recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
|
|
30071
|
+
create: (context) => {
|
|
30072
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
30073
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
30074
|
+
let fileHasUseClientDirective = false;
|
|
30075
|
+
let fileIsEmailTemplate = false;
|
|
30076
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30077
|
+
const reportIfRenderPhase = (match) => {
|
|
30078
|
+
if (reportedNodes.has(match.node)) return;
|
|
30079
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
30080
|
+
if (!componentOrHookNode) return;
|
|
30081
|
+
if (fileIsEmailTemplate) return;
|
|
30082
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30083
|
+
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30084
|
+
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30085
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
30086
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30087
|
+
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30088
|
+
reportedNodes.add(match.node);
|
|
30089
|
+
context.report({
|
|
30090
|
+
node: match.node,
|
|
30091
|
+
message: `This can cause a hydration mismatch because ${match.display} formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
|
|
30092
|
+
});
|
|
30093
|
+
};
|
|
30094
|
+
return {
|
|
30095
|
+
Program(node) {
|
|
30096
|
+
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30097
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30098
|
+
},
|
|
30099
|
+
CallExpression(node) {
|
|
30100
|
+
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
30101
|
+
if (match) reportIfRenderPhase(match);
|
|
30102
|
+
},
|
|
30103
|
+
TemplateLiteral(node) {
|
|
30104
|
+
const match = matchDateDefaultStringification(node);
|
|
30105
|
+
if (match) reportIfRenderPhase(match);
|
|
30106
|
+
},
|
|
30107
|
+
JSXExpressionContainer(node) {
|
|
30108
|
+
const expression = stripParenExpression(node.expression);
|
|
30109
|
+
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30110
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30111
|
+
const helperName = expression.callee.name;
|
|
30112
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
30113
|
+
if (!componentOrHookNode) return;
|
|
30114
|
+
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30115
|
+
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
30116
|
+
if (componentOrHookDisplayNameForFunction(helperNode)) return;
|
|
30117
|
+
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
30118
|
+
if (isFunctionLike$1(child)) return false;
|
|
30119
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30120
|
+
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
30121
|
+
if (!match || reportedNodes.has(match.node)) return;
|
|
30122
|
+
if (fileIsEmailTemplate) return;
|
|
30123
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30124
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
30125
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
30126
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
30127
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30128
|
+
reportedNodes.add(match.node);
|
|
30129
|
+
context.report({
|
|
30130
|
+
node: match.node,
|
|
30131
|
+
message: `This can cause a hydration mismatch because ${match.display} (reached from JSX through "${helperName}") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
|
|
30132
|
+
});
|
|
30133
|
+
});
|
|
30134
|
+
}
|
|
30135
|
+
};
|
|
30136
|
+
}
|
|
30137
|
+
});
|
|
30138
|
+
//#endregion
|
|
29366
30139
|
//#region src/plugin/rules/design/no-long-transition-duration.ts
|
|
29367
30140
|
const hasInfiniteIterationCount = (properties) => properties.some((property) => {
|
|
29368
30141
|
if (getStylePropertyKey(property) !== "animationIterationCount") return false;
|
|
@@ -30176,7 +30949,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30176
30949
|
"reverse",
|
|
30177
30950
|
"sort"
|
|
30178
30951
|
]);
|
|
30179
|
-
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30180
30952
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30181
30953
|
"assign",
|
|
30182
30954
|
"defineProperties",
|
|
@@ -30250,7 +31022,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
30250
31022
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
30251
31023
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
30252
31024
|
if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30253
|
-
if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30254
31025
|
}
|
|
30255
31026
|
}
|
|
30256
31027
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -30289,6 +31060,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
30289
31060
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
30290
31061
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
30291
31062
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
31063
|
+
if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
|
|
30292
31064
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
30293
31065
|
});
|
|
30294
31066
|
return mutations;
|
|
@@ -30521,7 +31293,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
30521
31293
|
id: "no-nested-component-definition",
|
|
30522
31294
|
title: "Component defined inside another component",
|
|
30523
31295
|
tags: ["test-noise", "react-jsx-only"],
|
|
30524
|
-
severity: "
|
|
31296
|
+
severity: "warn",
|
|
30525
31297
|
category: "Correctness",
|
|
30526
31298
|
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.",
|
|
30527
31299
|
create: (context) => {
|
|
@@ -31476,12 +32248,12 @@ const noPassDataToParent = defineRule({
|
|
|
31476
32248
|
if (calleeNode === identifier) {
|
|
31477
32249
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
31478
32250
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
31479
|
-
} else if (isNodeOfType(calleeNode, "MemberExpression") &&
|
|
32251
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
31480
32252
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
31481
32253
|
if (isCustomHookParameter(ref)) continue;
|
|
31482
32254
|
} else continue;
|
|
31483
32255
|
const methodName = getCallMethodName(calleeNode);
|
|
31484
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32256
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31485
32257
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31486
32258
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
31487
32259
|
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
@@ -31672,7 +32444,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
31672
32444
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
31673
32445
|
const calleeNode = callExpr.callee;
|
|
31674
32446
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
31675
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32447
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31676
32448
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31677
32449
|
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
31678
32450
|
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
@@ -32000,40 +32772,6 @@ const noPreventDefault = defineRule({
|
|
|
32000
32772
|
}
|
|
32001
32773
|
});
|
|
32002
32774
|
//#endregion
|
|
32003
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
32004
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
32005
|
-
let node = callExpression;
|
|
32006
|
-
let parent = node.parent;
|
|
32007
|
-
while (parent) {
|
|
32008
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
32009
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
32010
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
32011
|
-
node = parent;
|
|
32012
|
-
parent = node.parent;
|
|
32013
|
-
continue;
|
|
32014
|
-
}
|
|
32015
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
32016
|
-
node = parent;
|
|
32017
|
-
parent = node.parent;
|
|
32018
|
-
continue;
|
|
32019
|
-
}
|
|
32020
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
32021
|
-
node = parent;
|
|
32022
|
-
parent = node.parent;
|
|
32023
|
-
continue;
|
|
32024
|
-
}
|
|
32025
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
32026
|
-
const expressions = parent.expressions ?? [];
|
|
32027
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
32028
|
-
node = parent;
|
|
32029
|
-
parent = node.parent;
|
|
32030
|
-
continue;
|
|
32031
|
-
}
|
|
32032
|
-
return false;
|
|
32033
|
-
}
|
|
32034
|
-
return false;
|
|
32035
|
-
};
|
|
32036
|
-
//#endregion
|
|
32037
32775
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
32038
32776
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
32039
32777
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -32240,7 +32978,7 @@ const isAlwaysFreshExpression = (expression) => {
|
|
|
32240
32978
|
return `${callee.name}()`;
|
|
32241
32979
|
}
|
|
32242
32980
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
32243
|
-
const receiver = callee.object;
|
|
32981
|
+
const receiver = stripParenExpression(callee.object);
|
|
32244
32982
|
const property = callee.property;
|
|
32245
32983
|
if (!isNodeOfType(property, "Identifier")) return null;
|
|
32246
32984
|
if (isNodeOfType(receiver, "Identifier")) {
|
|
@@ -32361,8 +33099,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32361
33099
|
if (typeof sourceValue !== "string") return;
|
|
32362
33100
|
if (handleExtraSource?.(node, context)) return;
|
|
32363
33101
|
if (sourceValue !== source) return;
|
|
33102
|
+
if (isTypeOnlyImport(node)) return;
|
|
32364
33103
|
for (const specifier of node.specifiers ?? []) {
|
|
32365
33104
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
33105
|
+
if (specifier.importKind === "type") continue;
|
|
32366
33106
|
const importedName = getImportedName$1(specifier);
|
|
32367
33107
|
if (!importedName) continue;
|
|
32368
33108
|
const message = messages.get(importedName);
|
|
@@ -32381,8 +33121,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32381
33121
|
MemberExpression(node) {
|
|
32382
33122
|
if (namespaceBindings.size === 0) return;
|
|
32383
33123
|
if (node.computed) return;
|
|
32384
|
-
|
|
32385
|
-
if (!
|
|
33124
|
+
const receiver = stripParenExpression(node.object);
|
|
33125
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
33126
|
+
if (!namespaceBindings.has(receiver.name)) return;
|
|
32386
33127
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
32387
33128
|
const message = messages.get(node.property.name);
|
|
32388
33129
|
if (message) context.report({
|
|
@@ -32447,7 +33188,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
32447
33188
|
});
|
|
32448
33189
|
//#endregion
|
|
32449
33190
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
32450
|
-
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."]
|
|
33191
|
+
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."]]);
|
|
32451
33192
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
32452
33193
|
if (!rawFilename) return false;
|
|
32453
33194
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -32485,7 +33226,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
32485
33226
|
requires: ["react:19"],
|
|
32486
33227
|
tags: ["test-noise", "migration-hint"],
|
|
32487
33228
|
severity: "warn",
|
|
32488
|
-
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19.
|
|
33229
|
+
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
|
|
32489
33230
|
create: (context) => {
|
|
32490
33231
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
32491
33232
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -32809,34 +33550,11 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
32809
33550
|
});
|
|
32810
33551
|
//#endregion
|
|
32811
33552
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
32812
|
-
const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32813
|
-
if (!symbol || visitedSymbols.has(symbol)) return false;
|
|
32814
|
-
visitedSymbols.add(symbol);
|
|
32815
|
-
if (isComponentParameterSymbol(symbol)) return true;
|
|
32816
|
-
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
32817
|
-
const source = symbol.initializer;
|
|
32818
|
-
if (!source) return false;
|
|
32819
|
-
return initializerRootsInProps(source, scopes, visitedSymbols);
|
|
32820
|
-
};
|
|
32821
|
-
const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32822
|
-
if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
|
|
32823
|
-
if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
|
|
32824
|
-
return rootsInProps(node, scopes, visitedSymbols);
|
|
32825
|
-
};
|
|
32826
|
-
const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32827
|
-
let current = node;
|
|
32828
|
-
while (isNodeOfType(current, "MemberExpression")) {
|
|
32829
|
-
if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
|
|
32830
|
-
current = current.object;
|
|
32831
|
-
}
|
|
32832
|
-
if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
|
|
32833
|
-
return false;
|
|
32834
|
-
};
|
|
32835
33553
|
const isInsideComponentContext = (node) => {
|
|
32836
33554
|
let cursor = node.parent;
|
|
32837
33555
|
while (cursor) {
|
|
32838
|
-
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
32839
33556
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33557
|
+
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
32840
33558
|
cursor = cursor.parent ?? null;
|
|
32841
33559
|
}
|
|
32842
33560
|
return false;
|
|
@@ -32846,24 +33564,28 @@ const functionBodyOf = (node) => {
|
|
|
32846
33564
|
if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
|
|
32847
33565
|
return null;
|
|
32848
33566
|
};
|
|
33567
|
+
const isHookCallee = (callee) => {
|
|
33568
|
+
if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
|
|
33569
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
|
|
33570
|
+
return false;
|
|
33571
|
+
};
|
|
32849
33572
|
const containsHookCall = (body) => {
|
|
32850
33573
|
let found = false;
|
|
32851
33574
|
walkAst(body, (child) => {
|
|
32852
|
-
if (found) return;
|
|
33575
|
+
if (found) return false;
|
|
33576
|
+
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32853
33577
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32854
|
-
|
|
32855
|
-
if (name && isReactHookName(name)) found = true;
|
|
33578
|
+
if (isHookCallee(child.callee)) found = true;
|
|
32856
33579
|
});
|
|
32857
33580
|
return found;
|
|
32858
33581
|
};
|
|
32859
|
-
const
|
|
33582
|
+
const isHookCallingRenderHelper = (symbol) => {
|
|
32860
33583
|
if (!symbol) return false;
|
|
32861
33584
|
const declaration = symbol.declarationNode;
|
|
32862
33585
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
32863
33586
|
const body = functionBodyOf(declaration);
|
|
32864
33587
|
if (!body) return false;
|
|
32865
|
-
|
|
32866
|
-
return !containsHookCall(body);
|
|
33588
|
+
return containsHookCall(body);
|
|
32867
33589
|
};
|
|
32868
33590
|
const noRenderInRender = defineRule({
|
|
32869
33591
|
id: "no-render-in-render",
|
|
@@ -32872,20 +33594,13 @@ const noRenderInRender = defineRule({
|
|
|
32872
33594
|
tags: ["test-noise"],
|
|
32873
33595
|
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
32874
33596
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
32875
|
-
const expression = node.expression;
|
|
33597
|
+
const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
|
|
32876
33598
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
32877
|
-
|
|
32878
|
-
|
|
32879
|
-
|
|
32880
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33599
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
33600
|
+
const calleeName = expression.callee.name;
|
|
33601
|
+
if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
32881
33602
|
if (!isInsideComponentContext(node)) return;
|
|
32882
|
-
if (
|
|
32883
|
-
const calleeSymbol = context.scopes.symbolFor(expression.callee);
|
|
32884
|
-
if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
|
|
32885
|
-
if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
|
|
32886
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
32887
|
-
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
32888
|
-
}
|
|
33603
|
+
if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
|
|
32889
33604
|
context.report({
|
|
32890
33605
|
node: expression,
|
|
32891
33606
|
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.`
|
|
@@ -32946,13 +33661,7 @@ const noRenderPropChildren = defineRule({
|
|
|
32946
33661
|
//#endregion
|
|
32947
33662
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
32948
33663
|
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
32949
|
-
const isReactDomRenderCall = (node) =>
|
|
32950
|
-
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
32951
|
-
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
32952
|
-
if (node.callee.object.name !== "ReactDOM") return false;
|
|
32953
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
32954
|
-
return node.callee.property.name === "render";
|
|
32955
|
-
};
|
|
33664
|
+
const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
|
|
32956
33665
|
const isUsedAsReturnValue = (parent) => {
|
|
32957
33666
|
if (!parent) return false;
|
|
32958
33667
|
if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
|
|
@@ -33788,7 +34497,7 @@ const noSetState = defineRule({
|
|
|
33788
34497
|
category: "Architecture",
|
|
33789
34498
|
create: (context) => ({ CallExpression(node) {
|
|
33790
34499
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
33791
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
34500
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
33792
34501
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
33793
34502
|
if (!getParentComponent(node)) return;
|
|
33794
34503
|
context.report({
|
|
@@ -36234,6 +36943,36 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36234
36943
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36235
36944
|
return true;
|
|
36236
36945
|
};
|
|
36946
|
+
const isTrivialContainerLiteral = (node) => {
|
|
36947
|
+
if (!node) return false;
|
|
36948
|
+
const innerExpression = stripParenExpression(node);
|
|
36949
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
36950
|
+
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
36951
|
+
return false;
|
|
36952
|
+
};
|
|
36953
|
+
const isNonEscapingRead = (identifier) => {
|
|
36954
|
+
const readRoot = findTransparentExpressionRoot(identifier);
|
|
36955
|
+
const memberNode = readRoot.parent;
|
|
36956
|
+
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
|
|
36957
|
+
const memberUse = findTransparentExpressionRoot(memberNode);
|
|
36958
|
+
const memberUseParent = memberUse.parent;
|
|
36959
|
+
if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
|
|
36960
|
+
if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
|
|
36961
|
+
if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
|
|
36962
|
+
return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
|
|
36963
|
+
};
|
|
36964
|
+
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
36965
|
+
const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
|
|
36966
|
+
const parentNode = memoUsageRoot.parent;
|
|
36967
|
+
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
36968
|
+
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
|
|
36969
|
+
const bindingTarget = parentNode.id;
|
|
36970
|
+
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
36971
|
+
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
36972
|
+
const symbol = scopes.symbolFor(bindingTarget);
|
|
36973
|
+
if (!symbol) return false;
|
|
36974
|
+
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
36975
|
+
};
|
|
36237
36976
|
const noUsememoSimpleExpression = defineRule({
|
|
36238
36977
|
id: "no-usememo-simple-expression",
|
|
36239
36978
|
title: "useMemo on a cheap value",
|
|
@@ -36255,9 +36994,17 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36255
36994
|
let returnExpression = null;
|
|
36256
36995
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36257
36996
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36258
|
-
if (returnExpression
|
|
36997
|
+
if (!returnExpression) return;
|
|
36998
|
+
if (isTriviallyCheapExpression(returnExpression)) {
|
|
36999
|
+
context.report({
|
|
37000
|
+
node,
|
|
37001
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
37002
|
+
});
|
|
37003
|
+
return;
|
|
37004
|
+
}
|
|
37005
|
+
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36259
37006
|
node,
|
|
36260
|
-
message: "This
|
|
37007
|
+
message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
|
|
36261
37008
|
});
|
|
36262
37009
|
} })
|
|
36263
37010
|
});
|
|
@@ -36363,7 +37110,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
36363
37110
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
36364
37111
|
return { CallExpression(node) {
|
|
36365
37112
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
36366
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
37113
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36367
37114
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36368
37115
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36369
37116
|
context.report({
|
|
@@ -36680,6 +37427,10 @@ const isRouteFactoryCall = (expression) => {
|
|
|
36680
37427
|
}
|
|
36681
37428
|
return false;
|
|
36682
37429
|
};
|
|
37430
|
+
const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
|
|
37431
|
+
const expression = skipTsExpression(argument);
|
|
37432
|
+
return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
|
|
37433
|
+
});
|
|
36683
37434
|
const isReactHocName = (name, state) => state.customHocs.has(name);
|
|
36684
37435
|
const isHocCallee = (callee, state) => {
|
|
36685
37436
|
if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
|
|
@@ -36719,7 +37470,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
|
36719
37470
|
continue;
|
|
36720
37471
|
}
|
|
36721
37472
|
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
36722
|
-
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
37473
|
+
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
|
|
36723
37474
|
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
36724
37475
|
}
|
|
36725
37476
|
return false;
|
|
@@ -36827,18 +37578,22 @@ const onlyExportComponents = defineRule({
|
|
|
36827
37578
|
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36828
37579
|
allowExportNames: new Set(settings.allowExportNames),
|
|
36829
37580
|
allowConstantExport: settings.allowConstantExport,
|
|
36830
|
-
localComponentNames
|
|
37581
|
+
localComponentNames,
|
|
37582
|
+
scopes: context.scopes
|
|
36831
37583
|
};
|
|
36832
37584
|
for (const child of componentCandidates) {
|
|
36833
37585
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36834
|
-
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37586
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
|
|
36835
37587
|
}
|
|
36836
37588
|
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
36837
37589
|
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36838
37590
|
}
|
|
36839
37591
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36840
37592
|
const initializer = child.init;
|
|
36841
|
-
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child))
|
|
37593
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
|
|
37594
|
+
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
37595
|
+
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
37596
|
+
}
|
|
36842
37597
|
}
|
|
36843
37598
|
}
|
|
36844
37599
|
const exports = [];
|
|
@@ -36908,6 +37663,10 @@ const onlyExportComponents = defineRule({
|
|
|
36908
37663
|
return false;
|
|
36909
37664
|
})();
|
|
36910
37665
|
if (isHoc && firstArgIsValid) hasReactExport = true;
|
|
37666
|
+
else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
|
|
37667
|
+
kind: "non-component",
|
|
37668
|
+
reportNode: stripped
|
|
37669
|
+
});
|
|
36911
37670
|
else context.report({
|
|
36912
37671
|
node: stripped,
|
|
36913
37672
|
message: ANONYMOUS_MESSAGE
|
|
@@ -38503,11 +39262,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
|
|
|
38503
39262
|
}
|
|
38504
39263
|
return null;
|
|
38505
39264
|
};
|
|
39265
|
+
const isTrivialLiteralExpression = (expression) => {
|
|
39266
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
39267
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
39268
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
|
|
39269
|
+
if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
|
|
39270
|
+
return false;
|
|
39271
|
+
};
|
|
38506
39272
|
const getSingleSetterCallFromHandler = (handler) => {
|
|
38507
39273
|
const handlerStatements = getCallbackStatements(handler);
|
|
38508
39274
|
if (handlerStatements.length !== 1) return null;
|
|
38509
39275
|
const onlyStatement = handlerStatements[0];
|
|
38510
|
-
|
|
39276
|
+
let expression = onlyStatement;
|
|
39277
|
+
if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
|
|
39278
|
+
if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
|
|
39279
|
+
if (!expression) return null;
|
|
39280
|
+
expression = stripParenExpression(expression);
|
|
38511
39281
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
38512
39282
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
38513
39283
|
if (!isSetterIdentifier(expression.callee.name)) return null;
|
|
@@ -38517,6 +39287,106 @@ const getSingleSetterCallFromHandler = (handler) => {
|
|
|
38517
39287
|
setterArgument: expression.arguments[0]
|
|
38518
39288
|
};
|
|
38519
39289
|
};
|
|
39290
|
+
const isListenerCollectionInitializer = (init) => {
|
|
39291
|
+
if (!init) return false;
|
|
39292
|
+
if (isNodeOfType(init, "ArrayExpression")) return true;
|
|
39293
|
+
return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
|
|
39294
|
+
};
|
|
39295
|
+
const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
|
|
39296
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
39297
|
+
const firstParam = functionNode.params?.[0];
|
|
39298
|
+
if (!isNodeOfType(firstParam, "Identifier")) return false;
|
|
39299
|
+
const listenerParamName = firstParam.name;
|
|
39300
|
+
let registersListener = false;
|
|
39301
|
+
walkAst(functionNode.body, (child) => {
|
|
39302
|
+
if (registersListener) return false;
|
|
39303
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39304
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
39305
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
39306
|
+
if (!listenerCollectionNames.has(child.callee.object.name)) return;
|
|
39307
|
+
if (!isNodeOfType(child.callee.property, "Identifier")) return;
|
|
39308
|
+
if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
|
|
39309
|
+
const registeredArgument = child.arguments?.[0];
|
|
39310
|
+
if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
|
|
39311
|
+
});
|
|
39312
|
+
return registersListener;
|
|
39313
|
+
};
|
|
39314
|
+
const buildModuleScopeStoreIndex = (programRoot) => {
|
|
39315
|
+
const mutableBindingNames = /* @__PURE__ */ new Set();
|
|
39316
|
+
const listenerCollectionNames = /* @__PURE__ */ new Set();
|
|
39317
|
+
const moduleFunctionsByName = /* @__PURE__ */ new Map();
|
|
39318
|
+
if (!isNodeOfType(programRoot, "Program")) return {
|
|
39319
|
+
mutableBindingNames,
|
|
39320
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39321
|
+
};
|
|
39322
|
+
for (const statement of programRoot.body ?? []) {
|
|
39323
|
+
const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
|
|
39324
|
+
if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
|
|
39325
|
+
moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
|
|
39326
|
+
continue;
|
|
39327
|
+
}
|
|
39328
|
+
if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
|
|
39329
|
+
for (const declarator of unwrapped.declarations ?? []) {
|
|
39330
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
39331
|
+
const init = declarator.init ?? null;
|
|
39332
|
+
if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
|
|
39333
|
+
mutableBindingNames.add(declarator.id.name);
|
|
39334
|
+
continue;
|
|
39335
|
+
}
|
|
39336
|
+
if (isListenerCollectionInitializer(init)) {
|
|
39337
|
+
listenerCollectionNames.add(declarator.id.name);
|
|
39338
|
+
continue;
|
|
39339
|
+
}
|
|
39340
|
+
if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
|
|
39341
|
+
}
|
|
39342
|
+
}
|
|
39343
|
+
const subscribeFunctionNames = /* @__PURE__ */ new Set();
|
|
39344
|
+
for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
|
|
39345
|
+
return {
|
|
39346
|
+
mutableBindingNames,
|
|
39347
|
+
subscribeFunctionNames
|
|
39348
|
+
};
|
|
39349
|
+
};
|
|
39350
|
+
const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
|
|
39351
|
+
if (!isNodeOfType(useStateCall, "CallExpression")) return null;
|
|
39352
|
+
let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
|
|
39353
|
+
if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
|
|
39354
|
+
if (!isNodeOfType(initialArgument, "Identifier")) return null;
|
|
39355
|
+
if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
|
|
39356
|
+
const binding = findVariableInitializer(initialArgument, initialArgument.name);
|
|
39357
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
|
|
39358
|
+
return initialArgument.name;
|
|
39359
|
+
};
|
|
39360
|
+
const argumentForwardsSetter = (argument, setterName) => {
|
|
39361
|
+
if (!argument) return false;
|
|
39362
|
+
const unwrapped = stripParenExpression(argument);
|
|
39363
|
+
if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
|
|
39364
|
+
if (!isFunctionLike$1(unwrapped)) return false;
|
|
39365
|
+
let callsSetter = false;
|
|
39366
|
+
walkAst(unwrapped.body, (child) => {
|
|
39367
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
|
|
39368
|
+
callsSetter = true;
|
|
39369
|
+
return false;
|
|
39370
|
+
}
|
|
39371
|
+
});
|
|
39372
|
+
return callsSetter;
|
|
39373
|
+
};
|
|
39374
|
+
const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
|
|
39375
|
+
let matchedCall = null;
|
|
39376
|
+
walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
|
|
39377
|
+
if (matchedCall) return false;
|
|
39378
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39379
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
39380
|
+
if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
|
|
39381
|
+
const binding = findVariableInitializer(child.callee, child.callee.name);
|
|
39382
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
|
|
39383
|
+
for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
|
|
39384
|
+
matchedCall = child;
|
|
39385
|
+
return false;
|
|
39386
|
+
}
|
|
39387
|
+
});
|
|
39388
|
+
return matchedCall;
|
|
39389
|
+
};
|
|
38520
39390
|
const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
|
|
38521
39391
|
const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
|
|
38522
39392
|
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
@@ -38533,6 +39403,16 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38533
39403
|
severity: "warn",
|
|
38534
39404
|
recommendation: "Replace the `useState(getSnapshot())` + `useEffect(() => store.subscribe(() => setSnapshot(getSnapshot())))` pair with `useSyncExternalStore(store.subscribe, getSnapshot)`. The hook gets this right during concurrent rendering and on the server; the hand-rolled version doesn't.",
|
|
38535
39405
|
create: (context) => {
|
|
39406
|
+
let cachedStoreIndex = null;
|
|
39407
|
+
const storeIndexFor = (node) => {
|
|
39408
|
+
if (cachedStoreIndex) return cachedStoreIndex;
|
|
39409
|
+
const programRoot = findProgramRoot(node);
|
|
39410
|
+
cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
|
|
39411
|
+
mutableBindingNames: /* @__PURE__ */ new Set(),
|
|
39412
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39413
|
+
};
|
|
39414
|
+
return cachedStoreIndex;
|
|
39415
|
+
};
|
|
38536
39416
|
const checkComponent = (componentBody) => {
|
|
38537
39417
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38538
39418
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
@@ -38570,6 +39450,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38570
39450
|
const useStateInitializer = useStateInitializerByValueName.get(valueName);
|
|
38571
39451
|
if (!useStateInitializer) continue;
|
|
38572
39452
|
if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
|
|
39453
|
+
if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
|
|
38573
39454
|
if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
|
|
38574
39455
|
const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
|
|
38575
39456
|
context.report({
|
|
@@ -38577,14 +39458,47 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38577
39458
|
message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
|
|
38578
39459
|
});
|
|
38579
39460
|
}
|
|
39461
|
+
checkModuleStoreShape(componentBody, useStateBindings);
|
|
39462
|
+
};
|
|
39463
|
+
const checkModuleStoreShape = (componentBody, useStateBindings) => {
|
|
39464
|
+
const storeIndex = storeIndexFor(componentBody);
|
|
39465
|
+
if (storeIndex.mutableBindingNames.size === 0) return;
|
|
39466
|
+
if (storeIndex.subscribeFunctionNames.size === 0) return;
|
|
39467
|
+
const snapshotBindings = useStateBindings.map((binding) => ({
|
|
39468
|
+
binding,
|
|
39469
|
+
storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
|
|
39470
|
+
})).filter((candidate) => candidate.storeName !== null);
|
|
39471
|
+
if (snapshotBindings.length === 0) return;
|
|
39472
|
+
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
39473
|
+
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
39474
|
+
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
39475
|
+
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
39476
|
+
const depsNode = effectCall.arguments[1];
|
|
39477
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
39478
|
+
if ((depsNode.elements?.length ?? 0) !== 0) continue;
|
|
39479
|
+
const callback = getEffectCallback(effectCall);
|
|
39480
|
+
if (!callback || !isFunctionLike$1(callback)) continue;
|
|
39481
|
+
for (const { binding, storeName } of snapshotBindings) {
|
|
39482
|
+
if (reportedDeclarators.has(binding.declarator)) continue;
|
|
39483
|
+
if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
|
|
39484
|
+
reportedDeclarators.add(binding.declarator);
|
|
39485
|
+
context.report({
|
|
39486
|
+
node: binding.declarator,
|
|
39487
|
+
message: `Your users can miss updates or see torn values because useState "${binding.valueName}" snapshots module store "${storeName}" at render but only subscribes later in a useEffect.`
|
|
39488
|
+
});
|
|
39489
|
+
}
|
|
39490
|
+
}
|
|
38580
39491
|
};
|
|
38581
39492
|
return {
|
|
38582
39493
|
FunctionDeclaration(node) {
|
|
38583
|
-
|
|
39494
|
+
const functionName = node.id?.name;
|
|
39495
|
+
if (!functionName) return;
|
|
39496
|
+
if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
38584
39497
|
checkComponent(node.body);
|
|
38585
39498
|
},
|
|
38586
39499
|
VariableDeclarator(node) {
|
|
38587
|
-
|
|
39500
|
+
const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
|
|
39501
|
+
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
38588
39502
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
38589
39503
|
checkComponent(node.init.body);
|
|
38590
39504
|
}
|
|
@@ -39194,7 +40108,7 @@ const queryNoVoidQueryFn = defineRule({
|
|
|
39194
40108
|
if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
|
|
39195
40109
|
const body = queryFnValue.body;
|
|
39196
40110
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
39197
|
-
if ((body.body ?? []).length === 0) context.report({
|
|
40111
|
+
if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
|
|
39198
40112
|
node: queryFnProperty,
|
|
39199
40113
|
message: "This empty queryFn caches undefined, so the component never gets data."
|
|
39200
40114
|
});
|
|
@@ -39701,17 +40615,6 @@ const renderingHoistJsx = defineRule({
|
|
|
39701
40615
|
}
|
|
39702
40616
|
});
|
|
39703
40617
|
//#endregion
|
|
39704
|
-
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
39705
|
-
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
39706
|
-
let ancestor = bindingIdentifier.parent;
|
|
39707
|
-
while (ancestor) {
|
|
39708
|
-
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
39709
|
-
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
39710
|
-
ancestor = ancestor.parent ?? null;
|
|
39711
|
-
}
|
|
39712
|
-
return null;
|
|
39713
|
-
};
|
|
39714
|
-
//#endregion
|
|
39715
40618
|
//#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
|
|
39716
40619
|
const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
39717
40620
|
{
|
|
@@ -39720,19 +40623,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
|
39720
40623
|
},
|
|
39721
40624
|
{
|
|
39722
40625
|
display: "Date.now()",
|
|
39723
|
-
matches: (node) =>
|
|
40626
|
+
matches: (node) => isGlobalMethodCall(node, "Date", "now")
|
|
39724
40627
|
},
|
|
39725
40628
|
{
|
|
39726
40629
|
display: "Math.random()",
|
|
39727
|
-
matches: (node) =>
|
|
40630
|
+
matches: (node) => isGlobalMethodCall(node, "Math", "random")
|
|
39728
40631
|
},
|
|
39729
40632
|
{
|
|
39730
40633
|
display: "performance.now()",
|
|
39731
|
-
matches: (node) =>
|
|
40634
|
+
matches: (node) => isGlobalMethodCall(node, "performance", "now")
|
|
39732
40635
|
},
|
|
39733
40636
|
{
|
|
39734
40637
|
display: "crypto.randomUUID()",
|
|
39735
|
-
matches: (node) =>
|
|
40638
|
+
matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
|
|
39736
40639
|
}
|
|
39737
40640
|
];
|
|
39738
40641
|
const findOpeningElementOfChild = (jsxNode) => {
|
|
@@ -39744,54 +40647,6 @@ const findOpeningElementOfChild = (jsxNode) => {
|
|
|
39744
40647
|
}
|
|
39745
40648
|
return null;
|
|
39746
40649
|
};
|
|
39747
|
-
const executesDuringRender = (functionNode) => {
|
|
39748
|
-
const parent = functionNode.parent;
|
|
39749
|
-
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
39750
|
-
if (parent.callee === functionNode) return true;
|
|
39751
|
-
return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
|
|
39752
|
-
};
|
|
39753
|
-
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
39754
|
-
const referencesClientOnlyFlag = (expression) => {
|
|
39755
|
-
const unwrapped = stripParenExpression(expression);
|
|
39756
|
-
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
39757
|
-
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
39758
|
-
const property = unwrapped.property;
|
|
39759
|
-
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
39760
|
-
}
|
|
39761
|
-
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
39762
|
-
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
39763
|
-
return false;
|
|
39764
|
-
};
|
|
39765
|
-
const isFalsyLiteral = (node) => {
|
|
39766
|
-
if (!node) return true;
|
|
39767
|
-
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
39768
|
-
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
39769
|
-
};
|
|
39770
|
-
const isFalsyInitialStateBinding = (identifier) => {
|
|
39771
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
39772
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
39773
|
-
if (!binding) return false;
|
|
39774
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
39775
|
-
if (!declarator?.init) return false;
|
|
39776
|
-
const init = stripParenExpression(declarator.init);
|
|
39777
|
-
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
39778
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
39779
|
-
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
39780
|
-
return isFalsyLiteral(init.arguments?.[0]);
|
|
39781
|
-
};
|
|
39782
|
-
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
39783
|
-
const isGatedByFalsyInitialState = (node) => {
|
|
39784
|
-
let cursor = node;
|
|
39785
|
-
let parent = node.parent;
|
|
39786
|
-
while (parent) {
|
|
39787
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
39788
|
-
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39789
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39790
|
-
cursor = parent;
|
|
39791
|
-
parent = parent.parent ?? null;
|
|
39792
|
-
}
|
|
39793
|
-
return false;
|
|
39794
|
-
};
|
|
39795
40650
|
const isYearOnlyDateRead = (dateNode) => {
|
|
39796
40651
|
const member = dateNode.parent;
|
|
39797
40652
|
if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
|
|
@@ -39815,23 +40670,6 @@ const isInsideMotionTransitionAttribute = (node) => {
|
|
|
39815
40670
|
}
|
|
39816
40671
|
return false;
|
|
39817
40672
|
};
|
|
39818
|
-
const isInsideClientOnlyGuard = (node) => {
|
|
39819
|
-
let cursor = node;
|
|
39820
|
-
let parent = node.parent;
|
|
39821
|
-
while (parent) {
|
|
39822
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
39823
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
39824
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
39825
|
-
cursor = parent;
|
|
39826
|
-
parent = parent.parent ?? null;
|
|
39827
|
-
}
|
|
39828
|
-
return false;
|
|
39829
|
-
};
|
|
39830
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
39831
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
39832
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
39833
|
-
return false;
|
|
39834
|
-
};
|
|
39835
40673
|
const renderingHydrationMismatchTime = defineRule({
|
|
39836
40674
|
id: "rendering-hydration-mismatch-time",
|
|
39837
40675
|
title: "Time or random value in JSX",
|
|
@@ -39879,6 +40717,35 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
39879
40717
|
}
|
|
39880
40718
|
});
|
|
39881
40719
|
//#endregion
|
|
40720
|
+
//#region src/plugin/utils/contains-locale-environment-read.ts
|
|
40721
|
+
const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
|
|
40722
|
+
"toLocaleString",
|
|
40723
|
+
"toLocaleDateString",
|
|
40724
|
+
"toLocaleTimeString",
|
|
40725
|
+
"getTimezoneOffset"
|
|
40726
|
+
]);
|
|
40727
|
+
const containsLocaleEnvironmentRead = (expression) => {
|
|
40728
|
+
let readsLocaleEnvironment = false;
|
|
40729
|
+
walkAst(expression, (child) => {
|
|
40730
|
+
if (readsLocaleEnvironment) return false;
|
|
40731
|
+
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
|
|
40732
|
+
if (child.object.name === "Intl") {
|
|
40733
|
+
readsLocaleEnvironment = true;
|
|
40734
|
+
return false;
|
|
40735
|
+
}
|
|
40736
|
+
if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
|
|
40737
|
+
readsLocaleEnvironment = true;
|
|
40738
|
+
return false;
|
|
40739
|
+
}
|
|
40740
|
+
}
|
|
40741
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && !child.callee.computed && isNodeOfType(child.callee.property, "Identifier") && LOCALE_ENVIRONMENT_METHOD_NAMES.has(child.callee.property.name)) {
|
|
40742
|
+
readsLocaleEnvironment = true;
|
|
40743
|
+
return false;
|
|
40744
|
+
}
|
|
40745
|
+
});
|
|
40746
|
+
return readsLocaleEnvironment;
|
|
40747
|
+
};
|
|
40748
|
+
//#endregion
|
|
39882
40749
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
39883
40750
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
39884
40751
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -39944,14 +40811,15 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
39944
40811
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
39945
40812
|
const callback = getEffectCallback(node);
|
|
39946
40813
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
39947
|
-
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
39948
|
-
if (
|
|
40814
|
+
const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
|
|
40815
|
+
if (bodyStatements.length !== 1) return;
|
|
39949
40816
|
const soleStatement = bodyStatements[0];
|
|
39950
40817
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
39951
40818
|
const expression = soleStatement.expression;
|
|
39952
40819
|
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
|
|
39953
40820
|
if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
|
|
39954
40821
|
if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
|
|
40822
|
+
if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
|
|
39955
40823
|
context.report({
|
|
39956
40824
|
node,
|
|
39957
40825
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
@@ -40770,6 +41638,9 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
40770
41638
|
} })
|
|
40771
41639
|
});
|
|
40772
41640
|
//#endregion
|
|
41641
|
+
//#region src/plugin/utils/is-trivial-built-in-construction.ts
|
|
41642
|
+
const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
|
|
41643
|
+
//#endregion
|
|
40773
41644
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
40774
41645
|
const rerenderLazyRefInit = defineRule({
|
|
40775
41646
|
id: "rerender-lazy-ref-init",
|
|
@@ -40780,7 +41651,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40780
41651
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
40781
41652
|
create: (context) => ({ CallExpression(node) {
|
|
40782
41653
|
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
40783
|
-
const initializer = node.arguments[0];
|
|
41654
|
+
const initializer = stripParenExpression(node.arguments[0]);
|
|
40784
41655
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
40785
41656
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
40786
41657
|
if (!isPlainCall && !isNewCall) return;
|
|
@@ -40788,7 +41659,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40788
41659
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40789
41660
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40790
41661
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40791
|
-
if (
|
|
41662
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40792
41663
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40793
41664
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40794
41665
|
context.report({
|
|
@@ -40824,11 +41695,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
40824
41695
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
40825
41696
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
40826
41697
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
40827
|
-
|
|
40828
|
-
if (isNodeOfType(
|
|
40829
|
-
if (isNodeOfType(
|
|
40830
|
-
if (isNodeOfType(
|
|
40831
|
-
if (isNodeOfType(
|
|
41698
|
+
const innerExpression = stripParenExpression(expression);
|
|
41699
|
+
if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
|
|
41700
|
+
if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
|
|
41701
|
+
if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
|
|
41702
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
|
|
40832
41703
|
if (!element || !isNodeOfType(element, "SpreadElement")) continue;
|
|
40833
41704
|
const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
|
|
40834
41705
|
if (spreadCall) return spreadCall;
|
|
@@ -40851,7 +41722,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
40851
41722
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40852
41723
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40853
41724
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40854
|
-
if (
|
|
41725
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40855
41726
|
if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
|
|
40856
41727
|
if (isReactHookName(calleeName)) return;
|
|
40857
41728
|
const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
|
|
@@ -41518,7 +42389,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
41518
42389
|
const body = reactionFn.body;
|
|
41519
42390
|
let singleAssignment = null;
|
|
41520
42391
|
if (isNodeOfType(body, "BlockStatement")) {
|
|
41521
|
-
const statements = body.body ?? [];
|
|
42392
|
+
const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
|
|
41522
42393
|
if (statements.length !== 1) return;
|
|
41523
42394
|
const onlyStatement = statements[0];
|
|
41524
42395
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
@@ -41592,7 +42463,8 @@ const PROMISE_SETTLE_METHODS = new Set([
|
|
|
41592
42463
|
"catch",
|
|
41593
42464
|
"finally"
|
|
41594
42465
|
]);
|
|
41595
|
-
const findChainRoot = (
|
|
42466
|
+
const findChainRoot = (wrappedNode) => {
|
|
42467
|
+
const node = stripParenExpression(wrappedNode);
|
|
41596
42468
|
if (isNodeOfType(node, "CallExpression")) {
|
|
41597
42469
|
if (isNodeOfType(node.callee, "Identifier")) return {
|
|
41598
42470
|
calleeName: node.callee.name,
|
|
@@ -42180,20 +43052,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
|
|
|
42180
43052
|
};
|
|
42181
43053
|
const isReactNativeDimensionsCallee = (node, callee) => {
|
|
42182
43054
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
42183
|
-
|
|
42184
|
-
|
|
43055
|
+
const receiver = stripParenExpression(callee.object);
|
|
43056
|
+
if (isNodeOfType(receiver, "Identifier")) {
|
|
43057
|
+
const localName = receiver.name;
|
|
42185
43058
|
const binding = findVariableInitializer(node, localName);
|
|
42186
43059
|
if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
|
|
42187
43060
|
return localName === "Dimensions";
|
|
42188
43061
|
}
|
|
42189
|
-
if (isNodeOfType(
|
|
42190
|
-
if (getInitializerModuleSource(node,
|
|
42191
|
-
const rootName = getRootIdentifierName(
|
|
43062
|
+
if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
|
|
43063
|
+
if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
|
|
43064
|
+
const rootName = getRootIdentifierName(receiver.object);
|
|
42192
43065
|
if (rootName === null) return false;
|
|
42193
43066
|
const importBinding = getImportBindingForName(node, rootName);
|
|
42194
43067
|
return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
|
|
42195
43068
|
}
|
|
42196
|
-
if (getRequireCallSource(
|
|
43069
|
+
if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
|
|
42197
43070
|
return false;
|
|
42198
43071
|
};
|
|
42199
43072
|
const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
|
|
@@ -42637,7 +43510,8 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42637
43510
|
},
|
|
42638
43511
|
CallExpression(node) {
|
|
42639
43512
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
42640
|
-
|
|
43513
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
43514
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
|
|
42641
43515
|
if (!isMemberProperty(node.callee, "create")) return;
|
|
42642
43516
|
const stylesArgument = node.arguments?.[0];
|
|
42643
43517
|
if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
|
|
@@ -43487,7 +44361,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
43487
44361
|
const callee = node.callee;
|
|
43488
44362
|
if (!isStaticMemberNamed(callee, "setNativeProps")) return;
|
|
43489
44363
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
43490
|
-
if (!isStaticMemberNamed(callee.object, "current")) return;
|
|
44364
|
+
if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
|
|
43491
44365
|
context.report({
|
|
43492
44366
|
node,
|
|
43493
44367
|
message: "`setNativeProps` is a silent no-op under the New Architecture (Fabric), so this imperative update won't change the view. Drive the prop via state, an Animated.Value, or a Reanimated shared value."
|
|
@@ -43715,14 +44589,15 @@ const analyzeGestureChain = (expression) => {
|
|
|
43715
44589
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
43716
44590
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
43717
44591
|
const methodName = callee.property.name;
|
|
43718
|
-
|
|
44592
|
+
const receiver = stripParenExpression(callee.object);
|
|
44593
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
|
|
43719
44594
|
factoryName: methodName,
|
|
43720
44595
|
chainMethodNames,
|
|
43721
44596
|
numberOfTapsArgument
|
|
43722
44597
|
};
|
|
43723
44598
|
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
43724
44599
|
chainMethodNames.push(methodName);
|
|
43725
|
-
cursor =
|
|
44600
|
+
cursor = receiver;
|
|
43726
44601
|
}
|
|
43727
44602
|
return null;
|
|
43728
44603
|
};
|
|
@@ -47388,6 +48263,8 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47388
48263
|
const propName = propRawName.toLowerCase();
|
|
47389
48264
|
if (!propName.startsWith("aria-")) continue;
|
|
47390
48265
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
48266
|
+
const attributeValue = attributeNode.value;
|
|
48267
|
+
if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
|
|
47391
48268
|
(ariaAttributes ??= []).push({
|
|
47392
48269
|
attribute,
|
|
47393
48270
|
propName
|
|
@@ -47757,21 +48634,6 @@ const isInsideLoop = (descendant, ancestor) => {
|
|
|
47757
48634
|
}
|
|
47758
48635
|
return false;
|
|
47759
48636
|
};
|
|
47760
|
-
const isUseEffectEventSymbol = (symbol) => {
|
|
47761
|
-
const initializer = symbol.initializer;
|
|
47762
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47763
|
-
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47764
|
-
};
|
|
47765
|
-
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
47766
|
-
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
47767
|
-
return Boolean(symbol && symbol.kind !== "import");
|
|
47768
|
-
};
|
|
47769
|
-
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
|
|
47770
|
-
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47771
|
-
const initializer = symbol.initializer;
|
|
47772
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47773
|
-
return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
|
|
47774
|
-
};
|
|
47775
48637
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
47776
48638
|
let current = node.parent;
|
|
47777
48639
|
while (current) {
|
|
@@ -47961,8 +48823,7 @@ const rulesOfHooks = defineRule({
|
|
|
47961
48823
|
},
|
|
47962
48824
|
Identifier(node) {
|
|
47963
48825
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
47964
|
-
if (!symbol || !
|
|
47965
|
-
if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
|
|
48826
|
+
if (!symbol || !symbolHasReactUseEffectEventOrigin(symbol, context.scopes)) return;
|
|
47966
48827
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
47967
48828
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
47968
48829
|
context.report({
|
|
@@ -48110,7 +48971,8 @@ const serverAfterNonblocking = defineRule({
|
|
|
48110
48971
|
if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
|
|
48111
48972
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
48112
48973
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
48113
|
-
const
|
|
48974
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
48975
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
48114
48976
|
if (!objectName) return;
|
|
48115
48977
|
const methodName = node.callee.property.name;
|
|
48116
48978
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
@@ -48786,7 +49648,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
|
|
|
48786
49648
|
};
|
|
48787
49649
|
const ascendMemberChain = (referenceIdentifier) => {
|
|
48788
49650
|
let chainTip = referenceIdentifier;
|
|
48789
|
-
while (chainTip.parent
|
|
49651
|
+
while (chainTip.parent) {
|
|
49652
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
|
|
49653
|
+
chainTip = chainTip.parent;
|
|
49654
|
+
continue;
|
|
49655
|
+
}
|
|
49656
|
+
if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
|
|
49657
|
+
chainTip = chainTip.parent;
|
|
49658
|
+
continue;
|
|
49659
|
+
}
|
|
49660
|
+
break;
|
|
49661
|
+
}
|
|
48790
49662
|
return chainTip;
|
|
48791
49663
|
};
|
|
48792
49664
|
const isDirectContentsMutation = (referenceIdentifier) => {
|
|
@@ -48811,7 +49683,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
|
|
|
48811
49683
|
const callee = callExpression.callee;
|
|
48812
49684
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
48813
49685
|
const methodName = getMemberPropertyName$1(callee);
|
|
48814
|
-
|
|
49686
|
+
const calleeReceiver = stripParenExpression(callee.object);
|
|
49687
|
+
return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
48815
49688
|
}
|
|
48816
49689
|
if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
|
|
48817
49690
|
const calleeSymbol = scopes.symbolFor(callee);
|
|
@@ -49541,7 +50414,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49541
50414
|
};
|
|
49542
50415
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
49543
50416
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
49544
|
-
let currentNode = outerNode.callee.object;
|
|
50417
|
+
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
49545
50418
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
49546
50419
|
const calleeName = getCalleeName$2(currentNode);
|
|
49547
50420
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
@@ -49552,7 +50425,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49552
50425
|
}
|
|
49553
50426
|
}
|
|
49554
50427
|
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
49555
|
-
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
50428
|
+
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
|
|
49556
50429
|
else break;
|
|
49557
50430
|
}
|
|
49558
50431
|
return result;
|
|
@@ -50205,7 +51078,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
50205
51078
|
while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
|
|
50206
51079
|
const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
|
|
50207
51080
|
if (methodName) methodNames.unshift(methodName);
|
|
50208
|
-
currentNode = currentNode.callee.object;
|
|
51081
|
+
currentNode = stripParenExpression(currentNode.callee.object);
|
|
50209
51082
|
}
|
|
50210
51083
|
if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
|
|
50211
51084
|
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
@@ -53287,6 +54160,18 @@ const reactDoctorRules = [
|
|
|
53287
54160
|
category: "Bugs"
|
|
53288
54161
|
}
|
|
53289
54162
|
},
|
|
54163
|
+
{
|
|
54164
|
+
key: "react-doctor/no-locale-format-in-render",
|
|
54165
|
+
id: "no-locale-format-in-render",
|
|
54166
|
+
source: "react-doctor",
|
|
54167
|
+
originallyExternal: false,
|
|
54168
|
+
rule: {
|
|
54169
|
+
...noLocaleFormatInRender,
|
|
54170
|
+
framework: "global",
|
|
54171
|
+
category: "Bugs",
|
|
54172
|
+
requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
|
|
54173
|
+
}
|
|
54174
|
+
},
|
|
53290
54175
|
{
|
|
53291
54176
|
key: "react-doctor/no-long-transition-duration",
|
|
53292
54177
|
id: "no-long-transition-duration",
|
|
@@ -56163,6 +57048,7 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
56163
57048
|
"nextjs-no-use-search-params-without-suspense",
|
|
56164
57049
|
"no-dynamic-import-path",
|
|
56165
57050
|
"no-full-lodash-import",
|
|
57051
|
+
"no-locale-format-in-render",
|
|
56166
57052
|
"no-mutating-reducer-state",
|
|
56167
57053
|
"prefer-dynamic-import",
|
|
56168
57054
|
"rendering-hydration-mismatch-time",
|
|
@@ -56297,6 +57183,7 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
56297
57183
|
["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
|
|
56298
57184
|
["no-dynamic-import-path", collectNearestManifestDependencies],
|
|
56299
57185
|
["no-full-lodash-import", collectNearestManifestDependencies],
|
|
57186
|
+
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
56300
57187
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
56301
57188
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|
|
56302
57189
|
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
|