oxlint-plugin-react-doctor 0.7.6-dev.778f1a2 → 0.7.6-dev.81e6647
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 +247 -45
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6296,6 +6296,131 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6296
6296
|
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6297
6297
|
};
|
|
6298
6298
|
//#endregion
|
|
6299
|
+
//#region src/plugin/utils/collect-safely-validated-local-storage-keys.ts
|
|
6300
|
+
const isLocalStorageCall = (node, methodName) => {
|
|
6301
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6302
|
+
const callee = stripParenExpression(node.callee);
|
|
6303
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
6304
|
+
const receiver = stripParenExpression(callee.object);
|
|
6305
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "localStorage" && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6306
|
+
};
|
|
6307
|
+
const catchReturnsFallback = (tryStatement) => {
|
|
6308
|
+
const handler = tryStatement.handler;
|
|
6309
|
+
if (!handler) return false;
|
|
6310
|
+
let hasReturn = false;
|
|
6311
|
+
let hasThrow = false;
|
|
6312
|
+
walkAst(handler.body, (child) => {
|
|
6313
|
+
if (isFunctionLike$1(child)) return false;
|
|
6314
|
+
if (isNodeOfType(child, "ReturnStatement")) hasReturn = true;
|
|
6315
|
+
if (isNodeOfType(child, "ThrowStatement")) hasThrow = true;
|
|
6316
|
+
});
|
|
6317
|
+
return hasReturn && !hasThrow;
|
|
6318
|
+
};
|
|
6319
|
+
const resolveValidatorFunction = (callee, scopes) => {
|
|
6320
|
+
const unwrappedCallee = stripParenExpression(callee);
|
|
6321
|
+
if (!isNodeOfType(unwrappedCallee, "Identifier")) return null;
|
|
6322
|
+
const symbol = scopes.symbolFor(unwrappedCallee);
|
|
6323
|
+
if (!symbol) return null;
|
|
6324
|
+
const candidate = symbol.kind === "function" ? symbol.declarationNode : symbol.initializer;
|
|
6325
|
+
return isFunctionLike$1(candidate) ? candidate : null;
|
|
6326
|
+
};
|
|
6327
|
+
const validatorChecksPayloadProperties = (validatorFunction, scopes) => {
|
|
6328
|
+
if (!isFunctionLike$1(validatorFunction)) return false;
|
|
6329
|
+
const firstParameter = validatorFunction.params?.[0];
|
|
6330
|
+
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
6331
|
+
const parameterSymbol = scopes.symbolFor(firstParameter);
|
|
6332
|
+
if (!parameterSymbol) return false;
|
|
6333
|
+
const payloadSymbolIds = new Set([parameterSymbol.id]);
|
|
6334
|
+
let hasPropertyTypeCheck = false;
|
|
6335
|
+
walkAst(validatorFunction.body, (child) => {
|
|
6336
|
+
if (isFunctionLike$1(child)) return false;
|
|
6337
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier") && child.init) {
|
|
6338
|
+
const initializer = stripParenExpression(child.init);
|
|
6339
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
6340
|
+
const initializerSymbol = scopes.symbolFor(initializer);
|
|
6341
|
+
if (initializerSymbol && payloadSymbolIds.has(initializerSymbol.id)) {
|
|
6342
|
+
const aliasSymbol = scopes.symbolFor(child.id);
|
|
6343
|
+
if (aliasSymbol) payloadSymbolIds.add(aliasSymbol.id);
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
if (!isNodeOfType(child, "UnaryExpression") || child.operator !== "typeof") return;
|
|
6348
|
+
const checkedValue = stripParenExpression(child.argument);
|
|
6349
|
+
if (!isNodeOfType(checkedValue, "MemberExpression")) return;
|
|
6350
|
+
const receiver = stripParenExpression(checkedValue.object);
|
|
6351
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6352
|
+
const receiverSymbol = scopes.symbolFor(receiver);
|
|
6353
|
+
if (receiverSymbol && payloadSymbolIds.has(receiverSymbol.id)) hasPropertyTypeCheck = true;
|
|
6354
|
+
});
|
|
6355
|
+
return hasPropertyTypeCheck;
|
|
6356
|
+
};
|
|
6357
|
+
const incrementCount = (counts, keyValue) => {
|
|
6358
|
+
counts.set(keyValue, (counts.get(keyValue) ?? 0) + 1);
|
|
6359
|
+
};
|
|
6360
|
+
const isReturnedExpression = (expression) => {
|
|
6361
|
+
const parent = expression.parent;
|
|
6362
|
+
return Boolean(parent && isNodeOfType(parent, "ReturnStatement") && parent.argument === expression);
|
|
6363
|
+
};
|
|
6364
|
+
const collectSafelyValidatedLocalStorageKeys = ({ programNode, scopes, resolveKey }) => {
|
|
6365
|
+
const readCountsByKey = /* @__PURE__ */ new Map();
|
|
6366
|
+
const safeReadCountsByKey = /* @__PURE__ */ new Map();
|
|
6367
|
+
const safeReadSymbolIds = /* @__PURE__ */ new Set();
|
|
6368
|
+
walkAst(programNode, (child) => {
|
|
6369
|
+
if (!isNodeOfType(child, "CallExpression") || !isLocalStorageCall(child, "getItem")) return;
|
|
6370
|
+
const keyArgument = child.arguments?.[0];
|
|
6371
|
+
if (!keyArgument) return;
|
|
6372
|
+
const keyValue = resolveKey(keyArgument);
|
|
6373
|
+
if (keyValue !== null) incrementCount(readCountsByKey, keyValue);
|
|
6374
|
+
});
|
|
6375
|
+
walkAst(programNode, (child) => {
|
|
6376
|
+
if (!isNodeOfType(child, "TryStatement") || !catchReturnsFallback(child)) return;
|
|
6377
|
+
const rawValueKeys = /* @__PURE__ */ new Map();
|
|
6378
|
+
const parsedValueSources = /* @__PURE__ */ new Map();
|
|
6379
|
+
walkAst(child.block, (tryChild) => {
|
|
6380
|
+
if (isFunctionLike$1(tryChild)) return false;
|
|
6381
|
+
if (isNodeOfType(tryChild, "VariableDeclarator") && isNodeOfType(tryChild.id, "Identifier") && tryChild.init) {
|
|
6382
|
+
const initializer = stripParenExpression(tryChild.init);
|
|
6383
|
+
if (isNodeOfType(initializer, "CallExpression") && isLocalStorageCall(initializer, "getItem")) {
|
|
6384
|
+
const keyArgument = initializer.arguments?.[0];
|
|
6385
|
+
const bindingSymbol = scopes.symbolFor(tryChild.id);
|
|
6386
|
+
if (keyArgument && bindingSymbol) {
|
|
6387
|
+
const keyValue = resolveKey(keyArgument);
|
|
6388
|
+
if (keyValue !== null) rawValueKeys.set(bindingSymbol.id, keyValue);
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
if (isNodeOfType(initializer, "CallExpression") && isGlobalMethodCall(initializer, "JSON", "parse")) {
|
|
6392
|
+
const rawValueArgument = initializer.arguments?.[0];
|
|
6393
|
+
const parsedValueSymbol = scopes.symbolFor(tryChild.id);
|
|
6394
|
+
if (rawValueArgument && isNodeOfType(rawValueArgument, "Identifier") && parsedValueSymbol) {
|
|
6395
|
+
const rawValueSymbol = scopes.symbolFor(rawValueArgument);
|
|
6396
|
+
if (rawValueSymbol && rawValueKeys.has(rawValueSymbol.id)) parsedValueSources.set(parsedValueSymbol.id, rawValueSymbol.id);
|
|
6397
|
+
}
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
if (!isNodeOfType(tryChild, "ConditionalExpression") || !isReturnedExpression(tryChild)) return;
|
|
6401
|
+
const test = stripParenExpression(tryChild.test);
|
|
6402
|
+
if (!isNodeOfType(test, "CallExpression")) return;
|
|
6403
|
+
const testedValue = test.arguments?.[0];
|
|
6404
|
+
if (!testedValue || !isNodeOfType(testedValue, "Identifier")) return;
|
|
6405
|
+
const parsedValueSymbol = scopes.symbolFor(testedValue);
|
|
6406
|
+
if (!parsedValueSymbol) return;
|
|
6407
|
+
const rawValueSymbolId = parsedValueSources.get(parsedValueSymbol.id);
|
|
6408
|
+
if (rawValueSymbolId === void 0) return;
|
|
6409
|
+
const returnedValue = stripParenExpression(tryChild.consequent);
|
|
6410
|
+
if (!isNodeOfType(returnedValue, "Identifier") || scopes.symbolFor(returnedValue)?.id !== parsedValueSymbol.id) return;
|
|
6411
|
+
const validatorFunction = resolveValidatorFunction(test.callee, scopes);
|
|
6412
|
+
if (!validatorFunction || !validatorChecksPayloadProperties(validatorFunction, scopes)) return;
|
|
6413
|
+
const keyValue = rawValueKeys.get(rawValueSymbolId);
|
|
6414
|
+
if (keyValue === void 0 || safeReadSymbolIds.has(rawValueSymbolId)) return;
|
|
6415
|
+
safeReadSymbolIds.add(rawValueSymbolId);
|
|
6416
|
+
incrementCount(safeReadCountsByKey, keyValue);
|
|
6417
|
+
});
|
|
6418
|
+
});
|
|
6419
|
+
const safeKeys = /* @__PURE__ */ new Set();
|
|
6420
|
+
for (const [keyValue, readCount] of readCountsByKey) if (safeReadCountsByKey.get(keyValue) === readCount) safeKeys.add(keyValue);
|
|
6421
|
+
return safeKeys;
|
|
6422
|
+
};
|
|
6423
|
+
//#endregion
|
|
6299
6424
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6300
6425
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6301
6426
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
@@ -6317,26 +6442,39 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6317
6442
|
severity: "warn",
|
|
6318
6443
|
category: "Correctness",
|
|
6319
6444
|
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
|
-
|
|
6445
|
+
create: (context) => {
|
|
6446
|
+
let safelyValidatedStorageKeys = /* @__PURE__ */ new Set();
|
|
6447
|
+
return {
|
|
6448
|
+
Program(node) {
|
|
6449
|
+
safelyValidatedStorageKeys = collectSafelyValidatedLocalStorageKeys({
|
|
6450
|
+
programNode: node,
|
|
6451
|
+
scopes: context.scopes,
|
|
6452
|
+
resolveKey: (keyNode) => resolveStringKey(keyNode, context)
|
|
6453
|
+
});
|
|
6454
|
+
},
|
|
6455
|
+
CallExpression(node) {
|
|
6456
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6457
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6458
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6459
|
+
if (receiver.name !== "localStorage") return;
|
|
6460
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6461
|
+
if (node.callee.property.name !== "setItem") return;
|
|
6462
|
+
const keyArg = node.arguments?.[0];
|
|
6463
|
+
if (!keyArg) return;
|
|
6464
|
+
const keyValue = resolveStringKey(keyArg, context);
|
|
6465
|
+
if (keyValue === null) return;
|
|
6466
|
+
if (isVersionedKey(keyValue)) return;
|
|
6467
|
+
if (safelyValidatedStorageKeys.has(keyValue)) return;
|
|
6468
|
+
const valueArg = node.arguments?.[1];
|
|
6469
|
+
if (!valueArg) return;
|
|
6470
|
+
if (!isJsonStringifyCall(valueArg)) return;
|
|
6471
|
+
context.report({
|
|
6472
|
+
node: keyArg,
|
|
6473
|
+
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").`
|
|
6474
|
+
});
|
|
6475
|
+
}
|
|
6476
|
+
};
|
|
6477
|
+
}
|
|
6340
6478
|
});
|
|
6341
6479
|
//#endregion
|
|
6342
6480
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
@@ -20395,8 +20533,24 @@ const hasDirective = (programNode, directive) => {
|
|
|
20395
20533
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20396
20534
|
};
|
|
20397
20535
|
//#endregion
|
|
20398
|
-
//#region src/plugin/utils/
|
|
20399
|
-
const
|
|
20536
|
+
//#region src/plugin/utils/unwrap-object-integrity-expression.ts
|
|
20537
|
+
const OBJECT_INTEGRITY_METHOD_NAMES = new Set([
|
|
20538
|
+
"freeze",
|
|
20539
|
+
"seal",
|
|
20540
|
+
"preventExtensions"
|
|
20541
|
+
]);
|
|
20542
|
+
const OBJECT_FREEZE_OR_SEAL_METHOD_NAMES = new Set(["freeze", "seal"]);
|
|
20543
|
+
const unwrapObjectIntegrityExpression = (node, scopes, methodNames = OBJECT_INTEGRITY_METHOD_NAMES) => {
|
|
20544
|
+
let expression = stripParenExpression(node);
|
|
20545
|
+
while (isNodeOfType(expression, "CallExpression")) {
|
|
20546
|
+
const callee = stripParenExpression(expression.callee);
|
|
20547
|
+
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;
|
|
20548
|
+
const wrappedExpression = expression.arguments[0];
|
|
20549
|
+
if (!wrappedExpression || isNodeOfType(wrappedExpression, "SpreadElement")) break;
|
|
20550
|
+
expression = stripParenExpression(wrappedExpression);
|
|
20551
|
+
}
|
|
20552
|
+
return expression;
|
|
20553
|
+
};
|
|
20400
20554
|
//#endregion
|
|
20401
20555
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
20402
20556
|
const nextjsAsyncClientComponent = defineRule({
|
|
@@ -20422,10 +20576,10 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
20422
20576
|
},
|
|
20423
20577
|
VariableDeclarator(node) {
|
|
20424
20578
|
if (!fileHasUseClient) return;
|
|
20425
|
-
if (!isComponentAssignment(node)) return;
|
|
20426
|
-
if (!isInlineFunctionExpression(node.init)) return;
|
|
20427
|
-
if (!node.init.async) return;
|
|
20428
20579
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
20580
|
+
if (!isUppercaseName(node.id.name) || !node.init) return;
|
|
20581
|
+
const componentFunction = unwrapObjectIntegrityExpression(node.init, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES);
|
|
20582
|
+
if (!isInlineFunctionExpression(componentFunction) || !componentFunction.async) return;
|
|
20429
20583
|
context.report({
|
|
20430
20584
|
node,
|
|
20431
20585
|
message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
|
|
@@ -26161,6 +26315,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
|
26161
26315
|
return hasRenderOutput;
|
|
26162
26316
|
};
|
|
26163
26317
|
//#endregion
|
|
26318
|
+
//#region src/plugin/utils/is-component-assignment.ts
|
|
26319
|
+
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"));
|
|
26320
|
+
//#endregion
|
|
26164
26321
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
26165
26322
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
26166
26323
|
//#endregion
|
|
@@ -32210,11 +32367,12 @@ const isMemoCall = (node) => {
|
|
|
32210
32367
|
};
|
|
32211
32368
|
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
32212
32369
|
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
32213
|
-
const isInlineReference = (node) => {
|
|
32214
|
-
|
|
32215
|
-
if (isNodeOfType(
|
|
32216
|
-
if (isNodeOfType(
|
|
32217
|
-
if (isNodeOfType(
|
|
32370
|
+
const isInlineReference = (node, scopes) => {
|
|
32371
|
+
const referenceNode = unwrapObjectIntegrityExpression(node, scopes);
|
|
32372
|
+
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";
|
|
32373
|
+
if (isNodeOfType(referenceNode, "ObjectExpression")) return "objects";
|
|
32374
|
+
if (isNodeOfType(referenceNode, "ArrayExpression")) return "Arrays";
|
|
32375
|
+
if (isNodeOfType(referenceNode, "JSXElement") || isNodeOfType(referenceNode, "JSXFragment")) return "JSX";
|
|
32218
32376
|
return null;
|
|
32219
32377
|
};
|
|
32220
32378
|
const noInlinePropOnMemoComponent = defineRule({
|
|
@@ -32244,7 +32402,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
32244
32402
|
let elementName = null;
|
|
32245
32403
|
if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
|
|
32246
32404
|
if (!elementName || !memoizedComponentNames.has(elementName)) return;
|
|
32247
|
-
const propType = isInlineReference(node.value.expression);
|
|
32405
|
+
const propType = isInlineReference(node.value.expression, context.scopes);
|
|
32248
32406
|
if (propType) context.report({
|
|
32249
32407
|
node: node.value.expression,
|
|
32250
32408
|
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`
|
|
@@ -43692,13 +43850,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
43692
43850
|
}
|
|
43693
43851
|
return null;
|
|
43694
43852
|
};
|
|
43695
|
-
const resolveHookNameFromInitializer = (initializer) => {
|
|
43853
|
+
const resolveHookNameFromInitializer = (initializer, scopes) => {
|
|
43696
43854
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
43697
43855
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
43698
|
-
const
|
|
43699
|
-
if (
|
|
43700
|
-
if (!isNodeOfType(
|
|
43701
|
-
return resolveTanstackQueryHookName(
|
|
43856
|
+
const resolvedSymbol = resolveConstIdentifierAlias(initializer, scopes);
|
|
43857
|
+
if (resolvedSymbol?.kind !== "const" || !resolvedSymbol.initializer) return null;
|
|
43858
|
+
if (!isNodeOfType(resolvedSymbol.initializer, "CallExpression")) return null;
|
|
43859
|
+
return resolveTanstackQueryHookName(resolvedSymbol.initializer);
|
|
43702
43860
|
};
|
|
43703
43861
|
const queryNoRestDestructuring = defineRule({
|
|
43704
43862
|
id: "query-no-rest-destructuring",
|
|
@@ -43711,7 +43869,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
43711
43869
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
43712
43870
|
if (!node.init) return;
|
|
43713
43871
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
43714
|
-
const hookName = resolveHookNameFromInitializer(node.init);
|
|
43872
|
+
const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
|
|
43715
43873
|
if (!hookName) return;
|
|
43716
43874
|
context.report({
|
|
43717
43875
|
node: node.id,
|
|
@@ -45066,7 +45224,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
|
|
|
45066
45224
|
"useParams",
|
|
45067
45225
|
"usePathname"
|
|
45068
45226
|
]);
|
|
45069
|
-
const findHookCallBindings = (componentBody) => {
|
|
45227
|
+
const findHookCallBindings = (componentBody, scopes) => {
|
|
45070
45228
|
const bindings = [];
|
|
45071
45229
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
45072
45230
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -45077,8 +45235,10 @@ const findHookCallBindings = (componentBody) => {
|
|
|
45077
45235
|
const callee = declarator.init.callee;
|
|
45078
45236
|
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
45079
45237
|
if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
|
|
45238
|
+
const valueSymbol = scopes.symbolFor(declarator.id);
|
|
45239
|
+
if (!valueSymbol) continue;
|
|
45080
45240
|
bindings.push({
|
|
45081
|
-
|
|
45241
|
+
valueSymbol,
|
|
45082
45242
|
hookName: callee.name,
|
|
45083
45243
|
declarator
|
|
45084
45244
|
});
|
|
@@ -45086,6 +45246,49 @@ const findHookCallBindings = (componentBody) => {
|
|
|
45086
45246
|
}
|
|
45087
45247
|
return bindings;
|
|
45088
45248
|
};
|
|
45249
|
+
const collectExactAliasSymbols = (componentBody, sourceSymbol, scopes) => {
|
|
45250
|
+
const symbols = [sourceSymbol];
|
|
45251
|
+
const symbolIds = new Set([sourceSymbol.id]);
|
|
45252
|
+
const aliasSourceIdentifiers = /* @__PURE__ */ new Set();
|
|
45253
|
+
if (!isNodeOfType(componentBody, "BlockStatement")) return {
|
|
45254
|
+
symbols,
|
|
45255
|
+
aliasSourceIdentifiers
|
|
45256
|
+
};
|
|
45257
|
+
let didFindAlias = true;
|
|
45258
|
+
while (didFindAlias) {
|
|
45259
|
+
didFindAlias = false;
|
|
45260
|
+
for (const statement of componentBody.body ?? []) {
|
|
45261
|
+
if (!isNodeOfType(statement, "VariableDeclaration") || statement.kind !== "const") continue;
|
|
45262
|
+
for (const declarator of statement.declarations ?? []) {
|
|
45263
|
+
let aliasIdentifier = null;
|
|
45264
|
+
let sourceIdentifier = null;
|
|
45265
|
+
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
45266
|
+
const arrayBinding = isNodeOfType(declarator.id, "ArrayPattern") ? declarator.id.elements[0] : null;
|
|
45267
|
+
const arrayValueNode = isNodeOfType(initializer, "ArrayExpression") ? initializer.elements[0] : null;
|
|
45268
|
+
const arrayValue = arrayValueNode && !isNodeOfType(arrayValueNode, "SpreadElement") ? stripParenExpression(arrayValueNode) : null;
|
|
45269
|
+
if (isNodeOfType(declarator.id, "Identifier") && isNodeOfType(initializer, "Identifier")) {
|
|
45270
|
+
aliasIdentifier = declarator.id;
|
|
45271
|
+
sourceIdentifier = initializer;
|
|
45272
|
+
} else if (isNodeOfType(declarator.id, "ArrayPattern") && declarator.id.elements.length === 1 && isNodeOfType(arrayBinding, "Identifier") && isNodeOfType(initializer, "ArrayExpression") && initializer.elements.length === 1 && isNodeOfType(arrayValue, "Identifier")) {
|
|
45273
|
+
aliasIdentifier = arrayBinding;
|
|
45274
|
+
sourceIdentifier = arrayValue;
|
|
45275
|
+
}
|
|
45276
|
+
if (!aliasIdentifier || !sourceIdentifier) continue;
|
|
45277
|
+
const referencedSymbol = scopes.symbolFor(sourceIdentifier);
|
|
45278
|
+
const aliasSymbol = scopes.symbolFor(aliasIdentifier);
|
|
45279
|
+
if (!referencedSymbol || !symbolIds.has(referencedSymbol.id) || !aliasSymbol || aliasSymbol.kind !== "const" || symbolIds.has(aliasSymbol.id)) continue;
|
|
45280
|
+
symbolIds.add(aliasSymbol.id);
|
|
45281
|
+
symbols.push(aliasSymbol);
|
|
45282
|
+
aliasSourceIdentifiers.add(sourceIdentifier);
|
|
45283
|
+
didFindAlias = true;
|
|
45284
|
+
}
|
|
45285
|
+
}
|
|
45286
|
+
}
|
|
45287
|
+
return {
|
|
45288
|
+
symbols,
|
|
45289
|
+
aliasSourceIdentifiers
|
|
45290
|
+
};
|
|
45291
|
+
};
|
|
45089
45292
|
const rerenderDeferReadsHook = defineRule({
|
|
45090
45293
|
id: "rerender-defer-reads-hook",
|
|
45091
45294
|
title: "URL hook value only read in handlers",
|
|
@@ -45096,15 +45299,13 @@ const rerenderDeferReadsHook = defineRule({
|
|
|
45096
45299
|
create: (context) => {
|
|
45097
45300
|
const checkComponent = (componentBody) => {
|
|
45098
45301
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
45099
|
-
const bindings = findHookCallBindings(componentBody);
|
|
45302
|
+
const bindings = findHookCallBindings(componentBody, context.scopes);
|
|
45100
45303
|
if (bindings.length === 0) return;
|
|
45101
45304
|
const handlerBindingNames = collectHandlerBindingNames(componentBody);
|
|
45102
45305
|
for (const binding of bindings) {
|
|
45306
|
+
const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
|
|
45103
45307
|
const referenceLocations = [];
|
|
45104
|
-
|
|
45105
|
-
if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
|
|
45106
|
-
if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
|
|
45107
|
-
});
|
|
45308
|
+
for (const symbol of symbols) for (const reference of symbol.references) if (!aliasSourceIdentifiers.has(reference.identifier)) referenceLocations.push(reference.identifier);
|
|
45108
45309
|
if (referenceLocations.length === 0) continue;
|
|
45109
45310
|
if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
|
|
45110
45311
|
context.report({
|
|
@@ -53053,7 +53254,8 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
53053
53254
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
53054
53255
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
53055
53256
|
const firstArg = node.arguments?.[0];
|
|
53056
|
-
if (!isNodeOfType(firstArg, "
|
|
53257
|
+
if (!firstArg || isNodeOfType(firstArg, "SpreadElement")) return;
|
|
53258
|
+
if (!isNodeOfType(unwrapObjectIntegrityExpression(firstArg, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES), "ObjectExpression")) return;
|
|
53057
53259
|
context.report({
|
|
53058
53260
|
node,
|
|
53059
53261
|
message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
|