oxlint-plugin-react-doctor 0.7.2-dev.82e0475 → 0.7.2-dev.9b59d96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +46 -0
- package/dist/index.js +905 -279
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2277,6 +2277,7 @@ const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
|
|
|
2277
2277
|
if (remainingHops === 0) return null;
|
|
2278
2278
|
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
2279
2279
|
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
2280
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
2280
2281
|
return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
|
|
2281
2282
|
}
|
|
2282
2283
|
return null;
|
|
@@ -2316,7 +2317,8 @@ const isDirectChildOfLinkComponent = (openingElement) => {
|
|
|
2316
2317
|
const isKeyboardOperableWidgetAnchor = (openingElement) => Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "role")) && Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "tabindex")) && (Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeydown")) || Boolean(hasJsxPropIgnoreCase(openingElement.attributes, "onkeyup")));
|
|
2317
2318
|
const isInvalidHref = (value, validHrefs) => {
|
|
2318
2319
|
if (validHrefs.has(value)) return false;
|
|
2319
|
-
|
|
2320
|
+
const withoutLeadingNonWord = value.replace(/^[^a-zA-Z0-9_]+/, "");
|
|
2321
|
+
return value === "" || value === "#" || withoutLeadingNonWord.startsWith("javascript:");
|
|
2320
2322
|
};
|
|
2321
2323
|
const isNullishOrFragmentHref = (value) => {
|
|
2322
2324
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
@@ -5191,7 +5193,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
5191
5193
|
const callee = node.callee;
|
|
5192
5194
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
5193
5195
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
5194
|
-
if (!isWebStorageObject(callee.object)) return;
|
|
5196
|
+
if (!isWebStorageObject(stripParenExpression(callee.object))) return;
|
|
5195
5197
|
const keyArgument = node.arguments?.[0];
|
|
5196
5198
|
if (!keyArgument) return;
|
|
5197
5199
|
const keyString = resolveStaticKeyString(keyArgument);
|
|
@@ -6131,19 +6133,21 @@ const clickjackingRedirectRisk = defineRule({
|
|
|
6131
6133
|
})
|
|
6132
6134
|
});
|
|
6133
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
|
|
6134
6145
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6135
6146
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6136
6147
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
6137
6148
|
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
6138
6149
|
const isVersionedKey = (key) => VERSIONED_KEY_PATTERN.test(key) || CAMEL_CASE_VERSIONED_KEY_PATTERN.test(key);
|
|
6139
|
-
const isJsonStringifyCall = (node) =>
|
|
6140
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6141
|
-
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
6142
|
-
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
6143
|
-
if (node.callee.object.name !== "JSON") return false;
|
|
6144
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
6145
|
-
return node.callee.property.name === "stringify";
|
|
6146
|
-
};
|
|
6150
|
+
const isJsonStringifyCall = (node) => isGlobalMethodCall(node, "JSON", "stringify");
|
|
6147
6151
|
const resolveStringKey = (keyArg, context) => {
|
|
6148
6152
|
if (isNodeOfType(keyArg, "Literal")) return typeof keyArg.value === "string" ? keyArg.value : null;
|
|
6149
6153
|
if (!isNodeOfType(keyArg, "Identifier")) return null;
|
|
@@ -6162,8 +6166,9 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6162
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.",
|
|
6163
6167
|
create: (context) => ({ CallExpression(node) {
|
|
6164
6168
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6165
|
-
|
|
6166
|
-
if (!
|
|
6169
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6170
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6171
|
+
if (!STORAGE_OBJECTS.has(receiver.name)) return;
|
|
6167
6172
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6168
6173
|
if (node.callee.property.name !== "setItem") return;
|
|
6169
6174
|
const keyArg = node.arguments?.[0];
|
|
@@ -6176,7 +6181,7 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6176
6181
|
if (!isJsonStringifyCall(valueArg)) return;
|
|
6177
6182
|
context.report({
|
|
6178
6183
|
node: keyArg,
|
|
6179
|
-
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").`
|
|
6180
6185
|
});
|
|
6181
6186
|
} })
|
|
6182
6187
|
});
|
|
@@ -7718,6 +7723,7 @@ const isResultDiscardedCall = (callExpression) => {
|
|
|
7718
7723
|
let parent = node.parent;
|
|
7719
7724
|
while (parent) {
|
|
7720
7725
|
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
7726
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "void") return true;
|
|
7721
7727
|
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
7722
7728
|
if (isNodeOfType(parent, "ChainExpression")) {
|
|
7723
7729
|
node = parent;
|
|
@@ -7952,6 +7958,13 @@ const collectCleanupBindings = (effectCallback) => {
|
|
|
7952
7958
|
};
|
|
7953
7959
|
if (!isNodeOfType(effectCallback, "ArrowFunctionExpression") && !isNodeOfType(effectCallback, "FunctionExpression")) return bindings;
|
|
7954
7960
|
if (!isNodeOfType(effectCallback.body, "BlockStatement")) return bindings;
|
|
7961
|
+
walkAst(effectCallback.body, (child) => {
|
|
7962
|
+
if (!isSubscribeOrObserveCall(child)) return;
|
|
7963
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
7964
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
7965
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
7966
|
+
bindings.subscriptionNames.add(child.callee.object.name);
|
|
7967
|
+
});
|
|
7955
7968
|
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
7956
7969
|
if (!isNodeOfType(child, "VariableDeclaration")) return;
|
|
7957
7970
|
for (const declarator of child.declarations ?? []) {
|
|
@@ -7994,6 +8007,22 @@ const getRangeStart = (node) => {
|
|
|
7994
8007
|
const rangeStart = node.range?.[0];
|
|
7995
8008
|
return typeof rangeStart === "number" ? rangeStart : null;
|
|
7996
8009
|
};
|
|
8010
|
+
const removeSynchronouslyReleasedUsages = (callback, usages) => {
|
|
8011
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8012
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
8013
|
+
const releaseStarts = [];
|
|
8014
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
8015
|
+
if (!isReleaseLikeCall(child, EMPTY_NAME_SET$1, EMPTY_NAME_SET$1)) return;
|
|
8016
|
+
const releaseStart = getRangeStart(child);
|
|
8017
|
+
if (releaseStart !== null) releaseStarts.push(releaseStart);
|
|
8018
|
+
});
|
|
8019
|
+
if (releaseStarts.length === 0) return usages;
|
|
8020
|
+
return usages.filter((usage) => {
|
|
8021
|
+
const usageStart = getRangeStart(usage.node);
|
|
8022
|
+
if (usageStart === null) return true;
|
|
8023
|
+
return !releaseStarts.some((releaseStart) => releaseStart > usageStart);
|
|
8024
|
+
});
|
|
8025
|
+
};
|
|
7997
8026
|
const cleanupReturnRunsAfterUsage = (returnStatement, usages) => {
|
|
7998
8027
|
if (returnStatement.argument && isCleanupReturningSubscribeLikeCallExpression(returnStatement.argument)) return true;
|
|
7999
8028
|
const returnStart = getRangeStart(returnStatement);
|
|
@@ -8017,7 +8046,14 @@ const effectHasCleanupReturn = (callback, usages) => {
|
|
|
8017
8046
|
return didFindCleanupReturn;
|
|
8018
8047
|
};
|
|
8019
8048
|
const EMPTY_NAME_SET$1 = /* @__PURE__ */ new Set();
|
|
8020
|
-
const
|
|
8049
|
+
const isSelfReleasingListenerOptionProperty = (property) => {
|
|
8050
|
+
if (!isNodeOfType(property, "Property")) return false;
|
|
8051
|
+
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : isNodeOfType(property.key, "Literal") ? property.key.value : null;
|
|
8052
|
+
if (keyName === "signal") return true;
|
|
8053
|
+
if (keyName !== "once") return false;
|
|
8054
|
+
return isNodeOfType(property.value, "Literal") && property.value.value === true;
|
|
8055
|
+
};
|
|
8056
|
+
const hasSelfReleasingListenerOptions = (node) => isNodeOfType(node, "CallExpression") && (node.arguments ?? []).some((argument) => isNodeOfType(argument, "ObjectExpression") && (argument.properties ?? []).some(isSelfReleasingListenerOptionProperty));
|
|
8021
8057
|
const PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB = new Map([
|
|
8022
8058
|
["addEventListener", new Set(["removeEventListener", "abort"])],
|
|
8023
8059
|
["addListener", new Set([
|
|
@@ -8092,10 +8128,12 @@ const findRetainedFunctionLeak = (retainedFunction) => {
|
|
|
8092
8128
|
if (!isFunctionLike$1(retainedFunction)) return null;
|
|
8093
8129
|
const body = retainedFunction.body;
|
|
8094
8130
|
if (!body) return null;
|
|
8131
|
+
const conciseReturnValue = isNodeOfType(body, "BlockStatement") ? null : body;
|
|
8095
8132
|
let leak = null;
|
|
8096
8133
|
walkAst(body, (child) => {
|
|
8097
8134
|
if (leak !== null) return false;
|
|
8098
8135
|
if (isFunctionLike$1(child)) return false;
|
|
8136
|
+
if (child === conciseReturnValue && isNodeOfType(child, "CallExpression")) return;
|
|
8099
8137
|
if (isSocketConstruction(child) && isResultDiscardedCall(child) && !bodyContainsPairedReleaseCall(body, SOCKET_RELEASE_VERB_NAMES)) {
|
|
8100
8138
|
leak = {
|
|
8101
8139
|
kind: "socket",
|
|
@@ -8157,7 +8195,7 @@ const effectNeedsCleanup = defineRule({
|
|
|
8157
8195
|
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
8158
8196
|
const callback = getEffectCallback(node);
|
|
8159
8197
|
if (!callback) return;
|
|
8160
|
-
const usages = findSubscribeLikeUsages(callback);
|
|
8198
|
+
const usages = removeSynchronouslyReleasedUsages(callback, findSubscribeLikeUsages(callback));
|
|
8161
8199
|
if (usages.length === 0) return;
|
|
8162
8200
|
if (effectHasCleanupReturn(callback, usages)) return;
|
|
8163
8201
|
const firstUsage = usages[0];
|
|
@@ -10172,7 +10210,10 @@ const PRAGMA = "React";
|
|
|
10172
10210
|
const isReactFunctionCall = (node, expectedCall) => {
|
|
10173
10211
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
10174
10212
|
if (getCalleeName$2(node) !== expectedCall) return false;
|
|
10175
|
-
if (isNodeOfType(node.callee, "MemberExpression"))
|
|
10213
|
+
if (isNodeOfType(node.callee, "MemberExpression")) {
|
|
10214
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
10215
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === PRAGMA;
|
|
10216
|
+
}
|
|
10176
10217
|
return true;
|
|
10177
10218
|
};
|
|
10178
10219
|
//#endregion
|
|
@@ -12249,14 +12290,15 @@ const jsCacheStorage = defineRule({
|
|
|
12249
12290
|
"ArrowFunctionExpression:exit": exitFunctionScope,
|
|
12250
12291
|
CallExpression(node) {
|
|
12251
12292
|
if (!isMemberProperty(node.callee, "getItem")) return;
|
|
12252
|
-
|
|
12293
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
12294
|
+
if (!isNodeOfType(receiver, "Identifier") || !STORAGE_OBJECTS$1.has(receiver.name)) return;
|
|
12253
12295
|
if (!isNodeOfType(node.arguments?.[0], "Literal")) return;
|
|
12254
12296
|
const storageReadCounts = storageReadCountStack[storageReadCountStack.length - 1];
|
|
12255
12297
|
const storageKey = String(node.arguments[0].value);
|
|
12256
12298
|
const readCount = (storageReadCounts.get(storageKey) ?? 0) + 1;
|
|
12257
12299
|
storageReadCounts.set(storageKey, readCount);
|
|
12258
12300
|
if (readCount === 2) {
|
|
12259
|
-
const storageName =
|
|
12301
|
+
const storageName = receiver.name;
|
|
12260
12302
|
context.report({
|
|
12261
12303
|
node,
|
|
12262
12304
|
message: `This is slow because ${storageName}.getItem("${storageKey}") runs several times & re-parses the data each call, so read it once & reuse the value`
|
|
@@ -12270,13 +12312,16 @@ const jsCacheStorage = defineRule({
|
|
|
12270
12312
|
//#region src/plugin/rules/js-performance/js-combine-iterations.ts
|
|
12271
12313
|
const isIteratorProducingCall = (callExpression, generatorNamesInFile) => {
|
|
12272
12314
|
const callee = callExpression.callee;
|
|
12273
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12315
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
12316
|
+
const receiver = stripParenExpression(callee.object);
|
|
12317
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Iterator" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from") return true;
|
|
12318
|
+
if (isNodeOfType(callee.property, "Identifier") && ITERATOR_PRODUCING_METHOD_NAMES.has(callee.property.name)) {
|
|
12319
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Object") return false;
|
|
12320
|
+
return true;
|
|
12321
|
+
}
|
|
12322
|
+
return false;
|
|
12279
12323
|
}
|
|
12324
|
+
if (isNodeOfType(callee, "Identifier") && generatorNamesInFile.has(callee.name)) return true;
|
|
12280
12325
|
return false;
|
|
12281
12326
|
};
|
|
12282
12327
|
const isChainPassThroughCall = (callExpression) => {
|
|
@@ -12288,10 +12333,7 @@ const isChainPassThroughCall = (callExpression) => {
|
|
|
12288
12333
|
const isReceiverChainIteratorRooted = (receiverNode, generatorNamesInFile) => {
|
|
12289
12334
|
let cursor = receiverNode;
|
|
12290
12335
|
while (cursor) {
|
|
12291
|
-
|
|
12292
|
-
cursor = cursor.expression;
|
|
12293
|
-
continue;
|
|
12294
|
-
}
|
|
12336
|
+
cursor = stripParenExpression(cursor);
|
|
12295
12337
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12296
12338
|
if (isIteratorProducingCall(cursor, generatorNamesInFile)) return true;
|
|
12297
12339
|
if (!isChainPassThroughCall(cursor)) return false;
|
|
@@ -12367,10 +12409,7 @@ const isStringSplitRootedChain = (receiverNode) => {
|
|
|
12367
12409
|
let hops = 0;
|
|
12368
12410
|
while (cursor && hops < 12) {
|
|
12369
12411
|
hops += 1;
|
|
12370
|
-
|
|
12371
|
-
cursor = cursor.expression;
|
|
12372
|
-
continue;
|
|
12373
|
-
}
|
|
12412
|
+
cursor = stripParenExpression(cursor);
|
|
12374
12413
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
12375
12414
|
const callee = cursor.callee;
|
|
12376
12415
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
@@ -12394,10 +12433,7 @@ const isSmallLiteralArray = (node) => {
|
|
|
12394
12433
|
const isSmallLiteralArrayRootedChain = (receiverNode, smallConstArrayNames) => {
|
|
12395
12434
|
let cursor = receiverNode;
|
|
12396
12435
|
while (cursor) {
|
|
12397
|
-
|
|
12398
|
-
cursor = cursor.expression;
|
|
12399
|
-
continue;
|
|
12400
|
-
}
|
|
12436
|
+
cursor = stripParenExpression(cursor);
|
|
12401
12437
|
if (isNodeOfType(cursor, "ArrayExpression")) return isSmallLiteralArray(cursor);
|
|
12402
12438
|
if (isNodeOfType(cursor, "Identifier")) return smallConstArrayNames.has(cursor.name);
|
|
12403
12439
|
if (!isNodeOfType(cursor, "CallExpression")) return false;
|
|
@@ -12462,7 +12498,7 @@ const jsCombineIterations = defineRule({
|
|
|
12462
12498
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
12463
12499
|
const outerMethod = node.callee.property.name;
|
|
12464
12500
|
if (!CHAINABLE_ITERATION_METHODS.has(outerMethod)) return;
|
|
12465
|
-
const innerCall = node.callee.object;
|
|
12501
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12466
12502
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12467
12503
|
const innerMethod = innerCall.callee.property.name;
|
|
12468
12504
|
if (!CHAINABLE_ITERATION_METHODS.has(innerMethod)) return;
|
|
@@ -12535,10 +12571,10 @@ const jsFlatmapFilter = defineRule({
|
|
|
12535
12571
|
if (!filterArgument) return;
|
|
12536
12572
|
const isIdentityArrow = isNodeOfType(filterArgument, "ArrowFunctionExpression") && filterArgument.params?.length === 1 && isNodeOfType(filterArgument.body, "Identifier") && isNodeOfType(filterArgument.params[0], "Identifier") && filterArgument.body.name === filterArgument.params[0].name;
|
|
12537
12573
|
if (!(isNodeOfType(filterArgument, "Identifier") && filterArgument.name === "Boolean" || isIdentityArrow)) return;
|
|
12538
|
-
const innerCall = node.callee.object;
|
|
12574
|
+
const innerCall = stripParenExpression(node.callee.object);
|
|
12539
12575
|
if (!isNodeOfType(innerCall, "CallExpression") || !isNodeOfType(innerCall.callee, "MemberExpression") || !isNodeOfType(innerCall.callee.property, "Identifier")) return;
|
|
12540
12576
|
if (innerCall.callee.property.name !== "map") return;
|
|
12541
|
-
const receiver = innerCall.callee.object;
|
|
12577
|
+
const receiver = stripParenExpression(innerCall.callee.object);
|
|
12542
12578
|
if (receiver && isNodeOfType(receiver, "ArrayExpression")) {
|
|
12543
12579
|
const elements = receiver.elements ?? [];
|
|
12544
12580
|
if (elements.length > 0 && elements.length <= 8 && elements.every((element) => element == null || !isNodeOfType(element, "SpreadElement"))) return;
|
|
@@ -13798,7 +13834,7 @@ const jsTosortedImmutable = defineRule({
|
|
|
13798
13834
|
recommendation: "Use `array.toSorted()` (ES2023) instead of `[...array].sort()` so you sort without copying the array first",
|
|
13799
13835
|
create: (context) => ({ CallExpression(node) {
|
|
13800
13836
|
if (!isMemberProperty(node.callee, "sort")) return;
|
|
13801
|
-
const receiver = node.callee.object;
|
|
13837
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
13802
13838
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1 && isNodeOfType(receiver.elements[0], "SpreadElement")) {
|
|
13803
13839
|
const spreadArgument = receiver.elements[0].argument;
|
|
13804
13840
|
if (isFreshOrIteratorAllocation(spreadArgument)) return;
|
|
@@ -18835,7 +18871,8 @@ const isInsidePollingLoop = (navigationNode, effectCallback, timerScheduledNames
|
|
|
18835
18871
|
};
|
|
18836
18872
|
const describeClientSideNavigation = (node) => {
|
|
18837
18873
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression")) {
|
|
18838
|
-
const
|
|
18874
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
18875
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
18839
18876
|
const methodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
18840
18877
|
if (objectName === "router" && (methodName === "push" || methodName === "replace")) return `router.${methodName}() in useEffect flashes the wrong page before redirecting.`;
|
|
18841
18878
|
}
|
|
@@ -19455,7 +19492,7 @@ const getCookieMutationMethodName = (node, locallyScopedCookieBindings) => {
|
|
|
19455
19492
|
if (!isNodeOfType(node.callee, "MemberExpression")) return null;
|
|
19456
19493
|
if (!isNodeOfType(node.callee.property, "Identifier")) return null;
|
|
19457
19494
|
if (!COOKIE_MUTATION_METHOD_NAMES.has(node.callee.property.name)) return null;
|
|
19458
|
-
if (!isCookieReceiver(node.callee.object, locallyScopedCookieBindings)) return null;
|
|
19495
|
+
if (!isCookieReceiver(stripParenExpression(node.callee.object), locallyScopedCookieBindings)) return null;
|
|
19459
19496
|
return node.callee.property.name;
|
|
19460
19497
|
};
|
|
19461
19498
|
const isMutatingFetchCall = (node) => {
|
|
@@ -19469,7 +19506,7 @@ const isMutatingDbCall = (node, locallyScopedSafeBindings) => {
|
|
|
19469
19506
|
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
19470
19507
|
const { property, object } = node.callee;
|
|
19471
19508
|
if (!isNodeOfType(property, "Identifier") || !MUTATION_METHOD_NAMES.has(property.name)) return false;
|
|
19472
|
-
if (isSafeReceiverChain(object, locallyScopedSafeBindings)) return false;
|
|
19509
|
+
if (isSafeReceiverChain(stripParenExpression(object), locallyScopedSafeBindings)) return false;
|
|
19473
19510
|
return true;
|
|
19474
19511
|
};
|
|
19475
19512
|
const getDbCallDescription = (node) => {
|
|
@@ -19477,7 +19514,8 @@ const getDbCallDescription = (node) => {
|
|
|
19477
19514
|
if (!isNodeOfType(node.callee, "MemberExpression")) return ".unknown()";
|
|
19478
19515
|
if (!isNodeOfType(node.callee.property, "Identifier")) return ".unknown()";
|
|
19479
19516
|
const methodName = node.callee.property.name;
|
|
19480
|
-
const
|
|
19517
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
19518
|
+
const rootObjectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
19481
19519
|
return rootObjectName ? `${rootObjectName}.${methodName}()` : `.${methodName}()`;
|
|
19482
19520
|
};
|
|
19483
19521
|
const findSideEffect = (node, options = {}) => {
|
|
@@ -20877,13 +20915,13 @@ const getCallExpr = (ref, current = ref.identifier.parent) => {
|
|
|
20877
20915
|
if (isNodeOfType(current, "CallExpression")) {
|
|
20878
20916
|
let node = ref.identifier;
|
|
20879
20917
|
let parent = node.parent;
|
|
20880
|
-
while (parent && isNodeOfType(parent, "MemberExpression")) {
|
|
20918
|
+
while (parent && (isNodeOfType(parent, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type))) {
|
|
20881
20919
|
node = parent;
|
|
20882
20920
|
parent = node.parent;
|
|
20883
20921
|
}
|
|
20884
20922
|
if (current.callee === node) return current;
|
|
20885
20923
|
}
|
|
20886
|
-
if (isNodeOfType(current, "MemberExpression")) return getCallExpr(ref, current.parent);
|
|
20924
|
+
if (isNodeOfType(current, "MemberExpression") || TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(current.type)) return getCallExpr(ref, current.parent);
|
|
20887
20925
|
return null;
|
|
20888
20926
|
};
|
|
20889
20927
|
const getArgsUpstreamRefs = (analysis, ref) => {
|
|
@@ -21018,7 +21056,7 @@ const isReactNamedImportReference = (ref, importedName) => Boolean(ref?.resolved
|
|
|
21018
21056
|
const importDeclaration = declarationNode.parent;
|
|
21019
21057
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
21020
21058
|
}));
|
|
21021
|
-
const isHookCallee = (analysis, node, hookName) => {
|
|
21059
|
+
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
21022
21060
|
if (!node) return false;
|
|
21023
21061
|
if (isNodeOfType(node, "Identifier")) {
|
|
21024
21062
|
if (node.name === hookName) return true;
|
|
@@ -21027,15 +21065,19 @@ const isHookCallee = (analysis, node, hookName) => {
|
|
|
21027
21065
|
if (parent && isNodeOfType(parent, "MemberExpression") && isNodeOfType(parent.object, "Identifier") && parent.object.name === "React" && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
21028
21066
|
return false;
|
|
21029
21067
|
}
|
|
21030
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
21068
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
21069
|
+
const receiver = stripParenExpression(node.object);
|
|
21070
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
21071
|
+
}
|
|
21031
21072
|
return false;
|
|
21032
21073
|
};
|
|
21033
21074
|
const isUseEffect = (node) => {
|
|
21034
21075
|
if (!node || !isNodeOfType(node, "CallExpression")) return false;
|
|
21035
21076
|
const callee = node.callee;
|
|
21036
21077
|
if (isNodeOfType(callee, "Identifier") && callee.name === "useEffect") return true;
|
|
21037
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
21038
|
-
|
|
21078
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
21079
|
+
const receiver = stripParenExpression(callee.object);
|
|
21080
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "useEffect";
|
|
21039
21081
|
};
|
|
21040
21082
|
const getEffectFn = (analysis, node) => {
|
|
21041
21083
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
@@ -21063,7 +21105,7 @@ const isState = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
21063
21105
|
const node = def.node;
|
|
21064
21106
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21065
21107
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21066
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21108
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
21067
21109
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
21068
21110
|
const elements = node.id.elements ?? [];
|
|
21069
21111
|
if (elements.length !== 1 && elements.length !== 2) return false;
|
|
@@ -21074,7 +21116,7 @@ const isStateSetter = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) =
|
|
|
21074
21116
|
const node = def.node;
|
|
21075
21117
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21076
21118
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21077
|
-
if (!isHookCallee(analysis, node.init.callee, "useState")) return false;
|
|
21119
|
+
if (!isHookCallee$1(analysis, node.init.callee, "useState")) return false;
|
|
21078
21120
|
if (!isNodeOfType(node.id, "ArrayPattern")) return false;
|
|
21079
21121
|
const elements = node.id.elements ?? [];
|
|
21080
21122
|
if (elements.length !== 2) return false;
|
|
@@ -21121,7 +21163,7 @@ const isRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
21121
21163
|
const node = def.node;
|
|
21122
21164
|
if (!isNodeOfType(node, "VariableDeclarator")) return false;
|
|
21123
21165
|
if (!isNodeOfType(node.init, "CallExpression")) return false;
|
|
21124
|
-
return isHookCallee(analysis, node.init.callee, "useRef");
|
|
21166
|
+
return isHookCallee$1(analysis, node.init.callee, "useRef");
|
|
21125
21167
|
}));
|
|
21126
21168
|
const isRefCurrent = (ref) => {
|
|
21127
21169
|
const parent = ref.identifier.parent;
|
|
@@ -21134,11 +21176,15 @@ const isSyncStateSetterCall = (analysis, ref, effectFn) => isStateSetterCall(ana
|
|
|
21134
21176
|
const HANDLER_NAMED_METHOD_PATTERN = /^(on|handle)[A-Z]/;
|
|
21135
21177
|
const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
21136
21178
|
if (!isPropAlias(analysis, ref)) return false;
|
|
21137
|
-
|
|
21138
|
-
|
|
21179
|
+
let effectiveNode = ref.identifier;
|
|
21180
|
+
let parent = effectiveNode.parent;
|
|
21181
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === effectiveNode) {
|
|
21182
|
+
effectiveNode = parent;
|
|
21183
|
+
parent = effectiveNode.parent;
|
|
21184
|
+
}
|
|
21139
21185
|
if (!parent) return false;
|
|
21140
|
-
if (isNodeOfType(parent, "CallExpression") && parent.callee ===
|
|
21141
|
-
if (isNodeOfType(parent, "MemberExpression") && parent.object ===
|
|
21186
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === effectiveNode) return true;
|
|
21187
|
+
if (isNodeOfType(parent, "MemberExpression") && parent.object === effectiveNode) {
|
|
21142
21188
|
const memberParent = parent.parent;
|
|
21143
21189
|
if (isNodeOfType(memberParent, "CallExpression") && memberParent.callee === parent) {
|
|
21144
21190
|
if (!parent.computed && isNodeOfType(parent.property, "Identifier") && HANDLER_NAMED_METHOD_PATTERN.test(parent.property.name)) return true;
|
|
@@ -21149,7 +21195,7 @@ const isPropCallbackInvocationRef = (analysis, ref) => {
|
|
|
21149
21195
|
};
|
|
21150
21196
|
const isRefCall = (analysis, ref) => isEventualCallTo(analysis, ref, (innerRef) => isRefCurrent(innerRef) || isRef(analysis, innerRef));
|
|
21151
21197
|
const getUseStateDecl = (analysis, ref) => {
|
|
21152
|
-
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21198
|
+
let node = getUpstreamRefs(analysis, ref).find((upRef) => isHookCallee$1(analysis, upRef.identifier, "useState"))?.identifier;
|
|
21153
21199
|
while (node && !isNodeOfType(node, "VariableDeclarator")) node = node.parent;
|
|
21154
21200
|
return node ?? null;
|
|
21155
21201
|
};
|
|
@@ -21395,7 +21441,23 @@ const ALWAYS_FOCUSABLE_TAGS = new Set([
|
|
|
21395
21441
|
"summary",
|
|
21396
21442
|
"textarea"
|
|
21397
21443
|
]);
|
|
21444
|
+
const isStaticallyFalseBooleanAttribute = (attribute) => {
|
|
21445
|
+
const value = attribute.value;
|
|
21446
|
+
if (!value || !isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
21447
|
+
const expression = value.expression;
|
|
21448
|
+
return isNodeOfType(expression, "Literal") && expression.value === false;
|
|
21449
|
+
};
|
|
21450
|
+
const DISABLEABLE_TAGS = new Set([
|
|
21451
|
+
"button",
|
|
21452
|
+
"input",
|
|
21453
|
+
"select",
|
|
21454
|
+
"textarea"
|
|
21455
|
+
]);
|
|
21398
21456
|
const isNativelyFocusable = (tagName, openingElement) => {
|
|
21457
|
+
if (DISABLEABLE_TAGS.has(tagName)) {
|
|
21458
|
+
const disabledAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "disabled");
|
|
21459
|
+
if (disabledAttribute && !isStaticallyFalseBooleanAttribute(disabledAttribute)) return false;
|
|
21460
|
+
}
|
|
21399
21461
|
if (ALWAYS_FOCUSABLE_TAGS.has(tagName)) return true;
|
|
21400
21462
|
switch (tagName) {
|
|
21401
21463
|
case "input": {
|
|
@@ -21409,7 +21471,10 @@ const isNativelyFocusable = (tagName, openingElement) => {
|
|
|
21409
21471
|
case "a":
|
|
21410
21472
|
case "area": return hasJsxPropIgnoreCase(openingElement.attributes, "href") !== void 0;
|
|
21411
21473
|
case "audio":
|
|
21412
|
-
case "video":
|
|
21474
|
+
case "video": {
|
|
21475
|
+
const controlsAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "controls");
|
|
21476
|
+
return controlsAttribute !== void 0 && !isStaticallyFalseBooleanAttribute(controlsAttribute);
|
|
21477
|
+
}
|
|
21413
21478
|
default: return false;
|
|
21414
21479
|
}
|
|
21415
21480
|
};
|
|
@@ -22373,7 +22438,8 @@ const SECOND_INDEX_METHODS = new Set([
|
|
|
22373
22438
|
"some"
|
|
22374
22439
|
]);
|
|
22375
22440
|
const THIRD_INDEX_METHODS = new Set(["reduce", "reduceRight"]);
|
|
22376
|
-
const isPositionallyStableIterationReceiver = (
|
|
22441
|
+
const isPositionallyStableIterationReceiver = (receiverNode) => {
|
|
22442
|
+
const receiver = stripParenExpression(receiverNode);
|
|
22377
22443
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
22378
22444
|
if (isNodeOfType(receiver, "ArrayExpression") && receiver.elements?.length === 1) {
|
|
22379
22445
|
const only = receiver.elements[0];
|
|
@@ -22384,17 +22450,13 @@ const isPositionallyStableIterationReceiver = (receiver) => {
|
|
|
22384
22450
|
}
|
|
22385
22451
|
if (!isNodeOfType(receiver, "CallExpression")) return false;
|
|
22386
22452
|
const callee = receiver.callee;
|
|
22387
|
-
if (
|
|
22453
|
+
if (isGlobalMethodCall(receiver, "Array", "from") && receiver.arguments.length >= 1 && isNodeOfType(receiver.arguments[0], "ObjectExpression")) return true;
|
|
22388
22454
|
if (isNodeOfType(callee, "Identifier") && callee.name === "Array") return true;
|
|
22389
22455
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "split") return true;
|
|
22390
22456
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "fill" || callee.property.name === "flat")) return isPositionallyStableIterationReceiver(callee.object);
|
|
22391
22457
|
return false;
|
|
22392
22458
|
};
|
|
22393
|
-
const isArrayFromMapperCallback = (parentCall, callback) =>
|
|
22394
|
-
if (parentCall.arguments[1] !== callback) return false;
|
|
22395
|
-
const callee = parentCall.callee;
|
|
22396
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.object, "Identifier") && callee.object.name === "Array" && isNodeOfType(callee.property, "Identifier") && callee.property.name === "from";
|
|
22397
|
-
};
|
|
22459
|
+
const isArrayFromMapperCallback = (parentCall, callback) => parentCall.arguments[1] === callback && isGlobalMethodCall(parentCall, "Array", "from");
|
|
22398
22460
|
const isArrayFromSourcePositionallyStable = (source) => {
|
|
22399
22461
|
if (isNodeOfType(source, "ObjectExpression")) {
|
|
22400
22462
|
for (const property of source.properties ?? []) {
|
|
@@ -22453,18 +22515,12 @@ const expressionUsesIndex = (expression, paramName) => {
|
|
|
22453
22515
|
return false;
|
|
22454
22516
|
}
|
|
22455
22517
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
22456
|
-
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(expression.callee.object, paramName)) return true;
|
|
22518
|
+
if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier") && expression.callee.property.name === "toString" && isIndexReference(stripParenExpression(expression.callee.object), paramName)) return true;
|
|
22457
22519
|
if (isNodeOfType(expression.callee, "Identifier") && expression.callee.name === "String" && expression.arguments.length > 0 && isIndexReference(expression.arguments[0], paramName)) return true;
|
|
22458
22520
|
}
|
|
22459
22521
|
return false;
|
|
22460
22522
|
};
|
|
22461
|
-
const isReactCloneElement = (callExpression) =>
|
|
22462
|
-
const callee = callExpression.callee;
|
|
22463
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
22464
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
22465
|
-
if (callee.property.name !== "cloneElement") return false;
|
|
22466
|
-
return isNodeOfType(callee.object, "Identifier") && callee.object.name === "React";
|
|
22467
|
-
};
|
|
22523
|
+
const isReactCloneElement = (callExpression) => isGlobalMethodCall(callExpression, "React", "cloneElement");
|
|
22468
22524
|
const noArrayIndexKey = defineRule({
|
|
22469
22525
|
id: "no-array-index-key",
|
|
22470
22526
|
title: "Array index used as a key",
|
|
@@ -23188,7 +23244,7 @@ const isAsyncFunctionLike = (node) => {
|
|
|
23188
23244
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
23189
23245
|
return false;
|
|
23190
23246
|
};
|
|
23191
|
-
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
23247
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES$1 = new Set([
|
|
23192
23248
|
"forEach",
|
|
23193
23249
|
"map",
|
|
23194
23250
|
"filter",
|
|
@@ -23209,30 +23265,51 @@ const runsOnEffectDispatch = (functionNode) => {
|
|
|
23209
23265
|
if (parent.callee === functionNode) return true;
|
|
23210
23266
|
if (!(parent.arguments ?? []).some((argument) => argument === functionNode)) return false;
|
|
23211
23267
|
const callee = parent.callee;
|
|
23212
|
-
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(callee.property.name);
|
|
23268
|
+
return isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES$1.has(callee.property.name);
|
|
23213
23269
|
};
|
|
23214
23270
|
const isTerminatingStatement = (statement) => isNodeOfType(statement, "BreakStatement") || isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement") || isNodeOfType(statement, "ContinueStatement");
|
|
23215
|
-
const
|
|
23216
|
-
|
|
23217
|
-
if (statement.alternate) return null;
|
|
23218
|
-
const consequent = statement.consequent;
|
|
23219
|
-
if (isTerminatingStatement(consequent)) return consequent;
|
|
23220
|
-
if (isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner))) return consequent;
|
|
23221
|
-
return null;
|
|
23222
|
-
};
|
|
23223
|
-
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23271
|
+
const analyzeBranchStatements = (branch, context) => analyzeStatementSequence(isNodeOfType(branch, "BlockStatement") ? branch.body ?? [] : [branch], context);
|
|
23272
|
+
const analyzeStatementSequence = (statements, context) => {
|
|
23224
23273
|
let fallThroughCount = 0;
|
|
23225
|
-
let
|
|
23274
|
+
let maxTerminatedCount = 0;
|
|
23226
23275
|
for (const statement of statements) {
|
|
23227
|
-
|
|
23228
|
-
|
|
23229
|
-
|
|
23276
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
23277
|
+
fallThroughCount += countMaxPathSetStateCalls(statement.test, context);
|
|
23278
|
+
const thenSummary = analyzeBranchStatements(statement.consequent, context);
|
|
23279
|
+
const elseSummary = statement.alternate ? analyzeBranchStatements(statement.alternate, context) : {
|
|
23280
|
+
fallThroughCount: 0,
|
|
23281
|
+
maxTerminatedCount: 0,
|
|
23282
|
+
doAllPathsTerminate: false
|
|
23283
|
+
};
|
|
23284
|
+
maxTerminatedCount = Math.max(maxTerminatedCount, fallThroughCount + thenSummary.maxTerminatedCount, fallThroughCount + elseSummary.maxTerminatedCount);
|
|
23285
|
+
if (thenSummary.doAllPathsTerminate && elseSummary.doAllPathsTerminate) return {
|
|
23286
|
+
fallThroughCount: 0,
|
|
23287
|
+
maxTerminatedCount,
|
|
23288
|
+
doAllPathsTerminate: true
|
|
23289
|
+
};
|
|
23290
|
+
const fallThroughBranchCounts = [...thenSummary.doAllPathsTerminate ? [] : [thenSummary.fallThroughCount], ...elseSummary.doAllPathsTerminate ? [] : [elseSummary.fallThroughCount]];
|
|
23291
|
+
fallThroughCount += Math.max(...fallThroughBranchCounts);
|
|
23230
23292
|
continue;
|
|
23231
23293
|
}
|
|
23232
|
-
if (isTerminatingStatement(statement))
|
|
23294
|
+
if (isTerminatingStatement(statement)) {
|
|
23295
|
+
const terminatedPathCount = fallThroughCount + countMaxPathSetStateCalls(statement, context);
|
|
23296
|
+
return {
|
|
23297
|
+
fallThroughCount: 0,
|
|
23298
|
+
maxTerminatedCount: Math.max(maxTerminatedCount, terminatedPathCount),
|
|
23299
|
+
doAllPathsTerminate: true
|
|
23300
|
+
};
|
|
23301
|
+
}
|
|
23233
23302
|
fallThroughCount += countMaxPathSetStateCalls(statement, context);
|
|
23234
23303
|
}
|
|
23235
|
-
return
|
|
23304
|
+
return {
|
|
23305
|
+
fallThroughCount,
|
|
23306
|
+
maxTerminatedCount,
|
|
23307
|
+
doAllPathsTerminate: false
|
|
23308
|
+
};
|
|
23309
|
+
};
|
|
23310
|
+
const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
23311
|
+
const summary = analyzeStatementSequence(statements, context);
|
|
23312
|
+
return Math.max(summary.fallThroughCount, summary.maxTerminatedCount);
|
|
23236
23313
|
};
|
|
23237
23314
|
const collectLocalHelperFunctions = (root) => {
|
|
23238
23315
|
const helpersByName = /* @__PURE__ */ new Map();
|
|
@@ -23363,7 +23440,9 @@ const isDevOnlyGuardedEffect = (callback) => {
|
|
|
23363
23440
|
if (!body || !isNodeOfType(body, "BlockStatement")) return false;
|
|
23364
23441
|
const firstStatement = (body.body ?? [])[0];
|
|
23365
23442
|
if (!firstStatement) return false;
|
|
23366
|
-
if (!
|
|
23443
|
+
if (!isNodeOfType(firstStatement, "IfStatement") || firstStatement.alternate) return false;
|
|
23444
|
+
const consequent = firstStatement.consequent;
|
|
23445
|
+
if (!(isTerminatingStatement(consequent) || isNodeOfType(consequent, "BlockStatement") && (consequent.body ?? []).some((inner) => isTerminatingStatement(inner)))) return false;
|
|
23367
23446
|
return mentionsDevEnvFlag(firstStatement.test);
|
|
23368
23447
|
};
|
|
23369
23448
|
const noCascadingSetState = defineRule({
|
|
@@ -24692,11 +24771,21 @@ const isInitialOnlySetterCall = (callExpr) => {
|
|
|
24692
24771
|
return isInitialOnlyPropName(arg.name);
|
|
24693
24772
|
};
|
|
24694
24773
|
//#endregion
|
|
24774
|
+
//#region src/plugin/utils/is-no-op-statement.ts
|
|
24775
|
+
const isNoOpStatement = (statement) => {
|
|
24776
|
+
if (isNodeOfType(statement, "EmptyStatement")) return true;
|
|
24777
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
24778
|
+
const expression = stripParenExpression(statement.expression);
|
|
24779
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
24780
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
24781
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return isNodeOfType(stripParenExpression(expression.argument), "Literal");
|
|
24782
|
+
return false;
|
|
24783
|
+
};
|
|
24784
|
+
//#endregion
|
|
24695
24785
|
//#region src/plugin/utils/get-callback-statements.ts
|
|
24696
24786
|
const getCallbackStatements = (callback) => {
|
|
24697
24787
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") && !isNodeOfType(callback, "FunctionDeclaration")) return [];
|
|
24698
|
-
|
|
24699
|
-
return callback.body ? [callback.body] : [];
|
|
24788
|
+
return (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : callback.body ? [callback.body] : []).filter((statement) => !isNoOpStatement(statement));
|
|
24700
24789
|
};
|
|
24701
24790
|
//#endregion
|
|
24702
24791
|
//#region src/plugin/rules/state-and-effects/no-derived-state-effect.ts
|
|
@@ -24738,6 +24827,7 @@ const collectValueIdentifierNames = (node, into, localBindingNames = /* @__PURE_
|
|
|
24738
24827
|
const flattenGuardedStatements = (statements) => {
|
|
24739
24828
|
const flattened = [];
|
|
24740
24829
|
for (const statement of statements) {
|
|
24830
|
+
if (isNoOpStatement(statement)) continue;
|
|
24741
24831
|
if (isNodeOfType(statement, "ExpressionStatement")) {
|
|
24742
24832
|
flattened.push(statement);
|
|
24743
24833
|
continue;
|
|
@@ -25422,7 +25512,7 @@ const noDidMountSetState = defineRule({
|
|
|
25422
25512
|
const { mode } = resolveSettings$20(context.settings);
|
|
25423
25513
|
return { CallExpression(node) {
|
|
25424
25514
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25425
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25515
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25426
25516
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25427
25517
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25428
25518
|
if (isMountFlagArgument(node.arguments?.[0])) return;
|
|
@@ -25547,7 +25637,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
25547
25637
|
const { mode } = resolveSettings$19(context.settings);
|
|
25548
25638
|
return { CallExpression(node) {
|
|
25549
25639
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
25550
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
25640
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
25551
25641
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
25552
25642
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25553
25643
|
if (isInsideDiffGuard(node)) return;
|
|
@@ -25868,9 +25958,10 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
25868
25958
|
create: (context) => ({ CallExpression(node) {
|
|
25869
25959
|
const callee = node.callee;
|
|
25870
25960
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
25871
|
-
|
|
25961
|
+
const receiver = stripParenExpression(callee.object);
|
|
25962
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25872
25963
|
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "startViewTransition") return;
|
|
25873
|
-
if (context.scopes.symbolFor(
|
|
25964
|
+
if (context.scopes.symbolFor(receiver) !== null) return;
|
|
25874
25965
|
if (!importsReactViewTransition(node)) return;
|
|
25875
25966
|
context.report({
|
|
25876
25967
|
node,
|
|
@@ -25890,7 +25981,8 @@ const noDocumentWrite = defineRule({
|
|
|
25890
25981
|
create: (context) => ({ CallExpression(node) {
|
|
25891
25982
|
const callee = node.callee;
|
|
25892
25983
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
25893
|
-
|
|
25984
|
+
const receiver = stripParenExpression(callee.object);
|
|
25985
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "document") return;
|
|
25894
25986
|
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
25895
25987
|
context.report({
|
|
25896
25988
|
node,
|
|
@@ -26587,6 +26679,7 @@ const noEffectEventInDeps = defineRule({
|
|
|
26587
26679
|
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26588
26680
|
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26589
26681
|
}
|
|
26682
|
+
if (isNodeOfType(initializer.callee, "MemberExpression") && !initializer.callee.computed && isNodeOfType(initializer.callee.object, "Identifier") && isImportedFromNonReactModule(declaratorNode, initializer.callee.object.name)) return;
|
|
26590
26683
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26591
26684
|
} });
|
|
26592
26685
|
return {
|
|
@@ -29166,7 +29259,7 @@ const noIsMounted = defineRule({
|
|
|
29166
29259
|
recommendation: "`isMounted` doesn't work in modern React. Track mount state with a ref, or cancel the async work instead.",
|
|
29167
29260
|
create: (context) => ({ CallExpression(node) {
|
|
29168
29261
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
29169
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
29262
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
29170
29263
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "isMounted") return;
|
|
29171
29264
|
if (!getParentComponent(node)) return;
|
|
29172
29265
|
context.report({
|
|
@@ -29181,7 +29274,9 @@ const MESSAGE$20 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializin
|
|
|
29181
29274
|
const isJsonMethodCall = (node, method) => {
|
|
29182
29275
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
29183
29276
|
const callee = node.callee;
|
|
29184
|
-
|
|
29277
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
29278
|
+
const receiver = stripParenExpression(callee.object);
|
|
29279
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
29185
29280
|
};
|
|
29186
29281
|
const SNAPSHOT_FUNCTION_NAME_PATTERN = /snapshot|serializ|tojson/i;
|
|
29187
29282
|
const NORMALIZATION_BINDING_NAME_PATTERN = /normali[sz]/i;
|
|
@@ -29592,6 +29687,378 @@ const noLegacyContextApi = defineRule({
|
|
|
29592
29687
|
}
|
|
29593
29688
|
});
|
|
29594
29689
|
//#endregion
|
|
29690
|
+
//#region src/plugin/utils/executes-during-render.ts
|
|
29691
|
+
const SYNCHRONOUS_ITERATION_METHOD_NAMES = new Set([
|
|
29692
|
+
"map",
|
|
29693
|
+
"filter",
|
|
29694
|
+
"forEach",
|
|
29695
|
+
"flatMap",
|
|
29696
|
+
"reduce",
|
|
29697
|
+
"reduceRight",
|
|
29698
|
+
"some",
|
|
29699
|
+
"every",
|
|
29700
|
+
"find",
|
|
29701
|
+
"findIndex",
|
|
29702
|
+
"findLast",
|
|
29703
|
+
"findLastIndex",
|
|
29704
|
+
"sort",
|
|
29705
|
+
"toSorted"
|
|
29706
|
+
]);
|
|
29707
|
+
const executesDuringRender = (functionNode) => {
|
|
29708
|
+
const parent = functionNode.parent;
|
|
29709
|
+
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
29710
|
+
if (parent.callee === functionNode) return true;
|
|
29711
|
+
if (isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode) return true;
|
|
29712
|
+
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ITERATION_METHOD_NAMES.has(parent.callee.property.name) && parent.arguments?.[0] === functionNode;
|
|
29713
|
+
};
|
|
29714
|
+
//#endregion
|
|
29715
|
+
//#region src/plugin/utils/has-suppress-hydration-warning-attribute.ts
|
|
29716
|
+
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
29717
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
29718
|
+
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
29719
|
+
return false;
|
|
29720
|
+
};
|
|
29721
|
+
//#endregion
|
|
29722
|
+
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
29723
|
+
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
29724
|
+
let ancestor = bindingIdentifier.parent;
|
|
29725
|
+
while (ancestor) {
|
|
29726
|
+
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
29727
|
+
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
29728
|
+
ancestor = ancestor.parent ?? null;
|
|
29729
|
+
}
|
|
29730
|
+
return null;
|
|
29731
|
+
};
|
|
29732
|
+
//#endregion
|
|
29733
|
+
//#region src/plugin/utils/references-falsy-initial-state.ts
|
|
29734
|
+
const isFalsyLiteral = (node) => {
|
|
29735
|
+
if (!node) return true;
|
|
29736
|
+
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
29737
|
+
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
29738
|
+
};
|
|
29739
|
+
const isFalsyInitialStateBinding = (identifier) => {
|
|
29740
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
29741
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
29742
|
+
if (!binding) return false;
|
|
29743
|
+
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
29744
|
+
if (!declarator?.init) return false;
|
|
29745
|
+
const init = stripParenExpression(declarator.init);
|
|
29746
|
+
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
29747
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
29748
|
+
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
29749
|
+
return isFalsyLiteral(init.arguments?.[0]);
|
|
29750
|
+
};
|
|
29751
|
+
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
29752
|
+
//#endregion
|
|
29753
|
+
//#region src/plugin/utils/is-gated-by-falsy-initial-state.ts
|
|
29754
|
+
const isGatedByFalsyInitialState = (node) => {
|
|
29755
|
+
let cursor = node;
|
|
29756
|
+
let parent = node.parent;
|
|
29757
|
+
while (parent) {
|
|
29758
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
29759
|
+
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29760
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
29761
|
+
cursor = parent;
|
|
29762
|
+
parent = parent.parent ?? null;
|
|
29763
|
+
}
|
|
29764
|
+
return false;
|
|
29765
|
+
};
|
|
29766
|
+
//#endregion
|
|
29767
|
+
//#region src/plugin/utils/references-client-only-flag.ts
|
|
29768
|
+
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
29769
|
+
const referencesClientOnlyFlag = (expression) => {
|
|
29770
|
+
const unwrapped = stripParenExpression(expression);
|
|
29771
|
+
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
29772
|
+
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
29773
|
+
const property = unwrapped.property;
|
|
29774
|
+
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
29775
|
+
}
|
|
29776
|
+
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
29777
|
+
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
29778
|
+
return false;
|
|
29779
|
+
};
|
|
29780
|
+
//#endregion
|
|
29781
|
+
//#region src/plugin/utils/is-inside-client-only-guard.ts
|
|
29782
|
+
const isInsideClientOnlyGuard = (node) => {
|
|
29783
|
+
let cursor = node;
|
|
29784
|
+
let parent = node.parent;
|
|
29785
|
+
while (parent) {
|
|
29786
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
29787
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
29788
|
+
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
29789
|
+
cursor = parent;
|
|
29790
|
+
parent = parent.parent ?? null;
|
|
29791
|
+
}
|
|
29792
|
+
return false;
|
|
29793
|
+
};
|
|
29794
|
+
//#endregion
|
|
29795
|
+
//#region src/plugin/rules/performance/no-locale-format-in-render.ts
|
|
29796
|
+
const LOCALE_FORMAT_METHOD_NAMES = new Set([
|
|
29797
|
+
"toLocaleString",
|
|
29798
|
+
"toLocaleDateString",
|
|
29799
|
+
"toLocaleTimeString"
|
|
29800
|
+
]);
|
|
29801
|
+
const DATE_ONLY_LOCALE_METHOD_NAMES = new Set(["toLocaleDateString", "toLocaleTimeString"]);
|
|
29802
|
+
const INTL_FORMATTER_NAMES = new Set(["DateTimeFormat", "RelativeTimeFormat"]);
|
|
29803
|
+
const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
29804
|
+
"format",
|
|
29805
|
+
"formatToParts",
|
|
29806
|
+
"formatRange"
|
|
29807
|
+
]);
|
|
29808
|
+
const isProvableDateExpression = (expression) => {
|
|
29809
|
+
if (!expression) return false;
|
|
29810
|
+
const unwrapped = stripParenExpression(expression);
|
|
29811
|
+
return isNodeOfType(unwrapped, "NewExpression") && isNodeOfType(unwrapped.callee, "Identifier") && unwrapped.callee.name === "Date";
|
|
29812
|
+
};
|
|
29813
|
+
const DATE_FLAVORED_NAME_PATTERN = /(date|time|timestamp|deadline|created|updated|scheduled|expire|moment|when|birthday|dob)|(at)$/i;
|
|
29814
|
+
const receiverNameLooksDateFlavored = (expression) => {
|
|
29815
|
+
if (!expression) return false;
|
|
29816
|
+
const unwrapped = stripParenExpression(expression);
|
|
29817
|
+
if (isNodeOfType(unwrapped, "Identifier")) return DATE_FLAVORED_NAME_PATTERN.test(unwrapped.name);
|
|
29818
|
+
if (isNodeOfType(unwrapped, "MemberExpression") && !unwrapped.computed) return isNodeOfType(unwrapped.property, "Identifier") && DATE_FLAVORED_NAME_PATTERN.test(unwrapped.property.name);
|
|
29819
|
+
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
29820
|
+
return false;
|
|
29821
|
+
};
|
|
29822
|
+
const objectLiteralHasProperty = (objectExpression, propertyName) => {
|
|
29823
|
+
if (!objectExpression) return false;
|
|
29824
|
+
const unwrapped = stripParenExpression(objectExpression);
|
|
29825
|
+
if (!isNodeOfType(unwrapped, "ObjectExpression")) return false;
|
|
29826
|
+
for (const property of unwrapped.properties ?? []) {
|
|
29827
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
29828
|
+
if (property.computed) continue;
|
|
29829
|
+
if (isNodeOfType(property.key, "Identifier") && property.key.name === propertyName) return true;
|
|
29830
|
+
if (isNodeOfType(property.key, "Literal") && property.key.value === propertyName) return true;
|
|
29831
|
+
}
|
|
29832
|
+
return false;
|
|
29833
|
+
};
|
|
29834
|
+
const hasExplicitLocaleArgument = (argument) => {
|
|
29835
|
+
if (!argument) return false;
|
|
29836
|
+
const unwrapped = stripParenExpression(argument);
|
|
29837
|
+
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
29838
|
+
return true;
|
|
29839
|
+
};
|
|
29840
|
+
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
29841
|
+
const localeArgument = call.arguments?.[0];
|
|
29842
|
+
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
29843
|
+
const optionsArgument = call.arguments?.[1];
|
|
29844
|
+
if (objectLiteralHasProperty(optionsArgument, "timeZone")) return true;
|
|
29845
|
+
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
29846
|
+
};
|
|
29847
|
+
const matchLocaleMethodCall = (call) => {
|
|
29848
|
+
const callee = call.callee;
|
|
29849
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29850
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29851
|
+
const methodName = callee.property.name;
|
|
29852
|
+
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
29853
|
+
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
29854
|
+
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
29855
|
+
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
29856
|
+
return {
|
|
29857
|
+
node: call,
|
|
29858
|
+
display: `${methodName}()`
|
|
29859
|
+
};
|
|
29860
|
+
};
|
|
29861
|
+
const getIntlFormatterName = (expression) => {
|
|
29862
|
+
if (!expression) return null;
|
|
29863
|
+
const unwrapped = stripParenExpression(expression);
|
|
29864
|
+
if (!isNodeOfType(unwrapped, "CallExpression") && !isNodeOfType(unwrapped, "NewExpression")) return null;
|
|
29865
|
+
const callee = unwrapped.callee;
|
|
29866
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29867
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Intl") return null;
|
|
29868
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29869
|
+
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
29870
|
+
};
|
|
29871
|
+
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
29872
|
+
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
29873
|
+
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
29874
|
+
if (formatterName !== "DateTimeFormat") return true;
|
|
29875
|
+
return objectLiteralHasProperty(construction.arguments?.[1], "timeZone");
|
|
29876
|
+
};
|
|
29877
|
+
const matchIntlFormatCall = (call) => {
|
|
29878
|
+
const callee = call.callee;
|
|
29879
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
29880
|
+
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
29881
|
+
if (!INTL_FORMAT_METHOD_NAMES.has(callee.property.name)) return null;
|
|
29882
|
+
let construction = stripParenExpression(callee.object);
|
|
29883
|
+
if (isNodeOfType(construction, "Identifier")) {
|
|
29884
|
+
const binding = findVariableInitializer(construction, construction.name);
|
|
29885
|
+
construction = binding?.initializer ? stripParenExpression(binding.initializer) : null;
|
|
29886
|
+
}
|
|
29887
|
+
if (!construction) return null;
|
|
29888
|
+
const formatterName = getIntlFormatterName(construction);
|
|
29889
|
+
if (!formatterName) return null;
|
|
29890
|
+
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
29891
|
+
return {
|
|
29892
|
+
node: call,
|
|
29893
|
+
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
29894
|
+
};
|
|
29895
|
+
};
|
|
29896
|
+
const isDeterministicInputDateConstruction = (expression) => {
|
|
29897
|
+
if (!expression) return false;
|
|
29898
|
+
const unwrapped = stripParenExpression(expression);
|
|
29899
|
+
if (!isNodeOfType(unwrapped, "NewExpression")) return false;
|
|
29900
|
+
if (!isNodeOfType(unwrapped.callee, "Identifier") || unwrapped.callee.name !== "Date") return false;
|
|
29901
|
+
return (unwrapped.arguments?.length ?? 0) > 0;
|
|
29902
|
+
};
|
|
29903
|
+
const matchDateDefaultStringification = (node) => {
|
|
29904
|
+
if (isNodeOfType(node, "CallExpression")) {
|
|
29905
|
+
const callee = node.callee;
|
|
29906
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "toString" && isDeterministicInputDateConstruction(callee.object)) return {
|
|
29907
|
+
node,
|
|
29908
|
+
display: "Date.prototype.toString()"
|
|
29909
|
+
};
|
|
29910
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "String" && isDeterministicInputDateConstruction(node.arguments?.[0])) return {
|
|
29911
|
+
node,
|
|
29912
|
+
display: "String(new Date(…))"
|
|
29913
|
+
};
|
|
29914
|
+
return null;
|
|
29915
|
+
}
|
|
29916
|
+
if (isNodeOfType(node, "TemplateLiteral")) {
|
|
29917
|
+
for (const expression of node.expressions ?? []) if (isDeterministicInputDateConstruction(expression)) return {
|
|
29918
|
+
node: expression,
|
|
29919
|
+
display: "`${new Date(…)}`"
|
|
29920
|
+
};
|
|
29921
|
+
}
|
|
29922
|
+
return null;
|
|
29923
|
+
};
|
|
29924
|
+
const findRenderPhaseComponentOrHook = (node) => {
|
|
29925
|
+
let functionNode = findEnclosingFunction(node);
|
|
29926
|
+
while (functionNode) {
|
|
29927
|
+
if (componentOrHookDisplayNameForFunction(functionNode)) return functionNode;
|
|
29928
|
+
if (!executesDuringRender(functionNode)) return null;
|
|
29929
|
+
functionNode = findEnclosingFunction(functionNode);
|
|
29930
|
+
}
|
|
29931
|
+
return null;
|
|
29932
|
+
};
|
|
29933
|
+
const hasClientRenderEvidence = (componentOrHookNode, fileHasUseClientDirective) => {
|
|
29934
|
+
if (fileHasUseClientDirective) return true;
|
|
29935
|
+
const displayName = componentOrHookDisplayNameForFunction(componentOrHookNode);
|
|
29936
|
+
if (displayName && isReactHookName(displayName)) return true;
|
|
29937
|
+
let callsHook = false;
|
|
29938
|
+
walkAst((isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null) ?? componentOrHookNode, (child) => {
|
|
29939
|
+
if (callsHook) return false;
|
|
29940
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && isReactHookName(child.callee.name)) {
|
|
29941
|
+
callsHook = true;
|
|
29942
|
+
return false;
|
|
29943
|
+
}
|
|
29944
|
+
});
|
|
29945
|
+
return callsHook;
|
|
29946
|
+
};
|
|
29947
|
+
const isAfterClientOnlyEarlyReturn = (node, componentOrHookNode) => {
|
|
29948
|
+
const body = isFunctionLike$1(componentOrHookNode) ? componentOrHookNode.body : null;
|
|
29949
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
29950
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
29951
|
+
let cursor = node;
|
|
29952
|
+
while (cursor) {
|
|
29953
|
+
ancestors.add(cursor);
|
|
29954
|
+
cursor = cursor.parent ?? null;
|
|
29955
|
+
}
|
|
29956
|
+
for (const statement of body.body ?? []) {
|
|
29957
|
+
if (ancestors.has(statement)) return false;
|
|
29958
|
+
if (!isNodeOfType(statement, "IfStatement")) continue;
|
|
29959
|
+
if (!referencesClientOnlyFlag(statement.test) && !referencesFalsyInitialState(statement.test)) continue;
|
|
29960
|
+
let returnsEarly = false;
|
|
29961
|
+
walkAst(statement.consequent, (child) => {
|
|
29962
|
+
if (isFunctionLike$1(child)) return false;
|
|
29963
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
29964
|
+
returnsEarly = true;
|
|
29965
|
+
return false;
|
|
29966
|
+
}
|
|
29967
|
+
});
|
|
29968
|
+
if (returnsEarly) return true;
|
|
29969
|
+
}
|
|
29970
|
+
return false;
|
|
29971
|
+
};
|
|
29972
|
+
const findEnclosingJsxOpeningElement = (node) => {
|
|
29973
|
+
let cursor = node.parent;
|
|
29974
|
+
while (cursor) {
|
|
29975
|
+
if (isNodeOfType(cursor, "JSXElement")) return cursor.openingElement;
|
|
29976
|
+
if (isNodeOfType(cursor, "JSXFragment")) return null;
|
|
29977
|
+
cursor = cursor.parent ?? null;
|
|
29978
|
+
}
|
|
29979
|
+
return null;
|
|
29980
|
+
};
|
|
29981
|
+
const noLocaleFormatInRender = defineRule({
|
|
29982
|
+
id: "no-locale-format-in-render",
|
|
29983
|
+
title: "Locale/timezone formatting during render",
|
|
29984
|
+
severity: "warn",
|
|
29985
|
+
category: "Correctness",
|
|
29986
|
+
disabledWhen: [
|
|
29987
|
+
"vite",
|
|
29988
|
+
"cra",
|
|
29989
|
+
"expo",
|
|
29990
|
+
"react-native",
|
|
29991
|
+
"unknown"
|
|
29992
|
+
],
|
|
29993
|
+
recommendation: "Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.",
|
|
29994
|
+
create: (context) => {
|
|
29995
|
+
if (isTestlikeFilename(context.filename)) return {};
|
|
29996
|
+
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
29997
|
+
let fileHasUseClientDirective = false;
|
|
29998
|
+
let fileIsEmailTemplate = false;
|
|
29999
|
+
const reportedNodes = /* @__PURE__ */ new Set();
|
|
30000
|
+
const reportIfRenderPhase = (match) => {
|
|
30001
|
+
if (reportedNodes.has(match.node)) return;
|
|
30002
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(match.node);
|
|
30003
|
+
if (!componentOrHookNode) return;
|
|
30004
|
+
if (fileIsEmailTemplate) return;
|
|
30005
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30006
|
+
if (isInsideClientOnlyGuard(match.node)) return;
|
|
30007
|
+
if (isGatedByFalsyInitialState(match.node)) return;
|
|
30008
|
+
if (isAfterClientOnlyEarlyReturn(match.node, componentOrHookNode)) return;
|
|
30009
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(match.node))) return;
|
|
30010
|
+
if (isGeneratedImageRenderContext(context, findEnclosingJsxOpeningElement(match.node)?.parent ?? match.node)) return;
|
|
30011
|
+
reportedNodes.add(match.node);
|
|
30012
|
+
context.report({
|
|
30013
|
+
node: match.node,
|
|
30014
|
+
message: `This can cause a hydration mismatch because ${match.display} formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
|
|
30015
|
+
});
|
|
30016
|
+
};
|
|
30017
|
+
return {
|
|
30018
|
+
Program(node) {
|
|
30019
|
+
fileHasUseClientDirective = hasDirective(node, "use client");
|
|
30020
|
+
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
30021
|
+
},
|
|
30022
|
+
CallExpression(node) {
|
|
30023
|
+
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
30024
|
+
if (match) reportIfRenderPhase(match);
|
|
30025
|
+
},
|
|
30026
|
+
TemplateLiteral(node) {
|
|
30027
|
+
const match = matchDateDefaultStringification(node);
|
|
30028
|
+
if (match) reportIfRenderPhase(match);
|
|
30029
|
+
},
|
|
30030
|
+
JSXExpressionContainer(node) {
|
|
30031
|
+
const expression = stripParenExpression(node.expression);
|
|
30032
|
+
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
30033
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
30034
|
+
const helperName = expression.callee.name;
|
|
30035
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(node);
|
|
30036
|
+
if (!componentOrHookNode) return;
|
|
30037
|
+
const helperNode = findVariableInitializer(expression.callee, helperName)?.initializer;
|
|
30038
|
+
if (!helperNode || !isFunctionLike$1(helperNode)) return;
|
|
30039
|
+
if (componentOrHookDisplayNameForFunction(helperNode)) return;
|
|
30040
|
+
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
30041
|
+
if (isFunctionLike$1(child)) return false;
|
|
30042
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
30043
|
+
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
30044
|
+
if (!match || reportedNodes.has(match.node)) return;
|
|
30045
|
+
if (fileIsEmailTemplate) return;
|
|
30046
|
+
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
30047
|
+
if (isInsideClientOnlyGuard(node)) return;
|
|
30048
|
+
if (isGatedByFalsyInitialState(node)) return;
|
|
30049
|
+
if (isAfterClientOnlyEarlyReturn(node, componentOrHookNode)) return;
|
|
30050
|
+
if (hasSuppressHydrationWarningAttribute(findEnclosingJsxOpeningElement(node))) return;
|
|
30051
|
+
reportedNodes.add(match.node);
|
|
30052
|
+
context.report({
|
|
30053
|
+
node: match.node,
|
|
30054
|
+
message: `This can cause a hydration mismatch because ${match.display} (reached from JSX through "${helperName}") formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.`
|
|
30055
|
+
});
|
|
30056
|
+
});
|
|
30057
|
+
}
|
|
30058
|
+
};
|
|
30059
|
+
}
|
|
30060
|
+
});
|
|
30061
|
+
//#endregion
|
|
29595
30062
|
//#region src/plugin/rules/design/no-long-transition-duration.ts
|
|
29596
30063
|
const hasInfiniteIterationCount = (properties) => properties.some((property) => {
|
|
29597
30064
|
if (getStylePropertyKey(property) !== "animationIterationCount") return false;
|
|
@@ -31704,12 +32171,12 @@ const noPassDataToParent = defineRule({
|
|
|
31704
32171
|
if (calleeNode === identifier) {
|
|
31705
32172
|
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
31706
32173
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
31707
|
-
} else if (isNodeOfType(calleeNode, "MemberExpression") &&
|
|
32174
|
+
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
31708
32175
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
31709
32176
|
if (isCustomHookParameter(ref)) continue;
|
|
31710
32177
|
} else continue;
|
|
31711
32178
|
const methodName = getCallMethodName(calleeNode);
|
|
31712
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32179
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31713
32180
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31714
32181
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
31715
32182
|
if (isNamespacedApiCallee(calleeNode)) continue;
|
|
@@ -31900,7 +32367,7 @@ const noPassLiveStateToParent = defineRule({
|
|
|
31900
32367
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
31901
32368
|
const calleeNode = callExpr.callee;
|
|
31902
32369
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
31903
|
-
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && calleeNode.object === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
32370
|
+
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
31904
32371
|
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
31905
32372
|
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
31906
32373
|
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
@@ -32434,7 +32901,7 @@ const isAlwaysFreshExpression = (expression) => {
|
|
|
32434
32901
|
return `${callee.name}()`;
|
|
32435
32902
|
}
|
|
32436
32903
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
32437
|
-
const receiver = callee.object;
|
|
32904
|
+
const receiver = stripParenExpression(callee.object);
|
|
32438
32905
|
const property = callee.property;
|
|
32439
32906
|
if (!isNodeOfType(property, "Identifier")) return null;
|
|
32440
32907
|
if (isNodeOfType(receiver, "Identifier")) {
|
|
@@ -32555,8 +33022,10 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32555
33022
|
if (typeof sourceValue !== "string") return;
|
|
32556
33023
|
if (handleExtraSource?.(node, context)) return;
|
|
32557
33024
|
if (sourceValue !== source) return;
|
|
33025
|
+
if (isTypeOnlyImport(node)) return;
|
|
32558
33026
|
for (const specifier of node.specifiers ?? []) {
|
|
32559
33027
|
if (isNodeOfType(specifier, "ImportSpecifier")) {
|
|
33028
|
+
if (specifier.importKind === "type") continue;
|
|
32560
33029
|
const importedName = getImportedName$1(specifier);
|
|
32561
33030
|
if (!importedName) continue;
|
|
32562
33031
|
const message = messages.get(importedName);
|
|
@@ -32575,8 +33044,9 @@ const createDeprecatedReactImportRule = ({ source, messages, handleExtraSource }
|
|
|
32575
33044
|
MemberExpression(node) {
|
|
32576
33045
|
if (namespaceBindings.size === 0) return;
|
|
32577
33046
|
if (node.computed) return;
|
|
32578
|
-
|
|
32579
|
-
if (!
|
|
33047
|
+
const receiver = stripParenExpression(node.object);
|
|
33048
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
33049
|
+
if (!namespaceBindings.has(receiver.name)) return;
|
|
32580
33050
|
if (!isNodeOfType(node.property, "Identifier")) return;
|
|
32581
33051
|
const message = messages.get(node.property.name);
|
|
32582
33052
|
if (message) context.report({
|
|
@@ -32641,7 +33111,7 @@ const noReactDomDeprecatedApis = defineRule({
|
|
|
32641
33111
|
});
|
|
32642
33112
|
//#endregion
|
|
32643
33113
|
//#region src/plugin/rules/architecture/no-react19-deprecated-apis.ts
|
|
32644
|
-
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]
|
|
33114
|
+
const REACT_19_DEPRECATED_MESSAGES = new Map([["forwardRef", "forwardRef is dead weight in React 19, since ref is a normal prop now, so drop it & pass ref straight through."]]);
|
|
32645
33115
|
const isVendoredShadcnUiFilename = (rawFilename) => {
|
|
32646
33116
|
if (!rawFilename) return false;
|
|
32647
33117
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
@@ -32679,7 +33149,7 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
32679
33149
|
requires: ["react:19"],
|
|
32680
33150
|
tags: ["test-noise", "migration-hint"],
|
|
32681
33151
|
severity: "warn",
|
|
32682
|
-
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19.
|
|
33152
|
+
recommendation: "Pass `ref` as a normal prop on function components, since `forwardRef` isn't needed in React 19. Only runs on React 19+ projects.",
|
|
32683
33153
|
create: (context) => {
|
|
32684
33154
|
if (isVendoredShadcnUiFilename(context.filename)) return {};
|
|
32685
33155
|
return deprecatedReactImportRule.create(buildOncePerApiContext(context));
|
|
@@ -33003,34 +33473,11 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
33003
33473
|
});
|
|
33004
33474
|
//#endregion
|
|
33005
33475
|
//#region src/plugin/rules/architecture/no-render-in-render.ts
|
|
33006
|
-
const tracesToPropOrParameter = (symbol, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
33007
|
-
if (!symbol || visitedSymbols.has(symbol)) return false;
|
|
33008
|
-
visitedSymbols.add(symbol);
|
|
33009
|
-
if (isComponentParameterSymbol(symbol)) return true;
|
|
33010
|
-
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
33011
|
-
const source = symbol.initializer;
|
|
33012
|
-
if (!source) return false;
|
|
33013
|
-
return initializerRootsInProps(source, scopes, visitedSymbols);
|
|
33014
|
-
};
|
|
33015
|
-
const initializerRootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
33016
|
-
if (isNodeOfType(node, "LogicalExpression")) return initializerRootsInProps(node.left, scopes, visitedSymbols) || initializerRootsInProps(node.right, scopes, visitedSymbols);
|
|
33017
|
-
if (isNodeOfType(node, "ConditionalExpression")) return initializerRootsInProps(node.consequent, scopes, visitedSymbols) || initializerRootsInProps(node.alternate, scopes, visitedSymbols);
|
|
33018
|
-
return rootsInProps(node, scopes, visitedSymbols);
|
|
33019
|
-
};
|
|
33020
|
-
const rootsInProps = (node, scopes, visitedSymbols = /* @__PURE__ */ new Set()) => {
|
|
33021
|
-
let current = node;
|
|
33022
|
-
while (isNodeOfType(current, "MemberExpression")) {
|
|
33023
|
-
if (isNodeOfType(current.object, "ThisExpression") && isNodeOfType(current.property, "Identifier") && current.property.name === "props") return true;
|
|
33024
|
-
current = current.object;
|
|
33025
|
-
}
|
|
33026
|
-
if (isNodeOfType(current, "Identifier")) return tracesToPropOrParameter(scopes.symbolFor(current), scopes, visitedSymbols);
|
|
33027
|
-
return false;
|
|
33028
|
-
};
|
|
33029
33476
|
const isInsideComponentContext = (node) => {
|
|
33030
33477
|
let cursor = node.parent;
|
|
33031
33478
|
while (cursor) {
|
|
33032
|
-
if (isNodeOfType(cursor, "ClassDeclaration") || isNodeOfType(cursor, "ClassExpression")) return true;
|
|
33033
33479
|
if (isFunctionLike$1(cursor) && isComponentFunction$1(cursor)) return true;
|
|
33480
|
+
if (isEs5Component(cursor) || isEs6Component(cursor)) return true;
|
|
33034
33481
|
cursor = cursor.parent ?? null;
|
|
33035
33482
|
}
|
|
33036
33483
|
return false;
|
|
@@ -33040,24 +33487,28 @@ const functionBodyOf = (node) => {
|
|
|
33040
33487
|
if (isNodeOfType(node, "VariableDeclarator") && node.init && isFunctionLike$1(node.init)) return node.init.body ?? null;
|
|
33041
33488
|
return null;
|
|
33042
33489
|
};
|
|
33490
|
+
const isHookCallee = (callee) => {
|
|
33491
|
+
if (isNodeOfType(callee, "Identifier")) return isReactHookName(callee.name);
|
|
33492
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && isUppercaseName(callee.object.name) && isNodeOfType(callee.property, "Identifier")) return isReactHookName(callee.property.name);
|
|
33493
|
+
return false;
|
|
33494
|
+
};
|
|
33043
33495
|
const containsHookCall = (body) => {
|
|
33044
33496
|
let found = false;
|
|
33045
33497
|
walkAst(body, (child) => {
|
|
33046
|
-
if (found) return;
|
|
33498
|
+
if (found) return false;
|
|
33499
|
+
if (child !== body && isFunctionLike$1(child) && isComponentFunction$1(child)) return false;
|
|
33047
33500
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
33048
|
-
|
|
33049
|
-
if (name && isReactHookName(name)) found = true;
|
|
33501
|
+
if (isHookCallee(child.callee)) found = true;
|
|
33050
33502
|
});
|
|
33051
33503
|
return found;
|
|
33052
33504
|
};
|
|
33053
|
-
const
|
|
33505
|
+
const isHookCallingRenderHelper = (symbol) => {
|
|
33054
33506
|
if (!symbol) return false;
|
|
33055
33507
|
const declaration = symbol.declarationNode;
|
|
33056
33508
|
if (!isNodeOfType(declaration, "FunctionDeclaration") && !isNodeOfType(declaration, "VariableDeclarator")) return false;
|
|
33057
33509
|
const body = functionBodyOf(declaration);
|
|
33058
33510
|
if (!body) return false;
|
|
33059
|
-
|
|
33060
|
-
return !containsHookCall(body);
|
|
33511
|
+
return containsHookCall(body);
|
|
33061
33512
|
};
|
|
33062
33513
|
const noRenderInRender = defineRule({
|
|
33063
33514
|
id: "no-render-in-render",
|
|
@@ -33066,20 +33517,13 @@ const noRenderInRender = defineRule({
|
|
|
33066
33517
|
tags: ["test-noise"],
|
|
33067
33518
|
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
33068
33519
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
33069
|
-
const expression = node.expression;
|
|
33520
|
+
const expression = isNodeOfType(node.expression, "ChainExpression") ? node.expression.expression : node.expression;
|
|
33070
33521
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
33071
|
-
|
|
33072
|
-
|
|
33073
|
-
|
|
33074
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33522
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return;
|
|
33523
|
+
const calleeName = expression.callee.name;
|
|
33524
|
+
if (!RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
33075
33525
|
if (!isInsideComponentContext(node)) return;
|
|
33076
|
-
if (
|
|
33077
|
-
const calleeSymbol = context.scopes.symbolFor(expression.callee);
|
|
33078
|
-
if (tracesToPropOrParameter(calleeSymbol, context.scopes)) return;
|
|
33079
|
-
if (isModuleScopeHookFreeHelper(calleeSymbol)) return;
|
|
33080
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
33081
|
-
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
33082
|
-
}
|
|
33526
|
+
if (!isHookCallingRenderHelper(context.scopes.symbolFor(expression.callee))) return;
|
|
33083
33527
|
context.report({
|
|
33084
33528
|
node: expression,
|
|
33085
33529
|
message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
|
|
@@ -33140,13 +33584,7 @@ const noRenderPropChildren = defineRule({
|
|
|
33140
33584
|
//#endregion
|
|
33141
33585
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
33142
33586
|
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
33143
|
-
const isReactDomRenderCall = (node) =>
|
|
33144
|
-
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
33145
|
-
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
33146
|
-
if (node.callee.object.name !== "ReactDOM") return false;
|
|
33147
|
-
if (!isNodeOfType(node.callee.property, "Identifier")) return false;
|
|
33148
|
-
return node.callee.property.name === "render";
|
|
33149
|
-
};
|
|
33587
|
+
const isReactDomRenderCall = (node) => isGlobalMethodCall(node, "ReactDOM", "render");
|
|
33150
33588
|
const isUsedAsReturnValue = (parent) => {
|
|
33151
33589
|
if (!parent) return false;
|
|
33152
33590
|
if (isNodeOfType(parent, "VariableDeclarator") || isNodeOfType(parent, "Property") || isNodeOfType(parent, "ReturnStatement") || isNodeOfType(parent, "AssignmentExpression")) return true;
|
|
@@ -33982,7 +34420,7 @@ const noSetState = defineRule({
|
|
|
33982
34420
|
category: "Architecture",
|
|
33983
34421
|
create: (context) => ({ CallExpression(node) {
|
|
33984
34422
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
33985
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
34423
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
33986
34424
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
33987
34425
|
if (!getParentComponent(node)) return;
|
|
33988
34426
|
context.report({
|
|
@@ -36428,6 +36866,36 @@ const isTriviallyCheapExpression = (node) => {
|
|
|
36428
36866
|
if (isNodeOfType(innerExpression, "MemberExpression")) return false;
|
|
36429
36867
|
return true;
|
|
36430
36868
|
};
|
|
36869
|
+
const isTrivialContainerLiteral = (node) => {
|
|
36870
|
+
if (!node) return false;
|
|
36871
|
+
const innerExpression = stripParenExpression(node);
|
|
36872
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) return (innerExpression.elements ?? []).every((element) => element !== null && isSimpleExpression(element));
|
|
36873
|
+
if (isNodeOfType(innerExpression, "ObjectExpression")) return (innerExpression.properties ?? []).every((property) => isNodeOfType(property, "Property") && !property.computed && isSimpleExpression(property.value));
|
|
36874
|
+
return false;
|
|
36875
|
+
};
|
|
36876
|
+
const isNonEscapingRead = (identifier) => {
|
|
36877
|
+
const readRoot = findTransparentExpressionRoot(identifier);
|
|
36878
|
+
const memberNode = readRoot.parent;
|
|
36879
|
+
if (!isNodeOfType(memberNode, "MemberExpression") || memberNode.object !== readRoot) return false;
|
|
36880
|
+
const memberUse = findTransparentExpressionRoot(memberNode);
|
|
36881
|
+
const memberUseParent = memberUse.parent;
|
|
36882
|
+
if (isNodeOfType(memberUseParent, "AssignmentExpression") && memberUseParent.left === memberUse) return false;
|
|
36883
|
+
if (isNodeOfType(memberUseParent, "UpdateExpression")) return false;
|
|
36884
|
+
if (isNodeOfType(memberUseParent, "UnaryExpression") && memberUseParent.operator === "delete") return false;
|
|
36885
|
+
return !(isNodeOfType(memberUseParent, "CallExpression") && memberUseParent.callee === memberUse && !memberNode.computed && isNodeOfType(memberNode.property, "Identifier") && MUTATING_ARRAY_METHODS.has(memberNode.property.name));
|
|
36886
|
+
};
|
|
36887
|
+
const isMemoIdentityUnused = (memoCallNode, scopes) => {
|
|
36888
|
+
const memoUsageRoot = findTransparentExpressionRoot(memoCallNode);
|
|
36889
|
+
const parentNode = memoUsageRoot.parent;
|
|
36890
|
+
if (isNodeOfType(parentNode, "ExpressionStatement")) return true;
|
|
36891
|
+
if (!isNodeOfType(parentNode, "VariableDeclarator") || parentNode.init !== memoUsageRoot) return false;
|
|
36892
|
+
const bindingTarget = parentNode.id;
|
|
36893
|
+
if (isNodeOfType(bindingTarget, "ArrayPattern") || isNodeOfType(bindingTarget, "ObjectPattern")) return true;
|
|
36894
|
+
if (!isNodeOfType(bindingTarget, "Identifier")) return false;
|
|
36895
|
+
const symbol = scopes.symbolFor(bindingTarget);
|
|
36896
|
+
if (!symbol) return false;
|
|
36897
|
+
return symbol.references.every((reference) => reference.flag === "read" && isNonEscapingRead(reference.identifier));
|
|
36898
|
+
};
|
|
36431
36899
|
const noUsememoSimpleExpression = defineRule({
|
|
36432
36900
|
id: "no-usememo-simple-expression",
|
|
36433
36901
|
title: "useMemo on a cheap value",
|
|
@@ -36449,9 +36917,17 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
36449
36917
|
let returnExpression = null;
|
|
36450
36918
|
if (!isNodeOfType(callback.body, "BlockStatement")) returnExpression = callback.body;
|
|
36451
36919
|
else if (callback.body.body?.length === 1 && isNodeOfType(callback.body.body[0], "ReturnStatement")) returnExpression = callback.body.body[0].argument;
|
|
36452
|
-
if (returnExpression
|
|
36920
|
+
if (!returnExpression) return;
|
|
36921
|
+
if (isTriviallyCheapExpression(returnExpression)) {
|
|
36922
|
+
context.report({
|
|
36923
|
+
node,
|
|
36924
|
+
message: "This costs more than it saves because useMemo is wrapping a value that's already cheap, so remove the useMemo"
|
|
36925
|
+
});
|
|
36926
|
+
return;
|
|
36927
|
+
}
|
|
36928
|
+
if (isTrivialContainerLiteral(returnExpression) && isMemoIdentityUnused(node, context.scopes)) context.report({
|
|
36453
36929
|
node,
|
|
36454
|
-
message: "This
|
|
36930
|
+
message: "This useMemo rebuilds a tiny literal whose reference is never relied on, so remove the useMemo and build the value inline"
|
|
36455
36931
|
});
|
|
36456
36932
|
} })
|
|
36457
36933
|
});
|
|
@@ -36557,7 +37033,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
36557
37033
|
const activeLifecycleNames = isReactBelow16_3(context.settings) ? new Set(["componentWillUpdate"]) : LIFECYCLE_NAMES;
|
|
36558
37034
|
return { CallExpression(node) {
|
|
36559
37035
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
36560
|
-
if (!isNodeOfType(node.callee.object, "ThisExpression")) return;
|
|
37036
|
+
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36561
37037
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36562
37038
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36563
37039
|
context.report({
|
|
@@ -36913,7 +37389,7 @@ const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
|
36913
37389
|
continue;
|
|
36914
37390
|
}
|
|
36915
37391
|
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
36916
|
-
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
37392
|
+
if ((isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) && functionContainsReactRenderOutput(value, state.scopes)) return true;
|
|
36917
37393
|
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
36918
37394
|
}
|
|
36919
37395
|
return false;
|
|
@@ -37021,18 +37497,22 @@ const onlyExportComponents = defineRule({
|
|
|
37021
37497
|
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
37022
37498
|
allowExportNames: new Set(settings.allowExportNames),
|
|
37023
37499
|
allowConstantExport: settings.allowConstantExport,
|
|
37024
|
-
localComponentNames
|
|
37500
|
+
localComponentNames,
|
|
37501
|
+
scopes: context.scopes
|
|
37025
37502
|
};
|
|
37026
37503
|
for (const child of componentCandidates) {
|
|
37027
37504
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
37028
|
-
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37505
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
|
|
37029
37506
|
}
|
|
37030
37507
|
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
37031
37508
|
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
37032
37509
|
}
|
|
37033
37510
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
37034
37511
|
const initializer = child.init;
|
|
37035
|
-
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child))
|
|
37512
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
|
|
37513
|
+
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
37514
|
+
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
37515
|
+
}
|
|
37036
37516
|
}
|
|
37037
37517
|
}
|
|
37038
37518
|
const exports = [];
|
|
@@ -38697,11 +39177,22 @@ const getSubscriptionHandlerArgument = (subscribeCall, effectBodyStatements) =>
|
|
|
38697
39177
|
}
|
|
38698
39178
|
return null;
|
|
38699
39179
|
};
|
|
39180
|
+
const isTrivialLiteralExpression = (expression) => {
|
|
39181
|
+
if (isNodeOfType(expression, "Literal")) return true;
|
|
39182
|
+
if (isNodeOfType(expression, "Identifier")) return expression.name === "undefined";
|
|
39183
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-") return isNodeOfType(expression.argument, "Literal");
|
|
39184
|
+
if (isNodeOfType(expression, "TemplateLiteral")) return (expression.expressions?.length ?? 0) === 0;
|
|
39185
|
+
return false;
|
|
39186
|
+
};
|
|
38700
39187
|
const getSingleSetterCallFromHandler = (handler) => {
|
|
38701
39188
|
const handlerStatements = getCallbackStatements(handler);
|
|
38702
39189
|
if (handlerStatements.length !== 1) return null;
|
|
38703
39190
|
const onlyStatement = handlerStatements[0];
|
|
38704
|
-
|
|
39191
|
+
let expression = onlyStatement;
|
|
39192
|
+
if (isNodeOfType(onlyStatement, "ExpressionStatement")) expression = onlyStatement.expression;
|
|
39193
|
+
if (isNodeOfType(onlyStatement, "ReturnStatement")) expression = onlyStatement.argument;
|
|
39194
|
+
if (!expression) return null;
|
|
39195
|
+
expression = stripParenExpression(expression);
|
|
38705
39196
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
38706
39197
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
38707
39198
|
if (!isSetterIdentifier(expression.callee.name)) return null;
|
|
@@ -38711,6 +39202,106 @@ const getSingleSetterCallFromHandler = (handler) => {
|
|
|
38711
39202
|
setterArgument: expression.arguments[0]
|
|
38712
39203
|
};
|
|
38713
39204
|
};
|
|
39205
|
+
const isListenerCollectionInitializer = (init) => {
|
|
39206
|
+
if (!init) return false;
|
|
39207
|
+
if (isNodeOfType(init, "ArrayExpression")) return true;
|
|
39208
|
+
return isNodeOfType(init, "NewExpression") && isNodeOfType(init.callee, "Identifier") && init.callee.name === "Set";
|
|
39209
|
+
};
|
|
39210
|
+
const functionRegistersParameterIntoCollection = (functionNode, listenerCollectionNames) => {
|
|
39211
|
+
if (!isFunctionLike$1(functionNode)) return false;
|
|
39212
|
+
const firstParam = functionNode.params?.[0];
|
|
39213
|
+
if (!isNodeOfType(firstParam, "Identifier")) return false;
|
|
39214
|
+
const listenerParamName = firstParam.name;
|
|
39215
|
+
let registersListener = false;
|
|
39216
|
+
walkAst(functionNode.body, (child) => {
|
|
39217
|
+
if (registersListener) return false;
|
|
39218
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39219
|
+
if (!isNodeOfType(child.callee, "MemberExpression")) return;
|
|
39220
|
+
if (!isNodeOfType(child.callee.object, "Identifier")) return;
|
|
39221
|
+
if (!listenerCollectionNames.has(child.callee.object.name)) return;
|
|
39222
|
+
if (!isNodeOfType(child.callee.property, "Identifier")) return;
|
|
39223
|
+
if (child.callee.property.name !== "add" && child.callee.property.name !== "push") return;
|
|
39224
|
+
const registeredArgument = child.arguments?.[0];
|
|
39225
|
+
if (isNodeOfType(registeredArgument, "Identifier") && registeredArgument.name === listenerParamName) registersListener = true;
|
|
39226
|
+
});
|
|
39227
|
+
return registersListener;
|
|
39228
|
+
};
|
|
39229
|
+
const buildModuleScopeStoreIndex = (programRoot) => {
|
|
39230
|
+
const mutableBindingNames = /* @__PURE__ */ new Set();
|
|
39231
|
+
const listenerCollectionNames = /* @__PURE__ */ new Set();
|
|
39232
|
+
const moduleFunctionsByName = /* @__PURE__ */ new Map();
|
|
39233
|
+
if (!isNodeOfType(programRoot, "Program")) return {
|
|
39234
|
+
mutableBindingNames,
|
|
39235
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39236
|
+
};
|
|
39237
|
+
for (const statement of programRoot.body ?? []) {
|
|
39238
|
+
const unwrapped = isNodeOfType(statement, "ExportNamedDeclaration") && statement.declaration ? statement.declaration : statement;
|
|
39239
|
+
if (isNodeOfType(unwrapped, "FunctionDeclaration") && unwrapped.id) {
|
|
39240
|
+
moduleFunctionsByName.set(unwrapped.id.name, unwrapped);
|
|
39241
|
+
continue;
|
|
39242
|
+
}
|
|
39243
|
+
if (!isNodeOfType(unwrapped, "VariableDeclaration")) continue;
|
|
39244
|
+
for (const declarator of unwrapped.declarations ?? []) {
|
|
39245
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
39246
|
+
const init = declarator.init ?? null;
|
|
39247
|
+
if ((unwrapped.kind === "let" || unwrapped.kind === "var") && init && !isFunctionLike$1(init)) {
|
|
39248
|
+
mutableBindingNames.add(declarator.id.name);
|
|
39249
|
+
continue;
|
|
39250
|
+
}
|
|
39251
|
+
if (isListenerCollectionInitializer(init)) {
|
|
39252
|
+
listenerCollectionNames.add(declarator.id.name);
|
|
39253
|
+
continue;
|
|
39254
|
+
}
|
|
39255
|
+
if (init && isFunctionLike$1(init)) moduleFunctionsByName.set(declarator.id.name, init);
|
|
39256
|
+
}
|
|
39257
|
+
}
|
|
39258
|
+
const subscribeFunctionNames = /* @__PURE__ */ new Set();
|
|
39259
|
+
for (const [functionName, functionNode] of moduleFunctionsByName) if (functionRegistersParameterIntoCollection(functionNode, listenerCollectionNames)) subscribeFunctionNames.add(functionName);
|
|
39260
|
+
return {
|
|
39261
|
+
mutableBindingNames,
|
|
39262
|
+
subscribeFunctionNames
|
|
39263
|
+
};
|
|
39264
|
+
};
|
|
39265
|
+
const getModuleStoreSnapshotName = (useStateCall, storeIndex) => {
|
|
39266
|
+
if (!isNodeOfType(useStateCall, "CallExpression")) return null;
|
|
39267
|
+
let initialArgument = stripParenExpression(useStateCall.arguments?.[0]);
|
|
39268
|
+
if (initialArgument && isFunctionLike$1(initialArgument) && !isNodeOfType(initialArgument.body, "BlockStatement")) initialArgument = stripParenExpression(initialArgument.body);
|
|
39269
|
+
if (!isNodeOfType(initialArgument, "Identifier")) return null;
|
|
39270
|
+
if (!storeIndex.mutableBindingNames.has(initialArgument.name)) return null;
|
|
39271
|
+
const binding = findVariableInitializer(initialArgument, initialArgument.name);
|
|
39272
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return null;
|
|
39273
|
+
return initialArgument.name;
|
|
39274
|
+
};
|
|
39275
|
+
const argumentForwardsSetter = (argument, setterName) => {
|
|
39276
|
+
if (!argument) return false;
|
|
39277
|
+
const unwrapped = stripParenExpression(argument);
|
|
39278
|
+
if (isNodeOfType(unwrapped, "Identifier")) return unwrapped.name === setterName;
|
|
39279
|
+
if (!isFunctionLike$1(unwrapped)) return false;
|
|
39280
|
+
let callsSetter = false;
|
|
39281
|
+
walkAst(unwrapped.body, (child) => {
|
|
39282
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName) {
|
|
39283
|
+
callsSetter = true;
|
|
39284
|
+
return false;
|
|
39285
|
+
}
|
|
39286
|
+
});
|
|
39287
|
+
return callsSetter;
|
|
39288
|
+
};
|
|
39289
|
+
const findModuleSubscribeCallForwardingSetter = (effectCallback, setterName, storeIndex) => {
|
|
39290
|
+
let matchedCall = null;
|
|
39291
|
+
walkAst((isFunctionLike$1(effectCallback) ? effectCallback.body : null) ?? effectCallback, (child) => {
|
|
39292
|
+
if (matchedCall) return false;
|
|
39293
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
39294
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
39295
|
+
if (!storeIndex.subscribeFunctionNames.has(child.callee.name)) return;
|
|
39296
|
+
const binding = findVariableInitializer(child.callee, child.callee.name);
|
|
39297
|
+
if (!binding || !isNodeOfType(binding.scopeOwner, "Program")) return;
|
|
39298
|
+
for (const argument of child.arguments ?? []) if (argumentForwardsSetter(argument, setterName)) {
|
|
39299
|
+
matchedCall = child;
|
|
39300
|
+
return false;
|
|
39301
|
+
}
|
|
39302
|
+
});
|
|
39303
|
+
return matchedCall;
|
|
39304
|
+
};
|
|
38714
39305
|
const cleanupReleasesSubscription = (effectBodyStatements, boundReleaseName, boundSubscriptionName) => {
|
|
38715
39306
|
const lastStatement = effectBodyStatements[effectBodyStatements.length - 1];
|
|
38716
39307
|
if (!isNodeOfType(lastStatement, "ReturnStatement")) return false;
|
|
@@ -38727,6 +39318,16 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38727
39318
|
severity: "warn",
|
|
38728
39319
|
recommendation: "Replace the `useState(getSnapshot())` + `useEffect(() => store.subscribe(() => setSnapshot(getSnapshot())))` pair with `useSyncExternalStore(store.subscribe, getSnapshot)`. The hook gets this right during concurrent rendering and on the server; the hand-rolled version doesn't.",
|
|
38729
39320
|
create: (context) => {
|
|
39321
|
+
let cachedStoreIndex = null;
|
|
39322
|
+
const storeIndexFor = (node) => {
|
|
39323
|
+
if (cachedStoreIndex) return cachedStoreIndex;
|
|
39324
|
+
const programRoot = findProgramRoot(node);
|
|
39325
|
+
cachedStoreIndex = programRoot ? buildModuleScopeStoreIndex(programRoot) : {
|
|
39326
|
+
mutableBindingNames: /* @__PURE__ */ new Set(),
|
|
39327
|
+
subscribeFunctionNames: /* @__PURE__ */ new Set()
|
|
39328
|
+
};
|
|
39329
|
+
return cachedStoreIndex;
|
|
39330
|
+
};
|
|
38730
39331
|
const checkComponent = (componentBody) => {
|
|
38731
39332
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
38732
39333
|
const useStateBindings = collectUseStateBindings(componentBody);
|
|
@@ -38764,6 +39365,7 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38764
39365
|
const useStateInitializer = useStateInitializerByValueName.get(valueName);
|
|
38765
39366
|
if (!useStateInitializer) continue;
|
|
38766
39367
|
if (!areExpressionsStructurallyEqual(useStateInitializer, setterPayload.setterArgument)) continue;
|
|
39368
|
+
if (isTrivialLiteralExpression(setterPayload.setterArgument)) continue;
|
|
38767
39369
|
if (!cleanupReleasesSubscription(effectBodyStatements, subscription.boundReleaseName, subscription.boundSubscriptionName)) continue;
|
|
38768
39370
|
const matchingBinding = useStateBindings.find((binding) => binding.valueName === valueName);
|
|
38769
39371
|
context.report({
|
|
@@ -38771,14 +39373,47 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
38771
39373
|
message: `Your users can see stale or torn values because useState "${valueName}" syncs an outside store through a useEffect.`
|
|
38772
39374
|
});
|
|
38773
39375
|
}
|
|
39376
|
+
checkModuleStoreShape(componentBody, useStateBindings);
|
|
39377
|
+
};
|
|
39378
|
+
const checkModuleStoreShape = (componentBody, useStateBindings) => {
|
|
39379
|
+
const storeIndex = storeIndexFor(componentBody);
|
|
39380
|
+
if (storeIndex.mutableBindingNames.size === 0) return;
|
|
39381
|
+
if (storeIndex.subscribeFunctionNames.size === 0) return;
|
|
39382
|
+
const snapshotBindings = useStateBindings.map((binding) => ({
|
|
39383
|
+
binding,
|
|
39384
|
+
storeName: isNodeOfType(binding.declarator.init, "CallExpression") ? getModuleStoreSnapshotName(binding.declarator.init, storeIndex) : null
|
|
39385
|
+
})).filter((candidate) => candidate.storeName !== null);
|
|
39386
|
+
if (snapshotBindings.length === 0) return;
|
|
39387
|
+
const reportedDeclarators = /* @__PURE__ */ new Set();
|
|
39388
|
+
for (const effectCall of findUseEffectsInComponent(componentBody)) {
|
|
39389
|
+
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
39390
|
+
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
39391
|
+
const depsNode = effectCall.arguments[1];
|
|
39392
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) continue;
|
|
39393
|
+
if ((depsNode.elements?.length ?? 0) !== 0) continue;
|
|
39394
|
+
const callback = getEffectCallback(effectCall);
|
|
39395
|
+
if (!callback || !isFunctionLike$1(callback)) continue;
|
|
39396
|
+
for (const { binding, storeName } of snapshotBindings) {
|
|
39397
|
+
if (reportedDeclarators.has(binding.declarator)) continue;
|
|
39398
|
+
if (!findModuleSubscribeCallForwardingSetter(callback, binding.setterName, storeIndex)) continue;
|
|
39399
|
+
reportedDeclarators.add(binding.declarator);
|
|
39400
|
+
context.report({
|
|
39401
|
+
node: binding.declarator,
|
|
39402
|
+
message: `Your users can miss updates or see torn values because useState "${binding.valueName}" snapshots module store "${storeName}" at render but only subscribes later in a useEffect.`
|
|
39403
|
+
});
|
|
39404
|
+
}
|
|
39405
|
+
}
|
|
38774
39406
|
};
|
|
38775
39407
|
return {
|
|
38776
39408
|
FunctionDeclaration(node) {
|
|
38777
|
-
|
|
39409
|
+
const functionName = node.id?.name;
|
|
39410
|
+
if (!functionName) return;
|
|
39411
|
+
if (!isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
38778
39412
|
checkComponent(node.body);
|
|
38779
39413
|
},
|
|
38780
39414
|
VariableDeclarator(node) {
|
|
38781
|
-
|
|
39415
|
+
const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name);
|
|
39416
|
+
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
38782
39417
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
38783
39418
|
checkComponent(node.init.body);
|
|
38784
39419
|
}
|
|
@@ -39388,7 +40023,7 @@ const queryNoVoidQueryFn = defineRule({
|
|
|
39388
40023
|
if (isNodeOfType(queryFnValue, "ArrowFunctionExpression") || isNodeOfType(queryFnValue, "FunctionExpression")) {
|
|
39389
40024
|
const body = queryFnValue.body;
|
|
39390
40025
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
39391
|
-
if ((body.body ?? []).length === 0) context.report({
|
|
40026
|
+
if ((body.body ?? []).filter((statement) => !isNoOpStatement(statement)).length === 0) context.report({
|
|
39392
40027
|
node: queryFnProperty,
|
|
39393
40028
|
message: "This empty queryFn caches undefined, so the component never gets data."
|
|
39394
40029
|
});
|
|
@@ -39895,17 +40530,6 @@ const renderingHoistJsx = defineRule({
|
|
|
39895
40530
|
}
|
|
39896
40531
|
});
|
|
39897
40532
|
//#endregion
|
|
39898
|
-
//#region src/plugin/utils/find-declarator-for-binding.ts
|
|
39899
|
-
const findDeclaratorForBinding = (bindingIdentifier) => {
|
|
39900
|
-
let ancestor = bindingIdentifier.parent;
|
|
39901
|
-
while (ancestor) {
|
|
39902
|
-
if (isNodeOfType(ancestor, "VariableDeclarator")) return ancestor;
|
|
39903
|
-
if (isNodeOfType(ancestor, "FunctionDeclaration") || isNodeOfType(ancestor, "FunctionExpression") || isNodeOfType(ancestor, "ArrowFunctionExpression") || isNodeOfType(ancestor, "Program")) return null;
|
|
39904
|
-
ancestor = ancestor.parent ?? null;
|
|
39905
|
-
}
|
|
39906
|
-
return null;
|
|
39907
|
-
};
|
|
39908
|
-
//#endregion
|
|
39909
40533
|
//#region src/plugin/rules/performance/rendering-hydration-mismatch-time.ts
|
|
39910
40534
|
const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
39911
40535
|
{
|
|
@@ -39914,19 +40538,19 @@ const NONDETERMINISTIC_RENDER_PATTERNS = [
|
|
|
39914
40538
|
},
|
|
39915
40539
|
{
|
|
39916
40540
|
display: "Date.now()",
|
|
39917
|
-
matches: (node) =>
|
|
40541
|
+
matches: (node) => isGlobalMethodCall(node, "Date", "now")
|
|
39918
40542
|
},
|
|
39919
40543
|
{
|
|
39920
40544
|
display: "Math.random()",
|
|
39921
|
-
matches: (node) =>
|
|
40545
|
+
matches: (node) => isGlobalMethodCall(node, "Math", "random")
|
|
39922
40546
|
},
|
|
39923
40547
|
{
|
|
39924
40548
|
display: "performance.now()",
|
|
39925
|
-
matches: (node) =>
|
|
40549
|
+
matches: (node) => isGlobalMethodCall(node, "performance", "now")
|
|
39926
40550
|
},
|
|
39927
40551
|
{
|
|
39928
40552
|
display: "crypto.randomUUID()",
|
|
39929
|
-
matches: (node) =>
|
|
40553
|
+
matches: (node) => isGlobalMethodCall(node, "crypto", "randomUUID")
|
|
39930
40554
|
}
|
|
39931
40555
|
];
|
|
39932
40556
|
const findOpeningElementOfChild = (jsxNode) => {
|
|
@@ -39938,54 +40562,6 @@ const findOpeningElementOfChild = (jsxNode) => {
|
|
|
39938
40562
|
}
|
|
39939
40563
|
return null;
|
|
39940
40564
|
};
|
|
39941
|
-
const executesDuringRender = (functionNode) => {
|
|
39942
|
-
const parent = functionNode.parent;
|
|
39943
|
-
if (!isNodeOfType(parent, "CallExpression")) return false;
|
|
39944
|
-
if (parent.callee === functionNode) return true;
|
|
39945
|
-
return isHookCall$2(parent, "useMemo") && parent.arguments?.[0] === functionNode;
|
|
39946
|
-
};
|
|
39947
|
-
const CLIENT_ONLY_FLAG_NAME_PATTERN = /^(?:is|has|did)?_?(?:client|mounted|hydrated|browser)(?:_?(?:side|ready|only))?$/i;
|
|
39948
|
-
const referencesClientOnlyFlag = (expression) => {
|
|
39949
|
-
const unwrapped = stripParenExpression(expression);
|
|
39950
|
-
if (isNodeOfType(unwrapped, "Identifier")) return CLIENT_ONLY_FLAG_NAME_PATTERN.test(unwrapped.name);
|
|
39951
|
-
if (isNodeOfType(unwrapped, "MemberExpression")) {
|
|
39952
|
-
const property = unwrapped.property;
|
|
39953
|
-
return isNodeOfType(property, "Identifier") && CLIENT_ONLY_FLAG_NAME_PATTERN.test(property.name);
|
|
39954
|
-
}
|
|
39955
|
-
if (isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "!") return referencesClientOnlyFlag(unwrapped.argument);
|
|
39956
|
-
if (isNodeOfType(unwrapped, "LogicalExpression")) return referencesClientOnlyFlag(unwrapped.left) || referencesClientOnlyFlag(unwrapped.right);
|
|
39957
|
-
return false;
|
|
39958
|
-
};
|
|
39959
|
-
const isFalsyLiteral = (node) => {
|
|
39960
|
-
if (!node) return true;
|
|
39961
|
-
if (isNodeOfType(node, "Literal")) return !node.value;
|
|
39962
|
-
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
39963
|
-
};
|
|
39964
|
-
const isFalsyInitialStateBinding = (identifier) => {
|
|
39965
|
-
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
39966
|
-
const binding = findVariableInitializer(identifier, identifier.name);
|
|
39967
|
-
if (!binding) return false;
|
|
39968
|
-
const declarator = findDeclaratorForBinding(binding.bindingIdentifier);
|
|
39969
|
-
if (!declarator?.init) return false;
|
|
39970
|
-
const init = stripParenExpression(declarator.init);
|
|
39971
|
-
if (!isHookCall$2(init, "useState") || !isNodeOfType(init, "CallExpression")) return false;
|
|
39972
|
-
if (!isNodeOfType(declarator.id, "ArrayPattern")) return false;
|
|
39973
|
-
if (declarator.id.elements?.[0] !== binding.bindingIdentifier) return false;
|
|
39974
|
-
return isFalsyLiteral(init.arguments?.[0]);
|
|
39975
|
-
};
|
|
39976
|
-
const referencesFalsyInitialState = (expression) => flattenLogicalAndChain(stripParenExpression(expression)).some((operand) => isFalsyInitialStateBinding(stripParenExpression(operand)));
|
|
39977
|
-
const isGatedByFalsyInitialState = (node) => {
|
|
39978
|
-
let cursor = node;
|
|
39979
|
-
let parent = node.parent;
|
|
39980
|
-
while (parent) {
|
|
39981
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesFalsyInitialState(parent.left)) return true;
|
|
39982
|
-
if (isNodeOfType(parent, "ConditionalExpression") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39983
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesFalsyInitialState(parent.test)) return true;
|
|
39984
|
-
cursor = parent;
|
|
39985
|
-
parent = parent.parent ?? null;
|
|
39986
|
-
}
|
|
39987
|
-
return false;
|
|
39988
|
-
};
|
|
39989
40565
|
const isYearOnlyDateRead = (dateNode) => {
|
|
39990
40566
|
const member = dateNode.parent;
|
|
39991
40567
|
if (!isNodeOfType(member, "MemberExpression") || member.object !== dateNode) return false;
|
|
@@ -40009,23 +40585,6 @@ const isInsideMotionTransitionAttribute = (node) => {
|
|
|
40009
40585
|
}
|
|
40010
40586
|
return false;
|
|
40011
40587
|
};
|
|
40012
|
-
const isInsideClientOnlyGuard = (node) => {
|
|
40013
|
-
let cursor = node;
|
|
40014
|
-
let parent = node.parent;
|
|
40015
|
-
while (parent) {
|
|
40016
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.operator === "&&" && parent.right === cursor && referencesClientOnlyFlag(parent.left)) return true;
|
|
40017
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === cursor || parent.alternate === cursor) && referencesClientOnlyFlag(parent.test)) return true;
|
|
40018
|
-
if (isNodeOfType(parent, "IfStatement") && parent.consequent === cursor && referencesClientOnlyFlag(parent.test)) return true;
|
|
40019
|
-
cursor = parent;
|
|
40020
|
-
parent = parent.parent ?? null;
|
|
40021
|
-
}
|
|
40022
|
-
return false;
|
|
40023
|
-
};
|
|
40024
|
-
const hasSuppressHydrationWarningAttribute = (openingElement) => {
|
|
40025
|
-
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
40026
|
-
for (const attr of openingElement.attributes ?? []) if (isNodeOfType(attr, "JSXAttribute") && isNodeOfType(attr.name, "JSXIdentifier") && attr.name.name === "suppressHydrationWarning") return true;
|
|
40027
|
-
return false;
|
|
40028
|
-
};
|
|
40029
40588
|
const renderingHydrationMismatchTime = defineRule({
|
|
40030
40589
|
id: "rendering-hydration-mismatch-time",
|
|
40031
40590
|
title: "Time or random value in JSX",
|
|
@@ -40073,6 +40632,35 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
40073
40632
|
}
|
|
40074
40633
|
});
|
|
40075
40634
|
//#endregion
|
|
40635
|
+
//#region src/plugin/utils/contains-locale-environment-read.ts
|
|
40636
|
+
const LOCALE_ENVIRONMENT_METHOD_NAMES = new Set([
|
|
40637
|
+
"toLocaleString",
|
|
40638
|
+
"toLocaleDateString",
|
|
40639
|
+
"toLocaleTimeString",
|
|
40640
|
+
"getTimezoneOffset"
|
|
40641
|
+
]);
|
|
40642
|
+
const containsLocaleEnvironmentRead = (expression) => {
|
|
40643
|
+
let readsLocaleEnvironment = false;
|
|
40644
|
+
walkAst(expression, (child) => {
|
|
40645
|
+
if (readsLocaleEnvironment) return false;
|
|
40646
|
+
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.object, "Identifier")) {
|
|
40647
|
+
if (child.object.name === "Intl") {
|
|
40648
|
+
readsLocaleEnvironment = true;
|
|
40649
|
+
return false;
|
|
40650
|
+
}
|
|
40651
|
+
if (child.object.name === "navigator" && isNodeOfType(child.property, "Identifier") && (child.property.name === "language" || child.property.name === "languages")) {
|
|
40652
|
+
readsLocaleEnvironment = true;
|
|
40653
|
+
return false;
|
|
40654
|
+
}
|
|
40655
|
+
}
|
|
40656
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "MemberExpression") && !child.callee.computed && isNodeOfType(child.callee.property, "Identifier") && LOCALE_ENVIRONMENT_METHOD_NAMES.has(child.callee.property.name)) {
|
|
40657
|
+
readsLocaleEnvironment = true;
|
|
40658
|
+
return false;
|
|
40659
|
+
}
|
|
40660
|
+
});
|
|
40661
|
+
return readsLocaleEnvironment;
|
|
40662
|
+
};
|
|
40663
|
+
//#endregion
|
|
40076
40664
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
40077
40665
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
40078
40666
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
@@ -40138,14 +40726,15 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
40138
40726
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
40139
40727
|
const callback = getEffectCallback(node);
|
|
40140
40728
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
40141
|
-
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
40142
|
-
if (
|
|
40729
|
+
const bodyStatements = (isNodeOfType(callback.body, "BlockStatement") ? callback.body.body ?? [] : [callback.body]).filter((statement) => !isNoOpStatement(statement));
|
|
40730
|
+
if (bodyStatements.length !== 1) return;
|
|
40143
40731
|
const soleStatement = bodyStatements[0];
|
|
40144
40732
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
40145
40733
|
const expression = soleStatement.expression;
|
|
40146
40734
|
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) {
|
|
40147
40735
|
if (argumentsReadRefCurrent(expression.arguments ?? [])) return;
|
|
40148
40736
|
if (isStateUsedOnlyInIdOrAriaAttributes(expression, expression.callee.name)) return;
|
|
40737
|
+
if ((expression.arguments ?? []).some(containsLocaleEnvironmentRead)) return;
|
|
40149
40738
|
context.report({
|
|
40150
40739
|
node,
|
|
40151
40740
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
@@ -40964,6 +41553,9 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
40964
41553
|
} })
|
|
40965
41554
|
});
|
|
40966
41555
|
//#endregion
|
|
41556
|
+
//#region src/plugin/utils/is-trivial-built-in-construction.ts
|
|
41557
|
+
const isTrivialBuiltInConstruction = (expression) => isNodeOfType(expression, "NewExpression") && isNodeOfType(expression.callee, "Identifier") && TRIVIAL_CONSTRUCTOR_NAMES.has(expression.callee.name) && (expression.arguments ?? []).length === 0;
|
|
41558
|
+
//#endregion
|
|
40967
41559
|
//#region src/plugin/rules/state-and-effects/rerender-lazy-ref-init.ts
|
|
40968
41560
|
const rerenderLazyRefInit = defineRule({
|
|
40969
41561
|
id: "rerender-lazy-ref-init",
|
|
@@ -40974,7 +41566,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40974
41566
|
recommendation: "Initialize the ref lazily so expensive values are not rebuilt and discarded on every render.",
|
|
40975
41567
|
create: (context) => ({ CallExpression(node) {
|
|
40976
41568
|
if (!isHookCall$2(node, "useRef") || !node.arguments?.length) return;
|
|
40977
|
-
const initializer = node.arguments[0];
|
|
41569
|
+
const initializer = stripParenExpression(node.arguments[0]);
|
|
40978
41570
|
const isPlainCall = isNodeOfType(initializer, "CallExpression");
|
|
40979
41571
|
const isNewCall = isNodeOfType(initializer, "NewExpression");
|
|
40980
41572
|
if (!isPlainCall && !isNewCall) return;
|
|
@@ -40982,7 +41574,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40982
41574
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40983
41575
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40984
41576
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40985
|
-
if (
|
|
41577
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
40986
41578
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40987
41579
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40988
41580
|
context.report({
|
|
@@ -41018,11 +41610,11 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
41018
41610
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
41019
41611
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
41020
41612
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
41021
|
-
|
|
41022
|
-
if (isNodeOfType(
|
|
41023
|
-
if (isNodeOfType(
|
|
41024
|
-
if (isNodeOfType(
|
|
41025
|
-
if (isNodeOfType(
|
|
41613
|
+
const innerExpression = stripParenExpression(expression);
|
|
41614
|
+
if (isNodeOfType(innerExpression, "CallExpression") || isNodeOfType(innerExpression, "NewExpression")) return innerExpression;
|
|
41615
|
+
if (isNodeOfType(innerExpression, "LogicalExpression")) return findEagerInitializerCall(innerExpression.left, depth + 1);
|
|
41616
|
+
if (isNodeOfType(innerExpression, "MemberExpression")) return findEagerInitializerCall(innerExpression.object, depth + 1);
|
|
41617
|
+
if (isNodeOfType(innerExpression, "ArrayExpression")) for (const element of innerExpression.elements ?? []) {
|
|
41026
41618
|
if (!element || !isNodeOfType(element, "SpreadElement")) continue;
|
|
41027
41619
|
const spreadCall = findEagerInitializerCall(element.argument, depth + 1);
|
|
41028
41620
|
if (spreadCall) return spreadCall;
|
|
@@ -41045,7 +41637,7 @@ const rerenderLazyStateInit = defineRule({
|
|
|
41045
41637
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
41046
41638
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
41047
41639
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
41048
|
-
if (
|
|
41640
|
+
if (isTrivialBuiltInConstruction(initializer)) return;
|
|
41049
41641
|
if (memberPropertyName && (initializer.arguments ?? []).length === 0 && TRIVIAL_DATE_GETTER_NAMES.has(memberPropertyName)) return;
|
|
41050
41642
|
if (isReactHookName(calleeName)) return;
|
|
41051
41643
|
const callDescription = isConstructor ? `new ${calleeName}()` : `${calleeName}()`;
|
|
@@ -41712,7 +42304,7 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
41712
42304
|
const body = reactionFn.body;
|
|
41713
42305
|
let singleAssignment = null;
|
|
41714
42306
|
if (isNodeOfType(body, "BlockStatement")) {
|
|
41715
|
-
const statements = body.body ?? [];
|
|
42307
|
+
const statements = (body.body ?? []).filter((statement) => !isNoOpStatement(statement));
|
|
41716
42308
|
if (statements.length !== 1) return;
|
|
41717
42309
|
const onlyStatement = statements[0];
|
|
41718
42310
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
@@ -41786,7 +42378,8 @@ const PROMISE_SETTLE_METHODS = new Set([
|
|
|
41786
42378
|
"catch",
|
|
41787
42379
|
"finally"
|
|
41788
42380
|
]);
|
|
41789
|
-
const findChainRoot = (
|
|
42381
|
+
const findChainRoot = (wrappedNode) => {
|
|
42382
|
+
const node = stripParenExpression(wrappedNode);
|
|
41790
42383
|
if (isNodeOfType(node, "CallExpression")) {
|
|
41791
42384
|
if (isNodeOfType(node.callee, "Identifier")) return {
|
|
41792
42385
|
calleeName: node.callee.name,
|
|
@@ -42374,20 +42967,21 @@ const isBindingReactNativeDimensions = (node, binding, localName) => {
|
|
|
42374
42967
|
};
|
|
42375
42968
|
const isReactNativeDimensionsCallee = (node, callee) => {
|
|
42376
42969
|
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
42377
|
-
|
|
42378
|
-
|
|
42970
|
+
const receiver = stripParenExpression(callee.object);
|
|
42971
|
+
if (isNodeOfType(receiver, "Identifier")) {
|
|
42972
|
+
const localName = receiver.name;
|
|
42379
42973
|
const binding = findVariableInitializer(node, localName);
|
|
42380
42974
|
if (binding !== null) return isBindingReactNativeDimensions(node, binding, localName);
|
|
42381
42975
|
return localName === "Dimensions";
|
|
42382
42976
|
}
|
|
42383
|
-
if (isNodeOfType(
|
|
42384
|
-
if (getInitializerModuleSource(node,
|
|
42385
|
-
const rootName = getRootIdentifierName(
|
|
42977
|
+
if (isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions") {
|
|
42978
|
+
if (getInitializerModuleSource(node, receiver.object) === REACT_NATIVE_MODULE) return true;
|
|
42979
|
+
const rootName = getRootIdentifierName(receiver.object);
|
|
42386
42980
|
if (rootName === null) return false;
|
|
42387
42981
|
const importBinding = getImportBindingForName(node, rootName);
|
|
42388
42982
|
return importBinding !== null && importBinding.isNamespace && importBinding.source === REACT_NATIVE_MODULE;
|
|
42389
42983
|
}
|
|
42390
|
-
if (getRequireCallSource(
|
|
42984
|
+
if (getRequireCallSource(receiver) === REACT_NATIVE_MODULE) return isNodeOfType(receiver, "MemberExpression") && !receiver.computed && isNodeOfType(receiver.property, "Identifier") && receiver.property.name === "Dimensions";
|
|
42391
42985
|
return false;
|
|
42392
42986
|
};
|
|
42393
42987
|
const STYLE_FACTORY_CALLEE_PATTERN$1 = /(?:^|\.)(?:make|create)(?:Use)?Styles$/;
|
|
@@ -42831,7 +43425,8 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42831
43425
|
},
|
|
42832
43426
|
CallExpression(node) {
|
|
42833
43427
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
42834
|
-
|
|
43428
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
43429
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "StyleSheet") return;
|
|
42835
43430
|
if (!isMemberProperty(node.callee, "create")) return;
|
|
42836
43431
|
const stylesArgument = node.arguments?.[0];
|
|
42837
43432
|
if (!isNodeOfType(stylesArgument, "ObjectExpression")) return;
|
|
@@ -43681,7 +44276,7 @@ const rnNoSetNativeProps = defineRule({
|
|
|
43681
44276
|
const callee = node.callee;
|
|
43682
44277
|
if (!isStaticMemberNamed(callee, "setNativeProps")) return;
|
|
43683
44278
|
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
43684
|
-
if (!isStaticMemberNamed(callee.object, "current")) return;
|
|
44279
|
+
if (!isStaticMemberNamed(stripParenExpression(callee.object), "current")) return;
|
|
43685
44280
|
context.report({
|
|
43686
44281
|
node,
|
|
43687
44282
|
message: "`setNativeProps` is a silent no-op under the New Architecture (Fabric), so this imperative update won't change the view. Drive the prop via state, an Animated.Value, or a Reanimated shared value."
|
|
@@ -43909,14 +44504,15 @@ const analyzeGestureChain = (expression) => {
|
|
|
43909
44504
|
if (!isNodeOfType(callee, "MemberExpression")) return null;
|
|
43910
44505
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
43911
44506
|
const methodName = callee.property.name;
|
|
43912
|
-
|
|
44507
|
+
const receiver = stripParenExpression(callee.object);
|
|
44508
|
+
if (isNodeOfType(receiver, "Identifier") && receiver.name === "Gesture") return {
|
|
43913
44509
|
factoryName: methodName,
|
|
43914
44510
|
chainMethodNames,
|
|
43915
44511
|
numberOfTapsArgument
|
|
43916
44512
|
};
|
|
43917
44513
|
if (methodName === "numberOfTaps" && numberOfTapsArgument === null && callExpression.arguments?.length === 1) numberOfTapsArgument = callExpression.arguments[0] ?? null;
|
|
43918
44514
|
chainMethodNames.push(methodName);
|
|
43919
|
-
cursor =
|
|
44515
|
+
cursor = receiver;
|
|
43920
44516
|
}
|
|
43921
44517
|
return null;
|
|
43922
44518
|
};
|
|
@@ -47582,6 +48178,8 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47582
48178
|
const propName = propRawName.toLowerCase();
|
|
47583
48179
|
if (!propName.startsWith("aria-")) continue;
|
|
47584
48180
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
48181
|
+
const attributeValue = attributeNode.value;
|
|
48182
|
+
if (attributeValue && isNodeOfType(attributeValue, "JSXExpressionContainer") && isNullishExpression(attributeValue.expression)) continue;
|
|
47585
48183
|
(ariaAttributes ??= []).push({
|
|
47586
48184
|
attribute,
|
|
47587
48185
|
propName
|
|
@@ -47960,7 +48558,11 @@ const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
|
47960
48558
|
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
47961
48559
|
return Boolean(symbol && symbol.kind !== "import");
|
|
47962
48560
|
};
|
|
47963
|
-
const isNonReactEffectEventCallee = (callee, contextNode, scopes) =>
|
|
48561
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => {
|
|
48562
|
+
if (isNodeOfType(callee, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes);
|
|
48563
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier")) return isImportedFromNonReactModule(contextNode, callee.object.name);
|
|
48564
|
+
return false;
|
|
48565
|
+
};
|
|
47964
48566
|
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47965
48567
|
const initializer = symbol.initializer;
|
|
47966
48568
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
@@ -48304,7 +48906,8 @@ const serverAfterNonblocking = defineRule({
|
|
|
48304
48906
|
if (!fileHasUseServerDirective && serverFunctionDepth === 0) return;
|
|
48305
48907
|
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
48306
48908
|
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
48307
|
-
const
|
|
48909
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
48910
|
+
const objectName = isNodeOfType(receiver, "Identifier") ? receiver.name : null;
|
|
48308
48911
|
if (!objectName) return;
|
|
48309
48912
|
const methodName = node.callee.property.name;
|
|
48310
48913
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
@@ -48980,7 +49583,17 @@ const getMemberPropertyName$1 = (memberExpression) => {
|
|
|
48980
49583
|
};
|
|
48981
49584
|
const ascendMemberChain = (referenceIdentifier) => {
|
|
48982
49585
|
let chainTip = referenceIdentifier;
|
|
48983
|
-
while (chainTip.parent
|
|
49586
|
+
while (chainTip.parent) {
|
|
49587
|
+
if (TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(chainTip.parent.type) && "expression" in chainTip.parent && chainTip.parent.expression === chainTip) {
|
|
49588
|
+
chainTip = chainTip.parent;
|
|
49589
|
+
continue;
|
|
49590
|
+
}
|
|
49591
|
+
if (isNodeOfType(chainTip.parent, "MemberExpression") && chainTip.parent.object === chainTip) {
|
|
49592
|
+
chainTip = chainTip.parent;
|
|
49593
|
+
continue;
|
|
49594
|
+
}
|
|
49595
|
+
break;
|
|
49596
|
+
}
|
|
48984
49597
|
return chainTip;
|
|
48985
49598
|
};
|
|
48986
49599
|
const isDirectContentsMutation = (referenceIdentifier) => {
|
|
@@ -49005,7 +49618,8 @@ const isMutatedThroughCallArgument = (referenceIdentifier, scopes, mayFollowCall
|
|
|
49005
49618
|
const callee = callExpression.callee;
|
|
49006
49619
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
49007
49620
|
const methodName = getMemberPropertyName$1(callee);
|
|
49008
|
-
|
|
49621
|
+
const calleeReceiver = stripParenExpression(callee.object);
|
|
49622
|
+
return Boolean(isNodeOfType(calleeReceiver, "Identifier") && calleeReceiver.name === "Object" && methodName !== null && OBJECT_MUTATING_METHODS.has(methodName) && referenceArgumentIndex === 0);
|
|
49009
49623
|
}
|
|
49010
49624
|
if (!mayFollowCalleeHop || !isNodeOfType(callee, "Identifier")) return false;
|
|
49011
49625
|
const calleeSymbol = scopes.symbolFor(callee);
|
|
@@ -49735,7 +50349,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49735
50349
|
};
|
|
49736
50350
|
if (!isNodeOfType(outerNode, "CallExpression")) return result;
|
|
49737
50351
|
if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
|
|
49738
|
-
let currentNode = outerNode.callee.object;
|
|
50352
|
+
let currentNode = stripParenExpression(outerNode.callee.object);
|
|
49739
50353
|
while (isNodeOfType(currentNode, "CallExpression")) {
|
|
49740
50354
|
const calleeName = getCalleeName$2(currentNode);
|
|
49741
50355
|
if (calleeName && TANSTACK_SERVER_FN_NAMES.has(calleeName)) {
|
|
@@ -49746,7 +50360,7 @@ const walkServerFnChain = (outerNode) => {
|
|
|
49746
50360
|
}
|
|
49747
50361
|
}
|
|
49748
50362
|
if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
|
|
49749
|
-
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
|
|
50363
|
+
if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = stripParenExpression(currentNode.callee.object);
|
|
49750
50364
|
else break;
|
|
49751
50365
|
}
|
|
49752
50366
|
return result;
|
|
@@ -50399,7 +51013,7 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
50399
51013
|
while (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "MemberExpression")) {
|
|
50400
51014
|
const methodName = isNodeOfType(currentNode.callee.property, "Identifier") ? currentNode.callee.property.name : null;
|
|
50401
51015
|
if (methodName) methodNames.unshift(methodName);
|
|
50402
|
-
currentNode = currentNode.callee.object;
|
|
51016
|
+
currentNode = stripParenExpression(currentNode.callee.object);
|
|
50403
51017
|
}
|
|
50404
51018
|
if (isNodeOfType(currentNode, "CallExpression") && isNodeOfType(currentNode.callee, "Identifier")) {
|
|
50405
51019
|
if (!TANSTACK_SERVER_FN_NAMES.has(currentNode.callee.name)) return;
|
|
@@ -53481,6 +54095,18 @@ const reactDoctorRules = [
|
|
|
53481
54095
|
category: "Bugs"
|
|
53482
54096
|
}
|
|
53483
54097
|
},
|
|
54098
|
+
{
|
|
54099
|
+
key: "react-doctor/no-locale-format-in-render",
|
|
54100
|
+
id: "no-locale-format-in-render",
|
|
54101
|
+
source: "react-doctor",
|
|
54102
|
+
originallyExternal: false,
|
|
54103
|
+
rule: {
|
|
54104
|
+
...noLocaleFormatInRender,
|
|
54105
|
+
framework: "global",
|
|
54106
|
+
category: "Bugs",
|
|
54107
|
+
requires: [...new Set(["react", ...noLocaleFormatInRender.requires ?? []])]
|
|
54108
|
+
}
|
|
54109
|
+
},
|
|
53484
54110
|
{
|
|
53485
54111
|
key: "react-doctor/no-long-transition-duration",
|
|
53486
54112
|
id: "no-long-transition-duration",
|