oxlint-plugin-react-doctor 0.7.6-dev.4fb3694 → 0.7.6-dev.543ebfe
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +724 -108
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1180,8 +1180,8 @@ const stripParenExpression = (node) => {
|
|
|
1180
1180
|
return current;
|
|
1181
1181
|
};
|
|
1182
1182
|
//#endregion
|
|
1183
|
-
//#region src/plugin/utils/get-root-identifier
|
|
1184
|
-
const
|
|
1183
|
+
//#region src/plugin/utils/get-root-identifier.ts
|
|
1184
|
+
const getRootIdentifier$1 = (node, options) => {
|
|
1185
1185
|
if (!node) return null;
|
|
1186
1186
|
const followCallChains = options?.followCallChains === true;
|
|
1187
1187
|
let cursor = node;
|
|
@@ -1192,16 +1192,18 @@ const getRootIdentifierName = (node, options) => {
|
|
|
1192
1192
|
continue;
|
|
1193
1193
|
}
|
|
1194
1194
|
if (followCallChains && isNodeOfType(cursor, "CallExpression")) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
cursor = callee.object;
|
|
1195
|
+
if (!isNodeOfType(cursor.callee, "MemberExpression")) return null;
|
|
1196
|
+
cursor = cursor.callee.object;
|
|
1198
1197
|
continue;
|
|
1199
1198
|
}
|
|
1200
1199
|
break;
|
|
1201
1200
|
}
|
|
1202
|
-
return isNodeOfType(cursor, "Identifier") ? cursor
|
|
1201
|
+
return isNodeOfType(cursor, "Identifier") ? cursor : null;
|
|
1203
1202
|
};
|
|
1204
1203
|
//#endregion
|
|
1204
|
+
//#region src/plugin/utils/get-root-identifier-name.ts
|
|
1205
|
+
const getRootIdentifierName = (node, options) => getRootIdentifier$1(node, options)?.name ?? null;
|
|
1206
|
+
//#endregion
|
|
1205
1207
|
//#region src/plugin/rules/state-and-effects/advanced-event-handler-refs.ts
|
|
1206
1208
|
const STABLE_HANDLER_HOOK_NAMES = new Set([
|
|
1207
1209
|
"useCallback",
|
|
@@ -5962,7 +5964,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5962
5964
|
//#endregion
|
|
5963
5965
|
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
5964
5966
|
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
5965
|
-
if (!isNodeOfType(node, "Property")) return null;
|
|
5967
|
+
if (!isNodeOfType(node, "Property") && !isNodeOfType(node, "MethodDefinition")) return null;
|
|
5966
5968
|
if (node.computed) {
|
|
5967
5969
|
if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
|
|
5968
5970
|
return null;
|
|
@@ -6021,7 +6023,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
6021
6023
|
//#endregion
|
|
6022
6024
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
6023
6025
|
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
6024
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
6026
|
+
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
6025
6027
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6026
6028
|
let symbol = scopes.symbolFor(identifier);
|
|
6027
6029
|
while (symbol?.kind === "const") {
|
|
@@ -6296,6 +6298,131 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6296
6298
|
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6297
6299
|
};
|
|
6298
6300
|
//#endregion
|
|
6301
|
+
//#region src/plugin/utils/collect-safely-validated-local-storage-keys.ts
|
|
6302
|
+
const isLocalStorageCall = (node, methodName) => {
|
|
6303
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6304
|
+
const callee = stripParenExpression(node.callee);
|
|
6305
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
6306
|
+
const receiver = stripParenExpression(callee.object);
|
|
6307
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "localStorage" && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6308
|
+
};
|
|
6309
|
+
const catchReturnsFallback = (tryStatement) => {
|
|
6310
|
+
const handler = tryStatement.handler;
|
|
6311
|
+
if (!handler) return false;
|
|
6312
|
+
let hasReturn = false;
|
|
6313
|
+
let hasThrow = false;
|
|
6314
|
+
walkAst(handler.body, (child) => {
|
|
6315
|
+
if (isFunctionLike$1(child)) return false;
|
|
6316
|
+
if (isNodeOfType(child, "ReturnStatement")) hasReturn = true;
|
|
6317
|
+
if (isNodeOfType(child, "ThrowStatement")) hasThrow = true;
|
|
6318
|
+
});
|
|
6319
|
+
return hasReturn && !hasThrow;
|
|
6320
|
+
};
|
|
6321
|
+
const resolveValidatorFunction = (callee, scopes) => {
|
|
6322
|
+
const unwrappedCallee = stripParenExpression(callee);
|
|
6323
|
+
if (!isNodeOfType(unwrappedCallee, "Identifier")) return null;
|
|
6324
|
+
const symbol = scopes.symbolFor(unwrappedCallee);
|
|
6325
|
+
if (!symbol) return null;
|
|
6326
|
+
const candidate = symbol.kind === "function" ? symbol.declarationNode : symbol.initializer;
|
|
6327
|
+
return isFunctionLike$1(candidate) ? candidate : null;
|
|
6328
|
+
};
|
|
6329
|
+
const validatorChecksPayloadProperties = (validatorFunction, scopes) => {
|
|
6330
|
+
if (!isFunctionLike$1(validatorFunction)) return false;
|
|
6331
|
+
const firstParameter = validatorFunction.params?.[0];
|
|
6332
|
+
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
6333
|
+
const parameterSymbol = scopes.symbolFor(firstParameter);
|
|
6334
|
+
if (!parameterSymbol) return false;
|
|
6335
|
+
const payloadSymbolIds = new Set([parameterSymbol.id]);
|
|
6336
|
+
let hasPropertyTypeCheck = false;
|
|
6337
|
+
walkAst(validatorFunction.body, (child) => {
|
|
6338
|
+
if (isFunctionLike$1(child)) return false;
|
|
6339
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier") && child.init) {
|
|
6340
|
+
const initializer = stripParenExpression(child.init);
|
|
6341
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
6342
|
+
const initializerSymbol = scopes.symbolFor(initializer);
|
|
6343
|
+
if (initializerSymbol && payloadSymbolIds.has(initializerSymbol.id)) {
|
|
6344
|
+
const aliasSymbol = scopes.symbolFor(child.id);
|
|
6345
|
+
if (aliasSymbol) payloadSymbolIds.add(aliasSymbol.id);
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
}
|
|
6349
|
+
if (!isNodeOfType(child, "UnaryExpression") || child.operator !== "typeof") return;
|
|
6350
|
+
const checkedValue = stripParenExpression(child.argument);
|
|
6351
|
+
if (!isNodeOfType(checkedValue, "MemberExpression")) return;
|
|
6352
|
+
const receiver = stripParenExpression(checkedValue.object);
|
|
6353
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6354
|
+
const receiverSymbol = scopes.symbolFor(receiver);
|
|
6355
|
+
if (receiverSymbol && payloadSymbolIds.has(receiverSymbol.id)) hasPropertyTypeCheck = true;
|
|
6356
|
+
});
|
|
6357
|
+
return hasPropertyTypeCheck;
|
|
6358
|
+
};
|
|
6359
|
+
const incrementCount = (counts, keyValue) => {
|
|
6360
|
+
counts.set(keyValue, (counts.get(keyValue) ?? 0) + 1);
|
|
6361
|
+
};
|
|
6362
|
+
const isReturnedExpression = (expression) => {
|
|
6363
|
+
const parent = expression.parent;
|
|
6364
|
+
return Boolean(parent && isNodeOfType(parent, "ReturnStatement") && parent.argument === expression);
|
|
6365
|
+
};
|
|
6366
|
+
const collectSafelyValidatedLocalStorageKeys = ({ programNode, scopes, resolveKey }) => {
|
|
6367
|
+
const readCountsByKey = /* @__PURE__ */ new Map();
|
|
6368
|
+
const safeReadCountsByKey = /* @__PURE__ */ new Map();
|
|
6369
|
+
const safeReadSymbolIds = /* @__PURE__ */ new Set();
|
|
6370
|
+
walkAst(programNode, (child) => {
|
|
6371
|
+
if (!isNodeOfType(child, "CallExpression") || !isLocalStorageCall(child, "getItem")) return;
|
|
6372
|
+
const keyArgument = child.arguments?.[0];
|
|
6373
|
+
if (!keyArgument) return;
|
|
6374
|
+
const keyValue = resolveKey(keyArgument);
|
|
6375
|
+
if (keyValue !== null) incrementCount(readCountsByKey, keyValue);
|
|
6376
|
+
});
|
|
6377
|
+
walkAst(programNode, (child) => {
|
|
6378
|
+
if (!isNodeOfType(child, "TryStatement") || !catchReturnsFallback(child)) return;
|
|
6379
|
+
const rawValueKeys = /* @__PURE__ */ new Map();
|
|
6380
|
+
const parsedValueSources = /* @__PURE__ */ new Map();
|
|
6381
|
+
walkAst(child.block, (tryChild) => {
|
|
6382
|
+
if (isFunctionLike$1(tryChild)) return false;
|
|
6383
|
+
if (isNodeOfType(tryChild, "VariableDeclarator") && isNodeOfType(tryChild.id, "Identifier") && tryChild.init) {
|
|
6384
|
+
const initializer = stripParenExpression(tryChild.init);
|
|
6385
|
+
if (isNodeOfType(initializer, "CallExpression") && isLocalStorageCall(initializer, "getItem")) {
|
|
6386
|
+
const keyArgument = initializer.arguments?.[0];
|
|
6387
|
+
const bindingSymbol = scopes.symbolFor(tryChild.id);
|
|
6388
|
+
if (keyArgument && bindingSymbol) {
|
|
6389
|
+
const keyValue = resolveKey(keyArgument);
|
|
6390
|
+
if (keyValue !== null) rawValueKeys.set(bindingSymbol.id, keyValue);
|
|
6391
|
+
}
|
|
6392
|
+
}
|
|
6393
|
+
if (isNodeOfType(initializer, "CallExpression") && isGlobalMethodCall(initializer, "JSON", "parse")) {
|
|
6394
|
+
const rawValueArgument = initializer.arguments?.[0];
|
|
6395
|
+
const parsedValueSymbol = scopes.symbolFor(tryChild.id);
|
|
6396
|
+
if (rawValueArgument && isNodeOfType(rawValueArgument, "Identifier") && parsedValueSymbol) {
|
|
6397
|
+
const rawValueSymbol = scopes.symbolFor(rawValueArgument);
|
|
6398
|
+
if (rawValueSymbol && rawValueKeys.has(rawValueSymbol.id)) parsedValueSources.set(parsedValueSymbol.id, rawValueSymbol.id);
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
if (!isNodeOfType(tryChild, "ConditionalExpression") || !isReturnedExpression(tryChild)) return;
|
|
6403
|
+
const test = stripParenExpression(tryChild.test);
|
|
6404
|
+
if (!isNodeOfType(test, "CallExpression")) return;
|
|
6405
|
+
const testedValue = test.arguments?.[0];
|
|
6406
|
+
if (!testedValue || !isNodeOfType(testedValue, "Identifier")) return;
|
|
6407
|
+
const parsedValueSymbol = scopes.symbolFor(testedValue);
|
|
6408
|
+
if (!parsedValueSymbol) return;
|
|
6409
|
+
const rawValueSymbolId = parsedValueSources.get(parsedValueSymbol.id);
|
|
6410
|
+
if (rawValueSymbolId === void 0) return;
|
|
6411
|
+
const returnedValue = stripParenExpression(tryChild.consequent);
|
|
6412
|
+
if (!isNodeOfType(returnedValue, "Identifier") || scopes.symbolFor(returnedValue)?.id !== parsedValueSymbol.id) return;
|
|
6413
|
+
const validatorFunction = resolveValidatorFunction(test.callee, scopes);
|
|
6414
|
+
if (!validatorFunction || !validatorChecksPayloadProperties(validatorFunction, scopes)) return;
|
|
6415
|
+
const keyValue = rawValueKeys.get(rawValueSymbolId);
|
|
6416
|
+
if (keyValue === void 0 || safeReadSymbolIds.has(rawValueSymbolId)) return;
|
|
6417
|
+
safeReadSymbolIds.add(rawValueSymbolId);
|
|
6418
|
+
incrementCount(safeReadCountsByKey, keyValue);
|
|
6419
|
+
});
|
|
6420
|
+
});
|
|
6421
|
+
const safeKeys = /* @__PURE__ */ new Set();
|
|
6422
|
+
for (const [keyValue, readCount] of readCountsByKey) if (safeReadCountsByKey.get(keyValue) === readCount) safeKeys.add(keyValue);
|
|
6423
|
+
return safeKeys;
|
|
6424
|
+
};
|
|
6425
|
+
//#endregion
|
|
6299
6426
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6300
6427
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6301
6428
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
@@ -6317,26 +6444,39 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6317
6444
|
severity: "warn",
|
|
6318
6445
|
category: "Correctness",
|
|
6319
6446
|
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.",
|
|
6320
|
-
create: (context) =>
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6447
|
+
create: (context) => {
|
|
6448
|
+
let safelyValidatedStorageKeys = /* @__PURE__ */ new Set();
|
|
6449
|
+
return {
|
|
6450
|
+
Program(node) {
|
|
6451
|
+
safelyValidatedStorageKeys = collectSafelyValidatedLocalStorageKeys({
|
|
6452
|
+
programNode: node,
|
|
6453
|
+
scopes: context.scopes,
|
|
6454
|
+
resolveKey: (keyNode) => resolveStringKey(keyNode, context)
|
|
6455
|
+
});
|
|
6456
|
+
},
|
|
6457
|
+
CallExpression(node) {
|
|
6458
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6459
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6460
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6461
|
+
if (receiver.name !== "localStorage") return;
|
|
6462
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6463
|
+
if (node.callee.property.name !== "setItem") return;
|
|
6464
|
+
const keyArg = node.arguments?.[0];
|
|
6465
|
+
if (!keyArg) return;
|
|
6466
|
+
const keyValue = resolveStringKey(keyArg, context);
|
|
6467
|
+
if (keyValue === null) return;
|
|
6468
|
+
if (isVersionedKey(keyValue)) return;
|
|
6469
|
+
if (safelyValidatedStorageKeys.has(keyValue)) return;
|
|
6470
|
+
const valueArg = node.arguments?.[1];
|
|
6471
|
+
if (!valueArg) return;
|
|
6472
|
+
if (!isJsonStringifyCall(valueArg)) return;
|
|
6473
|
+
context.report({
|
|
6474
|
+
node: keyArg,
|
|
6475
|
+
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").`
|
|
6476
|
+
});
|
|
6477
|
+
}
|
|
6478
|
+
};
|
|
6479
|
+
}
|
|
6340
6480
|
});
|
|
6341
6481
|
//#endregion
|
|
6342
6482
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
@@ -15972,14 +16112,21 @@ const jsxFilenameExtension = defineRule({
|
|
|
15972
16112
|
});
|
|
15973
16113
|
//#endregion
|
|
15974
16114
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
15975
|
-
const isJsxFragmentElement = (node) => {
|
|
16115
|
+
const isJsxFragmentElement = (node, scopes) => {
|
|
15976
16116
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
15977
16117
|
const elementName = node.name;
|
|
15978
|
-
if (isNodeOfType(elementName, "JSXIdentifier"))
|
|
16118
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
16119
|
+
if (!scopes) return elementName.name === "Fragment";
|
|
16120
|
+
const symbol = resolveConstIdentifierAlias(elementName, scopes);
|
|
16121
|
+
if (!symbol) return elementName.name === "Fragment" && scopes.isGlobalReference(elementName);
|
|
16122
|
+
return isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "Fragment";
|
|
16123
|
+
}
|
|
15979
16124
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
15980
16125
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
15981
|
-
if (elementName.
|
|
15982
|
-
return elementName.
|
|
16126
|
+
if (elementName.property.name !== "Fragment") return false;
|
|
16127
|
+
if (!scopes) return elementName.object.name === "React";
|
|
16128
|
+
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
16129
|
+
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15983
16130
|
}
|
|
15984
16131
|
return false;
|
|
15985
16132
|
};
|
|
@@ -16005,7 +16152,7 @@ const jsxFragments = defineRule({
|
|
|
16005
16152
|
if (mode !== "syntax") return;
|
|
16006
16153
|
if (!node.closingElement) return;
|
|
16007
16154
|
const openingElement = node.openingElement;
|
|
16008
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
16155
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
16009
16156
|
if (openingElement.attributes.length > 0) return;
|
|
16010
16157
|
context.report({
|
|
16011
16158
|
node: openingElement,
|
|
@@ -19146,7 +19293,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19146
19293
|
return {
|
|
19147
19294
|
JSXElement(node) {
|
|
19148
19295
|
const openingElement = node.openingElement;
|
|
19149
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
19296
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
19150
19297
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19151
19298
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19152
19299
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20388,8 +20535,24 @@ const hasDirective = (programNode, directive) => {
|
|
|
20388
20535
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20389
20536
|
};
|
|
20390
20537
|
//#endregion
|
|
20391
|
-
//#region src/plugin/utils/
|
|
20392
|
-
const
|
|
20538
|
+
//#region src/plugin/utils/unwrap-object-integrity-expression.ts
|
|
20539
|
+
const OBJECT_INTEGRITY_METHOD_NAMES = new Set([
|
|
20540
|
+
"freeze",
|
|
20541
|
+
"seal",
|
|
20542
|
+
"preventExtensions"
|
|
20543
|
+
]);
|
|
20544
|
+
const OBJECT_FREEZE_OR_SEAL_METHOD_NAMES = new Set(["freeze", "seal"]);
|
|
20545
|
+
const unwrapObjectIntegrityExpression = (node, scopes, methodNames = OBJECT_INTEGRITY_METHOD_NAMES) => {
|
|
20546
|
+
let expression = stripParenExpression(node);
|
|
20547
|
+
while (isNodeOfType(expression, "CallExpression")) {
|
|
20548
|
+
const callee = stripParenExpression(expression.callee);
|
|
20549
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Object" || !scopes.isGlobalReference(callee.object) || !isNodeOfType(callee.property, "Identifier") || !methodNames.has(callee.property.name)) break;
|
|
20550
|
+
const wrappedExpression = expression.arguments[0];
|
|
20551
|
+
if (!wrappedExpression || isNodeOfType(wrappedExpression, "SpreadElement")) break;
|
|
20552
|
+
expression = stripParenExpression(wrappedExpression);
|
|
20553
|
+
}
|
|
20554
|
+
return expression;
|
|
20555
|
+
};
|
|
20393
20556
|
//#endregion
|
|
20394
20557
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
20395
20558
|
const nextjsAsyncClientComponent = defineRule({
|
|
@@ -20415,10 +20578,10 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
20415
20578
|
},
|
|
20416
20579
|
VariableDeclarator(node) {
|
|
20417
20580
|
if (!fileHasUseClient) return;
|
|
20418
|
-
if (!isComponentAssignment(node)) return;
|
|
20419
|
-
if (!isInlineFunctionExpression(node.init)) return;
|
|
20420
|
-
if (!node.init.async) return;
|
|
20421
20581
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
20582
|
+
if (!isUppercaseName(node.id.name) || !node.init) return;
|
|
20583
|
+
const componentFunction = unwrapObjectIntegrityExpression(node.init, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES);
|
|
20584
|
+
if (!isInlineFunctionExpression(componentFunction) || !componentFunction.async) return;
|
|
20422
20585
|
context.report({
|
|
20423
20586
|
node,
|
|
20424
20587
|
message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
|
|
@@ -26154,6 +26317,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
|
26154
26317
|
return hasRenderOutput;
|
|
26155
26318
|
};
|
|
26156
26319
|
//#endregion
|
|
26320
|
+
//#region src/plugin/utils/is-component-assignment.ts
|
|
26321
|
+
const isComponentAssignment = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && isUppercaseName(node.id.name) && Boolean(node.init) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
|
|
26322
|
+
//#endregion
|
|
26157
26323
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
26158
26324
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
26159
26325
|
//#endregion
|
|
@@ -31795,6 +31961,26 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31795
31961
|
} })
|
|
31796
31962
|
});
|
|
31797
31963
|
//#endregion
|
|
31964
|
+
//#region src/plugin/utils/react-ref-origin.ts
|
|
31965
|
+
const resolveReactRefSymbol = (memberExpression, scopes) => {
|
|
31966
|
+
const receiver = isNodeOfType(memberExpression, "MemberExpression") ? stripParenExpression(memberExpression.object) : null;
|
|
31967
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticPropertyName(memberExpression) !== "current" || !isNodeOfType(receiver, "Identifier")) return null;
|
|
31968
|
+
const symbol = resolveConstIdentifierAlias(receiver, scopes);
|
|
31969
|
+
if (!symbol?.initializer) return null;
|
|
31970
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
31971
|
+
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
31972
|
+
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
31973
|
+
};
|
|
31974
|
+
const hasReactRefCurrentOrigin = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
31975
|
+
const expression = stripParenExpression(node);
|
|
31976
|
+
if (resolveReactRefSymbol(expression, scopes)) return true;
|
|
31977
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
31978
|
+
const symbol = resolveConstIdentifierAlias(expression, scopes);
|
|
31979
|
+
if (!symbol?.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
31980
|
+
visitedSymbolIds.add(symbol.id);
|
|
31981
|
+
return hasReactRefCurrentOrigin(symbol.initializer, scopes, visitedSymbolIds);
|
|
31982
|
+
};
|
|
31983
|
+
//#endregion
|
|
31798
31984
|
//#region src/plugin/rules/state-and-effects/no-impure-state-updater.ts
|
|
31799
31985
|
const TIMER_FUNCTION_NAMES = new Set([
|
|
31800
31986
|
"cancelAnimationFrame",
|
|
@@ -31826,6 +32012,33 @@ const NOTIFICATION_METHOD_NAMES = new Set([
|
|
|
31826
32012
|
"success",
|
|
31827
32013
|
"warning"
|
|
31828
32014
|
]);
|
|
32015
|
+
const NOTIFICATION_MODULE_SOURCES = new Set([
|
|
32016
|
+
"@chakra-ui/react",
|
|
32017
|
+
"@heroui/react",
|
|
32018
|
+
"@mantine/notifications",
|
|
32019
|
+
"antd",
|
|
32020
|
+
"react-hot-toast",
|
|
32021
|
+
"react-toastify",
|
|
32022
|
+
"sonner"
|
|
32023
|
+
]);
|
|
32024
|
+
const SYNCHRONOUS_ARRAY_METHOD_NAMES = new Set([
|
|
32025
|
+
"every",
|
|
32026
|
+
"filter",
|
|
32027
|
+
"find",
|
|
32028
|
+
"findIndex",
|
|
32029
|
+
"flatMap",
|
|
32030
|
+
"forEach",
|
|
32031
|
+
"map",
|
|
32032
|
+
"reduce",
|
|
32033
|
+
"reduceRight",
|
|
32034
|
+
"some",
|
|
32035
|
+
"sort"
|
|
32036
|
+
]);
|
|
32037
|
+
const isNotificationModuleSource = (source) => {
|
|
32038
|
+
if (!source) return false;
|
|
32039
|
+
for (const moduleSource of NOTIFICATION_MODULE_SOURCES) if (source === moduleSource || source.startsWith(`${moduleSource}/`)) return true;
|
|
32040
|
+
return /(?:^|[/_.-])(?:notification|toast)s?(?:$|[/_.-])/i.test(source);
|
|
32041
|
+
};
|
|
31829
32042
|
const getMemberCall = (node) => {
|
|
31830
32043
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
31831
32044
|
if (!isNodeOfType(node.callee, "MemberExpression") || node.callee.computed) return null;
|
|
@@ -31837,27 +32050,37 @@ const getMemberCall = (node) => {
|
|
|
31837
32050
|
};
|
|
31838
32051
|
const isNotificationReceiver = (receiver, scopes) => {
|
|
31839
32052
|
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
31840
|
-
|
|
32053
|
+
const importBinding = getImportBindingForName(receiver, receiver.name);
|
|
32054
|
+
if (importBinding) return isNotificationModuleSource(importBinding.source) && (importBinding.exportedName === "default" || importBinding.exportedName !== null && NOTIFICATION_RECEIVER_NAMES.has(importBinding.exportedName));
|
|
31841
32055
|
const symbol = scopes.symbolFor(receiver);
|
|
31842
|
-
if (symbol?.kind === "import") return true;
|
|
31843
32056
|
if (!isNodeOfType(symbol?.initializer, "CallExpression")) return false;
|
|
31844
32057
|
const callee = symbol.initializer.callee;
|
|
31845
|
-
|
|
32058
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
32059
|
+
const hookImport = getImportBindingForName(callee, callee.name);
|
|
32060
|
+
return Boolean(hookImport && isNotificationModuleSource(hookImport.source) && hookImport.exportedName !== null && /^use(?:Message|Notification|Toast)$/.test(hookImport.exportedName));
|
|
32061
|
+
};
|
|
32062
|
+
const isKnownGlobalObject = (node, expectedName, scopes) => isNodeOfType(node, "Identifier") && node.name === expectedName && scopes.isGlobalReference(node);
|
|
32063
|
+
const isGlobalMember = (node, expectedPropertyName, scopes) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.property, "Identifier") && node.property.name === expectedPropertyName && (isKnownGlobalObject(node.object, "window", scopes) || isKnownGlobalObject(node.object, "globalThis", scopes));
|
|
32064
|
+
const isStorageReceiver = (node, scopes) => {
|
|
32065
|
+
if (isNodeOfType(node, "Identifier") && STORAGE_RECEIVER_NAMES.has(node.name) && scopes.isGlobalReference(node)) return true;
|
|
32066
|
+
for (const receiverName of STORAGE_RECEIVER_NAMES) if (isGlobalMember(node, receiverName, scopes)) return true;
|
|
32067
|
+
return false;
|
|
31846
32068
|
};
|
|
31847
32069
|
const getKnownImpureCall = (callExpression, scopes) => {
|
|
31848
32070
|
if (isNodeOfType(callExpression.callee, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.name) && scopes.isGlobalReference(callExpression.callee)) return `${callExpression.callee.name}()`;
|
|
32071
|
+
if (isNodeOfType(callExpression.callee, "MemberExpression") && !callExpression.callee.computed && isNodeOfType(callExpression.callee.property, "Identifier") && TIMER_FUNCTION_NAMES.has(callExpression.callee.property.name) && (isKnownGlobalObject(callExpression.callee.object, "window", scopes) || isKnownGlobalObject(callExpression.callee.object, "globalThis", scopes))) return `${callExpression.callee.property.name}()`;
|
|
31849
32072
|
const memberCall = getMemberCall(callExpression);
|
|
31850
32073
|
if (!memberCall) return null;
|
|
31851
32074
|
const { methodName, receiver } = memberCall;
|
|
31852
|
-
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) &&
|
|
31853
|
-
if (EXTERNAL_READ_METHOD_NAMES.has(methodName)) return `.${methodName}()`;
|
|
31854
|
-
|
|
32075
|
+
if (STORAGE_MUTATION_METHOD_NAMES.has(methodName) && isStorageReceiver(receiver, scopes)) return `${methodName}()`;
|
|
32076
|
+
if (EXTERNAL_READ_METHOD_NAMES.has(methodName) && hasReactRefCurrentOrigin(receiver, scopes)) return `.${methodName}()`;
|
|
32077
|
+
const receiverRoot = getRootIdentifier$1(receiver);
|
|
32078
|
+
if (EXTERNAL_READ_METHOD_NAMES.has(methodName) && receiverRoot !== null && isKnownGlobalObject(receiverRoot, "document", scopes)) return `.${methodName}()`;
|
|
32079
|
+
if (NOTIFICATION_METHOD_NAMES.has(methodName) && isNotificationReceiver(receiver, scopes)) return `${methodName}()`;
|
|
31855
32080
|
return null;
|
|
31856
32081
|
};
|
|
31857
32082
|
const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) => {
|
|
31858
|
-
|
|
31859
|
-
if (isNodeOfType(assignmentTarget, "Identifier")) rootIdentifier = assignmentTarget;
|
|
31860
|
-
else if (isNodeOfType(assignmentTarget, "MemberExpression") && isNodeOfType(assignmentTarget.object, "Identifier")) rootIdentifier = assignmentTarget.object;
|
|
32083
|
+
const rootIdentifier = getRootIdentifier$1(assignmentTarget);
|
|
31861
32084
|
if (!rootIdentifier) return null;
|
|
31862
32085
|
const updaterScope = scopes.ownScopeFor(updater);
|
|
31863
32086
|
if (!updaterScope) return null;
|
|
@@ -31866,12 +32089,26 @@ const getExternalAssignmentDescription = (assignmentTarget, updater, scopes) =>
|
|
|
31866
32089
|
if (symbol.kind === "parameter" && symbol.scope === updaterScope) return `the updater argument "${rootIdentifier.name}"`;
|
|
31867
32090
|
return isDescendantScope(symbol.scope, updaterScope) ? null : `the captured value "${rootIdentifier.name}"`;
|
|
31868
32091
|
};
|
|
32092
|
+
const isArrayValue = (node, scopes) => {
|
|
32093
|
+
const expression = stripParenExpression(node);
|
|
32094
|
+
if (isNodeOfType(expression, "ArrayExpression")) return true;
|
|
32095
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
32096
|
+
return isNodeOfType(resolveConstIdentifierAlias(expression, scopes)?.initializer, "ArrayExpression");
|
|
32097
|
+
};
|
|
32098
|
+
const isDefinitelySynchronousCallback = (callback, scopes) => {
|
|
32099
|
+
const parent = callback.parent;
|
|
32100
|
+
if (!parent) return false;
|
|
32101
|
+
if (isNodeOfType(parent, "CallExpression") && parent.callee === callback) return true;
|
|
32102
|
+
if (!isNodeOfType(parent, "CallExpression") || !parent.arguments?.some((argument) => argument === callback)) return false;
|
|
32103
|
+
if (isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.property, "Identifier") && SYNCHRONOUS_ARRAY_METHOD_NAMES.has(parent.callee.property.name) && isArrayValue(parent.callee.object, scopes)) return true;
|
|
32104
|
+
return isNodeOfType(parent.callee, "MemberExpression") && !parent.callee.computed && isNodeOfType(parent.callee.object, "Identifier") && parent.callee.object.name === "Array" && scopes.isGlobalReference(parent.callee.object) && isNodeOfType(parent.callee.property, "Identifier") && parent.callee.property.name === "from" && parent.arguments[1] === callback;
|
|
32105
|
+
};
|
|
31869
32106
|
const findImpureUpdaterOperation = (updater, scopes) => {
|
|
31870
32107
|
const analysis = getProgramAnalysis(updater);
|
|
31871
32108
|
let operation = null;
|
|
31872
32109
|
walkAst(updater, (child) => {
|
|
31873
32110
|
if (operation) return false;
|
|
31874
|
-
if (child !== updater && isFunctionLike$1(child) && !
|
|
32111
|
+
if (child !== updater && isFunctionLike$1(child) && !isDefinitelySynchronousCallback(child, scopes)) return false;
|
|
31875
32112
|
if (isNodeOfType(child, "CallExpression")) {
|
|
31876
32113
|
if (isNodeOfType(child.callee, "Identifier") && analysis) {
|
|
31877
32114
|
const calleeReference = getRef(analysis, child.callee);
|
|
@@ -31903,18 +32140,25 @@ const noImpureStateUpdater = defineRule({
|
|
|
31903
32140
|
severity: "error",
|
|
31904
32141
|
recommendation: "Keep state updater callbacks pure and return only the next state. Move notifications, storage, timers, ref writes, and other external work into the event or effect that queues the update.",
|
|
31905
32142
|
create: (context) => ({ CallExpression(node) {
|
|
31906
|
-
const
|
|
31907
|
-
if (!
|
|
32143
|
+
const updaterArgument = node.arguments?.[0];
|
|
32144
|
+
if (!updaterArgument) return;
|
|
31908
32145
|
const analysis = getProgramAnalysis(node);
|
|
31909
32146
|
if (!analysis || !isNodeOfType(node.callee, "Identifier")) return;
|
|
31910
32147
|
const calleeReference = getRef(analysis, node.callee);
|
|
31911
32148
|
if (!calleeReference || !isStateSetterCall(analysis, calleeReference)) return;
|
|
31912
32149
|
const stateDeclarator = getUseStateDecl(analysis, calleeReference);
|
|
31913
32150
|
if (!isNodeOfType(stateDeclarator, "VariableDeclarator") || !isNodeOfType(stateDeclarator.init, "CallExpression") || !isReactApiCall(stateDeclarator.init, "useState", context.scopes, { allowGlobalReactNamespace: true })) return;
|
|
32151
|
+
let updater = null;
|
|
32152
|
+
if (isFunctionLike$1(updaterArgument)) updater = updaterArgument;
|
|
32153
|
+
else if (isNodeOfType(updaterArgument, "Identifier")) {
|
|
32154
|
+
const updaterReference = getRef(analysis, updaterArgument);
|
|
32155
|
+
if (updaterReference) updater = resolveToFunction(updaterReference);
|
|
32156
|
+
}
|
|
32157
|
+
if (!updater) return;
|
|
31914
32158
|
const operation = findImpureUpdaterOperation(updater, context.scopes);
|
|
31915
32159
|
if (!operation) return;
|
|
31916
32160
|
context.report({
|
|
31917
|
-
node:
|
|
32161
|
+
node: updaterArgument,
|
|
31918
32162
|
message: `This state updater performs ${operation}. React may run updater functions more than once, so side effects here can repeat or observe inconsistent external state.`
|
|
31919
32163
|
});
|
|
31920
32164
|
} })
|
|
@@ -32203,11 +32447,12 @@ const isMemoCall = (node) => {
|
|
|
32203
32447
|
};
|
|
32204
32448
|
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
32205
32449
|
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
32206
|
-
const isInlineReference = (node) => {
|
|
32207
|
-
|
|
32208
|
-
if (isNodeOfType(
|
|
32209
|
-
if (isNodeOfType(
|
|
32210
|
-
if (isNodeOfType(
|
|
32450
|
+
const isInlineReference = (node, scopes) => {
|
|
32451
|
+
const referenceNode = unwrapObjectIntegrityExpression(node, scopes);
|
|
32452
|
+
if (isNodeOfType(referenceNode, "ArrowFunctionExpression") || isNodeOfType(referenceNode, "FunctionExpression") || isNodeOfType(referenceNode, "CallExpression") && isNodeOfType(referenceNode.callee, "MemberExpression") && isNodeOfType(referenceNode.callee.property, "Identifier") && referenceNode.callee.property.name === "bind") return "functions";
|
|
32453
|
+
if (isNodeOfType(referenceNode, "ObjectExpression")) return "objects";
|
|
32454
|
+
if (isNodeOfType(referenceNode, "ArrayExpression")) return "Arrays";
|
|
32455
|
+
if (isNodeOfType(referenceNode, "JSXElement") || isNodeOfType(referenceNode, "JSXFragment")) return "JSX";
|
|
32211
32456
|
return null;
|
|
32212
32457
|
};
|
|
32213
32458
|
const noInlinePropOnMemoComponent = defineRule({
|
|
@@ -32237,7 +32482,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
32237
32482
|
let elementName = null;
|
|
32238
32483
|
if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
|
|
32239
32484
|
if (!elementName || !memoizedComponentNames.has(elementName)) return;
|
|
32240
|
-
const propType = isInlineReference(node.value.expression);
|
|
32485
|
+
const propType = isInlineReference(node.value.expression, context.scopes);
|
|
32241
32486
|
if (propType) context.report({
|
|
32242
32487
|
node: node.value.expression,
|
|
32243
32488
|
message: `This redraws ${elementName} on every render because the prop is ${propType} built right here, so memo() can't skip it. Move it to a stable value with useMemo, useCallback, or module scope`
|
|
@@ -32758,6 +33003,18 @@ const INTL_FORMAT_METHOD_NAMES = new Set([
|
|
|
32758
33003
|
"formatToParts",
|
|
32759
33004
|
"formatRange"
|
|
32760
33005
|
]);
|
|
33006
|
+
const ABSENT_PROPERTY_PROOF = { status: "absent" };
|
|
33007
|
+
const PRESENT_PROPERTY_PROOF = { status: "present" };
|
|
33008
|
+
const UNDEFINED_PROPERTY_PROOF = { status: "undefined" };
|
|
33009
|
+
const UNKNOWN_PROPERTY_PROOF = { status: "unknown" };
|
|
33010
|
+
const READ_ONLY_OBJECT_METHOD_NAMES = new Set([
|
|
33011
|
+
"hasOwnProperty",
|
|
33012
|
+
"isPrototypeOf",
|
|
33013
|
+
"propertyIsEnumerable",
|
|
33014
|
+
"toLocaleString",
|
|
33015
|
+
"toString",
|
|
33016
|
+
"valueOf"
|
|
33017
|
+
]);
|
|
32761
33018
|
const isProvableDateExpression = (expression) => {
|
|
32762
33019
|
if (!expression) return false;
|
|
32763
33020
|
const unwrapped = stripParenExpression(expression);
|
|
@@ -32772,32 +33029,290 @@ const receiverNameLooksDateFlavored = (expression) => {
|
|
|
32772
33029
|
if (isNodeOfType(unwrapped, "CallExpression")) return receiverNameLooksDateFlavored(unwrapped.callee);
|
|
32773
33030
|
return false;
|
|
32774
33031
|
};
|
|
32775
|
-
const
|
|
32776
|
-
|
|
32777
|
-
|
|
32778
|
-
if (!isNodeOfType(
|
|
32779
|
-
|
|
32780
|
-
|
|
32781
|
-
|
|
32782
|
-
|
|
32783
|
-
|
|
33032
|
+
const isStaticUndefined = (node, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
33033
|
+
const expression = stripParenExpression(node);
|
|
33034
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") return true;
|
|
33035
|
+
if (!isNodeOfType(expression, "Identifier")) return false;
|
|
33036
|
+
if (expression.name === "undefined" && scopes.isGlobalReference(expression)) return true;
|
|
33037
|
+
const symbol = scopes.symbolFor(expression);
|
|
33038
|
+
if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || visitedSymbolIds.has(symbol.id)) return false;
|
|
33039
|
+
visitedSymbolIds.add(symbol.id);
|
|
33040
|
+
return isStaticUndefined(symbol.initializer, scopes, visitedSymbolIds);
|
|
33041
|
+
};
|
|
33042
|
+
const isNestedAssignmentTarget = (expression) => {
|
|
33043
|
+
let target = expression;
|
|
33044
|
+
let parent = target.parent;
|
|
33045
|
+
while (parent) {
|
|
33046
|
+
if (isNodeOfType(parent, "AssignmentExpression")) return parent.left === target;
|
|
33047
|
+
if (isNodeOfType(parent, "ForInStatement") || isNodeOfType(parent, "ForOfStatement")) return parent.left === target;
|
|
33048
|
+
if (isNodeOfType(parent, "AssignmentPattern")) {
|
|
33049
|
+
if (parent.left !== target) return false;
|
|
33050
|
+
} else if (isNodeOfType(parent, "RestElement")) {
|
|
33051
|
+
if (parent.argument !== target) return false;
|
|
33052
|
+
} else if (isNodeOfType(parent, "ArrayPattern")) {
|
|
33053
|
+
if (!parent.elements?.some((element) => element === target)) return false;
|
|
33054
|
+
} else if (isNodeOfType(parent, "Property")) {
|
|
33055
|
+
if (parent.value !== target || !isNodeOfType(parent.parent, "ObjectPattern")) return false;
|
|
33056
|
+
} else if (isNodeOfType(parent, "ObjectPattern")) {
|
|
33057
|
+
if (!parent.properties?.some((property) => property === target)) return false;
|
|
33058
|
+
} else return false;
|
|
33059
|
+
target = parent;
|
|
33060
|
+
parent = target.parent;
|
|
33061
|
+
}
|
|
33062
|
+
return false;
|
|
33063
|
+
};
|
|
33064
|
+
const isPotentialMutationReference = (identifier, readNode) => {
|
|
33065
|
+
let expression = identifier;
|
|
33066
|
+
let parent = expression.parent;
|
|
33067
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
|
|
33068
|
+
expression = parent;
|
|
33069
|
+
parent = expression.parent;
|
|
33070
|
+
}
|
|
33071
|
+
const rootExpression = expression;
|
|
33072
|
+
let memberDepth = 0;
|
|
33073
|
+
while (parent && isNodeOfType(parent, "MemberExpression") && parent.object === expression) {
|
|
33074
|
+
memberDepth += 1;
|
|
33075
|
+
expression = parent;
|
|
33076
|
+
parent = expression.parent;
|
|
33077
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
|
|
33078
|
+
expression = parent;
|
|
33079
|
+
parent = expression.parent;
|
|
33080
|
+
}
|
|
33081
|
+
}
|
|
33082
|
+
if (!parent || isAstDescendant(identifier, readNode)) return false;
|
|
33083
|
+
if (isNestedAssignmentTarget(expression)) return true;
|
|
33084
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.left === expression) return true;
|
|
33085
|
+
if (isNodeOfType(parent, "UpdateExpression") && parent.argument === expression) return true;
|
|
33086
|
+
if (isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expression) return true;
|
|
33087
|
+
if (isNodeOfType(parent, "CallExpression")) {
|
|
33088
|
+
if (parent.callee === expression) {
|
|
33089
|
+
if (memberDepth === 0) return true;
|
|
33090
|
+
const memberExpression = stripParenExpression(expression);
|
|
33091
|
+
return memberDepth === 1 && isNodeOfType(memberExpression, "MemberExpression") && !READ_ONLY_OBJECT_METHOD_NAMES.has(getStaticPropertyName(memberExpression) ?? "");
|
|
33092
|
+
}
|
|
33093
|
+
return memberDepth === 0 && parent.arguments?.some((argument) => argument === rootExpression);
|
|
32784
33094
|
}
|
|
33095
|
+
if (isNodeOfType(parent, "NewExpression")) return memberDepth === 0 && parent.arguments?.some((argument) => argument === rootExpression);
|
|
33096
|
+
if (isNodeOfType(parent, "AssignmentExpression") && parent.right === rootExpression) return true;
|
|
32785
33097
|
return false;
|
|
32786
33098
|
};
|
|
32787
|
-
const
|
|
33099
|
+
const getSimpleAlias = (identifier, scopes) => {
|
|
33100
|
+
let expression = identifier;
|
|
33101
|
+
let parent = expression.parent;
|
|
33102
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === expression) {
|
|
33103
|
+
expression = parent;
|
|
33104
|
+
parent = expression.parent;
|
|
33105
|
+
}
|
|
33106
|
+
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== expression || !isNodeOfType(parent.id, "Identifier")) return null;
|
|
33107
|
+
const symbol = scopes.symbolFor(parent.id);
|
|
33108
|
+
return symbol ? {
|
|
33109
|
+
symbol,
|
|
33110
|
+
readNode: parent.id
|
|
33111
|
+
} : null;
|
|
33112
|
+
};
|
|
33113
|
+
const getDirectCallForExpression = (expression) => {
|
|
33114
|
+
let callee = expression;
|
|
33115
|
+
let parent = callee.parent;
|
|
33116
|
+
while (parent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(parent.type) && "expression" in parent && parent.expression === callee) {
|
|
33117
|
+
callee = parent;
|
|
33118
|
+
parent = callee.parent;
|
|
33119
|
+
}
|
|
33120
|
+
return isNodeOfType(parent, "CallExpression") && parent.callee === callee ? parent : null;
|
|
33121
|
+
};
|
|
33122
|
+
const getMethodOwner = (functionNode, scopes) => {
|
|
33123
|
+
const methodNode = functionNode.parent;
|
|
33124
|
+
if (isNodeOfType(methodNode, "Property") && methodNode.value === functionNode && isNodeOfType(methodNode.parent, "ObjectExpression")) {
|
|
33125
|
+
const objectParent = methodNode.parent.parent;
|
|
33126
|
+
if (!isNodeOfType(objectParent, "VariableDeclarator") || objectParent.init !== methodNode.parent || !isNodeOfType(objectParent.id, "Identifier")) return null;
|
|
33127
|
+
const ownerSymbol = scopes.symbolFor(objectParent.id);
|
|
33128
|
+
const methodName = getStaticPropertyKeyName(methodNode, { allowComputedString: true });
|
|
33129
|
+
return ownerSymbol && methodName ? {
|
|
33130
|
+
methodName,
|
|
33131
|
+
ownerKind: "object",
|
|
33132
|
+
ownerSymbol,
|
|
33133
|
+
isStatic: false
|
|
33134
|
+
} : null;
|
|
33135
|
+
}
|
|
33136
|
+
if (!isNodeOfType(methodNode, "MethodDefinition") || methodNode.value !== functionNode || !isNodeOfType(methodNode.parent, "ClassBody")) return null;
|
|
33137
|
+
const classNode = methodNode.parent.parent;
|
|
33138
|
+
if (!isNodeOfType(classNode, "ClassDeclaration") && !isNodeOfType(classNode, "ClassExpression")) return null;
|
|
33139
|
+
let bindingIdentifier = isNodeOfType(classNode.id, "Identifier") ? classNode.id : null;
|
|
33140
|
+
if (!bindingIdentifier && isNodeOfType(classNode.parent, "VariableDeclarator")) bindingIdentifier = isNodeOfType(classNode.parent.id, "Identifier") ? classNode.parent.id : null;
|
|
33141
|
+
if (!bindingIdentifier) return null;
|
|
33142
|
+
const ownerSymbol = scopes.symbolFor(bindingIdentifier);
|
|
33143
|
+
const methodName = getStaticPropertyKeyName(methodNode, { allowComputedString: true });
|
|
33144
|
+
return ownerSymbol && methodName ? {
|
|
33145
|
+
methodName,
|
|
33146
|
+
ownerKind: "class",
|
|
33147
|
+
ownerSymbol,
|
|
33148
|
+
isStatic: Boolean(methodNode.static)
|
|
33149
|
+
} : null;
|
|
33150
|
+
};
|
|
33151
|
+
const doesSymbolResolveToOwner = (symbol, ownerSymbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
33152
|
+
if (!symbol) return false;
|
|
33153
|
+
if (symbol.declarationNode === ownerSymbol.declarationNode) return true;
|
|
33154
|
+
if (symbol.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
33155
|
+
visitedSymbolIds.add(symbol.id);
|
|
33156
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
33157
|
+
return isNodeOfType(initializer, "Identifier") && doesSymbolResolveToOwner(scopes.symbolFor(initializer), ownerSymbol, scopes, visitedSymbolIds);
|
|
33158
|
+
};
|
|
33159
|
+
const isClassInstanceExpression = (expression, ownerSymbol, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
33160
|
+
const candidate = stripParenExpression(expression);
|
|
33161
|
+
if (isNodeOfType(candidate, "NewExpression") && isNodeOfType(candidate.callee, "Identifier")) return doesSymbolResolveToOwner(scopes.symbolFor(candidate.callee), ownerSymbol, scopes);
|
|
33162
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
33163
|
+
const symbol = scopes.symbolFor(candidate);
|
|
33164
|
+
if (symbol?.kind !== "const" || !symbol.initializer || visitedSymbolIds.has(symbol.id)) return false;
|
|
33165
|
+
visitedSymbolIds.add(symbol.id);
|
|
33166
|
+
return isClassInstanceExpression(symbol.initializer, ownerSymbol, scopes, visitedSymbolIds);
|
|
33167
|
+
};
|
|
33168
|
+
const getMethodCalls = (functionNode, scopes) => {
|
|
33169
|
+
const owner = getMethodOwner(functionNode, scopes);
|
|
33170
|
+
if (!owner) return [];
|
|
33171
|
+
const calls = [];
|
|
33172
|
+
walkAst(scopes.rootScope.node, (child) => {
|
|
33173
|
+
if (!isNodeOfType(child, "MemberExpression")) return;
|
|
33174
|
+
if (getStaticPropertyName(child) !== owner.methodName) return;
|
|
33175
|
+
const receiver = stripParenExpression(child.object);
|
|
33176
|
+
let doesReceiverMatch = isNodeOfType(receiver, "Identifier") && doesSymbolResolveToOwner(scopes.symbolFor(receiver), owner.ownerSymbol, scopes);
|
|
33177
|
+
if (owner.ownerKind === "class" && !owner.isStatic) doesReceiverMatch = isClassInstanceExpression(receiver, owner.ownerSymbol, scopes);
|
|
33178
|
+
if (!doesReceiverMatch) return;
|
|
33179
|
+
const call = getDirectCallForExpression(child);
|
|
33180
|
+
if (call) calls.push(call);
|
|
33181
|
+
});
|
|
33182
|
+
return calls;
|
|
33183
|
+
};
|
|
33184
|
+
const isFunctionInvokedBeforeUsage = (functionNode, usageNode, usageBoundary, scopes, visitedSymbolIds, visitedFunctionNodes = /* @__PURE__ */ new Set()) => {
|
|
33185
|
+
if (visitedFunctionNodes.has(functionNode)) return false;
|
|
33186
|
+
visitedFunctionNodes.add(functionNode);
|
|
33187
|
+
const usageFunction = findEnclosingFunction(usageNode);
|
|
33188
|
+
const immediateCall = getDirectCallForExpression(functionNode);
|
|
33189
|
+
if (immediateCall) {
|
|
33190
|
+
const immediateCallFunction = findEnclosingFunction(immediateCall);
|
|
33191
|
+
if (immediateCallFunction === usageFunction) {
|
|
33192
|
+
const immediateCallStart = getRangeStart(immediateCall);
|
|
33193
|
+
return immediateCallStart === null || immediateCallStart < usageBoundary;
|
|
33194
|
+
}
|
|
33195
|
+
if (!immediateCallFunction) return usageFunction !== null;
|
|
33196
|
+
return isFunctionInvokedBeforeUsage(immediateCallFunction, usageNode, usageBoundary, scopes, visitedSymbolIds, new Set(visitedFunctionNodes));
|
|
33197
|
+
}
|
|
33198
|
+
for (const methodCall of getMethodCalls(functionNode, scopes)) {
|
|
33199
|
+
const methodCallFunction = findEnclosingFunction(methodCall);
|
|
33200
|
+
if (methodCallFunction === usageFunction) {
|
|
33201
|
+
const methodCallStart = getRangeStart(methodCall);
|
|
33202
|
+
if (methodCallStart === null || methodCallStart < usageBoundary) return true;
|
|
33203
|
+
continue;
|
|
33204
|
+
}
|
|
33205
|
+
if (!methodCallFunction) {
|
|
33206
|
+
if (usageFunction) return true;
|
|
33207
|
+
continue;
|
|
33208
|
+
}
|
|
33209
|
+
if (isFunctionInvokedBeforeUsage(methodCallFunction, usageNode, usageBoundary, scopes, new Set(visitedSymbolIds), new Set(visitedFunctionNodes))) return true;
|
|
33210
|
+
}
|
|
33211
|
+
const bindingIdentifier = getFunctionBindingIdentifier(functionNode);
|
|
33212
|
+
if (!bindingIdentifier) return false;
|
|
33213
|
+
const symbol = scopes.symbolFor(bindingIdentifier);
|
|
33214
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
33215
|
+
visitedSymbolIds.add(symbol.id);
|
|
33216
|
+
let wasInvokedBeforeUsage = false;
|
|
33217
|
+
walkAst(scopes.rootScope.node, (child) => {
|
|
33218
|
+
if (wasInvokedBeforeUsage || !isNodeOfType(child, "Identifier")) return;
|
|
33219
|
+
if (scopes.symbolFor(child)?.declarationNode !== symbol.declarationNode) return;
|
|
33220
|
+
const call = getDirectCallForExpression(child);
|
|
33221
|
+
if (!call) return false;
|
|
33222
|
+
const callFunction = findEnclosingFunction(call);
|
|
33223
|
+
if (callFunction === usageFunction) {
|
|
33224
|
+
const callStart = getRangeStart(call);
|
|
33225
|
+
wasInvokedBeforeUsage = callStart === null || callStart < usageBoundary;
|
|
33226
|
+
return;
|
|
33227
|
+
}
|
|
33228
|
+
if (!callFunction) {
|
|
33229
|
+
wasInvokedBeforeUsage = usageFunction !== null;
|
|
33230
|
+
return;
|
|
33231
|
+
}
|
|
33232
|
+
wasInvokedBeforeUsage = isFunctionInvokedBeforeUsage(callFunction, usageNode, usageBoundary, scopes, new Set(visitedSymbolIds), new Set(visitedFunctionNodes));
|
|
33233
|
+
});
|
|
33234
|
+
return wasInvokedBeforeUsage;
|
|
33235
|
+
};
|
|
33236
|
+
const getMutationUsageBoundary = (usageNode, readNode) => {
|
|
33237
|
+
const readStart = getRangeStart(readNode);
|
|
33238
|
+
let readExpression = readNode;
|
|
33239
|
+
let readParent = readExpression.parent;
|
|
33240
|
+
while (readParent && TRANSPARENT_EXPRESSION_WRAPPER_TYPES.has(readParent.type) && "expression" in readParent && readParent.expression === readExpression) {
|
|
33241
|
+
readExpression = readParent;
|
|
33242
|
+
readParent = readExpression.parent;
|
|
33243
|
+
}
|
|
33244
|
+
if ((isNodeOfType(usageNode, "CallExpression") || isNodeOfType(usageNode, "NewExpression")) && usageNode.arguments?.some((argument) => argument === readExpression)) return usageNode.range?.[1] ?? null;
|
|
33245
|
+
if (isAstDescendant(readNode, usageNode)) return readStart;
|
|
33246
|
+
return getRangeStart(usageNode);
|
|
33247
|
+
};
|
|
33248
|
+
const wasMutatedBeforeUsage = (symbol, usageNode, readNode, scopes, visitedMutationSymbolIds = /* @__PURE__ */ new Set(), inheritedUsageBoundary, readNodesBySymbolId = /* @__PURE__ */ new Map()) => {
|
|
33249
|
+
if (visitedMutationSymbolIds.has(symbol.id)) return false;
|
|
33250
|
+
visitedMutationSymbolIds.add(symbol.id);
|
|
33251
|
+
const usageBoundary = inheritedUsageBoundary === void 0 ? getMutationUsageBoundary(usageNode, readNode) : inheritedUsageBoundary;
|
|
33252
|
+
if (typeof usageBoundary !== "number") return true;
|
|
33253
|
+
const usageFunction = findEnclosingFunction(usageNode);
|
|
33254
|
+
return symbol.references.some((reference) => {
|
|
33255
|
+
const referenceStart = getRangeStart(reference.identifier);
|
|
33256
|
+
const simpleAlias = getSimpleAlias(reference.identifier, scopes);
|
|
33257
|
+
if (simpleAlias) return wasMutatedBeforeUsage(simpleAlias.symbol, usageNode, readNodesBySymbolId.get(simpleAlias.symbol.id) ?? simpleAlias.readNode, scopes, new Set(visitedMutationSymbolIds), usageBoundary, readNodesBySymbolId);
|
|
33258
|
+
if (!isPotentialMutationReference(reference.identifier, readNode)) return false;
|
|
33259
|
+
if (referenceStart === null) return true;
|
|
33260
|
+
const mutationFunction = findEnclosingFunction(reference.identifier);
|
|
33261
|
+
if (mutationFunction === usageFunction) return referenceStart < usageBoundary;
|
|
33262
|
+
if (!mutationFunction) return usageFunction !== null;
|
|
33263
|
+
if (usageFunction && isAstDescendant(usageFunction, mutationFunction)) return true;
|
|
33264
|
+
return isFunctionInvokedBeforeUsage(mutationFunction, usageNode, usageBoundary, scopes, /* @__PURE__ */ new Set());
|
|
33265
|
+
});
|
|
33266
|
+
};
|
|
33267
|
+
const getObjectPropertyProof = (objectExpression, propertyName, scopes, usageNode, visitedSymbolIds = /* @__PURE__ */ new Set(), inheritedUsageBoundary, readNodesBySymbolId = /* @__PURE__ */ new Map()) => {
|
|
33268
|
+
if (!objectExpression) return ABSENT_PROPERTY_PROOF;
|
|
33269
|
+
const unwrapped = stripParenExpression(objectExpression);
|
|
33270
|
+
if (isNodeOfType(unwrapped, "Literal") || isStaticUndefined(unwrapped, scopes)) return ABSENT_PROPERTY_PROOF;
|
|
33271
|
+
if (isNodeOfType(unwrapped, "Identifier")) {
|
|
33272
|
+
const symbol = scopes.symbolFor(unwrapped);
|
|
33273
|
+
const nextReadNodesBySymbolId = new Map(readNodesBySymbolId);
|
|
33274
|
+
if (symbol) nextReadNodesBySymbolId.set(symbol.id, unwrapped);
|
|
33275
|
+
const usageBoundary = inheritedUsageBoundary === void 0 ? getMutationUsageBoundary(usageNode, unwrapped) : inheritedUsageBoundary;
|
|
33276
|
+
if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || visitedSymbolIds.has(symbol.id) || wasMutatedBeforeUsage(symbol, usageNode, unwrapped, scopes, /* @__PURE__ */ new Set(), usageBoundary, nextReadNodesBySymbolId)) return UNKNOWN_PROPERTY_PROOF;
|
|
33277
|
+
visitedSymbolIds.add(symbol.id);
|
|
33278
|
+
return getObjectPropertyProof(symbol.initializer, propertyName, scopes, usageNode, visitedSymbolIds, usageBoundary, nextReadNodesBySymbolId);
|
|
33279
|
+
}
|
|
33280
|
+
if (isNodeOfType(unwrapped, "ConditionalExpression")) {
|
|
33281
|
+
const consequent = getObjectPropertyProof(unwrapped.consequent, propertyName, scopes, usageNode, new Set(visitedSymbolIds), inheritedUsageBoundary, new Map(readNodesBySymbolId));
|
|
33282
|
+
const alternate = getObjectPropertyProof(unwrapped.alternate, propertyName, scopes, usageNode, new Set(visitedSymbolIds), inheritedUsageBoundary, new Map(readNodesBySymbolId));
|
|
33283
|
+
return consequent.status === alternate.status ? consequent : UNKNOWN_PROPERTY_PROOF;
|
|
33284
|
+
}
|
|
33285
|
+
if (isNodeOfType(unwrapped, "CallExpression") && isNodeOfType(unwrapped.callee, "MemberExpression") && !unwrapped.callee.computed && isNodeOfType(unwrapped.callee.object, "Identifier") && unwrapped.callee.object.name === "Object" && scopes.isGlobalReference(unwrapped.callee.object) && isNodeOfType(unwrapped.callee.property, "Identifier") && unwrapped.callee.property.name === "freeze") return getObjectPropertyProof(unwrapped.arguments?.[0], propertyName, scopes, usageNode, visitedSymbolIds, inheritedUsageBoundary, readNodesBySymbolId);
|
|
33286
|
+
if (!isNodeOfType(unwrapped, "ObjectExpression")) return UNKNOWN_PROPERTY_PROOF;
|
|
33287
|
+
const properties = unwrapped.properties ?? [];
|
|
33288
|
+
for (let propertyIndex = properties.length - 1; propertyIndex >= 0; propertyIndex -= 1) {
|
|
33289
|
+
const property = properties[propertyIndex];
|
|
33290
|
+
if (!property) continue;
|
|
33291
|
+
if (isNodeOfType(property, "SpreadElement")) {
|
|
33292
|
+
const spreadProof = getObjectPropertyProof(property.argument, propertyName, scopes, unwrapped, new Set(visitedSymbolIds), void 0);
|
|
33293
|
+
if (spreadProof.status !== "absent") return spreadProof;
|
|
33294
|
+
continue;
|
|
33295
|
+
}
|
|
33296
|
+
if (!isNodeOfType(property, "Property") || getStaticPropertyKeyName(property) !== propertyName) continue;
|
|
33297
|
+
return isStaticUndefined(property.value, scopes) ? UNDEFINED_PROPERTY_PROOF : PRESENT_PROPERTY_PROOF;
|
|
33298
|
+
}
|
|
33299
|
+
return ABSENT_PROPERTY_PROOF;
|
|
33300
|
+
};
|
|
33301
|
+
const objectHasExplicitProperty = (objectExpression, propertyName, scopes, usageNode) => getObjectPropertyProof(objectExpression, propertyName, scopes, usageNode).status === "present";
|
|
33302
|
+
const hasExplicitLocaleArgument = (argument, scopes) => {
|
|
32788
33303
|
if (!argument) return false;
|
|
32789
33304
|
const unwrapped = stripParenExpression(argument);
|
|
32790
|
-
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined") return false;
|
|
33305
|
+
if (isNodeOfType(unwrapped, "Identifier") && unwrapped.name === "undefined" && scopes.isGlobalReference(unwrapped) || isNodeOfType(unwrapped, "UnaryExpression") && unwrapped.operator === "void") return false;
|
|
32791
33306
|
return true;
|
|
32792
33307
|
};
|
|
32793
|
-
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate) => {
|
|
33308
|
+
const isDeterministicLocaleMethodCall = (call, methodName, receiverIsProvablyDate, scopes) => {
|
|
32794
33309
|
const localeArgument = call.arguments?.[0];
|
|
32795
|
-
if (!hasExplicitLocaleArgument(localeArgument)) return false;
|
|
33310
|
+
if (!hasExplicitLocaleArgument(localeArgument, scopes)) return false;
|
|
32796
33311
|
const optionsArgument = call.arguments?.[1];
|
|
32797
|
-
if (
|
|
33312
|
+
if (objectHasExplicitProperty(optionsArgument, "timeZone", scopes, call)) return true;
|
|
32798
33313
|
return !DATE_ONLY_LOCALE_METHOD_NAMES.has(methodName) && !receiverIsProvablyDate;
|
|
32799
33314
|
};
|
|
32800
|
-
const matchLocaleMethodCall = (call) => {
|
|
33315
|
+
const matchLocaleMethodCall = (call, scopes) => {
|
|
32801
33316
|
const callee = call.callee;
|
|
32802
33317
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
32803
33318
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
@@ -32805,7 +33320,7 @@ const matchLocaleMethodCall = (call) => {
|
|
|
32805
33320
|
if (!LOCALE_FORMAT_METHOD_NAMES.has(methodName)) return null;
|
|
32806
33321
|
const receiverIsProvablyDate = isProvableDateExpression(callee.object);
|
|
32807
33322
|
if (methodName === "toLocaleString" && !receiverIsProvablyDate && !receiverNameLooksDateFlavored(callee.object)) return null;
|
|
32808
|
-
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate)) return null;
|
|
33323
|
+
if (isDeterministicLocaleMethodCall(call, methodName, receiverIsProvablyDate, scopes)) return null;
|
|
32809
33324
|
return {
|
|
32810
33325
|
node: call,
|
|
32811
33326
|
display: `${methodName}()`
|
|
@@ -32821,13 +33336,13 @@ const getIntlFormatterName = (expression) => {
|
|
|
32821
33336
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
32822
33337
|
return INTL_FORMATTER_NAMES.has(callee.property.name) ? callee.property.name : null;
|
|
32823
33338
|
};
|
|
32824
|
-
const isDeterministicIntlConstruction = (construction, formatterName) => {
|
|
33339
|
+
const isDeterministicIntlConstruction = (construction, formatterName, scopes) => {
|
|
32825
33340
|
if (!isNodeOfType(construction, "CallExpression") && !isNodeOfType(construction, "NewExpression")) return false;
|
|
32826
|
-
if (!hasExplicitLocaleArgument(construction.arguments?.[0])) return false;
|
|
33341
|
+
if (!hasExplicitLocaleArgument(construction.arguments?.[0], scopes)) return false;
|
|
32827
33342
|
if (formatterName !== "DateTimeFormat") return true;
|
|
32828
|
-
return
|
|
33343
|
+
return objectHasExplicitProperty(construction.arguments?.[1], "timeZone", scopes, construction);
|
|
32829
33344
|
};
|
|
32830
|
-
const matchIntlFormatCall = (call) => {
|
|
33345
|
+
const matchIntlFormatCall = (call, scopes) => {
|
|
32831
33346
|
const callee = call.callee;
|
|
32832
33347
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return null;
|
|
32833
33348
|
if (!isNodeOfType(callee.property, "Identifier")) return null;
|
|
@@ -32840,7 +33355,7 @@ const matchIntlFormatCall = (call) => {
|
|
|
32840
33355
|
if (!construction) return null;
|
|
32841
33356
|
const formatterName = getIntlFormatterName(construction);
|
|
32842
33357
|
if (!formatterName) return null;
|
|
32843
|
-
if (isDeterministicIntlConstruction(construction, formatterName)) return null;
|
|
33358
|
+
if (isDeterministicIntlConstruction(construction, formatterName, scopes)) return null;
|
|
32844
33359
|
return {
|
|
32845
33360
|
node: call,
|
|
32846
33361
|
display: `Intl.${formatterName}().${callee.property.name}()`
|
|
@@ -32909,7 +33424,7 @@ const noLocaleFormatInRender = defineRule({
|
|
|
32909
33424
|
fileIsEmailTemplate = hasEmailTemplateImport(node);
|
|
32910
33425
|
},
|
|
32911
33426
|
CallExpression(node) {
|
|
32912
|
-
const match = matchLocaleMethodCall(node) ?? matchIntlFormatCall(node) ?? matchDateDefaultStringification(node);
|
|
33427
|
+
const match = matchLocaleMethodCall(node, context.scopes) ?? matchIntlFormatCall(node, context.scopes) ?? matchDateDefaultStringification(node);
|
|
32913
33428
|
if (match) reportIfRenderPhase(match);
|
|
32914
33429
|
},
|
|
32915
33430
|
TemplateLiteral(node) {
|
|
@@ -32929,7 +33444,7 @@ const noLocaleFormatInRender = defineRule({
|
|
|
32929
33444
|
walkAst(helperNode.body ?? helperNode, (child) => {
|
|
32930
33445
|
if (isFunctionLike$1(child)) return false;
|
|
32931
33446
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32932
|
-
const match = matchLocaleMethodCall(child) ?? matchIntlFormatCall(child);
|
|
33447
|
+
const match = matchLocaleMethodCall(child, context.scopes) ?? matchIntlFormatCall(child, context.scopes);
|
|
32933
33448
|
if (!match || reportedNodes.has(match.node)) return;
|
|
32934
33449
|
if (fileIsEmailTemplate) return;
|
|
32935
33450
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
@@ -36755,28 +37270,85 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
36755
37270
|
});
|
|
36756
37271
|
//#endregion
|
|
36757
37272
|
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
36758
|
-
const
|
|
36759
|
-
|
|
36760
|
-
|
|
36761
|
-
|
|
36762
|
-
|
|
36763
|
-
|
|
36764
|
-
|
|
36765
|
-
return isReactApiCall(initializer, "useRef", scopes, { allowGlobalReactNamespace: true }) ? symbol : null;
|
|
36766
|
-
};
|
|
37273
|
+
const REPEATED_ANCESTOR_TYPES = new Set([
|
|
37274
|
+
"DoWhileStatement",
|
|
37275
|
+
"ForInStatement",
|
|
37276
|
+
"ForOfStatement",
|
|
37277
|
+
"ForStatement",
|
|
37278
|
+
"WhileStatement"
|
|
37279
|
+
]);
|
|
36767
37280
|
const isSameRefCurrentMember = (node, refSymbol, scopes) => {
|
|
36768
|
-
if (!isNodeOfType(node, "MemberExpression") ||
|
|
37281
|
+
if (!isNodeOfType(node, "MemberExpression") || getStaticPropertyName(node) !== "current") return false;
|
|
36769
37282
|
const receiver = stripParenExpression(node.object);
|
|
36770
37283
|
return isNodeOfType(receiver, "Identifier") && resolveConstIdentifierAlias(receiver, scopes)?.id === refSymbol.id;
|
|
36771
37284
|
};
|
|
37285
|
+
const isSameRefCurrentAlias = (node, refSymbol, scopes) => {
|
|
37286
|
+
if (isSameRefCurrentMember(node, refSymbol, scopes)) return true;
|
|
37287
|
+
if (!isNodeOfType(node, "Identifier")) return false;
|
|
37288
|
+
const aliasSymbol = scopes.symbolFor(node);
|
|
37289
|
+
return aliasSymbol?.kind === "const" && aliasSymbol.initializer !== null && isSameRefCurrentMember(stripParenExpression(aliasSymbol.initializer), refSymbol, scopes);
|
|
37290
|
+
};
|
|
37291
|
+
const isEmptySentinel = (node, scopes) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined" && scopes.isGlobalReference(node);
|
|
37292
|
+
const hasRepeatedExecutionAncestor = (node, stop) => {
|
|
37293
|
+
let ancestor = node.parent;
|
|
37294
|
+
while (ancestor && ancestor !== stop) {
|
|
37295
|
+
if (isFunctionLike$1(ancestor) || REPEATED_ANCESTOR_TYPES.has(ancestor.type)) return true;
|
|
37296
|
+
ancestor = ancestor.parent;
|
|
37297
|
+
}
|
|
37298
|
+
return ancestor !== stop;
|
|
37299
|
+
};
|
|
37300
|
+
const getBranchConstraints = (node, branchRoot) => {
|
|
37301
|
+
const constraints = /* @__PURE__ */ new Map();
|
|
37302
|
+
let descendant = node;
|
|
37303
|
+
let ancestor = descendant.parent;
|
|
37304
|
+
while (ancestor && descendant !== branchRoot) {
|
|
37305
|
+
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
37306
|
+
if (ancestor.consequent === descendant) constraints.set(ancestor, true);
|
|
37307
|
+
if (ancestor.alternate === descendant) constraints.set(ancestor, false);
|
|
37308
|
+
}
|
|
37309
|
+
descendant = ancestor;
|
|
37310
|
+
ancestor = ancestor.parent;
|
|
37311
|
+
}
|
|
37312
|
+
return constraints;
|
|
37313
|
+
};
|
|
37314
|
+
const canExecuteTogether = (firstConstraints, secondConstraints) => {
|
|
37315
|
+
for (const [statement, branch] of firstConstraints) {
|
|
37316
|
+
const otherBranch = secondConstraints.get(statement);
|
|
37317
|
+
if (otherBranch !== void 0 && otherBranch !== branch) return false;
|
|
37318
|
+
}
|
|
37319
|
+
return true;
|
|
37320
|
+
};
|
|
37321
|
+
const hasNoPriorCoExecutableWrite = (assignmentExpression, branchRoot, refSymbol, scopes) => {
|
|
37322
|
+
const assignmentConstraints = getBranchConstraints(assignmentExpression, branchRoot);
|
|
37323
|
+
const assignmentStart = getRangeStart(assignmentExpression);
|
|
37324
|
+
let hasCoExecutableWrite = false;
|
|
37325
|
+
walkAst(branchRoot, (child) => {
|
|
37326
|
+
if (hasCoExecutableWrite) return false;
|
|
37327
|
+
const childStart = getRangeStart(child);
|
|
37328
|
+
if (child === assignmentExpression || !isNodeOfType(child, "AssignmentExpression") || assignmentStart === null || childStart === null || childStart >= assignmentStart || resolveReactRefSymbol(child.left, scopes)?.id !== refSymbol.id || hasRepeatedExecutionAncestor(child, branchRoot)) return;
|
|
37329
|
+
if (canExecuteTogether(assignmentConstraints, getBranchConstraints(child, branchRoot))) {
|
|
37330
|
+
hasCoExecutableWrite = true;
|
|
37331
|
+
return false;
|
|
37332
|
+
}
|
|
37333
|
+
});
|
|
37334
|
+
return !hasCoExecutableWrite;
|
|
37335
|
+
};
|
|
36772
37336
|
const isDocumentedLazyInitialization = (assignmentExpression, refSymbol, scopes) => {
|
|
36773
|
-
if (assignmentExpression.operator
|
|
37337
|
+
if (assignmentExpression.operator === "??=" || assignmentExpression.operator === "||=") return true;
|
|
37338
|
+
if (assignmentExpression.operator !== "=") return false;
|
|
36774
37339
|
let descendant = assignmentExpression;
|
|
36775
37340
|
let ancestor = descendant.parent;
|
|
36776
37341
|
while (ancestor) {
|
|
36777
|
-
if (isNodeOfType(ancestor, "IfStatement") &&
|
|
37342
|
+
if (isNodeOfType(ancestor, "IfStatement") && isNodeOfType(ancestor.test, "BinaryExpression") && [
|
|
37343
|
+
"===",
|
|
37344
|
+
"==",
|
|
37345
|
+
"!==",
|
|
37346
|
+
"!="
|
|
37347
|
+
].includes(ancestor.test.operator)) {
|
|
36778
37348
|
const { left, right } = ancestor.test;
|
|
36779
|
-
|
|
37349
|
+
const comparesEmptySentinel = isSameRefCurrentAlias(left, refSymbol, scopes) && isEmptySentinel(right, scopes) || isSameRefCurrentAlias(right, refSymbol, scopes) && isEmptySentinel(left, scopes);
|
|
37350
|
+
const guardedBranch = ancestor.test.operator === "===" || ancestor.test.operator === "==" ? ancestor.consequent : ancestor.alternate;
|
|
37351
|
+
if (comparesEmptySentinel && guardedBranch === descendant && guardedBranch && !hasRepeatedExecutionAncestor(assignmentExpression, guardedBranch) && hasNoPriorCoExecutableWrite(assignmentExpression, guardedBranch, refSymbol, scopes)) return true;
|
|
36780
37352
|
}
|
|
36781
37353
|
descendant = ancestor;
|
|
36782
37354
|
ancestor = descendant.parent;
|
|
@@ -43685,13 +44257,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
43685
44257
|
}
|
|
43686
44258
|
return null;
|
|
43687
44259
|
};
|
|
43688
|
-
const resolveHookNameFromInitializer = (initializer) => {
|
|
44260
|
+
const resolveHookNameFromInitializer = (initializer, scopes) => {
|
|
43689
44261
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
43690
44262
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
43691
|
-
const
|
|
43692
|
-
if (
|
|
43693
|
-
if (!isNodeOfType(
|
|
43694
|
-
return resolveTanstackQueryHookName(
|
|
44263
|
+
const resolvedSymbol = resolveConstIdentifierAlias(initializer, scopes);
|
|
44264
|
+
if (resolvedSymbol?.kind !== "const" || !resolvedSymbol.initializer) return null;
|
|
44265
|
+
if (!isNodeOfType(resolvedSymbol.initializer, "CallExpression")) return null;
|
|
44266
|
+
return resolveTanstackQueryHookName(resolvedSymbol.initializer);
|
|
43695
44267
|
};
|
|
43696
44268
|
const queryNoRestDestructuring = defineRule({
|
|
43697
44269
|
id: "query-no-rest-destructuring",
|
|
@@ -43704,7 +44276,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
43704
44276
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
43705
44277
|
if (!node.init) return;
|
|
43706
44278
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
43707
|
-
const hookName = resolveHookNameFromInitializer(node.init);
|
|
44279
|
+
const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
|
|
43708
44280
|
if (!hookName) return;
|
|
43709
44281
|
context.report({
|
|
43710
44282
|
node: node.id,
|
|
@@ -45059,7 +45631,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
|
|
|
45059
45631
|
"useParams",
|
|
45060
45632
|
"usePathname"
|
|
45061
45633
|
]);
|
|
45062
|
-
const findHookCallBindings = (componentBody) => {
|
|
45634
|
+
const findHookCallBindings = (componentBody, scopes) => {
|
|
45063
45635
|
const bindings = [];
|
|
45064
45636
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
45065
45637
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -45070,8 +45642,10 @@ const findHookCallBindings = (componentBody) => {
|
|
|
45070
45642
|
const callee = declarator.init.callee;
|
|
45071
45643
|
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
45072
45644
|
if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
|
|
45645
|
+
const valueSymbol = scopes.symbolFor(declarator.id);
|
|
45646
|
+
if (!valueSymbol) continue;
|
|
45073
45647
|
bindings.push({
|
|
45074
|
-
|
|
45648
|
+
valueSymbol,
|
|
45075
45649
|
hookName: callee.name,
|
|
45076
45650
|
declarator
|
|
45077
45651
|
});
|
|
@@ -45079,6 +45653,49 @@ const findHookCallBindings = (componentBody) => {
|
|
|
45079
45653
|
}
|
|
45080
45654
|
return bindings;
|
|
45081
45655
|
};
|
|
45656
|
+
const collectExactAliasSymbols = (componentBody, sourceSymbol, scopes) => {
|
|
45657
|
+
const symbols = [sourceSymbol];
|
|
45658
|
+
const symbolIds = new Set([sourceSymbol.id]);
|
|
45659
|
+
const aliasSourceIdentifiers = /* @__PURE__ */ new Set();
|
|
45660
|
+
if (!isNodeOfType(componentBody, "BlockStatement")) return {
|
|
45661
|
+
symbols,
|
|
45662
|
+
aliasSourceIdentifiers
|
|
45663
|
+
};
|
|
45664
|
+
let didFindAlias = true;
|
|
45665
|
+
while (didFindAlias) {
|
|
45666
|
+
didFindAlias = false;
|
|
45667
|
+
for (const statement of componentBody.body ?? []) {
|
|
45668
|
+
if (!isNodeOfType(statement, "VariableDeclaration") || statement.kind !== "const") continue;
|
|
45669
|
+
for (const declarator of statement.declarations ?? []) {
|
|
45670
|
+
let aliasIdentifier = null;
|
|
45671
|
+
let sourceIdentifier = null;
|
|
45672
|
+
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
45673
|
+
const arrayBinding = isNodeOfType(declarator.id, "ArrayPattern") ? declarator.id.elements[0] : null;
|
|
45674
|
+
const arrayValueNode = isNodeOfType(initializer, "ArrayExpression") ? initializer.elements[0] : null;
|
|
45675
|
+
const arrayValue = arrayValueNode && !isNodeOfType(arrayValueNode, "SpreadElement") ? stripParenExpression(arrayValueNode) : null;
|
|
45676
|
+
if (isNodeOfType(declarator.id, "Identifier") && isNodeOfType(initializer, "Identifier")) {
|
|
45677
|
+
aliasIdentifier = declarator.id;
|
|
45678
|
+
sourceIdentifier = initializer;
|
|
45679
|
+
} else if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements.length === 1 && isNodeOfType(arrayBinding, "Identifier") && isNodeOfType(initializer, "ArrayExpression") && initializer.elements.length === 1 && isNodeOfType(arrayValue, "Identifier")) {
|
|
45680
|
+
aliasIdentifier = arrayBinding;
|
|
45681
|
+
sourceIdentifier = arrayValue;
|
|
45682
|
+
}
|
|
45683
|
+
if (!aliasIdentifier || !sourceIdentifier) continue;
|
|
45684
|
+
const referencedSymbol = scopes.symbolFor(sourceIdentifier);
|
|
45685
|
+
const aliasSymbol = scopes.symbolFor(aliasIdentifier);
|
|
45686
|
+
if (!referencedSymbol || !symbolIds.has(referencedSymbol.id) || !aliasSymbol || aliasSymbol.kind !== "const" || symbolIds.has(aliasSymbol.id)) continue;
|
|
45687
|
+
symbolIds.add(aliasSymbol.id);
|
|
45688
|
+
symbols.push(aliasSymbol);
|
|
45689
|
+
aliasSourceIdentifiers.add(sourceIdentifier);
|
|
45690
|
+
didFindAlias = true;
|
|
45691
|
+
}
|
|
45692
|
+
}
|
|
45693
|
+
}
|
|
45694
|
+
return {
|
|
45695
|
+
symbols,
|
|
45696
|
+
aliasSourceIdentifiers
|
|
45697
|
+
};
|
|
45698
|
+
};
|
|
45082
45699
|
const rerenderDeferReadsHook = defineRule({
|
|
45083
45700
|
id: "rerender-defer-reads-hook",
|
|
45084
45701
|
title: "URL hook value only read in handlers",
|
|
@@ -45089,15 +45706,13 @@ const rerenderDeferReadsHook = defineRule({
|
|
|
45089
45706
|
create: (context) => {
|
|
45090
45707
|
const checkComponent = (componentBody) => {
|
|
45091
45708
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
45092
|
-
const bindings = findHookCallBindings(componentBody);
|
|
45709
|
+
const bindings = findHookCallBindings(componentBody, context.scopes);
|
|
45093
45710
|
if (bindings.length === 0) return;
|
|
45094
45711
|
const handlerBindingNames = collectHandlerBindingNames(componentBody);
|
|
45095
45712
|
for (const binding of bindings) {
|
|
45713
|
+
const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
|
|
45096
45714
|
const referenceLocations = [];
|
|
45097
|
-
|
|
45098
|
-
if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
|
|
45099
|
-
if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
|
|
45100
|
-
});
|
|
45715
|
+
for (const symbol of symbols) for (const reference of symbol.references) if (!aliasSourceIdentifiers.has(reference.identifier)) referenceLocations.push(reference.identifier);
|
|
45101
45716
|
if (referenceLocations.length === 0) continue;
|
|
45102
45717
|
if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
|
|
45103
45718
|
context.report({
|
|
@@ -53046,7 +53661,8 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
53046
53661
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
53047
53662
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
53048
53663
|
const firstArg = node.arguments?.[0];
|
|
53049
|
-
if (!isNodeOfType(firstArg, "
|
|
53664
|
+
if (!firstArg || isNodeOfType(firstArg, "SpreadElement")) return;
|
|
53665
|
+
if (!isNodeOfType(unwrapObjectIntegrityExpression(firstArg, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES), "ObjectExpression")) return;
|
|
53050
53666
|
context.report({
|
|
53051
53667
|
node,
|
|
53052
53668
|
message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
|