oxlint-plugin-react-doctor 0.7.2-dev.cb8f726 → 0.7.2-dev.e872767
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 +1211 -391
- 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;
|
|
@@ -9919,7 +10210,10 @@ const PRAGMA = "React";
|
|
|
9919
10210
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
9920
10211
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
9921
10212
|
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
9922
|
-
if (isNodeOfType(node.callee, "MemberExpression"))
|
|
10213
|
+
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
10214
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
10215
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
10216
|
+
}
|
|
9923
10217
|
return true;
|
|
9924
10218
|
};
|
|
9925
10219
|
//#endregion
|
|
@@ -11297,16 +11591,6 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11297
11591
|
return names;
|
|
11298
11592
|
};
|
|
11299
11593
|
//#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
11594
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11311
11595
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11312
11596
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -12006,14 +12290,15 @@ const jsCacheStorage = defineRule({
|
|
|
12006
12290
|
"ArrowFunctionExpression:exit": exitFunctionScope,
|
|
12007
12291
|
CallExpression(node) {
|
|
12008
12292
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
12009
|
-
|
|
12293
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
12294
|
+
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
|
|
12010
12295
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
12011
12296
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
12012
12297
|
const storageKey = String(node.arguments[0].value);
|
|
12013
12298
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
12014
12299
|
storageReadCounts.set(storageKey, readCount);
|
|
12015
12300
|
if (readCount === 2) {
|
|
12016
|
-
const storageName =
|
|
12301
|
+
const storageName = receiver.name;
|
|
12017
12302
|
context.report({
|
|
12018
12303
|
node,
|
|
12019
12304
|
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 +12312,16 @@ const jsCacheStorage = defineRule({
|
|
|
12027
12312
|
//#region src/plugin/rules/js-performance/js-combine-iterations.ts
|
|
12028
12313
|
const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
|
|
12029
12314
|
const callee = callExpression.callee;
|
|
12030
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12315
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
12316
|
+
const receiver = stripParenExpression(callee.object);
|
|
12317
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12318
|
+
if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
12319
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
12320
|
+
return true;
|
|
12321
|
+
}
|
|
12322
|
+
return false;
|
|
12036
12323
|
}
|
|
12324
|
+
if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
|
|
12037
12325
|
return false;
|
|
12038
12326
|
};
|
|
12039
12327
|
const isChainPassThroughCall = (callExpression) => {
|
|
@@ -12045,10 +12333,7 @@ const isChainPassThroughCall = (callExpression) => {
|
|
|
12045
12333
|
const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
|
|
12046
12334
|
let cursor = receiverNode;
|
|
12047
12335
|
while (cursor) {
|
|
12048
|
-
|
|
12049
|
-
cursor = cursor.expression;
|
|
12050
|
-
continue;
|
|
12051
|
-
}
|
|
12336
|
+
cursor = stripParenExpression(cursor);
|
|
12052
12337
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12053
12338
|
if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
|
|
12054
12339
|
if (!isChainPassThroughCall(cursor)) return false;
|
|
@@ -12124,10 +12409,7 @@ const isStringSplitRootedChain = (receiverNode) => {
|
|
|
12124
12409
|
let hops = 0;
|
|
12125
12410
|
while (cursor && hops < 12) {
|
|
12126
12411
|
hops += 1;
|
|
12127
|
-
|
|
12128
|
-
cursor = cursor.expression;
|
|
12129
|
-
continue;
|
|
12130
|
-
}
|
|
12412
|
+
cursor = stripParenExpression(cursor);
|
|
12131
12413
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12132
12414
|
const callee = cursor.callee;
|
|
12133
12415
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
@@ -12151,10 +12433,7 @@ const isSmallLiteralArray = (node) => {
|
|
|
12151
12433
|
const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
|
|
12152
12434
|
let cursor = receiverNode;
|
|
12153
12435
|
while (cursor) {
|
|
12154
|
-
|
|
12155
|
-
cursor = cursor.expression;
|
|
12156
|
-
continue;
|
|
12157
|
-
}
|
|
12436
|
+
cursor = stripParenExpression(cursor);
|
|
12158
12437
|
if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
|
|
12159
12438
|
if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
|
|
12160
12439
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
@@ -12219,7 +12498,7 @@ const jsCombineIterations = defineRule({
|
|
|
12219
12498
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
12220
12499
|
const outerMethod = node.callee.property.name;
|
|
12221
12500
|
if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
|
|
12222
|
-
const innerCall = node.callee.object;
|
|
12501
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12223
12502
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12224
12503
|
const innerMethod = innerCall.callee.property.name;
|
|
12225
12504
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
@@ -12292,10 +12571,10 @@ const jsFlatmapFilter = defineRule({
|
|
|
12292
12571
|
if (!filterArgument) return;
|
|
12293
12572
|
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
12573
|
if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
12295
|
-
const innerCall = node.callee.object;
|
|
12574
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12296
12575
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12297
12576
|
if (innerCall.callee.property.name !== "map") return;
|
|
12298
|
-
const receiver = innerCall.callee.object;
|
|
12577
|
+
const receiver = stripParenExpression(innerCall.callee.object);
|
|
12299
12578
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
12300
12579
|
const elements = receiver.elements ?? [];
|
|
12301
12580
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13555,7 +13834,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13555
13834
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13556
13835
|
create: (context) => ({ CallExpression(node) {
|
|
13557
13836
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
13558
|
-
const receiver = node.callee.object;
|
|
13837
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
13559
13838
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
|
|
13560
13839
|
const spreadArgument = receiver.elements[0].argument;
|
|
13561
13840
|
if (isFreshOrIteratorAllocation(spreadArgument)) return;
|
|
@@ -18592,7 +18871,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
|
|
|
18592
18871
|
};
|
|
18593
18872
|
const describeClientSideNavigation = (node) => {
|
|
18594
18873
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
|
|
18595
|
-
const
|
|
18874
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
18875
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
18596
18876
|
const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
18597
18877
|
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
|
|
18598
18878
|
}
|
|
@@ -19212,7 +19492,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
|
|
|
19212
19492
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
19213
19493
|
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
19214
19494
|
if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
|
|
19215
|
-
if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
|
|
19495
|
+
if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
|
|
19216
19496
|
return node.callee.property.name;
|
|
19217
19497
|
};
|
|
19218
19498
|
const isMutatingFetchCall = (node) => {
|
|
@@ -19226,7 +19506,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
|
|
|
19226
19506
|
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19227
19507
|
const { property, object } = node.callee;
|
|
19228
19508
|
if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
|
|
19229
|
-
if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
|
|
19509
|
+
if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
|
|
19230
19510
|
return true;
|
|
19231
19511
|
};
|
|
19232
19512
|
const getDbCallDescription = (node) => {
|
|
@@ -19234,7 +19514,8 @@ const getDbCallDescription = (node) => {
|
|
|
19234
19514
|
if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
|
|
19235
19515
|
if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
|
|
19236
19516
|
const methodName = node.callee.property.name;
|
|
19237
|
-
const
|
|
19517
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
19518
|
+
const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
19238
19519
|
return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
|
|
19239
19520
|
};
|
|
19240
19521
|
const findSideEffect = (node, options = {}) => {
|
|
@@ -20634,13 +20915,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
|
|
|
20634
20915
|
if (isNodeOfType(current, "CallExpression")) {
|
|
20635
20916
|
let node = ref.identifier;
|
|
20636
20917
|
let parent = node.parent;
|
|
20637
|
-
while (parent && isNodeOfType(parent, "MemberExpression")) {
|
|
20918
|
+
while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
|
|
20638
20919
|
node = parent;
|
|
20639
20920
|
parent = node.parent;
|
|
20640
20921
|
}
|
|
20641
20922
|
if (current.callee === node) return current;
|
|
20642
20923
|
}
|
|
20643
|
-
if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
|
|
20924
|
+
if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
|
|
20644
20925
|
return null;
|
|
20645
20926
|
};
|
|
20646
20927
|
const getArgsUpstreamRefs = (analysis, ref) => {
|
|
@@ -20775,7 +21056,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
20775
21056
|
const importDeclaration = declarationNode.parent;
|
|
20776
21057
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
20777
21058
|
}));
|
|
20778
|
-
const isHookCallee = (analysis, node, hookName) => {
|
|
21059
|
+
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
20779
21060
|
if (!node) return false;
|
|
20780
21061
|
if (isNodeOfType(node, "Identifier")) {
|
|
20781
21062
|
if (node.name === hookName) return true;
|
|
@@ -20784,15 +21065,19 @@ const isHookCallee = (analysis, node, hookName) => {
|
|
|
20784
21065
|
if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
20785
21066
|
return false;
|
|
20786
21067
|
}
|
|
20787
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
21068
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
21069
|
+
const receiver = stripParenExpression(node.object);
|
|
21070
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21071
|
+
}
|
|
20788
21072
|
return false;
|
|
20789
21073
|
};
|
|
20790
21074
|
const isUseEffect = (node) => {
|
|
20791
21075
|
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
20792
21076
|
const callee = node.callee;
|
|
20793
21077
|
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
20794
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
20795
|
-
|
|
21078
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
21079
|
+
const receiver = stripParenExpression(callee.object);
|
|
21080
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
20796
21081
|
};
|
|
20797
21082
|
const getEffectFn = (analysis, node) => {
|
|
20798
21083
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
@@ -20820,7 +21105,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20820
21105
|
const node = def.node;
|
|
20821
21106
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20822
21107
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20823
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21108
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20824
21109
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20825
21110
|
const elements = node.id.elements ?? [];
|
|
20826
21111
|
if (elements.length !== 1 && elements.length !== 2) return false;
|
|
@@ -20831,7 +21116,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
|
|
|
20831
21116
|
const node = def.node;
|
|
20832
21117
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20833
21118
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20834
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21119
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20835
21120
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20836
21121
|
const elements = node.id.elements ?? [];
|
|
20837
21122
|
if (elements.length !== 2) return false;
|
|
@@ -20878,7 +21163,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20878
21163
|
const node = def.node;
|
|
20879
21164
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20880
21165
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20881
|
-
return isHookCallee(analysis, node.init.callee, "useRef");
|
|
21166
|
+
return isHookCallee$1(analysis, node.init.callee, "useRef");
|
|
20882
21167
|
}));
|
|
20883
21168
|
const isRefCurrent = (ref) => {
|
|
20884
21169
|
const parent = ref.identifier.parent;
|
|
@@ -20891,11 +21176,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
|
|
|
20891
21176
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
20892
21177
|
const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
20893
21178
|
if (!isPropAlias(analysis, ref)) return false;
|
|
20894
|
-
|
|
20895
|
-
|
|
21179
|
+
let effectiveNode = ref.identifier;
|
|
21180
|
+
let parent = effectiveNode.parent;
|
|
21181
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
|
|
21182
|
+
effectiveNode = parent;
|
|
21183
|
+
parent = effectiveNode.parent;
|
|
21184
|
+
}
|
|
20896
21185
|
if (!parent) return false;
|
|
20897
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee ===
|
|
20898
|
-
if (isNodeOfType(parent, "MemberExpression") && parent.object ===
|
|
21186
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
|
|
21187
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
|
|
20899
21188
|
const memberParent = parent.parent;
|
|
20900
21189
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
20901
21190
|
if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
|
|
@@ -20906,7 +21195,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
20906
21195
|
};
|
|
20907
21196
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
20908
21197
|
const getUseStateDecl = (analysis, ref) => {
|
|
20909
|
-
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21198
|
+
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
|
|
20910
21199
|
while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
|
|
20911
21200
|
return node ?? null;
|
|
20912
21201
|
};
|
|
@@ -21111,7 +21400,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21111
21400
|
const noAdjustStateOnPropChange = defineRule({
|
|
21112
21401
|
id: "no-adjust-state-on-prop-change",
|
|
21113
21402
|
title: "State synced to a prop inside an effect",
|
|
21114
|
-
severity: "
|
|
21403
|
+
severity: "warn",
|
|
21115
21404
|
tags: ["test-noise"],
|
|
21116
21405
|
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
21406
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -21152,7 +21441,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
|
21152
21441
|
"summary",
|
|
21153
21442
|
"textarea"
|
|
21154
21443
|
]);
|
|
21444
|
+
const isStaticallyFalseBooleanAttribute = (attribute) => {
|
|
21445
|
+
const value = attribute.value;
|
|
21446
|
+
if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
21447
|
+
const expression = value.expression;
|
|
21448
|
+
return isNodeOfType(expression, "Literal") && expression.value === false;
|
|
21449
|
+
};
|
|
21450
|
+
const DISABLEABLE_TAGS = new Set([
|
|
21451
|
+
"button",
|
|
21452
|
+
"input",
|
|
21453
|
+
"select",
|
|
21454
|
+
"textarea"
|
|
21455
|
+
]);
|
|
21155
21456
|
const isNativelyFocusable = (tagName, openingElement) => {
|
|
21457
|
+
if (DISABLEABLE_TAGS.has(tagName)) {
|
|
21458
|
+
const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
|
|
21459
|
+
if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
|
|
21460
|
+
}
|
|
21156
21461
|
if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
|
|
21157
21462
|
switch (tagName) {
|
|
21158
21463
|
case "input": {
|
|
@@ -21166,7 +21471,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
|
|
|
21166
21471
|
case "a":
|
|
21167
21472
|
case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
|
|
21168
21473
|
case "audio":
|
|
21169
|
-
case "video":
|
|
21474
|
+
case "video": {
|
|
21475
|
+
const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
|
|
21476
|
+
return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
|
|
21477
|
+
}
|
|
21170
21478
|
default: return false;
|
|
21171
21479
|
}
|
|
21172
21480
|
};
|
|
@@ -22130,7 +22438,8 @@ const SECOND_INDEX_METHODS = new Set([
|
|
|
22130
22438
|
"some"
|
|
22131
22439
|
]);
|
|
22132
22440
|
const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
|
|
22133
|
-
const isPositionallyStableIterationReceiver = (
|
|
22441
|
+
const isPositionallyStableIterationReceiver = (receiverNode) => {
|
|
22442
|
+
const receiver = stripParenExpression(receiverNode);
|
|
22134
22443
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
22135
22444
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
|
|
22136
22445
|
const only = receiver.elements[0];
|
|
@@ -22141,17 +22450,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
|
|
|
22141
22450
|
}
|
|
22142
22451
|
if (!isNodeOfType(receiver, "CallExpression")) return false;
|
|
22143
22452
|
const callee = receiver.callee;
|
|
22144
|
-
if (
|
|
22453
|
+
if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
|
|
22145
22454
|
if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
|
|
22146
22455
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
|
|
22147
22456
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
|
|
22148
22457
|
return false;
|
|
22149
22458
|
};
|
|
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
|
-
};
|
|
22459
|
+
const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
|
|
22155
22460
|
const isArrayFromSourcePositionallyStable = (source) => {
|
|
22156
22461
|
if (isNodeOfType(source, "ObjectExpression")) {
|
|
22157
22462
|
for (const property of source.properties ?? []) {
|
|
@@ -22210,18 +22515,12 @@ const expressionUsesIndex = (expression, paramName) => {
|
|
|
22210
22515
|
return false;
|
|
22211
22516
|
}
|
|
22212
22517
|
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;
|
|
22518
|
+
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
|
|
22214
22519
|
if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
|
|
22215
22520
|
}
|
|
22216
22521
|
return false;
|
|
22217
22522
|
};
|
|
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
|
-
};
|
|
22523
|
+
const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
|
|
22225
22524
|
const noArrayIndexKey = defineRule({
|
|
22226
22525
|
id: "no-array-index-key",
|
|
22227
22526
|
title: "Array index used as a key",
|
|
@@ -22945,7 +23244,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
22945
23244
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
22946
23245
|
return false;
|
|
22947
23246
|
};
|
|
22948
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
23247
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
22949
23248
|
"forEach",
|
|
22950
23249
|
"map",
|
|
22951
23250
|
"filter",
|
|
@@ -22966,30 +23265,51 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
22966
23265
|
if (parent.callee === functionNode) return true;
|
|
22967
23266
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
22968
23267
|
const callee = parent.callee;
|
|
22969
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
23268
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
|
|
22970
23269
|
};
|
|
22971
23270
|
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) => {
|
|
23271
|
+
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
23272
|
+
const analyzeStatementSequence = (statements, context) => {
|
|
22981
23273
|
let fallThroughCount = 0;
|
|
22982
|
-
let
|
|
23274
|
+
let maxTerminatedCount = 0;
|
|
22983
23275
|
for (const statement of statements) {
|
|
22984
|
-
|
|
22985
|
-
|
|
22986
|
-
|
|
23276
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
23277
|
+
fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
|
|
23278
|
+
const thenSummary = analyzeBranchStatements(statement.consequent, context);
|
|
23279
|
+
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
23280
|
+
fallThroughCount: 0,
|
|
23281
|
+
maxTerminatedCount: 0,
|
|
23282
|
+
doAllPathsTerminate: false
|
|
23283
|
+
};
|
|
23284
|
+
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
23285
|
+
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
23286
|
+
fallThroughCount: 0,
|
|
23287
|
+
maxTerminatedCount,
|
|
23288
|
+
doAllPathsTerminate: true
|
|
23289
|
+
};
|
|
23290
|
+
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
23291
|
+
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
22987
23292
|
continue;
|
|
22988
23293
|
}
|
|
22989
|
-
if (isTerminatingStatement(statement))
|
|
23294
|
+
if (isTerminatingStatement(statement)) {
|
|
23295
|
+
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
23296
|
+
return {
|
|
23297
|
+
fallThroughCount: 0,
|
|
23298
|
+
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
23299
|
+
doAllPathsTerminate: true
|
|
23300
|
+
};
|
|
23301
|
+
}
|
|
22990
23302
|
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
22991
23303
|
}
|
|
22992
|
-
return
|
|
23304
|
+
return {
|
|
23305
|
+
fallThroughCount,
|
|
23306
|
+
maxTerminatedCount,
|
|
23307
|
+
doAllPathsTerminate: false
|
|
23308
|
+
};
|
|
23309
|
+
};
|
|
23310
|
+
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23311
|
+
const summary = analyzeStatementSequence(statements, context);
|
|
23312
|
+
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
22993
23313
|
};
|
|
22994
23314
|
const collectLocalHelperFunctions = (root) => {
|
|
22995
23315
|
const helpersByName = /* @__PURE__ */ new Map();
|
|
@@ -23120,7 +23440,9 @@ const isDevOnlyGuardedEffect = (callback) => {
|
|
|
23120
23440
|
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
23121
23441
|
const firstStatement = (body.body ?? [])[0];
|
|
23122
23442
|
if (!firstStatement) return false;
|
|
23123
|
-
if (!
|
|
23443
|
+
if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
|
|
23444
|
+
const consequent = firstStatement.consequent;
|
|
23445
|
+
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
23124
23446
|
return mentionsDevEnvFlag(firstStatement.test);
|
|
23125
23447
|
};
|
|
23126
23448
|
const noCascadingSetState = defineRule({
|
|
@@ -23479,40 +23801,6 @@ const noCloneElement = defineRule({
|
|
|
23479
23801
|
} })
|
|
23480
23802
|
});
|
|
23481
23803
|
//#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
23804
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23517
23805
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23518
23806
|
const CONTEXT_MODULES = [
|
|
@@ -24483,11 +24771,21 @@ const isInitialOnlySetterCall = (callExpr) => {
|
|
|
24483
24771
|
return isInitialOnlyPropName(arg.name);
|
|
24484
24772
|
};
|
|
24485
24773
|
//#endregion
|
|
24774
|
+
//#region src/plugin/utils/is-no-op-statement.ts
|
|
24775
|
+
const isNoOpStatement = (statement) => {
|
|
24776
|
+
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
24777
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24778
|
+
const expression = stripParenExpression(statement.expression);
|
|
24779
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
24780
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
24781
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
24782
|
+
return false;
|
|
24783
|
+
};
|
|
24784
|
+
//#endregion
|
|
24486
24785
|
//#region src/plugin/utils/get-callback-statements.ts
|
|
24487
24786
|
const getCallbackStatements = (callback) => {
|
|
24488
24787
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
24489
|
-
|
|
24490
|
-
return callback.body ? [callback.body] : [];
|
|
24788
|
+
return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
|
|
24491
24789
|
};
|
|
24492
24790
|
//#endregion
|
|
24493
24791
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
@@ -24526,6 +24824,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24526
24824
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24527
24825
|
}
|
|
24528
24826
|
};
|
|
24827
|
+
const flattenGuardedStatements = (statements) => {
|
|
24828
|
+
const flattened = [];
|
|
24829
|
+
for (const statement of statements) {
|
|
24830
|
+
if (isNoOpStatement(statement)) continue;
|
|
24831
|
+
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24832
|
+
flattened.push(statement);
|
|
24833
|
+
continue;
|
|
24834
|
+
}
|
|
24835
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
24836
|
+
for (const branch of [statement.consequent, statement.alternate]) {
|
|
24837
|
+
if (!branch) continue;
|
|
24838
|
+
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
24839
|
+
if (flattenedBranch === null) return null;
|
|
24840
|
+
flattened.push(...flattenedBranch);
|
|
24841
|
+
}
|
|
24842
|
+
continue;
|
|
24843
|
+
}
|
|
24844
|
+
return null;
|
|
24845
|
+
}
|
|
24846
|
+
return flattened;
|
|
24847
|
+
};
|
|
24529
24848
|
const noDerivedStateEffect = defineRule({
|
|
24530
24849
|
id: "no-derived-state-effect",
|
|
24531
24850
|
title: "Derived state stored in an effect",
|
|
@@ -24557,8 +24876,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24557
24876
|
}
|
|
24558
24877
|
}
|
|
24559
24878
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24560
|
-
const statements = getCallbackStatements(callback);
|
|
24561
|
-
if (statements.length === 0) return;
|
|
24879
|
+
const statements = flattenGuardedStatements(getCallbackStatements(callback));
|
|
24880
|
+
if (statements === null || statements.length === 0) return;
|
|
24562
24881
|
if (!statements.every((statement) => {
|
|
24563
24882
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24564
24883
|
const expression = statement.expression;
|
|
@@ -25193,7 +25512,7 @@ const noDidMountSetState = defineRule({
|
|
|
25193
25512
|
const { mode } = resolveSettings$20(context.settings);
|
|
25194
25513
|
return { CallExpression(node) {
|
|
25195
25514
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25196
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25515
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25197
25516
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25198
25517
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25199
25518
|
if (isMountFlagArgument(node.arguments?.[0])) return;
|
|
@@ -25318,7 +25637,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
25318
25637
|
const { mode } = resolveSettings$19(context.settings);
|
|
25319
25638
|
return { CallExpression(node) {
|
|
25320
25639
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25321
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25640
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25322
25641
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25323
25642
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25324
25643
|
if (isInsideDiffGuard(node)) return;
|
|
@@ -25639,9 +25958,10 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
25639
25958
|
create: (context) => ({ CallExpression(node) {
|
|
25640
25959
|
const callee = node.callee;
|
|
25641
25960
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
25642
|
-
|
|
25961
|
+
const receiver = stripParenExpression(callee.object);
|
|
25962
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25643
25963
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
25644
|
-
if (context.scopes.symbolFor(
|
|
25964
|
+
if (context.scopes.symbolFor(receiver) !== null) return;
|
|
25645
25965
|
if (!importsReactViewTransition(node)) return;
|
|
25646
25966
|
context.report({
|
|
25647
25967
|
node,
|
|
@@ -25661,7 +25981,8 @@ const noDocumentWrite = defineRule({
|
|
|
25661
25981
|
create: (context) => ({ CallExpression(node) {
|
|
25662
25982
|
const callee = node.callee;
|
|
25663
25983
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
25664
|
-
|
|
25984
|
+
const receiver = stripParenExpression(callee.object);
|
|
25985
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25665
25986
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
25666
25987
|
context.report({
|
|
25667
25988
|
node,
|
|
@@ -26358,6 +26679,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
26358
26679
|
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26359
26680
|
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26360
26681
|
}
|
|
26682
|
+
if (isNodeOfType(initializer.callee, "MemberExpression") && !initializer.callee.computed && isNodeOfType(initializer.callee.object, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.object.name)) return;
|
|
26361
26683
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26362
26684
|
} });
|
|
26363
26685
|
return {
|
|
@@ -28937,7 +29259,7 @@ const noIsMounted = defineRule({
|
|
|
28937
29259
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
28938
29260
|
create: (context) => ({ CallExpression(node) {
|
|
28939
29261
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
28940
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
29262
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
28941
29263
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
|
|
28942
29264
|
if (!getParentComponent(node)) return;
|
|
28943
29265
|
context.report({
|
|
@@ -28952,7 +29274,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
|
|
|
28952
29274
|
const isJsonMethodCall = (node, method) => {
|
|
28953
29275
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
28954
29276
|
const callee = node.callee;
|
|
28955
|
-
|
|
29277
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
29278
|
+
const receiver = stripParenExpression(callee.object);
|
|
29279
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
28956
29280
|
};
|
|
28957
29281
|
const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
|
|
28958
29282
|
const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
|
|
@@ -29037,7 +29361,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
29037
29361
|
const noJsxElementType = defineRule({
|
|
29038
29362
|
id: "no-jsx-element-type",
|
|
29039
29363
|
title: "No JSX.Element",
|
|
29040
|
-
severity: "
|
|
29364
|
+
severity: "warn",
|
|
29041
29365
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
29042
29366
|
create: (context) => {
|
|
29043
29367
|
let isJsxImported = false;
|
|
@@ -29363,6 +29687,378 @@ const noLegacyContextApi = defineRule({
|
|
|
29363
29687
|
}
|
|
29364
29688
|
});
|
|
29365
29689
|
//#endregion
|
|
29690
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
29691
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
29692
|
+
"map",
|
|
29693
|
+
"filter",
|
|
29694
|
+
"forEach",
|
|
29695
|
+
"flatMap",
|
|
29696
|
+
"reduce",
|
|
29697
|
+
"reduceRight",
|
|
29698
|
+
"some",
|
|
29699
|
+
"every",
|
|
29700
|
+
"find",
|
|
29701
|
+
"findIndex",
|
|
29702
|
+
"findLast",
|
|
29703
|
+
"findLastIndex",
|
|
29704
|
+
"sort",
|
|
29705
|
+
"toSorted"
|
|
29706
|
+
]);
|
|
29707
|
+
const executesDuringRender = (functionNode) => {
|
|
29708
|
+
const parent = functionNode.parent;
|
|
29709
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
29710
|
+
if (parent.callee === functionNode) return true;
|
|
29711
|
+
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
29712
|
+
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;
|
|
29713
|
+
};
|
|
29714
|
+
//#endregion
|
|
29715
|
+
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
29716
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
29717
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
29718
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
29719
|
+
return false;
|
|
29720
|
+
};
|
|
29721
|
+
//#endregion
|
|
29722
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
29723
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
29724
|
+
let ancestor = bindingIdentifier.parent;
|
|
29725
|
+
while (ancestor) {
|
|
29726
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
29727
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
29728
|
+
ancestor = ancestor.parent ?? null;
|
|
29729
|
+
}
|
|
29730
|
+
return null;
|
|
29731
|
+
};
|
|
29732
|
+
//#endregion
|
|
29733
|
+
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
29734
|
+
const isFalsyLiteral = (node) => {
|
|
29735
|
+
if (!node) return true;
|
|
29736
|
+
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
29737
|
+
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
29738
|
+
};
|
|
29739
|
+
const isFalsyInitialStateBinding = (identifier) => {
|
|
29740
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
29741
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
29742
|
+
if (!binding) return false;
|
|
29743
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29744
|
+
if (!declarator?.init) return false;
|
|
29745
|
+
const init = stripParenExpression(declarator.init);
|
|
29746
|
+
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
29747
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
29748
|
+
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
29749
|
+
return isFalsyLiteral(init.arguments?.[0]);
|
|
29750
|
+
};
|
|
29751
|
+
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
29752
|
+
//#endregion
|
|
29753
|
+
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
29754
|
+
const isGatedByFalsyInitialState = (node) => {
|
|
29755
|
+
let cursor = node;
|
|
29756
|
+
let parent = node.parent;
|
|
29757
|
+
while (parent) {
|
|
29758
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
29759
|
+
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29760
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29761
|
+
cursor = parent;
|
|
29762
|
+
parent = parent.parent ?? null;
|
|
29763
|
+
}
|
|
29764
|
+
return false;
|
|
29765
|
+
};
|
|
29766
|
+
//#endregion
|
|
29767
|
+
//#region src/plugin/utils/references-client-only-flag.ts
|
|
29768
|
+
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
29769
|
+
const referencesClientOnlyFlag = (expression) => {
|
|
29770
|
+
const unwrapped = stripParenExpression(expression);
|
|
29771
|
+
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
29772
|
+
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
29773
|
+
const property = unwrapped.property;
|
|
29774
|
+
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
29775
|
+
}
|
|
29776
|
+
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
29777
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
29778
|
+
return false;
|
|
29779
|
+
};
|
|
29780
|
+
//#endregion
|
|
29781
|
+
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
29782
|
+
const isInsideClientOnlyGuard = (node) => {
|
|
29783
|
+
let cursor = node;
|
|
29784
|
+
let parent = node.parent;
|
|
29785
|
+
while (parent) {
|
|
29786
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
29787
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
29788
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
29789
|
+
cursor = parent;
|
|
29790
|
+
parent = parent.parent ?? null;
|
|
29791
|
+
}
|
|
29792
|
+
return false;
|
|
29793
|
+
};
|
|
29794
|
+
//#endregion
|
|
29795
|
+
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
29796
|
+
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
29797
|
+
"toLocaleString",
|
|
29798
|
+
"toLocaleDateString",
|
|
29799
|
+
"toLocaleTimeString"
|
|
29800
|
+
]);
|
|
29801
|
+
const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
|
|
29802
|
+
const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
|
|
29803
|
+
const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
29804
|
+
"format",
|
|
29805
|
+
"formatToParts",
|
|
29806
|
+
"formatRange"
|
|
29807
|
+
]);
|
|
29808
|
+
const isProvableDateExpression = (expression) => {
|
|
29809
|
+
if (!expression) return false;
|
|
29810
|
+
const unwrapped = stripParenExpression(expression);
|
|
29811
|
+
return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
|
|
29812
|
+
};
|
|
29813
|
+
const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
|
|
29814
|
+
const receiverNameLooksDateFlavored = (expression) => {
|
|
29815
|
+
if (!expression) return false;
|
|
29816
|
+
const unwrapped = stripParenExpression(expression);
|
|
29817
|
+
if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
|
|
29818
|
+
if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
|
|
29819
|
+
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
29820
|
+
return false;
|
|
29821
|
+
};
|
|
29822
|
+
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
29823
|
+
if (!objectExpression) return false;
|
|
29824
|
+
const unwrapped = stripParenExpression(objectExpression);
|
|
29825
|
+
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
29826
|
+
for (const property of unwrapped.properties ?? []) {
|
|
29827
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
29828
|
+
if (property.computed) continue;
|
|
29829
|
+
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
29830
|
+
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
29831
|
+
}
|
|
29832
|
+
return false;
|
|
29833
|
+
};
|
|
29834
|
+
const hasExplicitLocaleArgument = (argument) => {
|
|
29835
|
+
if (!argument) return false;
|
|
29836
|
+
const unwrapped = stripParenExpression(argument);
|
|
29837
|
+
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
29838
|
+
return true;
|
|
29839
|
+
};
|
|
29840
|
+
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
29841
|
+
const localeArgument = call.arguments?.[0];
|
|
29842
|
+
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
29843
|
+
const optionsArgument = call.arguments?.[1];
|
|
29844
|
+
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
29845
|
+
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
29846
|
+
};
|
|
29847
|
+
const matchLocaleMethodCall = (call) => {
|
|
29848
|
+
const callee = call.callee;
|
|
29849
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29850
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29851
|
+
const methodName = callee.property.name;
|
|
29852
|
+
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
29853
|
+
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
29854
|
+
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
29855
|
+
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
29856
|
+
return {
|
|
29857
|
+
node: call,
|
|
29858
|
+
display: `${methodName}()`
|
|
29859
|
+
};
|
|
29860
|
+
};
|
|
29861
|
+
const getIntlFormatterName = (expression) => {
|
|
29862
|
+
if (!expression) return null;
|
|
29863
|
+
const unwrapped = stripParenExpression(expression);
|
|
29864
|
+
if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
|
|
29865
|
+
const callee = unwrapped.callee;
|
|
29866
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29867
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
|
|
29868
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29869
|
+
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
29870
|
+
};
|
|
29871
|
+
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
29872
|
+
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
29873
|
+
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
29874
|
+
if (formatterName !== "DateTimeFormat") return true;
|
|
29875
|
+
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
29876
|
+
};
|
|
29877
|
+
const matchIntlFormatCall = (call) => {
|
|
29878
|
+
const callee = call.callee;
|
|
29879
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29880
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29881
|
+
if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
|
|
29882
|
+
let construction = stripParenExpression(callee.object);
|
|
29883
|
+
if (isNodeOfType(construction, "Identifier")) {
|
|
29884
|
+
const binding = findVariableInitializer(construction, construction.name);
|
|
29885
|
+
construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
|
|
29886
|
+
}
|
|
29887
|
+
if (!construction) return null;
|
|
29888
|
+
const formatterName = getIntlFormatterName(construction);
|
|
29889
|
+
if (!formatterName) return null;
|
|
29890
|
+
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
29891
|
+
return {
|
|
29892
|
+
node: call,
|
|
29893
|
+
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
29894
|
+
};
|
|
29895
|
+
};
|
|
29896
|
+
const isDeterministicInputDateConstruction = (expression) => {
|
|
29897
|
+
if (!expression) return false;
|
|
29898
|
+
const unwrapped = stripParenExpression(expression);
|
|
29899
|
+
if (!isNodeOfType(unwrapped, "NewExpression")) return false;
|
|
29900
|
+
if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
|
|
29901
|
+
return (unwrapped.arguments?.length ?? 0) > 0;
|
|
29902
|
+
};
|
|
29903
|
+
const matchDateDefaultStringification = (node) => {
|
|
29904
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
29905
|
+
const callee = node.callee;
|
|
29906
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
|
|
29907
|
+
node,
|
|
29908
|
+
display: "Date.prototype.toString()"
|
|
29909
|
+
};
|
|
29910
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
|
|
29911
|
+
node,
|
|
29912
|
+
display: "String(new Date(…))"
|
|
29913
|
+
};
|
|
29914
|
+
return null;
|
|
29915
|
+
}
|
|
29916
|
+
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
29917
|
+
for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
|
|
29918
|
+
node: expression,
|
|
29919
|
+
display: "`${new Date(…)}`"
|
|
29920
|
+
};
|
|
29921
|
+
}
|
|
29922
|
+
return null;
|
|
29923
|
+
};
|
|
29924
|
+
const findRenderPhaseComponentOrHook = (node) => {
|
|
29925
|
+
let functionNode = findEnclosingFunction(node);
|
|
29926
|
+
while (functionNode) {
|
|
29927
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
29928
|
+
if (!executesDuringRender(functionNode)) return null;
|
|
29929
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
29930
|
+
}
|
|
29931
|
+
return null;
|
|
29932
|
+
};
|
|
29933
|
+
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
29934
|
+
if (fileHasUseClientDirective) return true;
|
|
29935
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
29936
|
+
if (displayName && isReactHookName(displayName)) return true;
|
|
29937
|
+
let callsHook = false;
|
|
29938
|
+
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
29939
|
+
if (callsHook) return false;
|
|
29940
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
29941
|
+
callsHook = true;
|
|
29942
|
+
return false;
|
|
29943
|
+
}
|
|
29944
|
+
});
|
|
29945
|
+
return callsHook;
|
|
29946
|
+
};
|
|
29947
|
+
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
29948
|
+
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
29949
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
29950
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
29951
|
+
let cursor = node;
|
|
29952
|
+
while (cursor) {
|
|
29953
|
+
ancestors.add(cursor);
|
|
29954
|
+
cursor = cursor.parent ?? null;
|
|
29955
|
+
}
|
|
29956
|
+
for (const statement of body.body ?? []) {
|
|
29957
|
+
if (ancestors.has(statement)) return false;
|
|
29958
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
29959
|
+
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
29960
|
+
let returnsEarly = false;
|
|
29961
|
+
walkAst(statement.consequent, (child) => {
|
|
29962
|
+
if (isFunctionLike$1(child)) return false;
|
|
29963
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
29964
|
+
returnsEarly = true;
|
|
29965
|
+
return false;
|
|
29966
|
+
}
|
|
29967
|
+
});
|
|
29968
|
+
if (returnsEarly) return true;
|
|
29969
|
+
}
|
|
29970
|
+
return false;
|
|
29971
|
+
};
|
|
29972
|
+
const findEnclosingJsxOpeningElement = (node) => {
|
|
29973
|
+
let cursor = node.parent;
|
|
29974
|
+
while (cursor) {
|
|
29975
|
+
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
29976
|
+
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
29977
|
+
cursor = cursor.parent ?? null;
|
|
29978
|
+
}
|
|
29979
|
+
return null;
|
|
29980
|
+
};
|
|
29981
|
+
const noLocaleFormatInRender = defineRule({
|
|
29982
|
+
id: "no-locale-format-in-render",
|
|
29983
|
+
title: "Locale/timezone formatting during render",
|
|
29984
|
+
severity: "warn",
|
|
29985
|
+
category: "Correctness",
|
|
29986
|
+
disabledWhen: [
|
|
29987
|
+
"vite",
|
|
29988
|
+
"cra",
|
|
29989
|
+
"expo",
|
|
29990
|
+
"react-native",
|
|
29991
|
+
"unknown"
|
|
29992
|
+
],
|
|
29993
|
+
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.",
|
|
29994
|
+
create: (context) => {
|
|
29995
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
29996
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
29997
|
+
let fileHasUseClientDirective = false;
|
|
29998
|
+
let fileIsEmailTemplate = false;
|
|
29999
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30000
|
+
const reportIfRenderPhase = (match) => {
|
|
30001
|
+
if (reportedNodes.has(match.node)) return;
|
|
30002
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
30003
|
+
if (!componentOrHookNode) return;
|
|
30004
|
+
if (fileIsEmailTemplate) return;
|
|
30005
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30006
|
+
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30007
|
+
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30008
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
30009
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30010
|
+
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30011
|
+
reportedNodes.add(match.node);
|
|
30012
|
+
context.report({
|
|
30013
|
+
node: match.node,
|
|
30014
|
+
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.`
|
|
30015
|
+
});
|
|
30016
|
+
};
|
|
30017
|
+
return {
|
|
30018
|
+
Program(node) {
|
|
30019
|
+
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30020
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30021
|
+
},
|
|
30022
|
+
CallExpression(node) {
|
|
30023
|
+
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
30024
|
+
if (match) reportIfRenderPhase(match);
|
|
30025
|
+
},
|
|
30026
|
+
TemplateLiteral(node) {
|
|
30027
|
+
const match = matchDateDefaultStringification(node);
|
|
30028
|
+
if (match) reportIfRenderPhase(match);
|
|
30029
|
+
},
|
|
30030
|
+
JSXExpressionContainer(node) {
|
|
30031
|
+
const expression = stripParenExpression(node.expression);
|
|
30032
|
+
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30033
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30034
|
+
const helperName = expression.callee.name;
|
|
30035
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
30036
|
+
if (!componentOrHookNode) return;
|
|
30037
|
+
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30038
|
+
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
30039
|
+
if (componentOrHookDisplayNameForFunction(helperNode)) return;
|
|
30040
|
+
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
30041
|
+
if (isFunctionLike$1(child)) return false;
|
|
30042
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30043
|
+
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
30044
|
+
if (!match || reportedNodes.has(match.node)) return;
|
|
30045
|
+
if (fileIsEmailTemplate) return;
|
|
30046
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30047
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
30048
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
30049
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
30050
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30051
|
+
reportedNodes.add(match.node);
|
|
30052
|
+
context.report({
|
|
30053
|
+
node: match.node,
|
|
30054
|
+
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.`
|
|
30055
|
+
});
|
|
30056
|
+
});
|
|
30057
|
+
}
|
|
30058
|
+
};
|
|
30059
|
+
}
|
|
30060
|
+
});
|
|
30061
|
+
//#endregion
|
|
29366
30062
|
//#region src/plugin/rules/design/no-long-transition-duration.ts
|
|
29367
30063
|
const hasInfiniteIterationCount = (properties) => properties.some((property) => {
|
|
29368
30064
|
if (getStylePropertyKey(property) !== "animationIterationCount") return false;
|
|
@@ -30176,7 +30872,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30176
30872
|
"reverse",
|
|
30177
30873
|
"sort"
|
|
30178
30874
|
]);
|
|
30179
|
-
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30180
30875
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30181
30876
|
"assign",
|
|
30182
30877
|
"defineProperties",
|
|
@@ -30250,7 +30945,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
30250
30945
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
30251
30946
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
30252
30947
|
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
30948
|
}
|
|
30255
30949
|
}
|
|
30256
30950
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -30289,6 +30983,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
30289
30983
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
30290
30984
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
30291
30985
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
30986
|
+
if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
|
|
30292
30987
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
30293
30988
|
});
|
|
30294
30989
|
return mutations;
|
|
@@ -30521,7 +31216,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
30521
31216
|
id: "no-nested-component-definition",
|
|
30522
31217
|
title: "Component defined inside another component",
|
|
30523
31218
|
tags: ["test-noise", "react-jsx-only"],
|
|
30524
|
-
severity: "
|
|
31219
|
+
severity: "warn",
|
|
30525
31220
|
category: "Correctness",
|
|
30526
31221
|
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
31222
|
create: (context) => {
|
|
@@ -31476,12 +32171,12 @@ const noPassDataToParent = defineRule({
|
|
|
31476
32171
|
if (calleeNode === identifier) {
|
|
31477
32172
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
31478
32173
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
31479
|
-
} else if (isNodeOfType(calleeNode, "MemberExpression") &&
|
|
32174
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
31480
32175
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
31481
32176
|
if (isCustomHookParameter(ref)) continue;
|
|
31482
32177
|
} else continue;
|
|
31483
32178
|
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));
|
|
32179
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31485
32180
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31486
32181
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
31487
32182
|
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
@@ -31672,7 +32367,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
31672
32367
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
31673
32368
|
const calleeNode = callExpr.callee;
|
|
31674
32369
|
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));
|
|
32370
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31676
32371
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31677
32372
|
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
31678
32373
|
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
@@ -32000,40 +32695,6 @@ const noPreventDefault = defineRule({
|
|
|
32000
32695
|
}
|
|
32001
32696
|
});
|
|
32002
32697
|
//#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
32698
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
32038
32699
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
32039
32700
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -32240,7 +32901,7 @@ const isAlwaysFreshExpression = (expression) => {
|
|
|
32240
32901
|
return `${callee.name}()`;
|
|
32241
32902
|
}
|
|
32242
32903
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
32243
|
-
const receiver = callee.object;
|
|
32904
|
+
const receiver = stripParenExpression(callee.object);
|
|
32244
32905
|
const property = callee.property;
|
|
32245
32906
|
if (!isNodeOfType(property, "Identifier")) return null;
|
|
32246
32907
|
if (isNodeOfType(receiver, "Identifier")) {
|
|
@@ -32361,8 +33022,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32361
33022
|
if (typeof sourceValue !== "string") return;
|
|
32362
33023
|
if (handleExtraSource?.(node, context)) return;
|
|
32363
33024
|
if (sourceValue !== source) return;
|
|
33025
|
+
if (isTypeOnlyImport(node)) return;
|
|
32364
33026
|
for (const specifier of node.specifiers ?? []) {
|
|
32365
33027
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
33028
|
+
if (specifier.importKind === "type") continue;
|
|
32366
33029
|
const importedName = getImportedName$1(specifier);
|
|
32367
33030
|
if (!importedName) continue;
|
|
32368
33031
|
const message = messages.get(importedName);
|
|
@@ -32381,8 +33044,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32381
33044
|
MemberExpression(node) {
|
|
32382
33045
|
if (namespaceBindings.size === 0) return;
|
|
32383
33046
|
if (node.computed) return;
|
|
32384
|
-
|
|
32385
|
-
if (!
|
|
33047
|
+
const receiver = stripParenExpression(node.object);
|
|
33048
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
33049
|
+
if (!namespaceBindings.has(receiver.name)) return;
|
|
32386
33050
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
32387
33051
|
const message = messages.get(node.property.name);
|
|
32388
33052
|
if (message) context.report({
|
|
@@ -32447,7 +33111,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
32447
33111
|
});
|
|
32448
33112
|
//#endregion
|
|
32449
33113
|
//#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."]
|
|
33114
|
+
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
33115
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
32452
33116
|
if (!rawFilename) return false;
|
|
32453
33117
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -32485,7 +33149,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
32485
33149
|
requires: ["react:19"],
|
|
32486
33150
|
tags: ["test-noise", "migration-hint"],
|
|
32487
33151
|
severity: "warn",
|
|
32488
|
-
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19.
|
|
33152
|
+
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
33153
|
create: (context) => {
|
|
32490
33154
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
32491
33155
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -32809,34 +33473,11 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
32809
33473
|
});
|
|
32810
33474
|
//#endregion
|
|
32811
33475
|
//#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
33476
|
const isInsideComponentContext = (node) => {
|
|
32836
33477
|
let cursor = node.parent;
|
|
32837
33478
|
while (cursor) {
|
|
32838
|
-
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
32839
33479
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33480
|
+
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
32840
33481
|
cursor = cursor.parent ?? null;
|
|
32841
33482
|
}
|
|
32842
33483
|
return false;
|
|
@@ -32846,24 +33487,28 @@ const functionBodyOf = (node) => {
|
|
|
32846
33487
|
if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
|
|
32847
33488
|
return null;
|
|
32848
33489
|
};
|
|
33490
|
+
const isHookCallee = (callee) => {
|
|
33491
|
+
if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
|
|
33492
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
|
|
33493
|
+
return false;
|
|
33494
|
+
};
|
|
32849
33495
|
const containsHookCall = (body) => {
|
|
32850
33496
|
let found = false;
|
|
32851
33497
|
walkAst(body, (child) => {
|
|
32852
|
-
if (found) return;
|
|
33498
|
+
if (found) return false;
|
|
33499
|
+
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32853
33500
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32854
|
-
|
|
32855
|
-
if (name && isReactHookName(name)) found = true;
|
|
33501
|
+
if (isHookCallee(child.callee)) found = true;
|
|
32856
33502
|
});
|
|
32857
33503
|
return found;
|
|
32858
33504
|
};
|
|
32859
|
-
const
|
|
33505
|
+
const isHookCallingRenderHelper = (symbol) => {
|
|
32860
33506
|
if (!symbol) return false;
|
|
32861
33507
|
const declaration = symbol.declarationNode;
|
|
32862
33508
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
32863
33509
|
const body = functionBodyOf(declaration);
|
|
32864
33510
|
if (!body) return false;
|
|
32865
|
-
|
|
32866
|
-
return !containsHookCall(body);
|
|
33511
|
+
return containsHookCall(body);
|
|
32867
33512
|
};
|
|
32868
33513
|
const noRenderInRender = defineRule({
|
|
32869
33514
|
id: "no-render-in-render",
|
|
@@ -32872,20 +33517,13 @@ const noRenderInRender = defineRule({
|
|
|
32872
33517
|
tags: ["test-noise"],
|
|
32873
33518
|
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
32874
33519
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
32875
|
-
const expression = node.expression;
|
|
33520
|
+
const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
|
|
32876
33521
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
32877
|
-
|
|
32878
|
-
|
|
32879
|
-
|
|
32880
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33522
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
33523
|
+
const calleeName = expression.callee.name;
|
|
33524
|
+
if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
32881
33525
|
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
|
-
}
|
|
33526
|
+
if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
|
|
32889
33527
|
context.report({
|
|
32890
33528
|
node: expression,
|
|
32891
33529
|
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 +33584,7 @@ const noRenderPropChildren = defineRule({
|
|
|
32946
33584
|
//#endregion
|
|
32947
33585
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
32948
33586
|
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
|
-
};
|
|
33587
|
+
const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
|
|
32956
33588
|
const isUsedAsReturnValue = (parent) => {
|
|
32957
33589
|
if (!parent) return false;
|
|
32958
33590
|
if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
|
|
@@ -33788,7 +34420,7 @@ const noSetState = defineRule({
|
|
|
33788
34420
|
category: "Architecture",
|
|
33789
34421
|
create: (context) => ({ CallExpression(node) {
|
|
33790
34422
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
33791
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
34423
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
33792
34424
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
33793
34425
|
if (!getParentComponent(node)) return;
|
|
33794
34426
|
context.report({
|
|
@@ -36234,6 +36866,36 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36234
36866
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36235
36867
|
return true;
|
|
36236
36868
|
};
|
|
36869
|
+
const isTrivialContainerLiteral = (node) => {
|
|
36870
|
+
if (!node) return false;
|
|
36871
|
+
const innerExpression = stripParenExpression(node);
|
|
36872
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
36873
|
+
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
36874
|
+
return false;
|
|
36875
|
+
};
|
|
36876
|
+
const isNonEscapingRead = (identifier) => {
|
|
36877
|
+
const readRoot = findTransparentExpressionRoot(identifier);
|
|
36878
|
+
const memberNode = readRoot.parent;
|
|
36879
|
+
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
|
|
36880
|
+
const memberUse = findTransparentExpressionRoot(memberNode);
|
|
36881
|
+
const memberUseParent = memberUse.parent;
|
|
36882
|
+
if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
|
|
36883
|
+
if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
|
|
36884
|
+
if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
|
|
36885
|
+
return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
|
|
36886
|
+
};
|
|
36887
|
+
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
36888
|
+
const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
|
|
36889
|
+
const parentNode = memoUsageRoot.parent;
|
|
36890
|
+
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
36891
|
+
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
|
|
36892
|
+
const bindingTarget = parentNode.id;
|
|
36893
|
+
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
36894
|
+
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
36895
|
+
const symbol = scopes.symbolFor(bindingTarget);
|
|
36896
|
+
if (!symbol) return false;
|
|
36897
|
+
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
36898
|
+
};
|
|
36237
36899
|
const noUsememoSimpleExpression = defineRule({
|
|
36238
36900
|
id: "no-usememo-simple-expression",
|
|
36239
36901
|
title: "useMemo on a cheap value",
|
|
@@ -36255,9 +36917,17 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36255
36917
|
let returnExpression = null;
|
|
36256
36918
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36257
36919
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36258
|
-
if (returnExpression
|
|
36920
|
+
if (!returnExpression) return;
|
|
36921
|
+
if (isTriviallyCheapExpression(returnExpression)) {
|
|
36922
|
+
context.report({
|
|
36923
|
+
node,
|
|
36924
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
36925
|
+
});
|
|
36926
|
+
return;
|
|
36927
|
+
}
|
|
36928
|
+
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36259
36929
|
node,
|
|
36260
|
-
message: "This
|
|
36930
|
+
message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
|
|
36261
36931
|
});
|
|
36262
36932
|
} })
|
|
36263
36933
|
});
|
|
@@ -36363,7 +37033,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
36363
37033
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
36364
37034
|
return { CallExpression(node) {
|
|
36365
37035
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
36366
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
37036
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36367
37037
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36368
37038
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36369
37039
|
context.report({
|
|
@@ -36719,7 +37389,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
|
36719
37389
|
continue;
|
|
36720
37390
|
}
|
|
36721
37391
|
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
36722
|
-
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
37392
|
+
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
|
|
36723
37393
|
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
36724
37394
|
}
|
|
36725
37395
|
return false;
|
|
@@ -36827,18 +37497,22 @@ const onlyExportComponents = defineRule({
|
|
|
36827
37497
|
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36828
37498
|
allowExportNames: new Set(settings.allowExportNames),
|
|
36829
37499
|
allowConstantExport: settings.allowConstantExport,
|
|
36830
|
-
localComponentNames
|
|
37500
|
+
localComponentNames,
|
|
37501
|
+
scopes: context.scopes
|
|
36831
37502
|
};
|
|
36832
37503
|
for (const child of componentCandidates) {
|
|
36833
37504
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36834
|
-
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37505
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
|
|
36835
37506
|
}
|
|
36836
37507
|
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
36837
37508
|
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36838
37509
|
}
|
|
36839
37510
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36840
37511
|
const initializer = child.init;
|
|
36841
|
-
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child))
|
|
37512
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
|
|
37513
|
+
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
37514
|
+
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
37515
|
+
}
|
|
36842
37516
|
}
|
|
36843
37517
|
}
|
|
36844
37518
|
const exports = [];
|
|
@@ -38503,11 +39177,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
|
|
|
38503
39177
|
}
|
|
38504
39178
|
return null;
|
|
38505
39179
|
};
|
|
39180
|
+
const isTrivialLiteralExpression = (expression) => {
|
|
39181
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
39182
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
39183
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
|
|
39184
|
+
if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
|
|
39185
|
+
return false;
|
|
39186
|
+
};
|
|
38506
39187
|
const getSingleSetterCallFromHandler = (handler) => {
|
|
38507
39188
|
const handlerStatements = getCallbackStatements(handler);
|
|
38508
39189
|
if (handlerStatements.length !== 1) return null;
|
|
38509
39190
|
const onlyStatement = handlerStatements[0];
|
|
38510
|
-
|
|
39191
|
+
let expression = onlyStatement;
|
|
39192
|
+
if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
|
|
39193
|
+
if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
|
|
39194
|
+
if (!expression) return null;
|
|
39195
|
+
expression = stripParenExpression(expression);
|
|
38511
39196
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
38512
39197
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
38513
39198
|
if (!isSetterIdentifier(expression.callee.name)) return null;
|
|
@@ -38517,6 +39202,106 @@ const getSingleSetterCallFromHandler = (handler) => {
|
|
|
38517
39202
|
setterArgument: expression.arguments[0]
|
|
38518
39203
|
};
|
|
38519
39204
|
};
|
|
39205
|
+
const isListenerCollectionInitializer = (init) => {
|
|
39206
|
+
if (!init) return false;
|
|
39207
|
+
if (isNodeOfType(init, "ArrayExpression")) return true;
|
|
39208
|
+
return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
|
|
39209
|
+
};
|
|
39210
|
+
const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
|
|
39211
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
39212
|
+
const firstParam = functionNode.params?.[0];
|
|
39213
|
+
if (!isNodeOfType(firstParam, "Identifier")) return false;
|
|
39214
|
+
const listenerParamName = firstParam.name;
|
|
39215
|
+
let registersListener = false;
|
|
39216
|
+
walkAst(functionNode.body, (child) => {
|
|
39217
|
+
if (registersListener) return false;
|
|
39218
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39219
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
39220
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
39221
|
+
if (!listenerCollectionNames.has(child.callee.object.name)) return;
|
|
39222
|
+
if (!isNodeOfType(child.callee.property, "Identifier")) return;
|
|
39223
|
+
if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
|
|
39224
|
+
const registeredArgument = child.arguments?.[0];
|
|
39225
|
+
if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
|
|
39226
|
+
});
|
|
39227
|
+
return registersListener;
|
|
39228
|
+
};
|
|
39229
|
+
const buildModuleScopeStoreIndex = (programRoot) => {
|
|
39230
|
+
const mutableBindingNames = /* @__PURE__ */ new Set();
|
|
39231
|
+
const listenerCollectionNames = /* @__PURE__ */ new Set();
|
|
39232
|
+
const moduleFunctionsByName = /* @__PURE__ */ new Map();
|
|
39233
|
+
if (!isNodeOfType(programRoot, "Program")) return {
|
|
39234
|
+
mutableBindingNames,
|
|
39235
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39236
|
+
};
|
|
39237
|
+
for (const statement of programRoot.body ?? []) {
|
|
39238
|
+
const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
|
|
39239
|
+
if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
|
|
39240
|
+
moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
|
|
39241
|
+
continue;
|
|
39242
|
+
}
|
|
39243
|
+
if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
|
|
39244
|
+
for (const declarator of unwrapped.declarations ?? []) {
|
|
39245
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
39246
|
+
const init = declarator.init ?? null;
|
|
39247
|
+
if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
|
|
39248
|
+
mutableBindingNames.add(declarator.id.name);
|
|
39249
|
+
continue;
|
|
39250
|
+
}
|
|
39251
|
+
if (isListenerCollectionInitializer(init)) {
|
|
39252
|
+
listenerCollectionNames.add(declarator.id.name);
|
|
39253
|
+
continue;
|
|
39254
|
+
}
|
|
39255
|
+
if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
|
|
39256
|
+
}
|
|
39257
|
+
}
|
|
39258
|
+
const subscribeFunctionNames = /* @__PURE__ */ new Set();
|
|
39259
|
+
for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
|
|
39260
|
+
return {
|
|
39261
|
+
mutableBindingNames,
|
|
39262
|
+
subscribeFunctionNames
|
|
39263
|
+
};
|
|
39264
|
+
};
|
|
39265
|
+
const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
|
|
39266
|
+
if (!isNodeOfType(useStateCall, "CallExpression")) return null;
|
|
39267
|
+
let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
|
|
39268
|
+
if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
|
|
39269
|
+
if (!isNodeOfType(initialArgument, "Identifier")) return null;
|
|
39270
|
+
if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
|
|
39271
|
+
const binding = findVariableInitializer(initialArgument, initialArgument.name);
|
|
39272
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
|
|
39273
|
+
return initialArgument.name;
|
|
39274
|
+
};
|
|
39275
|
+
const argumentForwardsSetter = (argument, setterName) => {
|
|
39276
|
+
if (!argument) return false;
|
|
39277
|
+
const unwrapped = stripParenExpression(argument);
|
|
39278
|
+
if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
|
|
39279
|
+
if (!isFunctionLike$1(unwrapped)) return false;
|
|
39280
|
+
let callsSetter = false;
|
|
39281
|
+
walkAst(unwrapped.body, (child) => {
|
|
39282
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
|
|
39283
|
+
callsSetter = true;
|
|
39284
|
+
return false;
|
|
39285
|
+
}
|
|
39286
|
+
});
|
|
39287
|
+
return callsSetter;
|
|
39288
|
+
};
|
|
39289
|
+
const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
|
|
39290
|
+
let matchedCall = null;
|
|
39291
|
+
walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
|
|
39292
|
+
if (matchedCall) return false;
|
|
39293
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39294
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
39295
|
+
if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
|
|
39296
|
+
const binding = findVariableInitializer(child.callee, child.callee.name);
|
|
39297
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
|
|
39298
|
+
for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
|
|
39299
|
+
matchedCall = child;
|
|
39300
|
+
return false;
|
|
39301
|
+
}
|
|
39302
|
+
});
|
|
39303
|
+
return matchedCall;
|
|
39304
|
+
};
|
|
38520
39305
|
const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
|
|
38521
39306
|
const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
|
|
38522
39307
|
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
@@ -38533,6 +39318,16 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38533
39318
|
severity: "warn",
|
|
38534
39319
|
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
39320
|
create: (context) => {
|
|
39321
|
+
let cachedStoreIndex = null;
|
|
39322
|
+
const storeIndexFor = (node) => {
|
|
39323
|
+
if (cachedStoreIndex) return cachedStoreIndex;
|
|
39324
|
+
const programRoot = findProgramRoot(node);
|
|
39325
|
+
cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
|
|
39326
|
+
mutableBindingNames: /* @__PURE__ */ new Set(),
|
|
39327
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39328
|
+
};
|
|
39329
|
+
return cachedStoreIndex;
|
|
39330
|
+
};
|
|
38536
39331
|
const checkComponent = (componentBody) => {
|
|
38537
39332
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38538
39333
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
@@ -38570,6 +39365,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38570
39365
|
const useStateInitializer = useStateInitializerByValueName.get(valueName);
|
|
38571
39366
|
if (!useStateInitializer) continue;
|
|
38572
39367
|
if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
|
|
39368
|
+
if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
|
|
38573
39369
|
if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
|
|
38574
39370
|
const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
|
|
38575
39371
|
context.report({
|
|
@@ -38577,14 +39373,47 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38577
39373
|
message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
|
|
38578
39374
|
});
|
|
38579
39375
|
}
|
|
39376
|
+
checkModuleStoreShape(componentBody, useStateBindings);
|
|
39377
|
+
};
|
|
39378
|
+
const checkModuleStoreShape = (componentBody, useStateBindings) => {
|
|
39379
|
+
const storeIndex = storeIndexFor(componentBody);
|
|
39380
|
+
if (storeIndex.mutableBindingNames.size === 0) return;
|
|
39381
|
+
if (storeIndex.subscribeFunctionNames.size === 0) return;
|
|
39382
|
+
const snapshotBindings = useStateBindings.map((binding) => ({
|
|
39383
|
+
binding,
|
|
39384
|
+
storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
|
|
39385
|
+
})).filter((candidate) => candidate.storeName !== null);
|
|
39386
|
+
if (snapshotBindings.length === 0) return;
|
|
39387
|
+
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
39388
|
+
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
39389
|
+
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
39390
|
+
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
39391
|
+
const depsNode = effectCall.arguments[1];
|
|
39392
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
39393
|
+
if ((depsNode.elements?.length ?? 0) !== 0) continue;
|
|
39394
|
+
const callback = getEffectCallback(effectCall);
|
|
39395
|
+
if (!callback || !isFunctionLike$1(callback)) continue;
|
|
39396
|
+
for (const { binding, storeName } of snapshotBindings) {
|
|
39397
|
+
if (reportedDeclarators.has(binding.declarator)) continue;
|
|
39398
|
+
if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
|
|
39399
|
+
reportedDeclarators.add(binding.declarator);
|
|
39400
|
+
context.report({
|
|
39401
|
+
node: binding.declarator,
|
|
39402
|
+
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.`
|
|
39403
|
+
});
|
|
39404
|
+
}
|
|
39405
|
+
}
|
|
38580
39406
|
};
|
|
38581
39407
|
return {
|
|
38582
39408
|
FunctionDeclaration(node) {
|
|
38583
|
-
|
|
39409
|
+
const functionName = node.id?.name;
|
|
39410
|
+
if (!functionName) return;
|
|
39411
|
+
if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
38584
39412
|
checkComponent(node.body);
|
|
38585
39413
|
},
|
|
38586
39414
|
VariableDeclarator(node) {
|
|
38587
|
-
|
|
39415
|
+
const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
|
|
39416
|
+
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
38588
39417
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
38589
39418
|
checkComponent(node.init.body);
|
|
38590
39419
|
}
|
|
@@ -39194,7 +40023,7 @@ const queryNoVoidQueryFn = defineRule({
|
|
|
39194
40023
|
if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
|
|
39195
40024
|
const body = queryFnValue.body;
|
|
39196
40025
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
39197
|
-
if ((body.body ?? []).length === 0) context.report({
|
|
40026
|
+
if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
|
|
39198
40027
|
node: queryFnProperty,
|
|
39199
40028
|
message: "This empty queryFn caches undefined, so the component never gets data."
|
|
39200
40029
|
});
|
|
@@ -39701,17 +40530,6 @@ const renderingHoistJsx = defineRule({
|
|
|
39701
40530
|
}
|
|
39702
40531
|
});
|
|
39703
40532
|
//#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
40533
|
//#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
|
|
39716
40534
|
const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
39717
40535
|
{
|
|
@@ -39720,19 +40538,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
|
39720
40538
|
},
|
|
39721
40539
|
{
|
|
39722
40540
|
display: "Date.now()",
|
|
39723
|
-
matches: (node) =>
|
|
40541
|
+
matches: (node) => isGlobalMethodCall(node, "Date", "now")
|
|
39724
40542
|
},
|
|
39725
40543
|
{
|
|
39726
40544
|
display: "Math.random()",
|
|
39727
|
-
matches: (node) =>
|
|
40545
|
+
matches: (node) => isGlobalMethodCall(node, "Math", "random")
|
|
39728
40546
|
},
|
|
39729
40547
|
{
|
|
39730
40548
|
display: "performance.now()",
|
|
39731
|
-
matches: (node) =>
|
|
40549
|
+
matches: (node) => isGlobalMethodCall(node, "performance", "now")
|
|
39732
40550
|
},
|
|
39733
40551
|
{
|
|
39734
40552
|
display: "crypto.randomUUID()",
|
|
39735
|
-
matches: (node) =>
|
|
40553
|
+
matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
|
|
39736
40554
|
}
|
|
39737
40555
|
];
|
|
39738
40556
|
const findOpeningElementOfChild = (jsxNode) => {
|
|
@@ -39744,54 +40562,6 @@ const findOpeningElementOfChild = (jsxNode) => {
|
|
|
39744
40562
|
}
|
|
39745
40563
|
return null;
|
|
39746
40564
|
};
|
|
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
40565
|
const isYearOnlyDateRead = (dateNode) => {
|
|
39796
40566
|
const member = dateNode.parent;
|
|
39797
40567
|
if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
|
|
@@ -39815,23 +40585,6 @@ const isInsideMotionTransitionAttribute = (node) => {
|
|
|
39815
40585
|
}
|
|
39816
40586
|
return false;
|
|
39817
40587
|
};
|
|
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
40588
|
const renderingHydrationMismatchTime = defineRule({
|
|
39836
40589
|
id: "rendering-hydration-mismatch-time",
|
|
39837
40590
|
title: "Time or random value in JSX",
|
|
@@ -39879,6 +40632,35 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
39879
40632
|
}
|
|
39880
40633
|
});
|
|
39881
40634
|
//#endregion
|
|
40635
|
+
//#region src/plugin/utils/contains-locale-environment-read.ts
|
|
40636
|
+
const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
|
|
40637
|
+
"toLocaleString",
|
|
40638
|
+
"toLocaleDateString",
|
|
40639
|
+
"toLocaleTimeString",
|
|
40640
|
+
"getTimezoneOffset"
|
|
40641
|
+
]);
|
|
40642
|
+
const containsLocaleEnvironmentRead = (expression) => {
|
|
40643
|
+
let readsLocaleEnvironment = false;
|
|
40644
|
+
walkAst(expression, (child) => {
|
|
40645
|
+
if (readsLocaleEnvironment) return false;
|
|
40646
|
+
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
|
|
40647
|
+
if (child.object.name === "Intl") {
|
|
40648
|
+
readsLocaleEnvironment = true;
|
|
40649
|
+
return false;
|
|
40650
|
+
}
|
|
40651
|
+
if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
|
|
40652
|
+
readsLocaleEnvironment = true;
|
|
40653
|
+
return false;
|
|
40654
|
+
}
|
|
40655
|
+
}
|
|
40656
|
+
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)) {
|
|
40657
|
+
readsLocaleEnvironment = true;
|
|
40658
|
+
return false;
|
|
40659
|
+
}
|
|
40660
|
+
});
|
|
40661
|
+
return readsLocaleEnvironment;
|
|
40662
|
+
};
|
|
40663
|
+
//#endregion
|
|
39882
40664
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
39883
40665
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
39884
40666
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -39944,14 +40726,15 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
39944
40726
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
39945
40727
|
const callback = getEffectCallback(node);
|
|
39946
40728
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
39947
|
-
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
39948
|
-
if (
|
|
40729
|
+
const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
|
|
40730
|
+
if (bodyStatements.length !== 1) return;
|
|
39949
40731
|
const soleStatement = bodyStatements[0];
|
|
39950
40732
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
39951
40733
|
const expression = soleStatement.expression;
|
|
39952
40734
|
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
|
|
39953
40735
|
if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
|
|
39954
40736
|
if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
|
|
40737
|
+
if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
|
|
39955
40738
|
context.report({
|
|
39956
40739
|
node,
|
|
39957
40740
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
@@ -40770,6 +41553,9 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
40770
41553
|
} })
|
|
40771
41554
|
});
|
|
40772
41555
|
//#endregion
|
|
41556
|
+
//#region src/plugin/utils/is-trivial-built-in-construction.ts
|
|
41557
|
+
const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
|
|
41558
|
+
//#endregion
|
|
40773
41559
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
40774
41560
|
const rerenderLazyRefInit = defineRule({
|
|
40775
41561
|
id: "rerender-lazy-ref-init",
|
|
@@ -40780,7 +41566,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40780
41566
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
40781
41567
|
create: (context) => ({ CallExpression(node) {
|
|
40782
41568
|
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
40783
|
-
const initializer = node.arguments[0];
|
|
41569
|
+
const initializer = stripParenExpression(node.arguments[0]);
|
|
40784
41570
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
40785
41571
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
40786
41572
|
if (!isPlainCall && !isNewCall) return;
|
|
@@ -40788,7 +41574,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40788
41574
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40789
41575
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40790
41576
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40791
|
-
if (
|
|
41577
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40792
41578
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40793
41579
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40794
41580
|
context.report({
|
|
@@ -40824,11 +41610,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
40824
41610
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
40825
41611
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
40826
41612
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
40827
|
-
|
|
40828
|
-
if (isNodeOfType(
|
|
40829
|
-
if (isNodeOfType(
|
|
40830
|
-
if (isNodeOfType(
|
|
40831
|
-
if (isNodeOfType(
|
|
41613
|
+
const innerExpression = stripParenExpression(expression);
|
|
41614
|
+
if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
|
|
41615
|
+
if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
|
|
41616
|
+
if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
|
|
41617
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
|
|
40832
41618
|
if (!element || !isNodeOfType(element, "SpreadElement")) continue;
|
|
40833
41619
|
const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
|
|
40834
41620
|
if (spreadCall) return spreadCall;
|
|
@@ -40851,7 +41637,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
40851
41637
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40852
41638
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40853
41639
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40854
|
-
if (
|
|
41640
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40855
41641
|
if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
|
|
40856
41642
|
if (isReactHookName(calleeName)) return;
|
|
40857
41643
|
const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
|
|
@@ -41518,7 +42304,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
41518
42304
|
const body = reactionFn.body;
|
|
41519
42305
|
let singleAssignment = null;
|
|
41520
42306
|
if (isNodeOfType(body, "BlockStatement")) {
|
|
41521
|
-
const statements = body.body ?? [];
|
|
42307
|
+
const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
|
|
41522
42308
|
if (statements.length !== 1) return;
|
|
41523
42309
|
const onlyStatement = statements[0];
|
|
41524
42310
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
@@ -41592,7 +42378,8 @@ const PROMISE_SETTLE_METHODS = new Set([
|
|
|
41592
42378
|
"catch",
|
|
41593
42379
|
"finally"
|
|
41594
42380
|
]);
|
|
41595
|
-
const findChainRoot = (
|
|
42381
|
+
const findChainRoot = (wrappedNode) => {
|
|
42382
|
+
const node = stripParenExpression(wrappedNode);
|
|
41596
42383
|
if (isNodeOfType(node, "CallExpression")) {
|
|
41597
42384
|
if (isNodeOfType(node.callee, "Identifier")) return {
|
|
41598
42385
|
calleeName: node.callee.name,
|
|
@@ -42180,20 +42967,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
|
|
|
42180
42967
|
};
|
|
42181
42968
|
const isReactNativeDimensionsCallee = (node, callee) => {
|
|
42182
42969
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
42183
|
-
|
|
42184
|
-
|
|
42970
|
+
const receiver = stripParenExpression(callee.object);
|
|
42971
|
+
if (isNodeOfType(receiver, "Identifier")) {
|
|
42972
|
+
const localName = receiver.name;
|
|
42185
42973
|
const binding = findVariableInitializer(node, localName);
|
|
42186
42974
|
if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
|
|
42187
42975
|
return localName === "Dimensions";
|
|
42188
42976
|
}
|
|
42189
|
-
if (isNodeOfType(
|
|
42190
|
-
if (getInitializerModuleSource(node,
|
|
42191
|
-
const rootName = getRootIdentifierName(
|
|
42977
|
+
if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
|
|
42978
|
+
if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
|
|
42979
|
+
const rootName = getRootIdentifierName(receiver.object);
|
|
42192
42980
|
if (rootName === null) return false;
|
|
42193
42981
|
const importBinding = getImportBindingForName(node, rootName);
|
|
42194
42982
|
return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
|
|
42195
42983
|
}
|
|
42196
|
-
if (getRequireCallSource(
|
|
42984
|
+
if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
|
|
42197
42985
|
return false;
|
|
42198
42986
|
};
|
|
42199
42987
|
const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
|
|
@@ -42637,7 +43425,8 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42637
43425
|
},
|
|
42638
43426
|
CallExpression(node) {
|
|
42639
43427
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
42640
|
-
|
|
43428
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
43429
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
|
|
42641
43430
|
if (!isMemberProperty(node.callee, "create")) return;
|
|
42642
43431
|
const stylesArgument = node.arguments?.[0];
|
|
42643
43432
|
if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
|
|
@@ -43487,7 +44276,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
43487
44276
|
const callee = node.callee;
|
|
43488
44277
|
if (!isStaticMemberNamed(callee, "setNativeProps")) return;
|
|
43489
44278
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
43490
|
-
if (!isStaticMemberNamed(callee.object, "current")) return;
|
|
44279
|
+
if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
|
|
43491
44280
|
context.report({
|
|
43492
44281
|
node,
|
|
43493
44282
|
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 +44504,15 @@ const analyzeGestureChain = (expression) => {
|
|
|
43715
44504
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
43716
44505
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
43717
44506
|
const methodName = callee.property.name;
|
|
43718
|
-
|
|
44507
|
+
const receiver = stripParenExpression(callee.object);
|
|
44508
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
|
|
43719
44509
|
factoryName: methodName,
|
|
43720
44510
|
chainMethodNames,
|
|
43721
44511
|
numberOfTapsArgument
|
|
43722
44512
|
};
|
|
43723
44513
|
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
43724
44514
|
chainMethodNames.push(methodName);
|
|
43725
|
-
cursor =
|
|
44515
|
+
cursor = receiver;
|
|
43726
44516
|
}
|
|
43727
44517
|
return null;
|
|
43728
44518
|
};
|
|
@@ -47388,6 +48178,8 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47388
48178
|
const propName = propRawName.toLowerCase();
|
|
47389
48179
|
if (!propName.startsWith("aria-")) continue;
|
|
47390
48180
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
48181
|
+
const attributeValue = attributeNode.value;
|
|
48182
|
+
if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
|
|
47391
48183
|
(ariaAttributes ??= []).push({
|
|
47392
48184
|
attribute,
|
|
47393
48185
|
propName
|
|
@@ -47766,7 +48558,11 @@ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
|
47766
48558
|
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
47767
48559
|
return Boolean(symbol && symbol.kind !== "import");
|
|
47768
48560
|
};
|
|
47769
|
-
const isNonReactEffectEventCallee = (callee, contextNode, scopes) =>
|
|
48561
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
48562
|
+
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
48563
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
48564
|
+
return false;
|
|
48565
|
+
};
|
|
47770
48566
|
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47771
48567
|
const initializer = symbol.initializer;
|
|
47772
48568
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
@@ -48110,7 +48906,8 @@ const serverAfterNonblocking = defineRule({
|
|
|
48110
48906
|
if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
|
|
48111
48907
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
48112
48908
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
48113
|
-
const
|
|
48909
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
48910
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
48114
48911
|
if (!objectName) return;
|
|
48115
48912
|
const methodName = node.callee.property.name;
|
|
48116
48913
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
@@ -48786,7 +49583,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
|
|
|
48786
49583
|
};
|
|
48787
49584
|
const ascendMemberChain = (referenceIdentifier) => {
|
|
48788
49585
|
let chainTip = referenceIdentifier;
|
|
48789
|
-
while (chainTip.parent
|
|
49586
|
+
while (chainTip.parent) {
|
|
49587
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
|
|
49588
|
+
chainTip = chainTip.parent;
|
|
49589
|
+
continue;
|
|
49590
|
+
}
|
|
49591
|
+
if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
|
|
49592
|
+
chainTip = chainTip.parent;
|
|
49593
|
+
continue;
|
|
49594
|
+
}
|
|
49595
|
+
break;
|
|
49596
|
+
}
|
|
48790
49597
|
return chainTip;
|
|
48791
49598
|
};
|
|
48792
49599
|
const isDirectContentsMutation = (referenceIdentifier) => {
|
|
@@ -48811,7 +49618,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
|
|
|
48811
49618
|
const callee = callExpression.callee;
|
|
48812
49619
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
48813
49620
|
const methodName = getMemberPropertyName$1(callee);
|
|
48814
|
-
|
|
49621
|
+
const calleeReceiver = stripParenExpression(callee.object);
|
|
49622
|
+
return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
48815
49623
|
}
|
|
48816
49624
|
if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
|
|
48817
49625
|
const calleeSymbol = scopes.symbolFor(callee);
|
|
@@ -49541,7 +50349,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49541
50349
|
};
|
|
49542
50350
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
49543
50351
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
49544
|
-
let currentNode = outerNode.callee.object;
|
|
50352
|
+
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
49545
50353
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
49546
50354
|
const calleeName = getCalleeName$2(currentNode);
|
|
49547
50355
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
@@ -49552,7 +50360,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49552
50360
|
}
|
|
49553
50361
|
}
|
|
49554
50362
|
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
49555
|
-
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
50363
|
+
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
|
|
49556
50364
|
else break;
|
|
49557
50365
|
}
|
|
49558
50366
|
return result;
|
|
@@ -50205,7 +51013,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
50205
51013
|
while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
|
|
50206
51014
|
const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
|
|
50207
51015
|
if (methodName) methodNames.unshift(methodName);
|
|
50208
|
-
currentNode = currentNode.callee.object;
|
|
51016
|
+
currentNode = stripParenExpression(currentNode.callee.object);
|
|
50209
51017
|
}
|
|
50210
51018
|
if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
|
|
50211
51019
|
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
@@ -53287,6 +54095,18 @@ const reactDoctorRules = [
|
|
|
53287
54095
|
category: "Bugs"
|
|
53288
54096
|
}
|
|
53289
54097
|
},
|
|
54098
|
+
{
|
|
54099
|
+
key: "react-doctor/no-locale-format-in-render",
|
|
54100
|
+
id: "no-locale-format-in-render",
|
|
54101
|
+
source: "react-doctor",
|
|
54102
|
+
originallyExternal: false,
|
|
54103
|
+
rule: {
|
|
54104
|
+
...noLocaleFormatInRender,
|
|
54105
|
+
framework: "global",
|
|
54106
|
+
category: "Bugs",
|
|
54107
|
+
requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
|
|
54108
|
+
}
|
|
54109
|
+
},
|
|
53290
54110
|
{
|
|
53291
54111
|
key: "react-doctor/no-long-transition-duration",
|
|
53292
54112
|
id: "no-long-transition-duration",
|