oxlint-plugin-react-doctor 0.7.2-dev.11e9c87 → 0.7.2-dev.2953b25
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 +92 -0
- package/dist/index.js +1530 -430
- 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,102 @@ 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/get-range-start.ts
|
|
7721
|
+
const getRangeStart = (node) => {
|
|
7722
|
+
const rangeStart = node.range?.[0];
|
|
7723
|
+
return typeof rangeStart === "number" ? rangeStart : null;
|
|
7724
|
+
};
|
|
7725
|
+
//#endregion
|
|
7726
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
7727
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
7728
|
+
let node = callExpression;
|
|
7729
|
+
let parent = node.parent;
|
|
7730
|
+
while (parent) {
|
|
7731
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
7732
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
7733
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
7734
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
7735
|
+
node = parent;
|
|
7736
|
+
parent = node.parent;
|
|
7737
|
+
continue;
|
|
7738
|
+
}
|
|
7739
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
7740
|
+
node = parent;
|
|
7741
|
+
parent = node.parent;
|
|
7742
|
+
continue;
|
|
7743
|
+
}
|
|
7744
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
7745
|
+
node = parent;
|
|
7746
|
+
parent = node.parent;
|
|
7747
|
+
continue;
|
|
7748
|
+
}
|
|
7749
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
7750
|
+
const expressions = parent.expressions ?? [];
|
|
7751
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
7752
|
+
node = parent;
|
|
7753
|
+
parent = node.parent;
|
|
7754
|
+
continue;
|
|
7755
|
+
}
|
|
7756
|
+
return false;
|
|
7757
|
+
}
|
|
7758
|
+
return false;
|
|
7759
|
+
};
|
|
7760
|
+
//#endregion
|
|
7651
7761
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
7652
7762
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
7653
7763
|
if (!node || typeof node !== "object") return;
|
|
@@ -7799,6 +7909,17 @@ const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSub
|
|
|
7799
7909
|
};
|
|
7800
7910
|
//#endregion
|
|
7801
7911
|
//#region src/plugin/rules/state-and-effects/effect-needs-cleanup.ts
|
|
7912
|
+
const OBSERVER_REGISTRATION_METHOD_NAME = "observe";
|
|
7913
|
+
const RESOURCE_NOUN_BY_KIND = {
|
|
7914
|
+
subscribe: "subscription",
|
|
7915
|
+
timer: "timer",
|
|
7916
|
+
socket: "connection"
|
|
7917
|
+
};
|
|
7918
|
+
const isSocketConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && SOCKET_CONSTRUCTOR_NAMES_REQUIRING_CLEANUP.has(node.callee.name);
|
|
7919
|
+
const isSubscribeOrObserveCall = (node) => {
|
|
7920
|
+
if (isSubscribeLikeCallExpression(node)) return true;
|
|
7921
|
+
return isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === OBSERVER_REGISTRATION_METHOD_NAME;
|
|
7922
|
+
};
|
|
7802
7923
|
const findSubscribeLikeUsages = (callback) => {
|
|
7803
7924
|
const usages = [];
|
|
7804
7925
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
@@ -7810,6 +7931,14 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7810
7931
|
}
|
|
7811
7932
|
walkAst(callback, (child) => {
|
|
7812
7933
|
if (child === cleanupArgument && !isSubscribeLikeCallExpression(child)) return false;
|
|
7934
|
+
if (isSocketConstruction(child)) {
|
|
7935
|
+
usages.push({
|
|
7936
|
+
kind: "socket",
|
|
7937
|
+
node: child,
|
|
7938
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
7939
|
+
});
|
|
7940
|
+
return;
|
|
7941
|
+
}
|
|
7813
7942
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7814
7943
|
if (isNodeOfType(child.callee, "Identifier") && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(child.callee.name)) {
|
|
7815
7944
|
usages.push({
|
|
@@ -7819,7 +7948,7 @@ const findSubscribeLikeUsages = (callback) => {
|
|
|
7819
7948
|
});
|
|
7820
7949
|
return;
|
|
7821
7950
|
}
|
|
7822
|
-
if (isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") && SUBSCRIPTION_METHOD_NAMES.has(child.callee.property.name)) usages.push({
|
|
7951
|
+
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
7952
|
kind: "subscribe",
|
|
7824
7953
|
node: child,
|
|
7825
7954
|
resourceName: child.callee.property.name
|
|
@@ -7835,6 +7964,13 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7835
7964
|
};
|
|
7836
7965
|
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
7837
7966
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
7967
|
+
walkAst(effectCallback.body, (child) => {
|
|
7968
|
+
if (!isSubscribeOrObserveCall(child)) return;
|
|
7969
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7970
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
7971
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
7972
|
+
bindings.subscriptionNames.add(child.callee.object.name);
|
|
7973
|
+
});
|
|
7838
7974
|
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
7839
7975
|
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
7840
7976
|
for (const declarator of child.declarations ?? []) {
|
|
@@ -7842,7 +7978,12 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7842
7978
|
const bindingName = declarator.id.name;
|
|
7843
7979
|
bindings.effectScopeVariableNames.add(bindingName);
|
|
7844
7980
|
const init = declarator.init;
|
|
7845
|
-
if (!init
|
|
7981
|
+
if (!init) continue;
|
|
7982
|
+
if (isSocketConstruction(init)) {
|
|
7983
|
+
bindings.subscriptionNames.add(bindingName);
|
|
7984
|
+
continue;
|
|
7985
|
+
}
|
|
7986
|
+
if (!isNodeOfType(init, "CallExpression")) continue;
|
|
7846
7987
|
if (isSubscribeLikeCallExpression(init)) {
|
|
7847
7988
|
bindings.subscriptionNames.add(bindingName);
|
|
7848
7989
|
if (isCleanupReturningSubscribeLikeCallExpression(init)) bindings.cleanupFunctionNames.add(bindingName);
|
|
@@ -7868,9 +8009,21 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7868
8009
|
});
|
|
7869
8010
|
return bindings;
|
|
7870
8011
|
};
|
|
7871
|
-
const
|
|
7872
|
-
|
|
7873
|
-
|
|
8012
|
+
const removeSynchronouslyReleasedUsages = (callback, usages) => {
|
|
8013
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8014
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8015
|
+
const releaseStarts = [];
|
|
8016
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8017
|
+
if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
|
|
8018
|
+
const releaseStart = getRangeStart(child);
|
|
8019
|
+
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8020
|
+
});
|
|
8021
|
+
if (releaseStarts.length === 0) return usages;
|
|
8022
|
+
return usages.filter((usage) => {
|
|
8023
|
+
const usageStart = getRangeStart(usage.node);
|
|
8024
|
+
if (usageStart === null) return true;
|
|
8025
|
+
return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
|
|
8026
|
+
});
|
|
7874
8027
|
};
|
|
7875
8028
|
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
7876
8029
|
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
@@ -7894,26 +8047,177 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
7894
8047
|
});
|
|
7895
8048
|
return didFindCleanupReturn;
|
|
7896
8049
|
};
|
|
8050
|
+
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8051
|
+
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
8052
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
8053
|
+
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
8054
|
+
if (keyName === "signal") return true;
|
|
8055
|
+
if (keyName !== "once") return false;
|
|
8056
|
+
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
8057
|
+
};
|
|
8058
|
+
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
8059
|
+
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8060
|
+
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8061
|
+
["addListener", new Set([
|
|
8062
|
+
"removeListener",
|
|
8063
|
+
"off",
|
|
8064
|
+
"abort"
|
|
8065
|
+
])],
|
|
8066
|
+
["on", new Set([
|
|
8067
|
+
"off",
|
|
8068
|
+
"removeListener",
|
|
8069
|
+
"on"
|
|
8070
|
+
])],
|
|
8071
|
+
["subscribe", new Set(["unsubscribe", "unsub"])],
|
|
8072
|
+
["sub", new Set(["unsub", "unsubscribe"])],
|
|
8073
|
+
["watch", new Set(["unwatch", "close"])],
|
|
8074
|
+
["listen", new Set(["unlisten", "close"])],
|
|
8075
|
+
[OBSERVER_REGISTRATION_METHOD_NAME, new Set(["disconnect", "unobserve"])]
|
|
8076
|
+
]);
|
|
8077
|
+
const UNIVERSAL_RELEASE_VERB_NAMES = new Set([
|
|
8078
|
+
"cleanup",
|
|
8079
|
+
"dispose",
|
|
8080
|
+
"destroy",
|
|
8081
|
+
"teardown"
|
|
8082
|
+
]);
|
|
8083
|
+
const SOCKET_RELEASE_VERB_NAMES = new Set(["close"]);
|
|
8084
|
+
const INTERVAL_RELEASE_VERB_NAMES = new Set(["clearInterval"]);
|
|
8085
|
+
const getReleaseVerbName = (node) => {
|
|
8086
|
+
if (!isReleaseLikeCall(node, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return null;
|
|
8087
|
+
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
8088
|
+
if (!isNodeOfType(callNode, "CallExpression")) return null;
|
|
8089
|
+
const callee = isNodeOfType(callNode.callee, "ChainExpression") ? callNode.callee.expression : callNode.callee;
|
|
8090
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
8091
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
8092
|
+
return null;
|
|
8093
|
+
};
|
|
8094
|
+
const matchesPairedReleaseVerb = (releaseVerbName, pairedVerbNames) => pairedVerbNames.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName);
|
|
8095
|
+
const bodyContainsPairedReleaseCall = (body, pairedVerbNames) => {
|
|
8096
|
+
let didFindPairedRelease = false;
|
|
8097
|
+
walkAst(body, (child) => {
|
|
8098
|
+
if (didFindPairedRelease) return false;
|
|
8099
|
+
if (child !== body && isFunctionLike$1(child)) return false;
|
|
8100
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8101
|
+
if (releaseVerbName !== null && matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) {
|
|
8102
|
+
didFindPairedRelease = true;
|
|
8103
|
+
return false;
|
|
8104
|
+
}
|
|
8105
|
+
});
|
|
8106
|
+
return didFindPairedRelease;
|
|
8107
|
+
};
|
|
8108
|
+
const fileReleaseVerbNamesCache = /* @__PURE__ */ new WeakMap();
|
|
8109
|
+
const collectFileReleaseVerbNames = (anyNode) => {
|
|
8110
|
+
let programNode = anyNode;
|
|
8111
|
+
while (programNode.parent) programNode = programNode.parent;
|
|
8112
|
+
const cached = fileReleaseVerbNamesCache.get(programNode);
|
|
8113
|
+
if (cached) return cached;
|
|
8114
|
+
const releaseVerbNames = /* @__PURE__ */ new Set();
|
|
8115
|
+
walkAst(programNode, (child) => {
|
|
8116
|
+
const releaseVerbName = getReleaseVerbName(child);
|
|
8117
|
+
if (releaseVerbName !== null) releaseVerbNames.add(releaseVerbName);
|
|
8118
|
+
});
|
|
8119
|
+
fileReleaseVerbNamesCache.set(programNode, releaseVerbNames);
|
|
8120
|
+
return releaseVerbNames;
|
|
8121
|
+
};
|
|
8122
|
+
const fileContainsPairedReleaseCall = (registrationCall, registrationVerbName) => {
|
|
8123
|
+
const fileReleaseVerbNames = collectFileReleaseVerbNames(registrationCall);
|
|
8124
|
+
const pairedVerbNames = PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(registrationVerbName);
|
|
8125
|
+
if (!pairedVerbNames) return fileReleaseVerbNames.size > 0;
|
|
8126
|
+
for (const releaseVerbName of fileReleaseVerbNames) if (matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return true;
|
|
8127
|
+
return false;
|
|
8128
|
+
};
|
|
8129
|
+
const findRetainedFunctionLeak = (retainedFunction) => {
|
|
8130
|
+
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8131
|
+
const body = retainedFunction.body;
|
|
8132
|
+
if (!body) return null;
|
|
8133
|
+
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
8134
|
+
let leak = null;
|
|
8135
|
+
walkAst(body, (child) => {
|
|
8136
|
+
if (leak !== null) return false;
|
|
8137
|
+
if (isFunctionLike$1(child)) return false;
|
|
8138
|
+
if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
|
|
8139
|
+
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8140
|
+
leak = {
|
|
8141
|
+
kind: "socket",
|
|
8142
|
+
node: child,
|
|
8143
|
+
resourceName: isNodeOfType(child.callee, "Identifier") ? child.callee.name : "WebSocket"
|
|
8144
|
+
};
|
|
8145
|
+
return false;
|
|
8146
|
+
}
|
|
8147
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
8148
|
+
if (isNodeOfType(child.callee, "Identifier") && child.callee.name === "setInterval" && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, INTERVAL_RELEASE_VERB_NAMES)) {
|
|
8149
|
+
leak = {
|
|
8150
|
+
kind: "timer",
|
|
8151
|
+
node: child,
|
|
8152
|
+
resourceName: "setInterval"
|
|
8153
|
+
};
|
|
8154
|
+
return false;
|
|
8155
|
+
}
|
|
8156
|
+
if (isSubscribeOrObserveCall(child) && isResultDiscardedCall(child)) {
|
|
8157
|
+
const registrationVerbName = isNodeOfType(child.callee, "MemberExpression") && isNodeOfType(child.callee.property, "Identifier") ? child.callee.property.name : "subscribe";
|
|
8158
|
+
if (!hasSelfReleasingListenerOptions(child) && !fileContainsPairedReleaseCall(child, registrationVerbName)) leak = {
|
|
8159
|
+
kind: "subscribe",
|
|
8160
|
+
node: child,
|
|
8161
|
+
resourceName: registrationVerbName
|
|
8162
|
+
};
|
|
8163
|
+
return false;
|
|
8164
|
+
}
|
|
8165
|
+
});
|
|
8166
|
+
return leak;
|
|
8167
|
+
};
|
|
8168
|
+
const isRetainedComponentScopeFunction = (functionNode) => {
|
|
8169
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration")) return enclosingComponentOrHookName(functionNode) !== null;
|
|
8170
|
+
if (!isNodeOfType(functionNode, "ArrowFunctionExpression") && !isNodeOfType(functionNode, "FunctionExpression")) return false;
|
|
8171
|
+
if (!isNodeOfType(functionNode.parent, "VariableDeclarator")) return false;
|
|
8172
|
+
return enclosingComponentOrHookName(functionNode) !== null;
|
|
8173
|
+
};
|
|
7897
8174
|
const effectNeedsCleanup = defineRule({
|
|
7898
8175
|
id: "effect-needs-cleanup",
|
|
7899
8176
|
title: "Effect subscription or timer never cleaned up",
|
|
7900
8177
|
severity: "error",
|
|
7901
8178
|
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
|
-
|
|
8179
|
+
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.",
|
|
8180
|
+
create: (context) => {
|
|
8181
|
+
const reportRetainedLeak = (retainedFunction) => {
|
|
8182
|
+
const leak = findRetainedFunctionLeak(retainedFunction);
|
|
8183
|
+
if (!leak) return;
|
|
8184
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[leak.kind];
|
|
8185
|
+
context.report({
|
|
8186
|
+
node: leak.node,
|
|
8187
|
+
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.`
|
|
8188
|
+
});
|
|
8189
|
+
};
|
|
8190
|
+
return {
|
|
8191
|
+
CallExpression(node) {
|
|
8192
|
+
if (isHookCall$2(node, "useCallback")) {
|
|
8193
|
+
const retainedCallback = getEffectCallback(node);
|
|
8194
|
+
if (retainedCallback) reportRetainedLeak(retainedCallback);
|
|
8195
|
+
return;
|
|
8196
|
+
}
|
|
8197
|
+
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
8198
|
+
const callback = getEffectCallback(node);
|
|
8199
|
+
if (!callback) return;
|
|
8200
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
8201
|
+
if (usages.length === 0) return;
|
|
8202
|
+
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8203
|
+
const firstUsage = usages[0];
|
|
8204
|
+
const resourceNoun = RESOURCE_NOUN_BY_KIND[firstUsage.kind];
|
|
8205
|
+
context.report({
|
|
8206
|
+
node,
|
|
8207
|
+
message: `\`${firstUsage.resourceName}\` creates a ${resourceNoun} in useEffect without returning cleanup. Return a cleanup function so it does not leak after unmount.`
|
|
8208
|
+
});
|
|
8209
|
+
},
|
|
8210
|
+
FunctionDeclaration(node) {
|
|
8211
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8212
|
+
},
|
|
8213
|
+
ArrowFunctionExpression(node) {
|
|
8214
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8215
|
+
},
|
|
8216
|
+
FunctionExpression(node) {
|
|
8217
|
+
if (isRetainedComponentScopeFunction(node)) reportRetainedLeak(node);
|
|
8218
|
+
}
|
|
8219
|
+
};
|
|
8220
|
+
}
|
|
7917
8221
|
});
|
|
7918
8222
|
//#endregion
|
|
7919
8223
|
//#region src/plugin/semantic/scope-analysis.ts
|
|
@@ -8472,17 +8776,6 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
8472
8776
|
return computedCaptures;
|
|
8473
8777
|
};
|
|
8474
8778
|
//#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
8779
|
//#region src/plugin/utils/is-react-hoc-callback-argument.ts
|
|
8487
8780
|
const reactHocCalleeName = (callee) => {
|
|
8488
8781
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
@@ -8696,8 +8989,8 @@ const isAstDescendant = (inner, outer) => {
|
|
|
8696
8989
|
* One cohesive concept: "given a captured symbol, is its value
|
|
8697
8990
|
* structurally stable across re-renders (and therefore unnecessary
|
|
8698
8991
|
* in a deps array)?". The rule reads `symbolHasStableValue` /
|
|
8699
|
-
* `symbolHasStableHookOrigin` / `
|
|
8700
|
-
*
|
|
8992
|
+
* `symbolHasStableHookOrigin` / `isRecursiveInitializerCapture` at
|
|
8993
|
+
* multiple sites — extracting
|
|
8701
8994
|
* them lets the rule body stay focused on the diff-the-captured-vs-
|
|
8702
8995
|
* declared logic.
|
|
8703
8996
|
*
|
|
@@ -8753,11 +9046,6 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
8753
9046
|
}
|
|
8754
9047
|
return false;
|
|
8755
9048
|
};
|
|
8756
|
-
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
8757
|
-
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
8758
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
8759
|
-
return getHookName(initializer.callee) === "useEffectEvent";
|
|
8760
|
-
};
|
|
8761
9049
|
const getFunctionValueNode = (symbol) => {
|
|
8762
9050
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
8763
9051
|
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
@@ -8843,6 +9131,37 @@ const symbolHasStableFunctionOrigin = (symbol, scopes, visitedSymbolIds) => {
|
|
|
8843
9131
|
};
|
|
8844
9132
|
const symbolHasStableValue = (symbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => symbolHasStableHookOrigin(symbol) || symbolHasStableFunctionOrigin(symbol, scopes, visitedSymbolIds) || symbolHasStableMemoizedOrigin(symbol, scopes, visitedSymbolIds);
|
|
8845
9133
|
//#endregion
|
|
9134
|
+
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
9135
|
+
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
9136
|
+
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
9137
|
+
if (importSource === null) return false;
|
|
9138
|
+
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
9139
|
+
};
|
|
9140
|
+
//#endregion
|
|
9141
|
+
//#region src/plugin/utils/is-non-react-effect-event-callee.ts
|
|
9142
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
9143
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
9144
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
9145
|
+
};
|
|
9146
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
9147
|
+
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
9148
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
9149
|
+
return false;
|
|
9150
|
+
};
|
|
9151
|
+
//#endregion
|
|
9152
|
+
//#region src/plugin/utils/symbol-has-react-use-effect-event-origin.ts
|
|
9153
|
+
const getUseEffectEventCalleeName = (callee) => {
|
|
9154
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
9155
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
9156
|
+
return null;
|
|
9157
|
+
};
|
|
9158
|
+
const symbolHasReactUseEffectEventOrigin = (symbol, scopes) => {
|
|
9159
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
9160
|
+
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
9161
|
+
if (getUseEffectEventCalleeName(initializer.callee) !== "useEffectEvent") return false;
|
|
9162
|
+
return !isNonReactEffectEventCallee(initializer.callee, initializer, scopes);
|
|
9163
|
+
};
|
|
9164
|
+
//#endregion
|
|
8846
9165
|
//#region src/plugin/rules/react-builtins/exhaustive-deps.ts
|
|
8847
9166
|
const HOOKS_REQUIRING_DEPS_MATCH = new Set([
|
|
8848
9167
|
"useEffect",
|
|
@@ -9493,7 +9812,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9493
9812
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
9494
9813
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
9495
9814
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
9496
|
-
if (depSymbol &&
|
|
9815
|
+
if (depSymbol && symbolHasReactUseEffectEventOrigin(depSymbol, context.scopes)) {
|
|
9497
9816
|
context.report({
|
|
9498
9817
|
node: elementNode,
|
|
9499
9818
|
message: buildEffectEventDepMessage()
|
|
@@ -9919,7 +10238,10 @@ const PRAGMA = "React";
|
|
|
9919
10238
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
9920
10239
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
9921
10240
|
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
9922
|
-
if (isNodeOfType(node.callee, "MemberExpression"))
|
|
10241
|
+
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
10242
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
10243
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
10244
|
+
}
|
|
9923
10245
|
return true;
|
|
9924
10246
|
};
|
|
9925
10247
|
//#endregion
|
|
@@ -11297,16 +11619,6 @@ const collectHandlerReferencedNames = (root) => {
|
|
|
11297
11619
|
return names;
|
|
11298
11620
|
};
|
|
11299
11621
|
//#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
11622
|
//#region src/plugin/utils/get-function-binding-name.ts
|
|
11311
11623
|
const getFunctionBindingIdentifier = (functionNode) => {
|
|
11312
11624
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && isNodeOfType(functionNode.id, "Identifier")) return functionNode.id;
|
|
@@ -12006,14 +12318,15 @@ const jsCacheStorage = defineRule({
|
|
|
12006
12318
|
"ArrowFunctionExpression:exit": exitFunctionScope,
|
|
12007
12319
|
CallExpression(node) {
|
|
12008
12320
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
12009
|
-
|
|
12321
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
12322
|
+
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
|
|
12010
12323
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
12011
12324
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
12012
12325
|
const storageKey = String(node.arguments[0].value);
|
|
12013
12326
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
12014
12327
|
storageReadCounts.set(storageKey, readCount);
|
|
12015
12328
|
if (readCount === 2) {
|
|
12016
|
-
const storageName =
|
|
12329
|
+
const storageName = receiver.name;
|
|
12017
12330
|
context.report({
|
|
12018
12331
|
node,
|
|
12019
12332
|
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 +12340,16 @@ const jsCacheStorage = defineRule({
|
|
|
12027
12340
|
//#region src/plugin/rules/js-performance/js-combine-iterations.ts
|
|
12028
12341
|
const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
|
|
12029
12342
|
const callee = callExpression.callee;
|
|
12030
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12343
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
12344
|
+
const receiver = stripParenExpression(callee.object);
|
|
12345
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12346
|
+
if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
12347
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
12348
|
+
return true;
|
|
12349
|
+
}
|
|
12350
|
+
return false;
|
|
12036
12351
|
}
|
|
12352
|
+
if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
|
|
12037
12353
|
return false;
|
|
12038
12354
|
};
|
|
12039
12355
|
const isChainPassThroughCall = (callExpression) => {
|
|
@@ -12045,10 +12361,7 @@ const isChainPassThroughCall = (callExpression) => {
|
|
|
12045
12361
|
const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
|
|
12046
12362
|
let cursor = receiverNode;
|
|
12047
12363
|
while (cursor) {
|
|
12048
|
-
|
|
12049
|
-
cursor = cursor.expression;
|
|
12050
|
-
continue;
|
|
12051
|
-
}
|
|
12364
|
+
cursor = stripParenExpression(cursor);
|
|
12052
12365
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12053
12366
|
if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
|
|
12054
12367
|
if (!isChainPassThroughCall(cursor)) return false;
|
|
@@ -12124,10 +12437,7 @@ const isStringSplitRootedChain = (receiverNode) => {
|
|
|
12124
12437
|
let hops = 0;
|
|
12125
12438
|
while (cursor && hops < 12) {
|
|
12126
12439
|
hops += 1;
|
|
12127
|
-
|
|
12128
|
-
cursor = cursor.expression;
|
|
12129
|
-
continue;
|
|
12130
|
-
}
|
|
12440
|
+
cursor = stripParenExpression(cursor);
|
|
12131
12441
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12132
12442
|
const callee = cursor.callee;
|
|
12133
12443
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
@@ -12151,10 +12461,7 @@ const isSmallLiteralArray = (node) => {
|
|
|
12151
12461
|
const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
|
|
12152
12462
|
let cursor = receiverNode;
|
|
12153
12463
|
while (cursor) {
|
|
12154
|
-
|
|
12155
|
-
cursor = cursor.expression;
|
|
12156
|
-
continue;
|
|
12157
|
-
}
|
|
12464
|
+
cursor = stripParenExpression(cursor);
|
|
12158
12465
|
if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
|
|
12159
12466
|
if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
|
|
12160
12467
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
@@ -12219,7 +12526,7 @@ const jsCombineIterations = defineRule({
|
|
|
12219
12526
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
12220
12527
|
const outerMethod = node.callee.property.name;
|
|
12221
12528
|
if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
|
|
12222
|
-
const innerCall = node.callee.object;
|
|
12529
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12223
12530
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12224
12531
|
const innerMethod = innerCall.callee.property.name;
|
|
12225
12532
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
@@ -12292,10 +12599,10 @@ const jsFlatmapFilter = defineRule({
|
|
|
12292
12599
|
if (!filterArgument) return;
|
|
12293
12600
|
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
12601
|
if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
12295
|
-
const innerCall = node.callee.object;
|
|
12602
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12296
12603
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12297
12604
|
if (innerCall.callee.property.name !== "map") return;
|
|
12298
|
-
const receiver = innerCall.callee.object;
|
|
12605
|
+
const receiver = stripParenExpression(innerCall.callee.object);
|
|
12299
12606
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
12300
12607
|
const elements = receiver.elements ?? [];
|
|
12301
12608
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13555,7 +13862,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13555
13862
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13556
13863
|
create: (context) => ({ CallExpression(node) {
|
|
13557
13864
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
13558
|
-
const receiver = node.callee.object;
|
|
13865
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
13559
13866
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
|
|
13560
13867
|
const spreadArgument = receiver.elements[0].argument;
|
|
13561
13868
|
if (isFreshOrIteratorAllocation(spreadArgument)) return;
|
|
@@ -18592,7 +18899,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
|
|
|
18592
18899
|
};
|
|
18593
18900
|
const describeClientSideNavigation = (node) => {
|
|
18594
18901
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
|
|
18595
|
-
const
|
|
18902
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
18903
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
18596
18904
|
const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
18597
18905
|
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
|
|
18598
18906
|
}
|
|
@@ -19212,7 +19520,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
|
|
|
19212
19520
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
19213
19521
|
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
19214
19522
|
if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
|
|
19215
|
-
if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
|
|
19523
|
+
if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
|
|
19216
19524
|
return node.callee.property.name;
|
|
19217
19525
|
};
|
|
19218
19526
|
const isMutatingFetchCall = (node) => {
|
|
@@ -19226,7 +19534,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
|
|
|
19226
19534
|
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19227
19535
|
const { property, object } = node.callee;
|
|
19228
19536
|
if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
|
|
19229
|
-
if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
|
|
19537
|
+
if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
|
|
19230
19538
|
return true;
|
|
19231
19539
|
};
|
|
19232
19540
|
const getDbCallDescription = (node) => {
|
|
@@ -19234,7 +19542,8 @@ const getDbCallDescription = (node) => {
|
|
|
19234
19542
|
if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
|
|
19235
19543
|
if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
|
|
19236
19544
|
const methodName = node.callee.property.name;
|
|
19237
|
-
const
|
|
19545
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
19546
|
+
const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
19238
19547
|
return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
|
|
19239
19548
|
};
|
|
19240
19549
|
const findSideEffect = (node, options = {}) => {
|
|
@@ -20634,13 +20943,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
|
|
|
20634
20943
|
if (isNodeOfType(current, "CallExpression")) {
|
|
20635
20944
|
let node = ref.identifier;
|
|
20636
20945
|
let parent = node.parent;
|
|
20637
|
-
while (parent && isNodeOfType(parent, "MemberExpression")) {
|
|
20946
|
+
while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
|
|
20638
20947
|
node = parent;
|
|
20639
20948
|
parent = node.parent;
|
|
20640
20949
|
}
|
|
20641
20950
|
if (current.callee === node) return current;
|
|
20642
20951
|
}
|
|
20643
|
-
if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
|
|
20952
|
+
if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
|
|
20644
20953
|
return null;
|
|
20645
20954
|
};
|
|
20646
20955
|
const getArgsUpstreamRefs = (analysis, ref) => {
|
|
@@ -20775,7 +21084,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
20775
21084
|
const importDeclaration = declarationNode.parent;
|
|
20776
21085
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
20777
21086
|
}));
|
|
20778
|
-
const isHookCallee = (analysis, node, hookName) => {
|
|
21087
|
+
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
20779
21088
|
if (!node) return false;
|
|
20780
21089
|
if (isNodeOfType(node, "Identifier")) {
|
|
20781
21090
|
if (node.name === hookName) return true;
|
|
@@ -20784,15 +21093,19 @@ const isHookCallee = (analysis, node, hookName) => {
|
|
|
20784
21093
|
if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
20785
21094
|
return false;
|
|
20786
21095
|
}
|
|
20787
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
21096
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
21097
|
+
const receiver = stripParenExpression(node.object);
|
|
21098
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21099
|
+
}
|
|
20788
21100
|
return false;
|
|
20789
21101
|
};
|
|
20790
21102
|
const isUseEffect = (node) => {
|
|
20791
21103
|
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
20792
21104
|
const callee = node.callee;
|
|
20793
21105
|
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
20794
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
20795
|
-
|
|
21106
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
21107
|
+
const receiver = stripParenExpression(callee.object);
|
|
21108
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
20796
21109
|
};
|
|
20797
21110
|
const getEffectFn = (analysis, node) => {
|
|
20798
21111
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
@@ -20820,7 +21133,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20820
21133
|
const node = def.node;
|
|
20821
21134
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20822
21135
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20823
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21136
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20824
21137
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20825
21138
|
const elements = node.id.elements ?? [];
|
|
20826
21139
|
if (elements.length !== 1 && elements.length !== 2) return false;
|
|
@@ -20831,7 +21144,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
|
|
|
20831
21144
|
const node = def.node;
|
|
20832
21145
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20833
21146
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20834
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21147
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
20835
21148
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
20836
21149
|
const elements = node.id.elements ?? [];
|
|
20837
21150
|
if (elements.length !== 2) return false;
|
|
@@ -20878,7 +21191,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
20878
21191
|
const node = def.node;
|
|
20879
21192
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
20880
21193
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
20881
|
-
return isHookCallee(analysis, node.init.callee, "useRef");
|
|
21194
|
+
return isHookCallee$1(analysis, node.init.callee, "useRef");
|
|
20882
21195
|
}));
|
|
20883
21196
|
const isRefCurrent = (ref) => {
|
|
20884
21197
|
const parent = ref.identifier.parent;
|
|
@@ -20891,11 +21204,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
|
|
|
20891
21204
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
20892
21205
|
const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
20893
21206
|
if (!isPropAlias(analysis, ref)) return false;
|
|
20894
|
-
|
|
20895
|
-
|
|
21207
|
+
let effectiveNode = ref.identifier;
|
|
21208
|
+
let parent = effectiveNode.parent;
|
|
21209
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
|
|
21210
|
+
effectiveNode = parent;
|
|
21211
|
+
parent = effectiveNode.parent;
|
|
21212
|
+
}
|
|
20896
21213
|
if (!parent) return false;
|
|
20897
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee ===
|
|
20898
|
-
if (isNodeOfType(parent, "MemberExpression") && parent.object ===
|
|
21214
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
|
|
21215
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
|
|
20899
21216
|
const memberParent = parent.parent;
|
|
20900
21217
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
20901
21218
|
if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
|
|
@@ -20906,7 +21223,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
20906
21223
|
};
|
|
20907
21224
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
20908
21225
|
const getUseStateDecl = (analysis, ref) => {
|
|
20909
|
-
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21226
|
+
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
|
|
20910
21227
|
while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
|
|
20911
21228
|
return node ?? null;
|
|
20912
21229
|
};
|
|
@@ -21111,7 +21428,7 @@ const isObjectUrlLifecycleEffect = (effectFn) => {
|
|
|
21111
21428
|
const noAdjustStateOnPropChange = defineRule({
|
|
21112
21429
|
id: "no-adjust-state-on-prop-change",
|
|
21113
21430
|
title: "State synced to a prop inside an effect",
|
|
21114
|
-
severity: "
|
|
21431
|
+
severity: "warn",
|
|
21115
21432
|
tags: ["test-noise"],
|
|
21116
21433
|
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
21434
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -21152,7 +21469,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
|
21152
21469
|
"summary",
|
|
21153
21470
|
"textarea"
|
|
21154
21471
|
]);
|
|
21472
|
+
const isStaticallyFalseBooleanAttribute = (attribute) => {
|
|
21473
|
+
const value = attribute.value;
|
|
21474
|
+
if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
21475
|
+
const expression = value.expression;
|
|
21476
|
+
return isNodeOfType(expression, "Literal") && expression.value === false;
|
|
21477
|
+
};
|
|
21478
|
+
const DISABLEABLE_TAGS = new Set([
|
|
21479
|
+
"button",
|
|
21480
|
+
"input",
|
|
21481
|
+
"select",
|
|
21482
|
+
"textarea"
|
|
21483
|
+
]);
|
|
21155
21484
|
const isNativelyFocusable = (tagName, openingElement) => {
|
|
21485
|
+
if (DISABLEABLE_TAGS.has(tagName)) {
|
|
21486
|
+
const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
|
|
21487
|
+
if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
|
|
21488
|
+
}
|
|
21156
21489
|
if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
|
|
21157
21490
|
switch (tagName) {
|
|
21158
21491
|
case "input": {
|
|
@@ -21166,7 +21499,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
|
|
|
21166
21499
|
case "a":
|
|
21167
21500
|
case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
|
|
21168
21501
|
case "audio":
|
|
21169
|
-
case "video":
|
|
21502
|
+
case "video": {
|
|
21503
|
+
const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
|
|
21504
|
+
return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
|
|
21505
|
+
}
|
|
21170
21506
|
default: return false;
|
|
21171
21507
|
}
|
|
21172
21508
|
};
|
|
@@ -22130,7 +22466,8 @@ const SECOND_INDEX_METHODS = new Set([
|
|
|
22130
22466
|
"some"
|
|
22131
22467
|
]);
|
|
22132
22468
|
const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
|
|
22133
|
-
const isPositionallyStableIterationReceiver = (
|
|
22469
|
+
const isPositionallyStableIterationReceiver = (receiverNode) => {
|
|
22470
|
+
const receiver = stripParenExpression(receiverNode);
|
|
22134
22471
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
22135
22472
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
|
|
22136
22473
|
const only = receiver.elements[0];
|
|
@@ -22141,17 +22478,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
|
|
|
22141
22478
|
}
|
|
22142
22479
|
if (!isNodeOfType(receiver, "CallExpression")) return false;
|
|
22143
22480
|
const callee = receiver.callee;
|
|
22144
|
-
if (
|
|
22481
|
+
if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
|
|
22145
22482
|
if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
|
|
22146
22483
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
|
|
22147
22484
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
|
|
22148
22485
|
return false;
|
|
22149
22486
|
};
|
|
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
|
-
};
|
|
22487
|
+
const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
|
|
22155
22488
|
const isArrayFromSourcePositionallyStable = (source) => {
|
|
22156
22489
|
if (isNodeOfType(source, "ObjectExpression")) {
|
|
22157
22490
|
for (const property of source.properties ?? []) {
|
|
@@ -22210,18 +22543,12 @@ const expressionUsesIndex = (expression, paramName) => {
|
|
|
22210
22543
|
return false;
|
|
22211
22544
|
}
|
|
22212
22545
|
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;
|
|
22546
|
+
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
|
|
22214
22547
|
if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
|
|
22215
22548
|
}
|
|
22216
22549
|
return false;
|
|
22217
22550
|
};
|
|
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
|
-
};
|
|
22551
|
+
const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
|
|
22225
22552
|
const noArrayIndexKey = defineRule({
|
|
22226
22553
|
id: "no-array-index-key",
|
|
22227
22554
|
title: "Array index used as a key",
|
|
@@ -22945,7 +23272,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
22945
23272
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
22946
23273
|
return false;
|
|
22947
23274
|
};
|
|
22948
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
23275
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
22949
23276
|
"forEach",
|
|
22950
23277
|
"map",
|
|
22951
23278
|
"filter",
|
|
@@ -22966,30 +23293,51 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
22966
23293
|
if (parent.callee === functionNode) return true;
|
|
22967
23294
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
22968
23295
|
const callee = parent.callee;
|
|
22969
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
23296
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
|
|
22970
23297
|
};
|
|
22971
23298
|
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) => {
|
|
23299
|
+
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
23300
|
+
const analyzeStatementSequence = (statements, context) => {
|
|
22981
23301
|
let fallThroughCount = 0;
|
|
22982
|
-
let
|
|
23302
|
+
let maxTerminatedCount = 0;
|
|
22983
23303
|
for (const statement of statements) {
|
|
22984
|
-
|
|
22985
|
-
|
|
22986
|
-
|
|
23304
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
23305
|
+
fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
|
|
23306
|
+
const thenSummary = analyzeBranchStatements(statement.consequent, context);
|
|
23307
|
+
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
23308
|
+
fallThroughCount: 0,
|
|
23309
|
+
maxTerminatedCount: 0,
|
|
23310
|
+
doAllPathsTerminate: false
|
|
23311
|
+
};
|
|
23312
|
+
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
23313
|
+
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
23314
|
+
fallThroughCount: 0,
|
|
23315
|
+
maxTerminatedCount,
|
|
23316
|
+
doAllPathsTerminate: true
|
|
23317
|
+
};
|
|
23318
|
+
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
23319
|
+
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
22987
23320
|
continue;
|
|
22988
23321
|
}
|
|
22989
|
-
if (isTerminatingStatement(statement))
|
|
23322
|
+
if (isTerminatingStatement(statement)) {
|
|
23323
|
+
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
23324
|
+
return {
|
|
23325
|
+
fallThroughCount: 0,
|
|
23326
|
+
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
23327
|
+
doAllPathsTerminate: true
|
|
23328
|
+
};
|
|
23329
|
+
}
|
|
22990
23330
|
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
22991
23331
|
}
|
|
22992
|
-
return
|
|
23332
|
+
return {
|
|
23333
|
+
fallThroughCount,
|
|
23334
|
+
maxTerminatedCount,
|
|
23335
|
+
doAllPathsTerminate: false
|
|
23336
|
+
};
|
|
23337
|
+
};
|
|
23338
|
+
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23339
|
+
const summary = analyzeStatementSequence(statements, context);
|
|
23340
|
+
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
22993
23341
|
};
|
|
22994
23342
|
const collectLocalHelperFunctions = (root) => {
|
|
22995
23343
|
const helpersByName = /* @__PURE__ */ new Map();
|
|
@@ -23120,7 +23468,9 @@ const isDevOnlyGuardedEffect = (callback) => {
|
|
|
23120
23468
|
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
23121
23469
|
const firstStatement = (body.body ?? [])[0];
|
|
23122
23470
|
if (!firstStatement) return false;
|
|
23123
|
-
if (!
|
|
23471
|
+
if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
|
|
23472
|
+
const consequent = firstStatement.consequent;
|
|
23473
|
+
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
23124
23474
|
return mentionsDevEnvFlag(firstStatement.test);
|
|
23125
23475
|
};
|
|
23126
23476
|
const noCascadingSetState = defineRule({
|
|
@@ -23479,40 +23829,6 @@ const noCloneElement = defineRule({
|
|
|
23479
23829
|
} })
|
|
23480
23830
|
});
|
|
23481
23831
|
//#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
23832
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
23517
23833
|
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
23518
23834
|
const CONTEXT_MODULES = [
|
|
@@ -24483,11 +24799,21 @@ const isInitialOnlySetterCall = (callExpr) => {
|
|
|
24483
24799
|
return isInitialOnlyPropName(arg.name);
|
|
24484
24800
|
};
|
|
24485
24801
|
//#endregion
|
|
24802
|
+
//#region src/plugin/utils/is-no-op-statement.ts
|
|
24803
|
+
const isNoOpStatement = (statement) => {
|
|
24804
|
+
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
24805
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24806
|
+
const expression = stripParenExpression(statement.expression);
|
|
24807
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
24808
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
24809
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
24810
|
+
return false;
|
|
24811
|
+
};
|
|
24812
|
+
//#endregion
|
|
24486
24813
|
//#region src/plugin/utils/get-callback-statements.ts
|
|
24487
24814
|
const getCallbackStatements = (callback) => {
|
|
24488
24815
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
24489
|
-
|
|
24490
|
-
return callback.body ? [callback.body] : [];
|
|
24816
|
+
return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
|
|
24491
24817
|
};
|
|
24492
24818
|
//#endregion
|
|
24493
24819
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
@@ -24526,6 +24852,27 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24526
24852
|
} else if (child && typeof child === "object" && "type" in child) collectValueIdentifierNames(child, into, localBindingNames);
|
|
24527
24853
|
}
|
|
24528
24854
|
};
|
|
24855
|
+
const flattenGuardedStatements = (statements) => {
|
|
24856
|
+
const flattened = [];
|
|
24857
|
+
for (const statement of statements) {
|
|
24858
|
+
if (isNoOpStatement(statement)) continue;
|
|
24859
|
+
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24860
|
+
flattened.push(statement);
|
|
24861
|
+
continue;
|
|
24862
|
+
}
|
|
24863
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
24864
|
+
for (const branch of [statement.consequent, statement.alternate]) {
|
|
24865
|
+
if (!branch) continue;
|
|
24866
|
+
const flattenedBranch = flattenGuardedStatements(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch]);
|
|
24867
|
+
if (flattenedBranch === null) return null;
|
|
24868
|
+
flattened.push(...flattenedBranch);
|
|
24869
|
+
}
|
|
24870
|
+
continue;
|
|
24871
|
+
}
|
|
24872
|
+
return null;
|
|
24873
|
+
}
|
|
24874
|
+
return flattened;
|
|
24875
|
+
};
|
|
24529
24876
|
const noDerivedStateEffect = defineRule({
|
|
24530
24877
|
id: "no-derived-state-effect",
|
|
24531
24878
|
title: "Derived state stored in an effect",
|
|
@@ -24557,8 +24904,8 @@ const noDerivedStateEffect = defineRule({
|
|
|
24557
24904
|
}
|
|
24558
24905
|
}
|
|
24559
24906
|
if (sawAnyDep && allDepsAreInitialOnly) return;
|
|
24560
|
-
const statements = getCallbackStatements(callback);
|
|
24561
|
-
if (statements.length === 0) return;
|
|
24907
|
+
const statements = flattenGuardedStatements(getCallbackStatements(callback));
|
|
24908
|
+
if (statements === null || statements.length === 0) return;
|
|
24562
24909
|
if (!statements.every((statement) => {
|
|
24563
24910
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24564
24911
|
const expression = statement.expression;
|
|
@@ -25193,7 +25540,7 @@ const noDidMountSetState = defineRule({
|
|
|
25193
25540
|
const { mode } = resolveSettings$20(context.settings);
|
|
25194
25541
|
return { CallExpression(node) {
|
|
25195
25542
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25196
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25543
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25197
25544
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25198
25545
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25199
25546
|
if (isMountFlagArgument(node.arguments?.[0])) return;
|
|
@@ -25318,7 +25665,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
25318
25665
|
const { mode } = resolveSettings$19(context.settings);
|
|
25319
25666
|
return { CallExpression(node) {
|
|
25320
25667
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25321
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25668
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25322
25669
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25323
25670
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25324
25671
|
if (isInsideDiffGuard(node)) return;
|
|
@@ -25415,18 +25762,45 @@ const collectUseStateBindings = (componentBody) => {
|
|
|
25415
25762
|
//#region src/plugin/rules/state-and-effects/no-direct-state-mutation.ts
|
|
25416
25763
|
const PLAIN_DATA_PRODUCER_GLOBAL_NAMES = new Set(["Array", "structuredClone"]);
|
|
25417
25764
|
const PLAIN_DATA_ARRAY_STATIC_METHODS = new Set(["from", "of"]);
|
|
25765
|
+
const PLAIN_DATA_JSON_STATIC_METHODS = new Set(["parse"]);
|
|
25766
|
+
const PLAIN_DATA_OBJECT_STATIC_METHODS = new Set([
|
|
25767
|
+
"assign",
|
|
25768
|
+
"entries",
|
|
25769
|
+
"fromEntries",
|
|
25770
|
+
"keys",
|
|
25771
|
+
"values"
|
|
25772
|
+
]);
|
|
25773
|
+
const ARRAY_COPY_METHOD_NAMES = new Set([
|
|
25774
|
+
"map",
|
|
25775
|
+
"filter",
|
|
25776
|
+
"slice",
|
|
25777
|
+
"concat",
|
|
25778
|
+
"flat",
|
|
25779
|
+
"flatMap",
|
|
25780
|
+
"toSorted",
|
|
25781
|
+
"toReversed",
|
|
25782
|
+
"toSpliced",
|
|
25783
|
+
"with"
|
|
25784
|
+
]);
|
|
25418
25785
|
const isNullOrUndefinedExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
25419
25786
|
const isPlainDataProducerCall = (expression) => {
|
|
25420
25787
|
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25421
25788
|
const callee = expression.callee;
|
|
25422
25789
|
if (isNodeOfType(callee, "Identifier")) return PLAIN_DATA_PRODUCER_GLOBAL_NAMES.has(callee.name);
|
|
25423
25790
|
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier")) return false;
|
|
25424
|
-
if (isNodeOfType(callee.object, "Identifier")
|
|
25791
|
+
if (isNodeOfType(callee.object, "Identifier")) {
|
|
25792
|
+
if (callee.object.name === "Array") return PLAIN_DATA_ARRAY_STATIC_METHODS.has(callee.property.name);
|
|
25793
|
+
if (callee.object.name === "JSON") return PLAIN_DATA_JSON_STATIC_METHODS.has(callee.property.name);
|
|
25794
|
+
if (callee.object.name === "Object") return PLAIN_DATA_OBJECT_STATIC_METHODS.has(callee.property.name);
|
|
25795
|
+
}
|
|
25425
25796
|
return producesPlainStateValue(callee.object);
|
|
25426
25797
|
};
|
|
25798
|
+
const PLAIN_DATA_CONSTRUCTOR_NAMES = new Set(["Array", "Object"]);
|
|
25799
|
+
const isPlainDataNewExpression = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && PLAIN_DATA_CONSTRUCTOR_NAMES.has(expression.callee.name);
|
|
25427
25800
|
const producesPlainStateValue = (expression) => {
|
|
25428
25801
|
const unwrapped = stripParenExpression(expression);
|
|
25429
25802
|
if (isNodeOfType(unwrapped, "ObjectExpression") || isNodeOfType(unwrapped, "ArrayExpression")) return true;
|
|
25803
|
+
if (isPlainDataNewExpression(unwrapped)) return true;
|
|
25430
25804
|
if (isNullOrUndefinedExpression(unwrapped)) return true;
|
|
25431
25805
|
if (isNodeOfType(unwrapped, "MemberExpression") && getRootIdentifierName(unwrapped) === "props") return true;
|
|
25432
25806
|
return isPlainDataProducerCall(unwrapped);
|
|
@@ -25441,6 +25815,38 @@ const initializerMarksPlainState = (initializerArgument) => {
|
|
|
25441
25815
|
}
|
|
25442
25816
|
return producesPlainStateValue(unwrapped);
|
|
25443
25817
|
};
|
|
25818
|
+
const producesOpaqueInstanceValue = (expression) => {
|
|
25819
|
+
if (isNodeOfType(expression, "NewExpression")) return !isPlainDataNewExpression(expression);
|
|
25820
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
25821
|
+
const callee = expression.callee;
|
|
25822
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
25823
|
+
if (isPlainDataProducerCall(expression)) return false;
|
|
25824
|
+
if (!callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_COPY_METHOD_NAMES.has(callee.property.name)) return false;
|
|
25825
|
+
return true;
|
|
25826
|
+
};
|
|
25827
|
+
const collectSetterValueObservations = (componentBody, setterNames) => {
|
|
25828
|
+
const plainFedSetterNames = /* @__PURE__ */ new Set();
|
|
25829
|
+
const opaqueFedSetterNames = /* @__PURE__ */ new Set();
|
|
25830
|
+
walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
|
|
25831
|
+
if (!isNodeOfType(node, "CallExpression")) return;
|
|
25832
|
+
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
25833
|
+
const setterName = node.callee.name;
|
|
25834
|
+
if (!setterNames.has(setterName) || currentlyShadowed.has(setterName)) return;
|
|
25835
|
+
const argument = node.arguments?.[0];
|
|
25836
|
+
if (!argument) return;
|
|
25837
|
+
const unwrapped = stripParenExpression(argument);
|
|
25838
|
+
if (isNullOrUndefinedExpression(unwrapped)) return;
|
|
25839
|
+
if (producesPlainStateValue(unwrapped)) {
|
|
25840
|
+
plainFedSetterNames.add(setterName);
|
|
25841
|
+
return;
|
|
25842
|
+
}
|
|
25843
|
+
if (producesOpaqueInstanceValue(unwrapped)) opaqueFedSetterNames.add(setterName);
|
|
25844
|
+
}, true);
|
|
25845
|
+
return {
|
|
25846
|
+
plainFedSetterNames,
|
|
25847
|
+
opaqueFedSetterNames
|
|
25848
|
+
};
|
|
25849
|
+
};
|
|
25444
25850
|
const collectCallbackRefSetterNames = (componentBody) => {
|
|
25445
25851
|
const callbackRefSetterNames = /* @__PURE__ */ new Set();
|
|
25446
25852
|
walkAst(componentBody, (node) => {
|
|
@@ -25510,11 +25916,15 @@ const noDirectStateMutation = defineRule({
|
|
|
25510
25916
|
if (bindings.length === 0) return;
|
|
25511
25917
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
25512
25918
|
const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
|
|
25919
|
+
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
25513
25920
|
const plainObjectStateValueNames = /* @__PURE__ */ new Set();
|
|
25514
25921
|
for (const binding of bindings) {
|
|
25515
25922
|
if (callbackRefSetterNames.has(binding.setterName)) continue;
|
|
25516
25923
|
if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
|
|
25517
|
-
|
|
25924
|
+
const initializerArgument = binding.declarator.init.arguments?.[0];
|
|
25925
|
+
if (!initializerMarksPlainState(initializerArgument)) continue;
|
|
25926
|
+
if ((!initializerArgument || isNullOrUndefinedExpression(stripParenExpression(initializerArgument))) && setterValueObservations.opaqueFedSetterNames.has(binding.setterName) && !setterValueObservations.plainFedSetterNames.has(binding.setterName)) continue;
|
|
25927
|
+
plainObjectStateValueNames.add(binding.valueName);
|
|
25518
25928
|
}
|
|
25519
25929
|
const visitMutationCandidate = (child, currentlyShadowed) => {
|
|
25520
25930
|
if (isNodeOfType(child, "AssignmentExpression")) {
|
|
@@ -25639,9 +26049,10 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
25639
26049
|
create: (context) => ({ CallExpression(node) {
|
|
25640
26050
|
const callee = node.callee;
|
|
25641
26051
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
25642
|
-
|
|
26052
|
+
const receiver = stripParenExpression(callee.object);
|
|
26053
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25643
26054
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
25644
|
-
if (context.scopes.symbolFor(
|
|
26055
|
+
if (context.scopes.symbolFor(receiver) !== null) return;
|
|
25645
26056
|
if (!importsReactViewTransition(node)) return;
|
|
25646
26057
|
context.report({
|
|
25647
26058
|
node,
|
|
@@ -25661,7 +26072,8 @@ const noDocumentWrite = defineRule({
|
|
|
25661
26072
|
create: (context) => ({ CallExpression(node) {
|
|
25662
26073
|
const callee = node.callee;
|
|
25663
26074
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
25664
|
-
|
|
26075
|
+
const receiver = stripParenExpression(callee.object);
|
|
26076
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25665
26077
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
25666
26078
|
context.report({
|
|
25667
26079
|
node,
|
|
@@ -26293,13 +26705,6 @@ const noEffectEventHandler = defineRule({
|
|
|
26293
26705
|
}
|
|
26294
26706
|
});
|
|
26295
26707
|
//#endregion
|
|
26296
|
-
//#region src/plugin/utils/is-imported-from-non-react-module.ts
|
|
26297
|
-
const isImportedFromNonReactModule = (contextNode, localIdentifierName) => {
|
|
26298
|
-
const importSource = getImportSourceForName(contextNode, localIdentifierName);
|
|
26299
|
-
if (importSource === null) return false;
|
|
26300
|
-
return !REACT_RUNTIME_MODULE_SOURCES.has(importSource);
|
|
26301
|
-
};
|
|
26302
|
-
//#endregion
|
|
26303
26708
|
//#region src/plugin/rules/state-and-effects/no-effect-event-in-deps.ts
|
|
26304
26709
|
const createComponentBindingStackTracker = (callbacks) => {
|
|
26305
26710
|
const componentBindingStack = [];
|
|
@@ -26353,11 +26758,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
26353
26758
|
const initializer = declaratorNode.init;
|
|
26354
26759
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26355
26760
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26356
|
-
if (
|
|
26357
|
-
if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
|
|
26358
|
-
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26359
|
-
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26360
|
-
}
|
|
26761
|
+
if (isNonReactEffectEventCallee(initializer.callee, declaratorNode, context.scopes)) return;
|
|
26361
26762
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26362
26763
|
} });
|
|
26363
26764
|
return {
|
|
@@ -28937,7 +29338,7 @@ const noIsMounted = defineRule({
|
|
|
28937
29338
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
28938
29339
|
create: (context) => ({ CallExpression(node) {
|
|
28939
29340
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
28940
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
29341
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
28941
29342
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
|
|
28942
29343
|
if (!getParentComponent(node)) return;
|
|
28943
29344
|
context.report({
|
|
@@ -28952,7 +29353,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
|
|
|
28952
29353
|
const isJsonMethodCall = (node, method) => {
|
|
28953
29354
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
28954
29355
|
const callee = node.callee;
|
|
28955
|
-
|
|
29356
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
29357
|
+
const receiver = stripParenExpression(callee.object);
|
|
29358
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
28956
29359
|
};
|
|
28957
29360
|
const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
|
|
28958
29361
|
const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
|
|
@@ -29037,7 +29440,7 @@ const extractReturnTypeAnnotation = (returnType) => {
|
|
|
29037
29440
|
const noJsxElementType = defineRule({
|
|
29038
29441
|
id: "no-jsx-element-type",
|
|
29039
29442
|
title: "No JSX.Element",
|
|
29040
|
-
severity: "
|
|
29443
|
+
severity: "warn",
|
|
29041
29444
|
recommendation: "Replace `JSX.Element` with `React.ReactNode`. `JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return.",
|
|
29042
29445
|
create: (context) => {
|
|
29043
29446
|
let isJsxImported = false;
|
|
@@ -29363,6 +29766,378 @@ const noLegacyContextApi = defineRule({
|
|
|
29363
29766
|
}
|
|
29364
29767
|
});
|
|
29365
29768
|
//#endregion
|
|
29769
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
29770
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
29771
|
+
"map",
|
|
29772
|
+
"filter",
|
|
29773
|
+
"forEach",
|
|
29774
|
+
"flatMap",
|
|
29775
|
+
"reduce",
|
|
29776
|
+
"reduceRight",
|
|
29777
|
+
"some",
|
|
29778
|
+
"every",
|
|
29779
|
+
"find",
|
|
29780
|
+
"findIndex",
|
|
29781
|
+
"findLast",
|
|
29782
|
+
"findLastIndex",
|
|
29783
|
+
"sort",
|
|
29784
|
+
"toSorted"
|
|
29785
|
+
]);
|
|
29786
|
+
const executesDuringRender = (functionNode) => {
|
|
29787
|
+
const parent = functionNode.parent;
|
|
29788
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
29789
|
+
if (parent.callee === functionNode) return true;
|
|
29790
|
+
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
29791
|
+
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;
|
|
29792
|
+
};
|
|
29793
|
+
//#endregion
|
|
29794
|
+
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
29795
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
29796
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
29797
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
29798
|
+
return false;
|
|
29799
|
+
};
|
|
29800
|
+
//#endregion
|
|
29801
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
29802
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
29803
|
+
let ancestor = bindingIdentifier.parent;
|
|
29804
|
+
while (ancestor) {
|
|
29805
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
29806
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
29807
|
+
ancestor = ancestor.parent ?? null;
|
|
29808
|
+
}
|
|
29809
|
+
return null;
|
|
29810
|
+
};
|
|
29811
|
+
//#endregion
|
|
29812
|
+
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
29813
|
+
const isFalsyLiteral = (node) => {
|
|
29814
|
+
if (!node) return true;
|
|
29815
|
+
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
29816
|
+
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
29817
|
+
};
|
|
29818
|
+
const isFalsyInitialStateBinding = (identifier) => {
|
|
29819
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
29820
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
29821
|
+
if (!binding) return false;
|
|
29822
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29823
|
+
if (!declarator?.init) return false;
|
|
29824
|
+
const init = stripParenExpression(declarator.init);
|
|
29825
|
+
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
29826
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
29827
|
+
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
29828
|
+
return isFalsyLiteral(init.arguments?.[0]);
|
|
29829
|
+
};
|
|
29830
|
+
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
29831
|
+
//#endregion
|
|
29832
|
+
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
29833
|
+
const isGatedByFalsyInitialState = (node) => {
|
|
29834
|
+
let cursor = node;
|
|
29835
|
+
let parent = node.parent;
|
|
29836
|
+
while (parent) {
|
|
29837
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
29838
|
+
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29839
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29840
|
+
cursor = parent;
|
|
29841
|
+
parent = parent.parent ?? null;
|
|
29842
|
+
}
|
|
29843
|
+
return false;
|
|
29844
|
+
};
|
|
29845
|
+
//#endregion
|
|
29846
|
+
//#region src/plugin/utils/references-client-only-flag.ts
|
|
29847
|
+
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
29848
|
+
const referencesClientOnlyFlag = (expression) => {
|
|
29849
|
+
const unwrapped = stripParenExpression(expression);
|
|
29850
|
+
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
29851
|
+
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
29852
|
+
const property = unwrapped.property;
|
|
29853
|
+
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
29854
|
+
}
|
|
29855
|
+
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
29856
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
29857
|
+
return false;
|
|
29858
|
+
};
|
|
29859
|
+
//#endregion
|
|
29860
|
+
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
29861
|
+
const isInsideClientOnlyGuard = (node) => {
|
|
29862
|
+
let cursor = node;
|
|
29863
|
+
let parent = node.parent;
|
|
29864
|
+
while (parent) {
|
|
29865
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
29866
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
29867
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
29868
|
+
cursor = parent;
|
|
29869
|
+
parent = parent.parent ?? null;
|
|
29870
|
+
}
|
|
29871
|
+
return false;
|
|
29872
|
+
};
|
|
29873
|
+
//#endregion
|
|
29874
|
+
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
29875
|
+
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
29876
|
+
"toLocaleString",
|
|
29877
|
+
"toLocaleDateString",
|
|
29878
|
+
"toLocaleTimeString"
|
|
29879
|
+
]);
|
|
29880
|
+
const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
|
|
29881
|
+
const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
|
|
29882
|
+
const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
29883
|
+
"format",
|
|
29884
|
+
"formatToParts",
|
|
29885
|
+
"formatRange"
|
|
29886
|
+
]);
|
|
29887
|
+
const isProvableDateExpression = (expression) => {
|
|
29888
|
+
if (!expression) return false;
|
|
29889
|
+
const unwrapped = stripParenExpression(expression);
|
|
29890
|
+
return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
|
|
29891
|
+
};
|
|
29892
|
+
const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
|
|
29893
|
+
const receiverNameLooksDateFlavored = (expression) => {
|
|
29894
|
+
if (!expression) return false;
|
|
29895
|
+
const unwrapped = stripParenExpression(expression);
|
|
29896
|
+
if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
|
|
29897
|
+
if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
|
|
29898
|
+
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
29899
|
+
return false;
|
|
29900
|
+
};
|
|
29901
|
+
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
29902
|
+
if (!objectExpression) return false;
|
|
29903
|
+
const unwrapped = stripParenExpression(objectExpression);
|
|
29904
|
+
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
29905
|
+
for (const property of unwrapped.properties ?? []) {
|
|
29906
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
29907
|
+
if (property.computed) continue;
|
|
29908
|
+
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
29909
|
+
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
29910
|
+
}
|
|
29911
|
+
return false;
|
|
29912
|
+
};
|
|
29913
|
+
const hasExplicitLocaleArgument = (argument) => {
|
|
29914
|
+
if (!argument) return false;
|
|
29915
|
+
const unwrapped = stripParenExpression(argument);
|
|
29916
|
+
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
29917
|
+
return true;
|
|
29918
|
+
};
|
|
29919
|
+
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
29920
|
+
const localeArgument = call.arguments?.[0];
|
|
29921
|
+
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
29922
|
+
const optionsArgument = call.arguments?.[1];
|
|
29923
|
+
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
29924
|
+
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
29925
|
+
};
|
|
29926
|
+
const matchLocaleMethodCall = (call) => {
|
|
29927
|
+
const callee = call.callee;
|
|
29928
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29929
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29930
|
+
const methodName = callee.property.name;
|
|
29931
|
+
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
29932
|
+
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
29933
|
+
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
29934
|
+
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
29935
|
+
return {
|
|
29936
|
+
node: call,
|
|
29937
|
+
display: `${methodName}()`
|
|
29938
|
+
};
|
|
29939
|
+
};
|
|
29940
|
+
const getIntlFormatterName = (expression) => {
|
|
29941
|
+
if (!expression) return null;
|
|
29942
|
+
const unwrapped = stripParenExpression(expression);
|
|
29943
|
+
if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
|
|
29944
|
+
const callee = unwrapped.callee;
|
|
29945
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29946
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
|
|
29947
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29948
|
+
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
29949
|
+
};
|
|
29950
|
+
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
29951
|
+
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
29952
|
+
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
29953
|
+
if (formatterName !== "DateTimeFormat") return true;
|
|
29954
|
+
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
29955
|
+
};
|
|
29956
|
+
const matchIntlFormatCall = (call) => {
|
|
29957
|
+
const callee = call.callee;
|
|
29958
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29959
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29960
|
+
if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
|
|
29961
|
+
let construction = stripParenExpression(callee.object);
|
|
29962
|
+
if (isNodeOfType(construction, "Identifier")) {
|
|
29963
|
+
const binding = findVariableInitializer(construction, construction.name);
|
|
29964
|
+
construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
|
|
29965
|
+
}
|
|
29966
|
+
if (!construction) return null;
|
|
29967
|
+
const formatterName = getIntlFormatterName(construction);
|
|
29968
|
+
if (!formatterName) return null;
|
|
29969
|
+
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
29970
|
+
return {
|
|
29971
|
+
node: call,
|
|
29972
|
+
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
29973
|
+
};
|
|
29974
|
+
};
|
|
29975
|
+
const isDeterministicInputDateConstruction = (expression) => {
|
|
29976
|
+
if (!expression) return false;
|
|
29977
|
+
const unwrapped = stripParenExpression(expression);
|
|
29978
|
+
if (!isNodeOfType(unwrapped, "NewExpression")) return false;
|
|
29979
|
+
if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
|
|
29980
|
+
return (unwrapped.arguments?.length ?? 0) > 0;
|
|
29981
|
+
};
|
|
29982
|
+
const matchDateDefaultStringification = (node) => {
|
|
29983
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
29984
|
+
const callee = node.callee;
|
|
29985
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
|
|
29986
|
+
node,
|
|
29987
|
+
display: "Date.prototype.toString()"
|
|
29988
|
+
};
|
|
29989
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
|
|
29990
|
+
node,
|
|
29991
|
+
display: "String(new Date(…))"
|
|
29992
|
+
};
|
|
29993
|
+
return null;
|
|
29994
|
+
}
|
|
29995
|
+
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
29996
|
+
for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
|
|
29997
|
+
node: expression,
|
|
29998
|
+
display: "`${new Date(…)}`"
|
|
29999
|
+
};
|
|
30000
|
+
}
|
|
30001
|
+
return null;
|
|
30002
|
+
};
|
|
30003
|
+
const findRenderPhaseComponentOrHook = (node) => {
|
|
30004
|
+
let functionNode = findEnclosingFunction(node);
|
|
30005
|
+
while (functionNode) {
|
|
30006
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
30007
|
+
if (!executesDuringRender(functionNode)) return null;
|
|
30008
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
30009
|
+
}
|
|
30010
|
+
return null;
|
|
30011
|
+
};
|
|
30012
|
+
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
30013
|
+
if (fileHasUseClientDirective) return true;
|
|
30014
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
30015
|
+
if (displayName && isReactHookName(displayName)) return true;
|
|
30016
|
+
let callsHook = false;
|
|
30017
|
+
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
30018
|
+
if (callsHook) return false;
|
|
30019
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
30020
|
+
callsHook = true;
|
|
30021
|
+
return false;
|
|
30022
|
+
}
|
|
30023
|
+
});
|
|
30024
|
+
return callsHook;
|
|
30025
|
+
};
|
|
30026
|
+
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
30027
|
+
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
30028
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
30029
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
30030
|
+
let cursor = node;
|
|
30031
|
+
while (cursor) {
|
|
30032
|
+
ancestors.add(cursor);
|
|
30033
|
+
cursor = cursor.parent ?? null;
|
|
30034
|
+
}
|
|
30035
|
+
for (const statement of body.body ?? []) {
|
|
30036
|
+
if (ancestors.has(statement)) return false;
|
|
30037
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
30038
|
+
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
30039
|
+
let returnsEarly = false;
|
|
30040
|
+
walkAst(statement.consequent, (child) => {
|
|
30041
|
+
if (isFunctionLike$1(child)) return false;
|
|
30042
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
30043
|
+
returnsEarly = true;
|
|
30044
|
+
return false;
|
|
30045
|
+
}
|
|
30046
|
+
});
|
|
30047
|
+
if (returnsEarly) return true;
|
|
30048
|
+
}
|
|
30049
|
+
return false;
|
|
30050
|
+
};
|
|
30051
|
+
const findEnclosingJsxOpeningElement = (node) => {
|
|
30052
|
+
let cursor = node.parent;
|
|
30053
|
+
while (cursor) {
|
|
30054
|
+
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
30055
|
+
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
30056
|
+
cursor = cursor.parent ?? null;
|
|
30057
|
+
}
|
|
30058
|
+
return null;
|
|
30059
|
+
};
|
|
30060
|
+
const noLocaleFormatInRender = defineRule({
|
|
30061
|
+
id: "no-locale-format-in-render",
|
|
30062
|
+
title: "Locale/timezone formatting during render",
|
|
30063
|
+
severity: "warn",
|
|
30064
|
+
category: "Correctness",
|
|
30065
|
+
disabledWhen: [
|
|
30066
|
+
"vite",
|
|
30067
|
+
"cra",
|
|
30068
|
+
"expo",
|
|
30069
|
+
"react-native",
|
|
30070
|
+
"unknown"
|
|
30071
|
+
],
|
|
30072
|
+
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.",
|
|
30073
|
+
create: (context) => {
|
|
30074
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
30075
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
30076
|
+
let fileHasUseClientDirective = false;
|
|
30077
|
+
let fileIsEmailTemplate = false;
|
|
30078
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30079
|
+
const reportIfRenderPhase = (match) => {
|
|
30080
|
+
if (reportedNodes.has(match.node)) return;
|
|
30081
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
30082
|
+
if (!componentOrHookNode) return;
|
|
30083
|
+
if (fileIsEmailTemplate) return;
|
|
30084
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30085
|
+
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30086
|
+
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30087
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
30088
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30089
|
+
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30090
|
+
reportedNodes.add(match.node);
|
|
30091
|
+
context.report({
|
|
30092
|
+
node: match.node,
|
|
30093
|
+
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.`
|
|
30094
|
+
});
|
|
30095
|
+
};
|
|
30096
|
+
return {
|
|
30097
|
+
Program(node) {
|
|
30098
|
+
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30099
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30100
|
+
},
|
|
30101
|
+
CallExpression(node) {
|
|
30102
|
+
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
30103
|
+
if (match) reportIfRenderPhase(match);
|
|
30104
|
+
},
|
|
30105
|
+
TemplateLiteral(node) {
|
|
30106
|
+
const match = matchDateDefaultStringification(node);
|
|
30107
|
+
if (match) reportIfRenderPhase(match);
|
|
30108
|
+
},
|
|
30109
|
+
JSXExpressionContainer(node) {
|
|
30110
|
+
const expression = stripParenExpression(node.expression);
|
|
30111
|
+
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30112
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30113
|
+
const helperName = expression.callee.name;
|
|
30114
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
30115
|
+
if (!componentOrHookNode) return;
|
|
30116
|
+
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30117
|
+
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
30118
|
+
if (componentOrHookDisplayNameForFunction(helperNode)) return;
|
|
30119
|
+
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
30120
|
+
if (isFunctionLike$1(child)) return false;
|
|
30121
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30122
|
+
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
30123
|
+
if (!match || reportedNodes.has(match.node)) return;
|
|
30124
|
+
if (fileIsEmailTemplate) return;
|
|
30125
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30126
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
30127
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
30128
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
30129
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30130
|
+
reportedNodes.add(match.node);
|
|
30131
|
+
context.report({
|
|
30132
|
+
node: match.node,
|
|
30133
|
+
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.`
|
|
30134
|
+
});
|
|
30135
|
+
});
|
|
30136
|
+
}
|
|
30137
|
+
};
|
|
30138
|
+
}
|
|
30139
|
+
});
|
|
30140
|
+
//#endregion
|
|
29366
30141
|
//#region src/plugin/rules/design/no-long-transition-duration.ts
|
|
29367
30142
|
const hasInfiniteIterationCount = (properties) => properties.some((property) => {
|
|
29368
30143
|
if (getStylePropertyKey(property) !== "animationIterationCount") return false;
|
|
@@ -30079,40 +30854,6 @@ const noMutableInDeps = defineRule({
|
|
|
30079
30854
|
}
|
|
30080
30855
|
});
|
|
30081
30856
|
//#endregion
|
|
30082
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
30083
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
30084
|
-
let node = callExpression;
|
|
30085
|
-
let parent = node.parent;
|
|
30086
|
-
while (parent) {
|
|
30087
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
30088
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
30089
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
30090
|
-
node = parent;
|
|
30091
|
-
parent = node.parent;
|
|
30092
|
-
continue;
|
|
30093
|
-
}
|
|
30094
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
30095
|
-
node = parent;
|
|
30096
|
-
parent = node.parent;
|
|
30097
|
-
continue;
|
|
30098
|
-
}
|
|
30099
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
30100
|
-
node = parent;
|
|
30101
|
-
parent = node.parent;
|
|
30102
|
-
continue;
|
|
30103
|
-
}
|
|
30104
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
30105
|
-
const expressions = parent.expressions ?? [];
|
|
30106
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
30107
|
-
node = parent;
|
|
30108
|
-
parent = node.parent;
|
|
30109
|
-
continue;
|
|
30110
|
-
}
|
|
30111
|
-
return false;
|
|
30112
|
-
}
|
|
30113
|
-
return false;
|
|
30114
|
-
};
|
|
30115
|
-
//#endregion
|
|
30116
30857
|
//#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
|
|
30117
30858
|
const LODASH_MUTATOR_NAMES = new Set([
|
|
30118
30859
|
"set",
|
|
@@ -30554,7 +31295,7 @@ const noNestedComponentDefinition = defineRule({
|
|
|
30554
31295
|
id: "no-nested-component-definition",
|
|
30555
31296
|
title: "Component defined inside another component",
|
|
30556
31297
|
tags: ["test-noise", "react-jsx-only"],
|
|
30557
|
-
severity: "
|
|
31298
|
+
severity: "warn",
|
|
30558
31299
|
category: "Correctness",
|
|
30559
31300
|
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.",
|
|
30560
31301
|
create: (context) => {
|
|
@@ -31509,12 +32250,12 @@ const noPassDataToParent = defineRule({
|
|
|
31509
32250
|
if (calleeNode === identifier) {
|
|
31510
32251
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
31511
32252
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
31512
|
-
} else if (isNodeOfType(calleeNode, "MemberExpression") &&
|
|
32253
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
31513
32254
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
31514
32255
|
if (isCustomHookParameter(ref)) continue;
|
|
31515
32256
|
} else continue;
|
|
31516
32257
|
const methodName = getCallMethodName(calleeNode);
|
|
31517
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32258
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31518
32259
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31519
32260
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
31520
32261
|
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
@@ -31705,7 +32446,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
31705
32446
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
31706
32447
|
const calleeNode = callExpr.callee;
|
|
31707
32448
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
31708
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32449
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31709
32450
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31710
32451
|
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
31711
32452
|
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
@@ -32239,7 +32980,7 @@ const isAlwaysFreshExpression = (expression) => {
|
|
|
32239
32980
|
return `${callee.name}()`;
|
|
32240
32981
|
}
|
|
32241
32982
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
32242
|
-
const receiver = callee.object;
|
|
32983
|
+
const receiver = stripParenExpression(callee.object);
|
|
32243
32984
|
const property = callee.property;
|
|
32244
32985
|
if (!isNodeOfType(property, "Identifier")) return null;
|
|
32245
32986
|
if (isNodeOfType(receiver, "Identifier")) {
|
|
@@ -32360,8 +33101,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32360
33101
|
if (typeof sourceValue !== "string") return;
|
|
32361
33102
|
if (handleExtraSource?.(node, context)) return;
|
|
32362
33103
|
if (sourceValue !== source) return;
|
|
33104
|
+
if (isTypeOnlyImport(node)) return;
|
|
32363
33105
|
for (const specifier of node.specifiers ?? []) {
|
|
32364
33106
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
33107
|
+
if (specifier.importKind === "type") continue;
|
|
32365
33108
|
const importedName = getImportedName$1(specifier);
|
|
32366
33109
|
if (!importedName) continue;
|
|
32367
33110
|
const message = messages.get(importedName);
|
|
@@ -32380,8 +33123,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32380
33123
|
MemberExpression(node) {
|
|
32381
33124
|
if (namespaceBindings.size === 0) return;
|
|
32382
33125
|
if (node.computed) return;
|
|
32383
|
-
|
|
32384
|
-
if (!
|
|
33126
|
+
const receiver = stripParenExpression(node.object);
|
|
33127
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
33128
|
+
if (!namespaceBindings.has(receiver.name)) return;
|
|
32385
33129
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
32386
33130
|
const message = messages.get(node.property.name);
|
|
32387
33131
|
if (message) context.report({
|
|
@@ -32446,7 +33190,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
32446
33190
|
});
|
|
32447
33191
|
//#endregion
|
|
32448
33192
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
32449
|
-
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]
|
|
33193
|
+
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."]]);
|
|
32450
33194
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
32451
33195
|
if (!rawFilename) return false;
|
|
32452
33196
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -32484,7 +33228,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
32484
33228
|
requires: ["react:19"],
|
|
32485
33229
|
tags: ["test-noise", "migration-hint"],
|
|
32486
33230
|
severity: "warn",
|
|
32487
|
-
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19.
|
|
33231
|
+
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
|
|
32488
33232
|
create: (context) => {
|
|
32489
33233
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
32490
33234
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -32808,34 +33552,11 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
32808
33552
|
});
|
|
32809
33553
|
//#endregion
|
|
32810
33554
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
32811
|
-
const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32812
|
-
if (!symbol || visitedSymbols.has(symbol)) return false;
|
|
32813
|
-
visitedSymbols.add(symbol);
|
|
32814
|
-
if (isComponentParameterSymbol(symbol)) return true;
|
|
32815
|
-
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
32816
|
-
const source = symbol.initializer;
|
|
32817
|
-
if (!source) return false;
|
|
32818
|
-
return initializerRootsInProps(source, scopes, visitedSymbols);
|
|
32819
|
-
};
|
|
32820
|
-
const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32821
|
-
if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
|
|
32822
|
-
if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
|
|
32823
|
-
return rootsInProps(node, scopes, visitedSymbols);
|
|
32824
|
-
};
|
|
32825
|
-
const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
32826
|
-
let current = node;
|
|
32827
|
-
while (isNodeOfType(current, "MemberExpression")) {
|
|
32828
|
-
if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
|
|
32829
|
-
current = current.object;
|
|
32830
|
-
}
|
|
32831
|
-
if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
|
|
32832
|
-
return false;
|
|
32833
|
-
};
|
|
32834
33555
|
const isInsideComponentContext = (node) => {
|
|
32835
33556
|
let cursor = node.parent;
|
|
32836
33557
|
while (cursor) {
|
|
32837
|
-
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
32838
33558
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33559
|
+
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
32839
33560
|
cursor = cursor.parent ?? null;
|
|
32840
33561
|
}
|
|
32841
33562
|
return false;
|
|
@@ -32845,24 +33566,28 @@ const functionBodyOf = (node) => {
|
|
|
32845
33566
|
if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
|
|
32846
33567
|
return null;
|
|
32847
33568
|
};
|
|
33569
|
+
const isHookCallee = (callee) => {
|
|
33570
|
+
if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
|
|
33571
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
|
|
33572
|
+
return false;
|
|
33573
|
+
};
|
|
32848
33574
|
const containsHookCall = (body) => {
|
|
32849
33575
|
let found = false;
|
|
32850
33576
|
walkAst(body, (child) => {
|
|
32851
|
-
if (found) return;
|
|
33577
|
+
if (found) return false;
|
|
33578
|
+
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
32852
33579
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32853
|
-
|
|
32854
|
-
if (name && isReactHookName(name)) found = true;
|
|
33580
|
+
if (isHookCallee(child.callee)) found = true;
|
|
32855
33581
|
});
|
|
32856
33582
|
return found;
|
|
32857
33583
|
};
|
|
32858
|
-
const
|
|
33584
|
+
const isHookCallingRenderHelper = (symbol) => {
|
|
32859
33585
|
if (!symbol) return false;
|
|
32860
33586
|
const declaration = symbol.declarationNode;
|
|
32861
33587
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
32862
33588
|
const body = functionBodyOf(declaration);
|
|
32863
33589
|
if (!body) return false;
|
|
32864
|
-
|
|
32865
|
-
return !containsHookCall(body);
|
|
33590
|
+
return containsHookCall(body);
|
|
32866
33591
|
};
|
|
32867
33592
|
const noRenderInRender = defineRule({
|
|
32868
33593
|
id: "no-render-in-render",
|
|
@@ -32871,20 +33596,13 @@ const noRenderInRender = defineRule({
|
|
|
32871
33596
|
tags: ["test-noise"],
|
|
32872
33597
|
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
32873
33598
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
32874
|
-
const expression = node.expression;
|
|
33599
|
+
const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
|
|
32875
33600
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
32876
|
-
|
|
32877
|
-
|
|
32878
|
-
|
|
32879
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33601
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
33602
|
+
const calleeName = expression.callee.name;
|
|
33603
|
+
if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
32880
33604
|
if (!isInsideComponentContext(node)) return;
|
|
32881
|
-
if (
|
|
32882
|
-
const calleeSymbol = context.scopes.symbolFor(expression.callee);
|
|
32883
|
-
if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
|
|
32884
|
-
if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
|
|
32885
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
32886
|
-
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
32887
|
-
}
|
|
33605
|
+
if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
|
|
32888
33606
|
context.report({
|
|
32889
33607
|
node: expression,
|
|
32890
33608
|
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.`
|
|
@@ -32945,13 +33663,7 @@ const noRenderPropChildren = defineRule({
|
|
|
32945
33663
|
//#endregion
|
|
32946
33664
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
32947
33665
|
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
32948
|
-
const isReactDomRenderCall = (node) =>
|
|
32949
|
-
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
32950
|
-
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
32951
|
-
if (node.callee.object.name !== "ReactDOM") return false;
|
|
32952
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
32953
|
-
return node.callee.property.name === "render";
|
|
32954
|
-
};
|
|
33666
|
+
const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
|
|
32955
33667
|
const isUsedAsReturnValue = (parent) => {
|
|
32956
33668
|
if (!parent) return false;
|
|
32957
33669
|
if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
|
|
@@ -33787,7 +34499,7 @@ const noSetState = defineRule({
|
|
|
33787
34499
|
category: "Architecture",
|
|
33788
34500
|
create: (context) => ({ CallExpression(node) {
|
|
33789
34501
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
33790
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
34502
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
33791
34503
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
33792
34504
|
if (!getParentComponent(node)) return;
|
|
33793
34505
|
context.report({
|
|
@@ -33953,6 +34665,204 @@ const noSideTabBorder = defineRule({
|
|
|
33953
34665
|
})
|
|
33954
34666
|
});
|
|
33955
34667
|
//#endregion
|
|
34668
|
+
//#region src/plugin/rules/state-and-effects/no-stale-timer-ref.ts
|
|
34669
|
+
const GLOBAL_TIMER_RECEIVER_NAMES = new Set([
|
|
34670
|
+
"window",
|
|
34671
|
+
"globalThis",
|
|
34672
|
+
"self"
|
|
34673
|
+
]);
|
|
34674
|
+
const NULLISH_COMPARISON_OPERATORS = new Set([
|
|
34675
|
+
"==",
|
|
34676
|
+
"===",
|
|
34677
|
+
"!=",
|
|
34678
|
+
"!=="
|
|
34679
|
+
]);
|
|
34680
|
+
const getGlobalTimerCalleeName = (node) => {
|
|
34681
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
34682
|
+
const callee = stripParenExpression(node.callee);
|
|
34683
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
34684
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) {
|
|
34685
|
+
const receiver = stripParenExpression(callee.object);
|
|
34686
|
+
if (isNodeOfType(receiver, "Identifier") && GLOBAL_TIMER_RECEIVER_NAMES.has(receiver.name)) return callee.property.name;
|
|
34687
|
+
}
|
|
34688
|
+
return null;
|
|
34689
|
+
};
|
|
34690
|
+
const getRefCurrentReceiverName = (node) => {
|
|
34691
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed) return null;
|
|
34692
|
+
if (!isNodeOfType(node.property, "Identifier") || node.property.name !== "current") return null;
|
|
34693
|
+
const receiver = stripParenExpression(node.object);
|
|
34694
|
+
return isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
34695
|
+
};
|
|
34696
|
+
const isRefCurrentMemberOf = (node, refName) => getRefCurrentReceiverName(node) === refName;
|
|
34697
|
+
const parseClearCallOnTimerRef = (node) => {
|
|
34698
|
+
const clearCalleeName = getGlobalTimerCalleeName(node);
|
|
34699
|
+
if (clearCalleeName === null || !TIMER_CLEANUP_CALLEE_NAMES.has(clearCalleeName)) return null;
|
|
34700
|
+
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
34701
|
+
const clearedArgument = node.arguments?.[0];
|
|
34702
|
+
if (!clearedArgument) return null;
|
|
34703
|
+
const refName = getRefCurrentReceiverName(stripParenExpression(clearedArgument));
|
|
34704
|
+
return refName === null ? null : {
|
|
34705
|
+
clearCalleeName,
|
|
34706
|
+
refName
|
|
34707
|
+
};
|
|
34708
|
+
};
|
|
34709
|
+
const isClearCallOnRef = (node, refName) => parseClearCallOnTimerRef(node)?.refName === refName;
|
|
34710
|
+
const isNullishResetExpression = (node) => isNullishExpression(stripParenExpression(node));
|
|
34711
|
+
const isAssignmentToRefCurrent = (node, refName) => isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isRefCurrentMemberOf(node.left, refName);
|
|
34712
|
+
const isClearGuardIfIdiom = (ifStatement, refName) => {
|
|
34713
|
+
if (ifStatement.alternate) return false;
|
|
34714
|
+
const consequent = ifStatement.consequent;
|
|
34715
|
+
return (isNodeOfType(consequent, "BlockStatement") ? consequent.body ?? [] : [consequent]).every((statement) => {
|
|
34716
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
34717
|
+
const expression = stripParenExpression(statement.expression);
|
|
34718
|
+
if (isClearCallOnRef(expression, refName)) return true;
|
|
34719
|
+
return isAssignmentToRefCurrent(expression, refName) && isNullishResetExpression(expression.right);
|
|
34720
|
+
});
|
|
34721
|
+
};
|
|
34722
|
+
const climbBooleanProjection = (refCurrentMember) => {
|
|
34723
|
+
let cursor = refCurrentMember;
|
|
34724
|
+
const logicalAncestors = [];
|
|
34725
|
+
let didPassBooleanProjection = false;
|
|
34726
|
+
while (cursor.parent) {
|
|
34727
|
+
const parent = cursor.parent;
|
|
34728
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type)) {
|
|
34729
|
+
cursor = parent;
|
|
34730
|
+
continue;
|
|
34731
|
+
}
|
|
34732
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "!") {
|
|
34733
|
+
didPassBooleanProjection = true;
|
|
34734
|
+
cursor = parent;
|
|
34735
|
+
continue;
|
|
34736
|
+
}
|
|
34737
|
+
if (isNodeOfType(parent, "BinaryExpression") && NULLISH_COMPARISON_OPERATORS.has(parent.operator) && isNullishResetExpression(parent.left === cursor ? parent.right : parent.left)) {
|
|
34738
|
+
didPassBooleanProjection = true;
|
|
34739
|
+
cursor = parent;
|
|
34740
|
+
continue;
|
|
34741
|
+
}
|
|
34742
|
+
if (isNodeOfType(parent, "LogicalExpression")) {
|
|
34743
|
+
logicalAncestors.push(parent);
|
|
34744
|
+
cursor = parent;
|
|
34745
|
+
continue;
|
|
34746
|
+
}
|
|
34747
|
+
break;
|
|
34748
|
+
}
|
|
34749
|
+
return {
|
|
34750
|
+
conditionRoot: cursor,
|
|
34751
|
+
logicalAncestors,
|
|
34752
|
+
didPassBooleanProjection
|
|
34753
|
+
};
|
|
34754
|
+
};
|
|
34755
|
+
const isLogicalClearGuardIdiom = (logicalAncestors, refName) => logicalAncestors.some((logical) => logical.operator === "&&" && isClearCallOnRef(stripParenExpression(logical.right), refName));
|
|
34756
|
+
const isPendingSignalRead = (refCurrentMember, refName) => {
|
|
34757
|
+
const { conditionRoot, logicalAncestors, didPassBooleanProjection } = climbBooleanProjection(refCurrentMember);
|
|
34758
|
+
const conditionParent = conditionRoot.parent;
|
|
34759
|
+
if (isNodeOfType(conditionParent, "IfStatement") && conditionParent.test === conditionRoot) return !isClearGuardIfIdiom(conditionParent, refName);
|
|
34760
|
+
if ((isNodeOfType(conditionParent, "ConditionalExpression") || isNodeOfType(conditionParent, "WhileStatement") || isNodeOfType(conditionParent, "DoWhileStatement") || isNodeOfType(conditionParent, "ForStatement")) && conditionParent.test === conditionRoot) return true;
|
|
34761
|
+
if (logicalAncestors.length > 0) return !isLogicalClearGuardIdiom(logicalAncestors, refName);
|
|
34762
|
+
return didPassBooleanProjection;
|
|
34763
|
+
};
|
|
34764
|
+
const collectTimerRefUsageFacts = (ownerScope, refName) => {
|
|
34765
|
+
const facts = {
|
|
34766
|
+
holdsScheduledTimerId: false,
|
|
34767
|
+
hasPendingSignalRead: false
|
|
34768
|
+
};
|
|
34769
|
+
walkAst(ownerScope, (child) => {
|
|
34770
|
+
if (isAssignmentToRefCurrent(child, refName)) {
|
|
34771
|
+
const assignedCalleeName = getGlobalTimerCalleeName(stripParenExpression(child.right));
|
|
34772
|
+
if (assignedCalleeName !== null && TIMER_CALLEE_NAMES_REQUIRING_CLEANUP.has(assignedCalleeName)) facts.holdsScheduledTimerId = true;
|
|
34773
|
+
return;
|
|
34774
|
+
}
|
|
34775
|
+
if (isRefCurrentMemberOf(child, refName) && isPendingSignalRead(child, refName)) facts.hasPendingSignalRead = true;
|
|
34776
|
+
});
|
|
34777
|
+
return facts;
|
|
34778
|
+
};
|
|
34779
|
+
const isEffectCallbackFunction = (functionNode) => {
|
|
34780
|
+
const parent = functionNode.parent;
|
|
34781
|
+
if (!parent || !isNodeOfType(parent, "CallExpression")) return false;
|
|
34782
|
+
return isHookCall$2(parent, EFFECT_HOOK_NAMES$1) && getEffectCallback(parent) === functionNode;
|
|
34783
|
+
};
|
|
34784
|
+
const doesEffectCallbackReturnName = (effectCallback, name) => {
|
|
34785
|
+
if (!isFunctionLike$1(effectCallback)) return false;
|
|
34786
|
+
let isReturnedByName = false;
|
|
34787
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
34788
|
+
if (isReturnedByName || !isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
34789
|
+
const returned = stripParenExpression(child.argument);
|
|
34790
|
+
if (isNodeOfType(returned, "Identifier") && returned.name === name) isReturnedByName = true;
|
|
34791
|
+
});
|
|
34792
|
+
return isReturnedByName;
|
|
34793
|
+
};
|
|
34794
|
+
const isFunctionReturnedFromEffectCallback = (functionNode, effectCallback) => {
|
|
34795
|
+
if (!isFunctionLike$1(effectCallback)) return false;
|
|
34796
|
+
if (isNodeOfType(functionNode.parent, "ReturnStatement")) return true;
|
|
34797
|
+
if (effectCallback.body === functionNode) return true;
|
|
34798
|
+
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
34799
|
+
return cleanupBindingName !== null && doesEffectCallbackReturnName(effectCallback, cleanupBindingName);
|
|
34800
|
+
};
|
|
34801
|
+
const isReturnedFromAnyEffectInScope = (functionNode, ownerScope) => {
|
|
34802
|
+
const cleanupBindingName = getFunctionBindingName$1(functionNode);
|
|
34803
|
+
if (cleanupBindingName === null) return false;
|
|
34804
|
+
let isReturnedFromEffect = false;
|
|
34805
|
+
walkAst(ownerScope, (child) => {
|
|
34806
|
+
if (isReturnedFromEffect) return false;
|
|
34807
|
+
if (!isNodeOfType(child, "CallExpression") || !isHookCall$2(child, EFFECT_HOOK_NAMES$1)) return;
|
|
34808
|
+
const effectCallback = getEffectCallback(child);
|
|
34809
|
+
if (effectCallback && doesEffectCallbackReturnName(effectCallback, cleanupBindingName)) isReturnedFromEffect = true;
|
|
34810
|
+
});
|
|
34811
|
+
return isReturnedFromEffect;
|
|
34812
|
+
};
|
|
34813
|
+
const isInsideEffectCleanupReturn = (node, ownerScope) => {
|
|
34814
|
+
let functionNode = findEnclosingFunction(node);
|
|
34815
|
+
while (functionNode) {
|
|
34816
|
+
const outerFunction = findEnclosingFunction(functionNode);
|
|
34817
|
+
if (outerFunction && isEffectCallbackFunction(outerFunction) && isFunctionReturnedFromEffectCallback(functionNode, outerFunction)) return true;
|
|
34818
|
+
if (isReturnedFromAnyEffectInScope(functionNode, ownerScope)) return true;
|
|
34819
|
+
functionNode = outerFunction;
|
|
34820
|
+
}
|
|
34821
|
+
return false;
|
|
34822
|
+
};
|
|
34823
|
+
const hasRefCurrentReassignmentAfterClear = (clearCall, refName) => {
|
|
34824
|
+
const enclosingFunction = findEnclosingFunction(clearCall);
|
|
34825
|
+
if (!isFunctionLike$1(enclosingFunction)) return false;
|
|
34826
|
+
if (!isNodeOfType(enclosingFunction.body, "BlockStatement")) return false;
|
|
34827
|
+
const clearStart = getRangeStart(clearCall);
|
|
34828
|
+
if (clearStart === null) return false;
|
|
34829
|
+
let didFindLaterReassignment = false;
|
|
34830
|
+
walkInsideStatementBlocks(enclosingFunction.body, (child) => {
|
|
34831
|
+
if (didFindLaterReassignment) return;
|
|
34832
|
+
if (!isAssignmentToRefCurrent(child, refName)) return;
|
|
34833
|
+
const assignmentStart = getRangeStart(child);
|
|
34834
|
+
if (assignmentStart !== null && assignmentStart > clearStart) didFindLaterReassignment = true;
|
|
34835
|
+
});
|
|
34836
|
+
return didFindLaterReassignment;
|
|
34837
|
+
};
|
|
34838
|
+
const isShadowedTimerGlobal = (clearCall) => {
|
|
34839
|
+
const callee = stripParenExpression(clearCall.callee);
|
|
34840
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
34841
|
+
return findVariableInitializer(callee, callee.name) !== null;
|
|
34842
|
+
};
|
|
34843
|
+
const noStaleTimerRef = defineRule({
|
|
34844
|
+
id: "no-stale-timer-ref",
|
|
34845
|
+
title: "Cleared timer ref keeps the stale id",
|
|
34846
|
+
severity: "warn",
|
|
34847
|
+
recommendation: "Reset the ref right after clearing (`clearTimeout(ref.current); ref.current = null`) so truthiness checks on the ref keep meaning “timer still pending”.",
|
|
34848
|
+
create: (context) => ({ CallExpression(node) {
|
|
34849
|
+
const clearCall = parseClearCallOnTimerRef(node);
|
|
34850
|
+
if (!clearCall) return;
|
|
34851
|
+
if (isShadowedTimerGlobal(node)) return;
|
|
34852
|
+
const { clearCalleeName, refName } = clearCall;
|
|
34853
|
+
const refBinding = findVariableInitializer(node, refName);
|
|
34854
|
+
if (!refBinding?.initializer || !isHookCall$2(refBinding.initializer, "useRef")) return;
|
|
34855
|
+
const usageFacts = collectTimerRefUsageFacts(refBinding.scopeOwner, refName);
|
|
34856
|
+
if (!usageFacts.holdsScheduledTimerId || !usageFacts.hasPendingSignalRead) return;
|
|
34857
|
+
if (isInsideEffectCleanupReturn(node, refBinding.scopeOwner)) return;
|
|
34858
|
+
if (hasRefCurrentReassignmentAfterClear(node, refName)) return;
|
|
34859
|
+
context.report({
|
|
34860
|
+
node,
|
|
34861
|
+
message: `\`${clearCalleeName}(${refName}.current)\` cancels the timer but leaves the old id in \`${refName}.current\`, and this component reads \`${refName}.current\` as a \u201Ctimer pending\u201D signal — assign \`${refName}.current = null\` right after clearing so a cancelled timer does not look pending.`
|
|
34862
|
+
});
|
|
34863
|
+
} })
|
|
34864
|
+
});
|
|
34865
|
+
//#endregion
|
|
33956
34866
|
//#region src/plugin/utils/is-abstract-role.ts
|
|
33957
34867
|
const isAbstractRole = (openingElement, settings) => {
|
|
33958
34868
|
const elementType = getElementType(openingElement, settings);
|
|
@@ -36233,6 +37143,36 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36233
37143
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36234
37144
|
return true;
|
|
36235
37145
|
};
|
|
37146
|
+
const isTrivialContainerLiteral = (node) => {
|
|
37147
|
+
if (!node) return false;
|
|
37148
|
+
const innerExpression = stripParenExpression(node);
|
|
37149
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
37150
|
+
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
37151
|
+
return false;
|
|
37152
|
+
};
|
|
37153
|
+
const isNonEscapingRead = (identifier) => {
|
|
37154
|
+
const readRoot = findTransparentExpressionRoot(identifier);
|
|
37155
|
+
const memberNode = readRoot.parent;
|
|
37156
|
+
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
|
|
37157
|
+
const memberUse = findTransparentExpressionRoot(memberNode);
|
|
37158
|
+
const memberUseParent = memberUse.parent;
|
|
37159
|
+
if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
|
|
37160
|
+
if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
|
|
37161
|
+
if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
|
|
37162
|
+
return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
|
|
37163
|
+
};
|
|
37164
|
+
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
37165
|
+
const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
|
|
37166
|
+
const parentNode = memoUsageRoot.parent;
|
|
37167
|
+
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
37168
|
+
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
|
|
37169
|
+
const bindingTarget = parentNode.id;
|
|
37170
|
+
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
37171
|
+
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
37172
|
+
const symbol = scopes.symbolFor(bindingTarget);
|
|
37173
|
+
if (!symbol) return false;
|
|
37174
|
+
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
37175
|
+
};
|
|
36236
37176
|
const noUsememoSimpleExpression = defineRule({
|
|
36237
37177
|
id: "no-usememo-simple-expression",
|
|
36238
37178
|
title: "useMemo on a cheap value",
|
|
@@ -36254,9 +37194,17 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36254
37194
|
let returnExpression = null;
|
|
36255
37195
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36256
37196
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36257
|
-
if (returnExpression
|
|
37197
|
+
if (!returnExpression) return;
|
|
37198
|
+
if (isTriviallyCheapExpression(returnExpression)) {
|
|
37199
|
+
context.report({
|
|
37200
|
+
node,
|
|
37201
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
37202
|
+
});
|
|
37203
|
+
return;
|
|
37204
|
+
}
|
|
37205
|
+
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36258
37206
|
node,
|
|
36259
|
-
message: "This
|
|
37207
|
+
message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
|
|
36260
37208
|
});
|
|
36261
37209
|
} })
|
|
36262
37210
|
});
|
|
@@ -36362,7 +37310,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
36362
37310
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
36363
37311
|
return { CallExpression(node) {
|
|
36364
37312
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
36365
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
37313
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36366
37314
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36367
37315
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36368
37316
|
context.report({
|
|
@@ -36679,6 +37627,10 @@ const isRouteFactoryCall = (expression) => {
|
|
|
36679
37627
|
}
|
|
36680
37628
|
return false;
|
|
36681
37629
|
};
|
|
37630
|
+
const isConfigOnlyFactoryCall = (call) => call.arguments.length > 0 && call.arguments.every((argument) => {
|
|
37631
|
+
const expression = skipTsExpression(argument);
|
|
37632
|
+
return isNodeOfType(expression, "ObjectExpression") || isNodeOfType(expression, "Literal") || isNodeOfType(expression, "TemplateLiteral");
|
|
37633
|
+
});
|
|
36682
37634
|
const isReactHocName = (name, state) => state.customHocs.has(name);
|
|
36683
37635
|
const isHocCallee = (callee, state) => {
|
|
36684
37636
|
if (isNodeOfType(callee, "Identifier")) return isReactHocName(callee.name, state);
|
|
@@ -36718,7 +37670,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
|
36718
37670
|
continue;
|
|
36719
37671
|
}
|
|
36720
37672
|
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
36721
|
-
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
37673
|
+
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
|
|
36722
37674
|
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
36723
37675
|
}
|
|
36724
37676
|
return false;
|
|
@@ -36826,18 +37778,22 @@ const onlyExportComponents = defineRule({
|
|
|
36826
37778
|
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36827
37779
|
allowExportNames: new Set(settings.allowExportNames),
|
|
36828
37780
|
allowConstantExport: settings.allowConstantExport,
|
|
36829
|
-
localComponentNames
|
|
37781
|
+
localComponentNames,
|
|
37782
|
+
scopes: context.scopes
|
|
36830
37783
|
};
|
|
36831
37784
|
for (const child of componentCandidates) {
|
|
36832
37785
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36833
|
-
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37786
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
|
|
36834
37787
|
}
|
|
36835
37788
|
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
36836
37789
|
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36837
37790
|
}
|
|
36838
37791
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36839
37792
|
const initializer = child.init;
|
|
36840
|
-
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child))
|
|
37793
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
|
|
37794
|
+
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
37795
|
+
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
37796
|
+
}
|
|
36841
37797
|
}
|
|
36842
37798
|
}
|
|
36843
37799
|
const exports = [];
|
|
@@ -36907,6 +37863,10 @@ const onlyExportComponents = defineRule({
|
|
|
36907
37863
|
return false;
|
|
36908
37864
|
})();
|
|
36909
37865
|
if (isHoc && firstArgIsValid) hasReactExport = true;
|
|
37866
|
+
else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
|
|
37867
|
+
kind: "non-component",
|
|
37868
|
+
reportNode: stripped
|
|
37869
|
+
});
|
|
36910
37870
|
else context.report({
|
|
36911
37871
|
node: stripped,
|
|
36912
37872
|
message: ANONYMOUS_MESSAGE
|
|
@@ -38502,11 +39462,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
|
|
|
38502
39462
|
}
|
|
38503
39463
|
return null;
|
|
38504
39464
|
};
|
|
39465
|
+
const isTrivialLiteralExpression = (expression) => {
|
|
39466
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
39467
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
39468
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
|
|
39469
|
+
if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
|
|
39470
|
+
return false;
|
|
39471
|
+
};
|
|
38505
39472
|
const getSingleSetterCallFromHandler = (handler) => {
|
|
38506
39473
|
const handlerStatements = getCallbackStatements(handler);
|
|
38507
39474
|
if (handlerStatements.length !== 1) return null;
|
|
38508
39475
|
const onlyStatement = handlerStatements[0];
|
|
38509
|
-
|
|
39476
|
+
let expression = onlyStatement;
|
|
39477
|
+
if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
|
|
39478
|
+
if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
|
|
39479
|
+
if (!expression) return null;
|
|
39480
|
+
expression = stripParenExpression(expression);
|
|
38510
39481
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
38511
39482
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
38512
39483
|
if (!isSetterIdentifier(expression.callee.name)) return null;
|
|
@@ -38516,6 +39487,106 @@ const getSingleSetterCallFromHandler = (handler) => {
|
|
|
38516
39487
|
setterArgument: expression.arguments[0]
|
|
38517
39488
|
};
|
|
38518
39489
|
};
|
|
39490
|
+
const isListenerCollectionInitializer = (init) => {
|
|
39491
|
+
if (!init) return false;
|
|
39492
|
+
if (isNodeOfType(init, "ArrayExpression")) return true;
|
|
39493
|
+
return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
|
|
39494
|
+
};
|
|
39495
|
+
const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
|
|
39496
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
39497
|
+
const firstParam = functionNode.params?.[0];
|
|
39498
|
+
if (!isNodeOfType(firstParam, "Identifier")) return false;
|
|
39499
|
+
const listenerParamName = firstParam.name;
|
|
39500
|
+
let registersListener = false;
|
|
39501
|
+
walkAst(functionNode.body, (child) => {
|
|
39502
|
+
if (registersListener) return false;
|
|
39503
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39504
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
39505
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
39506
|
+
if (!listenerCollectionNames.has(child.callee.object.name)) return;
|
|
39507
|
+
if (!isNodeOfType(child.callee.property, "Identifier")) return;
|
|
39508
|
+
if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
|
|
39509
|
+
const registeredArgument = child.arguments?.[0];
|
|
39510
|
+
if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
|
|
39511
|
+
});
|
|
39512
|
+
return registersListener;
|
|
39513
|
+
};
|
|
39514
|
+
const buildModuleScopeStoreIndex = (programRoot) => {
|
|
39515
|
+
const mutableBindingNames = /* @__PURE__ */ new Set();
|
|
39516
|
+
const listenerCollectionNames = /* @__PURE__ */ new Set();
|
|
39517
|
+
const moduleFunctionsByName = /* @__PURE__ */ new Map();
|
|
39518
|
+
if (!isNodeOfType(programRoot, "Program")) return {
|
|
39519
|
+
mutableBindingNames,
|
|
39520
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39521
|
+
};
|
|
39522
|
+
for (const statement of programRoot.body ?? []) {
|
|
39523
|
+
const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
|
|
39524
|
+
if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
|
|
39525
|
+
moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
|
|
39526
|
+
continue;
|
|
39527
|
+
}
|
|
39528
|
+
if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
|
|
39529
|
+
for (const declarator of unwrapped.declarations ?? []) {
|
|
39530
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
39531
|
+
const init = declarator.init ?? null;
|
|
39532
|
+
if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
|
|
39533
|
+
mutableBindingNames.add(declarator.id.name);
|
|
39534
|
+
continue;
|
|
39535
|
+
}
|
|
39536
|
+
if (isListenerCollectionInitializer(init)) {
|
|
39537
|
+
listenerCollectionNames.add(declarator.id.name);
|
|
39538
|
+
continue;
|
|
39539
|
+
}
|
|
39540
|
+
if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
|
|
39541
|
+
}
|
|
39542
|
+
}
|
|
39543
|
+
const subscribeFunctionNames = /* @__PURE__ */ new Set();
|
|
39544
|
+
for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
|
|
39545
|
+
return {
|
|
39546
|
+
mutableBindingNames,
|
|
39547
|
+
subscribeFunctionNames
|
|
39548
|
+
};
|
|
39549
|
+
};
|
|
39550
|
+
const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
|
|
39551
|
+
if (!isNodeOfType(useStateCall, "CallExpression")) return null;
|
|
39552
|
+
let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
|
|
39553
|
+
if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
|
|
39554
|
+
if (!isNodeOfType(initialArgument, "Identifier")) return null;
|
|
39555
|
+
if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
|
|
39556
|
+
const binding = findVariableInitializer(initialArgument, initialArgument.name);
|
|
39557
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
|
|
39558
|
+
return initialArgument.name;
|
|
39559
|
+
};
|
|
39560
|
+
const argumentForwardsSetter = (argument, setterName) => {
|
|
39561
|
+
if (!argument) return false;
|
|
39562
|
+
const unwrapped = stripParenExpression(argument);
|
|
39563
|
+
if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
|
|
39564
|
+
if (!isFunctionLike$1(unwrapped)) return false;
|
|
39565
|
+
let callsSetter = false;
|
|
39566
|
+
walkAst(unwrapped.body, (child) => {
|
|
39567
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
|
|
39568
|
+
callsSetter = true;
|
|
39569
|
+
return false;
|
|
39570
|
+
}
|
|
39571
|
+
});
|
|
39572
|
+
return callsSetter;
|
|
39573
|
+
};
|
|
39574
|
+
const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
|
|
39575
|
+
let matchedCall = null;
|
|
39576
|
+
walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
|
|
39577
|
+
if (matchedCall) return false;
|
|
39578
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39579
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
39580
|
+
if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
|
|
39581
|
+
const binding = findVariableInitializer(child.callee, child.callee.name);
|
|
39582
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
|
|
39583
|
+
for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
|
|
39584
|
+
matchedCall = child;
|
|
39585
|
+
return false;
|
|
39586
|
+
}
|
|
39587
|
+
});
|
|
39588
|
+
return matchedCall;
|
|
39589
|
+
};
|
|
38519
39590
|
const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
|
|
38520
39591
|
const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
|
|
38521
39592
|
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
@@ -38532,6 +39603,16 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38532
39603
|
severity: "warn",
|
|
38533
39604
|
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.",
|
|
38534
39605
|
create: (context) => {
|
|
39606
|
+
let cachedStoreIndex = null;
|
|
39607
|
+
const storeIndexFor = (node) => {
|
|
39608
|
+
if (cachedStoreIndex) return cachedStoreIndex;
|
|
39609
|
+
const programRoot = findProgramRoot(node);
|
|
39610
|
+
cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
|
|
39611
|
+
mutableBindingNames: /* @__PURE__ */ new Set(),
|
|
39612
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39613
|
+
};
|
|
39614
|
+
return cachedStoreIndex;
|
|
39615
|
+
};
|
|
38535
39616
|
const checkComponent = (componentBody) => {
|
|
38536
39617
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38537
39618
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
@@ -38569,6 +39650,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38569
39650
|
const useStateInitializer = useStateInitializerByValueName.get(valueName);
|
|
38570
39651
|
if (!useStateInitializer) continue;
|
|
38571
39652
|
if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
|
|
39653
|
+
if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
|
|
38572
39654
|
if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
|
|
38573
39655
|
const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
|
|
38574
39656
|
context.report({
|
|
@@ -38576,14 +39658,47 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38576
39658
|
message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
|
|
38577
39659
|
});
|
|
38578
39660
|
}
|
|
39661
|
+
checkModuleStoreShape(componentBody, useStateBindings);
|
|
39662
|
+
};
|
|
39663
|
+
const checkModuleStoreShape = (componentBody, useStateBindings) => {
|
|
39664
|
+
const storeIndex = storeIndexFor(componentBody);
|
|
39665
|
+
if (storeIndex.mutableBindingNames.size === 0) return;
|
|
39666
|
+
if (storeIndex.subscribeFunctionNames.size === 0) return;
|
|
39667
|
+
const snapshotBindings = useStateBindings.map((binding) => ({
|
|
39668
|
+
binding,
|
|
39669
|
+
storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
|
|
39670
|
+
})).filter((candidate) => candidate.storeName !== null);
|
|
39671
|
+
if (snapshotBindings.length === 0) return;
|
|
39672
|
+
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
39673
|
+
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
39674
|
+
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
39675
|
+
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
39676
|
+
const depsNode = effectCall.arguments[1];
|
|
39677
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
39678
|
+
if ((depsNode.elements?.length ?? 0) !== 0) continue;
|
|
39679
|
+
const callback = getEffectCallback(effectCall);
|
|
39680
|
+
if (!callback || !isFunctionLike$1(callback)) continue;
|
|
39681
|
+
for (const { binding, storeName } of snapshotBindings) {
|
|
39682
|
+
if (reportedDeclarators.has(binding.declarator)) continue;
|
|
39683
|
+
if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
|
|
39684
|
+
reportedDeclarators.add(binding.declarator);
|
|
39685
|
+
context.report({
|
|
39686
|
+
node: binding.declarator,
|
|
39687
|
+
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.`
|
|
39688
|
+
});
|
|
39689
|
+
}
|
|
39690
|
+
}
|
|
38579
39691
|
};
|
|
38580
39692
|
return {
|
|
38581
39693
|
FunctionDeclaration(node) {
|
|
38582
|
-
|
|
39694
|
+
const functionName = node.id?.name;
|
|
39695
|
+
if (!functionName) return;
|
|
39696
|
+
if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
38583
39697
|
checkComponent(node.body);
|
|
38584
39698
|
},
|
|
38585
39699
|
VariableDeclarator(node) {
|
|
38586
|
-
|
|
39700
|
+
const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
|
|
39701
|
+
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
38587
39702
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
38588
39703
|
checkComponent(node.init.body);
|
|
38589
39704
|
}
|
|
@@ -39193,7 +40308,7 @@ const queryNoVoidQueryFn = defineRule({
|
|
|
39193
40308
|
if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
|
|
39194
40309
|
const body = queryFnValue.body;
|
|
39195
40310
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
39196
|
-
if ((body.body ?? []).length === 0) context.report({
|
|
40311
|
+
if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
|
|
39197
40312
|
node: queryFnProperty,
|
|
39198
40313
|
message: "This empty queryFn caches undefined, so the component never gets data."
|
|
39199
40314
|
});
|
|
@@ -39700,17 +40815,6 @@ const renderingHoistJsx = defineRule({
|
|
|
39700
40815
|
}
|
|
39701
40816
|
});
|
|
39702
40817
|
//#endregion
|
|
39703
|
-
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
39704
|
-
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
39705
|
-
let ancestor = bindingIdentifier.parent;
|
|
39706
|
-
while (ancestor) {
|
|
39707
|
-
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
39708
|
-
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
39709
|
-
ancestor = ancestor.parent ?? null;
|
|
39710
|
-
}
|
|
39711
|
-
return null;
|
|
39712
|
-
};
|
|
39713
|
-
//#endregion
|
|
39714
40818
|
//#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
|
|
39715
40819
|
const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
39716
40820
|
{
|
|
@@ -39719,19 +40823,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
|
39719
40823
|
},
|
|
39720
40824
|
{
|
|
39721
40825
|
display: "Date.now()",
|
|
39722
|
-
matches: (node) =>
|
|
40826
|
+
matches: (node) => isGlobalMethodCall(node, "Date", "now")
|
|
39723
40827
|
},
|
|
39724
40828
|
{
|
|
39725
40829
|
display: "Math.random()",
|
|
39726
|
-
matches: (node) =>
|
|
40830
|
+
matches: (node) => isGlobalMethodCall(node, "Math", "random")
|
|
39727
40831
|
},
|
|
39728
40832
|
{
|
|
39729
40833
|
display: "performance.now()",
|
|
39730
|
-
matches: (node) =>
|
|
40834
|
+
matches: (node) => isGlobalMethodCall(node, "performance", "now")
|
|
39731
40835
|
},
|
|
39732
40836
|
{
|
|
39733
40837
|
display: "crypto.randomUUID()",
|
|
39734
|
-
matches: (node) =>
|
|
40838
|
+
matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
|
|
39735
40839
|
}
|
|
39736
40840
|
];
|
|
39737
40841
|
const findOpeningElementOfChild = (jsxNode) => {
|
|
@@ -39743,54 +40847,6 @@ const findOpeningElementOfChild = (jsxNode) => {
|
|
|
39743
40847
|
}
|
|
39744
40848
|
return null;
|
|
39745
40849
|
};
|
|
39746
|
-
const executesDuringRender = (functionNode) => {
|
|
39747
|
-
const parent = functionNode.parent;
|
|
39748
|
-
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
39749
|
-
if (parent.callee === functionNode) return true;
|
|
39750
|
-
return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
|
|
39751
|
-
};
|
|
39752
|
-
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
39753
|
-
const referencesClientOnlyFlag = (expression) => {
|
|
39754
|
-
const unwrapped = stripParenExpression(expression);
|
|
39755
|
-
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
39756
|
-
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
39757
|
-
const property = unwrapped.property;
|
|
39758
|
-
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
39759
|
-
}
|
|
39760
|
-
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
39761
|
-
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
39762
|
-
return false;
|
|
39763
|
-
};
|
|
39764
|
-
const isFalsyLiteral = (node) => {
|
|
39765
|
-
if (!node) return true;
|
|
39766
|
-
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
39767
|
-
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
39768
|
-
};
|
|
39769
|
-
const isFalsyInitialStateBinding = (identifier) => {
|
|
39770
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
39771
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
39772
|
-
if (!binding) return false;
|
|
39773
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
39774
|
-
if (!declarator?.init) return false;
|
|
39775
|
-
const init = stripParenExpression(declarator.init);
|
|
39776
|
-
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
39777
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
39778
|
-
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
39779
|
-
return isFalsyLiteral(init.arguments?.[0]);
|
|
39780
|
-
};
|
|
39781
|
-
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
39782
|
-
const isGatedByFalsyInitialState = (node) => {
|
|
39783
|
-
let cursor = node;
|
|
39784
|
-
let parent = node.parent;
|
|
39785
|
-
while (parent) {
|
|
39786
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
39787
|
-
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39788
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39789
|
-
cursor = parent;
|
|
39790
|
-
parent = parent.parent ?? null;
|
|
39791
|
-
}
|
|
39792
|
-
return false;
|
|
39793
|
-
};
|
|
39794
40850
|
const isYearOnlyDateRead = (dateNode) => {
|
|
39795
40851
|
const member = dateNode.parent;
|
|
39796
40852
|
if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
|
|
@@ -39814,23 +40870,6 @@ const isInsideMotionTransitionAttribute = (node) => {
|
|
|
39814
40870
|
}
|
|
39815
40871
|
return false;
|
|
39816
40872
|
};
|
|
39817
|
-
const isInsideClientOnlyGuard = (node) => {
|
|
39818
|
-
let cursor = node;
|
|
39819
|
-
let parent = node.parent;
|
|
39820
|
-
while (parent) {
|
|
39821
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
39822
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
39823
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
39824
|
-
cursor = parent;
|
|
39825
|
-
parent = parent.parent ?? null;
|
|
39826
|
-
}
|
|
39827
|
-
return false;
|
|
39828
|
-
};
|
|
39829
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
39830
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
39831
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
39832
|
-
return false;
|
|
39833
|
-
};
|
|
39834
40873
|
const renderingHydrationMismatchTime = defineRule({
|
|
39835
40874
|
id: "rendering-hydration-mismatch-time",
|
|
39836
40875
|
title: "Time or random value in JSX",
|
|
@@ -39878,6 +40917,35 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
39878
40917
|
}
|
|
39879
40918
|
});
|
|
39880
40919
|
//#endregion
|
|
40920
|
+
//#region src/plugin/utils/contains-locale-environment-read.ts
|
|
40921
|
+
const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
|
|
40922
|
+
"toLocaleString",
|
|
40923
|
+
"toLocaleDateString",
|
|
40924
|
+
"toLocaleTimeString",
|
|
40925
|
+
"getTimezoneOffset"
|
|
40926
|
+
]);
|
|
40927
|
+
const containsLocaleEnvironmentRead = (expression) => {
|
|
40928
|
+
let readsLocaleEnvironment = false;
|
|
40929
|
+
walkAst(expression, (child) => {
|
|
40930
|
+
if (readsLocaleEnvironment) return false;
|
|
40931
|
+
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
|
|
40932
|
+
if (child.object.name === "Intl") {
|
|
40933
|
+
readsLocaleEnvironment = true;
|
|
40934
|
+
return false;
|
|
40935
|
+
}
|
|
40936
|
+
if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
|
|
40937
|
+
readsLocaleEnvironment = true;
|
|
40938
|
+
return false;
|
|
40939
|
+
}
|
|
40940
|
+
}
|
|
40941
|
+
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)) {
|
|
40942
|
+
readsLocaleEnvironment = true;
|
|
40943
|
+
return false;
|
|
40944
|
+
}
|
|
40945
|
+
});
|
|
40946
|
+
return readsLocaleEnvironment;
|
|
40947
|
+
};
|
|
40948
|
+
//#endregion
|
|
39881
40949
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
39882
40950
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
39883
40951
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -39943,14 +41011,15 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
39943
41011
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
39944
41012
|
const callback = getEffectCallback(node);
|
|
39945
41013
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
39946
|
-
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
39947
|
-
if (
|
|
41014
|
+
const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
|
|
41015
|
+
if (bodyStatements.length !== 1) return;
|
|
39948
41016
|
const soleStatement = bodyStatements[0];
|
|
39949
41017
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
39950
41018
|
const expression = soleStatement.expression;
|
|
39951
41019
|
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
|
|
39952
41020
|
if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
|
|
39953
41021
|
if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
|
|
41022
|
+
if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
|
|
39954
41023
|
context.report({
|
|
39955
41024
|
node,
|
|
39956
41025
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
@@ -40769,6 +41838,9 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
40769
41838
|
} })
|
|
40770
41839
|
});
|
|
40771
41840
|
//#endregion
|
|
41841
|
+
//#region src/plugin/utils/is-trivial-built-in-construction.ts
|
|
41842
|
+
const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
|
|
41843
|
+
//#endregion
|
|
40772
41844
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
40773
41845
|
const rerenderLazyRefInit = defineRule({
|
|
40774
41846
|
id: "rerender-lazy-ref-init",
|
|
@@ -40779,7 +41851,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40779
41851
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
40780
41852
|
create: (context) => ({ CallExpression(node) {
|
|
40781
41853
|
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
40782
|
-
const initializer = node.arguments[0];
|
|
41854
|
+
const initializer = stripParenExpression(node.arguments[0]);
|
|
40783
41855
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
40784
41856
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
40785
41857
|
if (!isPlainCall && !isNewCall) return;
|
|
@@ -40787,7 +41859,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40787
41859
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40788
41860
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40789
41861
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40790
|
-
if (
|
|
41862
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40791
41863
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40792
41864
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40793
41865
|
context.report({
|
|
@@ -40823,11 +41895,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
40823
41895
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
40824
41896
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
40825
41897
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
40826
|
-
|
|
40827
|
-
if (isNodeOfType(
|
|
40828
|
-
if (isNodeOfType(
|
|
40829
|
-
if (isNodeOfType(
|
|
40830
|
-
if (isNodeOfType(
|
|
41898
|
+
const innerExpression = stripParenExpression(expression);
|
|
41899
|
+
if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
|
|
41900
|
+
if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
|
|
41901
|
+
if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
|
|
41902
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
|
|
40831
41903
|
if (!element || !isNodeOfType(element, "SpreadElement")) continue;
|
|
40832
41904
|
const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
|
|
40833
41905
|
if (spreadCall) return spreadCall;
|
|
@@ -40850,7 +41922,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
40850
41922
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40851
41923
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40852
41924
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40853
|
-
if (
|
|
41925
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40854
41926
|
if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
|
|
40855
41927
|
if (isReactHookName(calleeName)) return;
|
|
40856
41928
|
const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
|
|
@@ -41517,7 +42589,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
41517
42589
|
const body = reactionFn.body;
|
|
41518
42590
|
let singleAssignment = null;
|
|
41519
42591
|
if (isNodeOfType(body, "BlockStatement")) {
|
|
41520
|
-
const statements = body.body ?? [];
|
|
42592
|
+
const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
|
|
41521
42593
|
if (statements.length !== 1) return;
|
|
41522
42594
|
const onlyStatement = statements[0];
|
|
41523
42595
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
@@ -41591,7 +42663,8 @@ const PROMISE_SETTLE_METHODS = new Set([
|
|
|
41591
42663
|
"catch",
|
|
41592
42664
|
"finally"
|
|
41593
42665
|
]);
|
|
41594
|
-
const findChainRoot = (
|
|
42666
|
+
const findChainRoot = (wrappedNode) => {
|
|
42667
|
+
const node = stripParenExpression(wrappedNode);
|
|
41595
42668
|
if (isNodeOfType(node, "CallExpression")) {
|
|
41596
42669
|
if (isNodeOfType(node.callee, "Identifier")) return {
|
|
41597
42670
|
calleeName: node.callee.name,
|
|
@@ -42179,20 +43252,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
|
|
|
42179
43252
|
};
|
|
42180
43253
|
const isReactNativeDimensionsCallee = (node, callee) => {
|
|
42181
43254
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
42182
|
-
|
|
42183
|
-
|
|
43255
|
+
const receiver = stripParenExpression(callee.object);
|
|
43256
|
+
if (isNodeOfType(receiver, "Identifier")) {
|
|
43257
|
+
const localName = receiver.name;
|
|
42184
43258
|
const binding = findVariableInitializer(node, localName);
|
|
42185
43259
|
if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
|
|
42186
43260
|
return localName === "Dimensions";
|
|
42187
43261
|
}
|
|
42188
|
-
if (isNodeOfType(
|
|
42189
|
-
if (getInitializerModuleSource(node,
|
|
42190
|
-
const rootName = getRootIdentifierName(
|
|
43262
|
+
if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
|
|
43263
|
+
if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
|
|
43264
|
+
const rootName = getRootIdentifierName(receiver.object);
|
|
42191
43265
|
if (rootName === null) return false;
|
|
42192
43266
|
const importBinding = getImportBindingForName(node, rootName);
|
|
42193
43267
|
return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
|
|
42194
43268
|
}
|
|
42195
|
-
if (getRequireCallSource(
|
|
43269
|
+
if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
|
|
42196
43270
|
return false;
|
|
42197
43271
|
};
|
|
42198
43272
|
const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
|
|
@@ -42636,7 +43710,8 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42636
43710
|
},
|
|
42637
43711
|
CallExpression(node) {
|
|
42638
43712
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
42639
|
-
|
|
43713
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
43714
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
|
|
42640
43715
|
if (!isMemberProperty(node.callee, "create")) return;
|
|
42641
43716
|
const stylesArgument = node.arguments?.[0];
|
|
42642
43717
|
if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
|
|
@@ -43486,7 +44561,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
43486
44561
|
const callee = node.callee;
|
|
43487
44562
|
if (!isStaticMemberNamed(callee, "setNativeProps")) return;
|
|
43488
44563
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
43489
|
-
if (!isStaticMemberNamed(callee.object, "current")) return;
|
|
44564
|
+
if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
|
|
43490
44565
|
context.report({
|
|
43491
44566
|
node,
|
|
43492
44567
|
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."
|
|
@@ -43714,14 +44789,15 @@ const analyzeGestureChain = (expression) => {
|
|
|
43714
44789
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
43715
44790
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
43716
44791
|
const methodName = callee.property.name;
|
|
43717
|
-
|
|
44792
|
+
const receiver = stripParenExpression(callee.object);
|
|
44793
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
|
|
43718
44794
|
factoryName: methodName,
|
|
43719
44795
|
chainMethodNames,
|
|
43720
44796
|
numberOfTapsArgument
|
|
43721
44797
|
};
|
|
43722
44798
|
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
43723
44799
|
chainMethodNames.push(methodName);
|
|
43724
|
-
cursor =
|
|
44800
|
+
cursor = receiver;
|
|
43725
44801
|
}
|
|
43726
44802
|
return null;
|
|
43727
44803
|
};
|
|
@@ -47387,6 +48463,8 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47387
48463
|
const propName = propRawName.toLowerCase();
|
|
47388
48464
|
if (!propName.startsWith("aria-")) continue;
|
|
47389
48465
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
48466
|
+
const attributeValue = attributeNode.value;
|
|
48467
|
+
if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
|
|
47390
48468
|
(ariaAttributes ??= []).push({
|
|
47391
48469
|
attribute,
|
|
47392
48470
|
propName
|
|
@@ -47756,21 +48834,6 @@ const isInsideLoop = (descendant, ancestor) => {
|
|
|
47756
48834
|
}
|
|
47757
48835
|
return false;
|
|
47758
48836
|
};
|
|
47759
|
-
const isUseEffectEventSymbol = (symbol) => {
|
|
47760
|
-
const initializer = symbol.initializer;
|
|
47761
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47762
|
-
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47763
|
-
};
|
|
47764
|
-
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
47765
|
-
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
47766
|
-
return Boolean(symbol && symbol.kind !== "import");
|
|
47767
|
-
};
|
|
47768
|
-
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
|
|
47769
|
-
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47770
|
-
const initializer = symbol.initializer;
|
|
47771
|
-
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47772
|
-
return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
|
|
47773
|
-
};
|
|
47774
48837
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
47775
48838
|
let current = node.parent;
|
|
47776
48839
|
while (current) {
|
|
@@ -47960,8 +49023,7 @@ const rulesOfHooks = defineRule({
|
|
|
47960
49023
|
},
|
|
47961
49024
|
Identifier(node) {
|
|
47962
49025
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
47963
|
-
if (!symbol || !
|
|
47964
|
-
if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
|
|
49026
|
+
if (!symbol || !symbolHasReactUseEffectEventOrigin(symbol, context.scopes)) return;
|
|
47965
49027
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
47966
49028
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
47967
49029
|
context.report({
|
|
@@ -48109,7 +49171,8 @@ const serverAfterNonblocking = defineRule({
|
|
|
48109
49171
|
if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
|
|
48110
49172
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
48111
49173
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
48112
|
-
const
|
|
49174
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
49175
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
48113
49176
|
if (!objectName) return;
|
|
48114
49177
|
const methodName = node.callee.property.name;
|
|
48115
49178
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
@@ -48785,7 +49848,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
|
|
|
48785
49848
|
};
|
|
48786
49849
|
const ascendMemberChain = (referenceIdentifier) => {
|
|
48787
49850
|
let chainTip = referenceIdentifier;
|
|
48788
|
-
while (chainTip.parent
|
|
49851
|
+
while (chainTip.parent) {
|
|
49852
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
|
|
49853
|
+
chainTip = chainTip.parent;
|
|
49854
|
+
continue;
|
|
49855
|
+
}
|
|
49856
|
+
if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
|
|
49857
|
+
chainTip = chainTip.parent;
|
|
49858
|
+
continue;
|
|
49859
|
+
}
|
|
49860
|
+
break;
|
|
49861
|
+
}
|
|
48789
49862
|
return chainTip;
|
|
48790
49863
|
};
|
|
48791
49864
|
const isDirectContentsMutation = (referenceIdentifier) => {
|
|
@@ -48810,7 +49883,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
|
|
|
48810
49883
|
const callee = callExpression.callee;
|
|
48811
49884
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
48812
49885
|
const methodName = getMemberPropertyName$1(callee);
|
|
48813
|
-
|
|
49886
|
+
const calleeReceiver = stripParenExpression(callee.object);
|
|
49887
|
+
return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
48814
49888
|
}
|
|
48815
49889
|
if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
|
|
48816
49890
|
const calleeSymbol = scopes.symbolFor(callee);
|
|
@@ -49540,7 +50614,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49540
50614
|
};
|
|
49541
50615
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
49542
50616
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
49543
|
-
let currentNode = outerNode.callee.object;
|
|
50617
|
+
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
49544
50618
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
49545
50619
|
const calleeName = getCalleeName$2(currentNode);
|
|
49546
50620
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
@@ -49551,7 +50625,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49551
50625
|
}
|
|
49552
50626
|
}
|
|
49553
50627
|
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
49554
|
-
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
50628
|
+
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
|
|
49555
50629
|
else break;
|
|
49556
50630
|
}
|
|
49557
50631
|
return result;
|
|
@@ -50204,7 +51278,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
50204
51278
|
while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
|
|
50205
51279
|
const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
|
|
50206
51280
|
if (methodName) methodNames.unshift(methodName);
|
|
50207
|
-
currentNode = currentNode.callee.object;
|
|
51281
|
+
currentNode = stripParenExpression(currentNode.callee.object);
|
|
50208
51282
|
}
|
|
50209
51283
|
if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
|
|
50210
51284
|
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
@@ -53286,6 +54360,18 @@ const reactDoctorRules = [
|
|
|
53286
54360
|
category: "Bugs"
|
|
53287
54361
|
}
|
|
53288
54362
|
},
|
|
54363
|
+
{
|
|
54364
|
+
key: "react-doctor/no-locale-format-in-render",
|
|
54365
|
+
id: "no-locale-format-in-render",
|
|
54366
|
+
source: "react-doctor",
|
|
54367
|
+
originallyExternal: false,
|
|
54368
|
+
rule: {
|
|
54369
|
+
...noLocaleFormatInRender,
|
|
54370
|
+
framework: "global",
|
|
54371
|
+
category: "Bugs",
|
|
54372
|
+
requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
|
|
54373
|
+
}
|
|
54374
|
+
},
|
|
53289
54375
|
{
|
|
53290
54376
|
key: "react-doctor/no-long-transition-duration",
|
|
53291
54377
|
id: "no-long-transition-duration",
|
|
@@ -53714,6 +54800,18 @@ const reactDoctorRules = [
|
|
|
53714
54800
|
category: "Maintainability"
|
|
53715
54801
|
}
|
|
53716
54802
|
},
|
|
54803
|
+
{
|
|
54804
|
+
key: "react-doctor/no-stale-timer-ref",
|
|
54805
|
+
id: "no-stale-timer-ref",
|
|
54806
|
+
source: "react-doctor",
|
|
54807
|
+
originallyExternal: false,
|
|
54808
|
+
rule: {
|
|
54809
|
+
...noStaleTimerRef,
|
|
54810
|
+
framework: "global",
|
|
54811
|
+
category: "Bugs",
|
|
54812
|
+
requires: [...new Set(["react", ...noStaleTimerRef.requires ?? []])]
|
|
54813
|
+
}
|
|
54814
|
+
},
|
|
53717
54815
|
{
|
|
53718
54816
|
key: "react-doctor/no-static-element-interactions",
|
|
53719
54817
|
id: "no-static-element-interactions",
|
|
@@ -56162,6 +57260,7 @@ const CROSS_FILE_RULE_IDS = new Set([
|
|
|
56162
57260
|
"nextjs-no-use-search-params-without-suspense",
|
|
56163
57261
|
"no-dynamic-import-path",
|
|
56164
57262
|
"no-full-lodash-import",
|
|
57263
|
+
"no-locale-format-in-render",
|
|
56165
57264
|
"no-mutating-reducer-state",
|
|
56166
57265
|
"prefer-dynamic-import",
|
|
56167
57266
|
"rendering-hydration-mismatch-time",
|
|
@@ -56296,6 +57395,7 @@ const CROSS_FILE_DEPENDENCY_COLLECTORS = new Map([
|
|
|
56296
57395
|
["nextjs-no-use-search-params-without-suspense", collectNextjsSearchParamsDependencies],
|
|
56297
57396
|
["no-dynamic-import-path", collectNearestManifestDependencies],
|
|
56298
57397
|
["no-full-lodash-import", collectNearestManifestDependencies],
|
|
57398
|
+
["no-locale-format-in-render", collectNearestManifestDependencies],
|
|
56299
57399
|
["no-mutating-reducer-state", collectMutatingReducerDependencies],
|
|
56300
57400
|
["prefer-dynamic-import", collectNearestManifestDependencies],
|
|
56301
57401
|
["rendering-hydration-mismatch-time", collectNearestManifestDependencies],
|