oxlint-plugin-react-doctor 0.7.6-dev.dfd47a7 → 0.7.6-dev.e141d90
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 +497 -163
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1518,6 +1518,37 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1518
1518
|
}
|
|
1519
1519
|
};
|
|
1520
1520
|
//#endregion
|
|
1521
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
1522
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
1523
|
+
//#endregion
|
|
1524
|
+
//#region src/plugin/utils/resolve-jsx-element-type.ts
|
|
1525
|
+
const resolveConstantStringBinding = (name) => {
|
|
1526
|
+
const binding = findVariableInitializer(name, name.name);
|
|
1527
|
+
if (!binding?.initializer) return null;
|
|
1528
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
1529
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
1530
|
+
if (declarator.id !== binding.bindingIdentifier) return null;
|
|
1531
|
+
const declaration = declarator.parent;
|
|
1532
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
1533
|
+
if (declaration.kind !== "const") return null;
|
|
1534
|
+
const initializer = stripParenExpression(binding.initializer);
|
|
1535
|
+
return isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
|
|
1536
|
+
};
|
|
1537
|
+
const flattenJsxName$2 = (name) => {
|
|
1538
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1539
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1540
|
+
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1541
|
+
return "";
|
|
1542
|
+
};
|
|
1543
|
+
const resolveJsxElementType = (openingElement) => {
|
|
1544
|
+
const name = openingElement.name;
|
|
1545
|
+
if (isNodeOfType(name, "JSXIdentifier")) {
|
|
1546
|
+
if (!isUppercaseName(name.name)) return name.name;
|
|
1547
|
+
return resolveConstantStringBinding(name) ?? name.name;
|
|
1548
|
+
}
|
|
1549
|
+
return flattenJsxName$2(name);
|
|
1550
|
+
};
|
|
1551
|
+
//#endregion
|
|
1521
1552
|
//#region src/plugin/utils/get-element-type.ts
|
|
1522
1553
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1523
1554
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1530,14 +1561,8 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1530
1561
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1531
1562
|
return a11ySettings;
|
|
1532
1563
|
};
|
|
1533
|
-
const flattenJsxName$2 = (name) => {
|
|
1534
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1535
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1536
|
-
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1537
|
-
return "";
|
|
1538
|
-
};
|
|
1539
1564
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1540
|
-
const baseName =
|
|
1565
|
+
const baseName = resolveJsxElementType(openingElement);
|
|
1541
1566
|
if (a11ySettings.polymorphicPropName) {
|
|
1542
1567
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1543
1568
|
if (polymorphicAttribute) {
|
|
@@ -2372,8 +2397,9 @@ const anchorIsValid = defineRule({
|
|
|
2372
2397
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2373
2398
|
return { JSXOpeningElement(node) {
|
|
2374
2399
|
if (isTestlikeFile) return;
|
|
2375
|
-
|
|
2376
|
-
if (
|
|
2400
|
+
const tag = getElementType(node, context.settings);
|
|
2401
|
+
if (!fileHasJsxA11ySettings && tag !== "a") return;
|
|
2402
|
+
if (tag !== "a") return;
|
|
2377
2403
|
let hrefAttribute;
|
|
2378
2404
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2379
2405
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5728,7 +5754,7 @@ const buttonHasType = defineRule({
|
|
|
5728
5754
|
return {
|
|
5729
5755
|
JSXOpeningElement(node) {
|
|
5730
5756
|
if (isTestlikeFile) return;
|
|
5731
|
-
if (
|
|
5757
|
+
if (resolveJsxElementType(node) !== "button") return;
|
|
5732
5758
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5733
5759
|
if (!typeAttr) {
|
|
5734
5760
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5918,7 +5944,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5918
5944
|
};
|
|
5919
5945
|
return {
|
|
5920
5946
|
JSXOpeningElement(node) {
|
|
5921
|
-
if (
|
|
5947
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
5922
5948
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5923
5949
|
},
|
|
5924
5950
|
CallExpression(node) {
|
|
@@ -5995,7 +6021,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
5995
6021
|
//#endregion
|
|
5996
6022
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
5997
6023
|
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
5998
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
6024
|
+
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
5999
6025
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6000
6026
|
let symbol = scopes.symbolFor(identifier);
|
|
6001
6027
|
while (symbol?.kind === "const") {
|
|
@@ -6270,6 +6296,131 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6270
6296
|
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6271
6297
|
};
|
|
6272
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
|
|
6273
6424
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6274
6425
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6275
6426
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
@@ -6291,26 +6442,39 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6291
6442
|
severity: "warn",
|
|
6292
6443
|
category: "Correctness",
|
|
6293
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.",
|
|
6294
|
-
create: (context) =>
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
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
|
+
}
|
|
6314
6478
|
});
|
|
6315
6479
|
//#endregion
|
|
6316
6480
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
@@ -7433,7 +7597,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7433
7597
|
const getOpeningElementTagName = (openingElement) => {
|
|
7434
7598
|
if (!openingElement) return null;
|
|
7435
7599
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7436
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement
|
|
7600
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return resolveJsxElementType(openingElement);
|
|
7437
7601
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7438
7602
|
let cursor = openingElement.name;
|
|
7439
7603
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7685,7 +7849,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7685
7849
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7686
7850
|
return { JSXOpeningElement(node) {
|
|
7687
7851
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7688
|
-
const tagName = node
|
|
7852
|
+
const tagName = resolveJsxElementType(node);
|
|
7689
7853
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7690
7854
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7691
7855
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -11917,7 +12081,7 @@ const forbidDomProps = defineRule({
|
|
|
11917
12081
|
return { JSXOpeningElement(node) {
|
|
11918
12082
|
if (forbidMap.size === 0) return;
|
|
11919
12083
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
11920
|
-
const elementName = node
|
|
12084
|
+
const elementName = resolveJsxElementType(node);
|
|
11921
12085
|
if (isReactComponentName(elementName)) return;
|
|
11922
12086
|
for (const attribute of node.attributes) {
|
|
11923
12087
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11995,7 +12159,7 @@ const forbidElements = defineRule({
|
|
|
11995
12159
|
return {
|
|
11996
12160
|
JSXOpeningElement(node) {
|
|
11997
12161
|
if (forbidMap.size === 0) return;
|
|
11998
|
-
const fullName =
|
|
12162
|
+
const fullName = resolveJsxElementType(node);
|
|
11999
12163
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
12000
12164
|
context.report({
|
|
12001
12165
|
node: node.name,
|
|
@@ -12357,8 +12521,7 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
12357
12521
|
const isParagraphElement = (candidate) => {
|
|
12358
12522
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12359
12523
|
const opening = candidate.openingElement;
|
|
12360
|
-
|
|
12361
|
-
return opening.name.name === "p";
|
|
12524
|
+
return resolveJsxElementType(opening) === "p";
|
|
12362
12525
|
};
|
|
12363
12526
|
const findEnclosingParagraph = (openingElement) => {
|
|
12364
12527
|
const owningElement = openingElement.parent;
|
|
@@ -12379,8 +12542,7 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
12379
12542
|
severity: "warn",
|
|
12380
12543
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
12381
12544
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12382
|
-
|
|
12383
|
-
const childTagName = node.name.name;
|
|
12545
|
+
const childTagName = resolveJsxElementType(node);
|
|
12384
12546
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
12385
12547
|
if (!findEnclosingParagraph(node)) return;
|
|
12386
12548
|
context.report({
|
|
@@ -12411,7 +12573,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
12411
12573
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
12412
12574
|
const opening = jsxElement.openingElement;
|
|
12413
12575
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
12414
|
-
const tagName = opening
|
|
12576
|
+
const tagName = resolveJsxElementType(opening);
|
|
12415
12577
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
12416
12578
|
return tagName;
|
|
12417
12579
|
};
|
|
@@ -12441,7 +12603,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
12441
12603
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
12442
12604
|
const opening = ancestor.openingElement;
|
|
12443
12605
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
12444
|
-
const ancestorTag = opening
|
|
12606
|
+
const ancestorTag = resolveJsxElementType(opening);
|
|
12445
12607
|
if (ancestorTag.length === 0) {
|
|
12446
12608
|
previous = ancestor;
|
|
12447
12609
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12523,8 +12685,7 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12523
12685
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12524
12686
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12525
12687
|
const opening = candidate.openingElement;
|
|
12526
|
-
|
|
12527
|
-
return opening.name.name === tagName;
|
|
12688
|
+
return resolveJsxElementType(opening) === tagName;
|
|
12528
12689
|
};
|
|
12529
12690
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12530
12691
|
const owningElement = openingElement.parent;
|
|
@@ -12545,8 +12706,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12545
12706
|
severity: "warn",
|
|
12546
12707
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12547
12708
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12548
|
-
|
|
12549
|
-
const tagName = node.name.name;
|
|
12709
|
+
const tagName = resolveJsxElementType(node);
|
|
12550
12710
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12551
12711
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12552
12712
|
context.report({
|
|
@@ -12661,7 +12821,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12661
12821
|
matchByOccurrence: true,
|
|
12662
12822
|
create: skipNonProductionFiles((context) => ({
|
|
12663
12823
|
JSXOpeningElement(node) {
|
|
12664
|
-
if (
|
|
12824
|
+
if (resolveJsxElementType(node) !== "iframe") return;
|
|
12665
12825
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12666
12826
|
if (!sandboxAttr) {
|
|
12667
12827
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -13653,6 +13813,28 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
13653
13813
|
} })
|
|
13654
13814
|
});
|
|
13655
13815
|
//#endregion
|
|
13816
|
+
//#region src/plugin/utils/unwrap-discarded-expression.ts
|
|
13817
|
+
const unwrapDiscardedExpression = (node) => {
|
|
13818
|
+
let expression = isNodeOfType(node, "ExpressionStatement") ? node.expression : node;
|
|
13819
|
+
for (;;) {
|
|
13820
|
+
expression = stripParenExpression(expression);
|
|
13821
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") {
|
|
13822
|
+
expression = expression.argument;
|
|
13823
|
+
continue;
|
|
13824
|
+
}
|
|
13825
|
+
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
13826
|
+
const expressions = expression.expressions ?? [];
|
|
13827
|
+
const finalExpression = expressions.at(-1);
|
|
13828
|
+
const prefixExpressions = expressions.slice(0, -1);
|
|
13829
|
+
if (finalExpression && prefixExpressions.length > 0 && prefixExpressions.every((prefixExpression) => isNodeOfType(stripParenExpression(prefixExpression), "Literal"))) {
|
|
13830
|
+
expression = finalExpression;
|
|
13831
|
+
continue;
|
|
13832
|
+
}
|
|
13833
|
+
}
|
|
13834
|
+
return expression;
|
|
13835
|
+
}
|
|
13836
|
+
};
|
|
13837
|
+
//#endregion
|
|
13656
13838
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
13657
13839
|
const ITERATOR_METHOD_NAMES$2 = new Set([
|
|
13658
13840
|
"forEach",
|
|
@@ -13837,9 +14019,19 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
13837
14019
|
});
|
|
13838
14020
|
return foundAttachment;
|
|
13839
14021
|
};
|
|
14022
|
+
const getStyleAssignment = (node) => {
|
|
14023
|
+
if (!isNodeOfType(node, "ExpressionStatement")) return null;
|
|
14024
|
+
const expression = unwrapDiscardedExpression(node);
|
|
14025
|
+
if (!isNodeOfType(expression, "AssignmentExpression")) return null;
|
|
14026
|
+
if (!isNodeOfType(expression.left, "MemberExpression")) return null;
|
|
14027
|
+
if (!isNodeOfType(expression.left.object, "MemberExpression")) return null;
|
|
14028
|
+
if (!isNodeOfType(expression.left.object.property, "Identifier")) return null;
|
|
14029
|
+
return expression.left.object.property.name === "style" ? expression : null;
|
|
14030
|
+
};
|
|
13840
14031
|
const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
13841
|
-
|
|
13842
|
-
|
|
14032
|
+
const assignment = getStyleAssignment(styleWriteStatement);
|
|
14033
|
+
if (!assignment || !isNodeOfType(assignment.left, "MemberExpression") || !isNodeOfType(assignment.left.object, "MemberExpression")) return false;
|
|
14034
|
+
const elementExpression = assignment.left.object.object;
|
|
13843
14035
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
13844
14036
|
if (!creationRoot) return false;
|
|
13845
14037
|
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
|
|
@@ -13851,15 +14043,17 @@ const jsBatchDomCss = defineRule({
|
|
|
13851
14043
|
severity: "warn",
|
|
13852
14044
|
recommendation: "Do all your reads first, then all your writes. Mixing them inside a loop makes the browser recalculate the layout again and again, which is slow",
|
|
13853
14045
|
create: (context) => {
|
|
13854
|
-
const
|
|
13855
|
-
|
|
14046
|
+
const writesLayoutAffectingProperty = (node) => {
|
|
14047
|
+
const assignment = getStyleAssignment(node);
|
|
14048
|
+
return assignment !== null && isNodeOfType(assignment.left, "MemberExpression") && isNodeOfType(assignment.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(assignment.left.property.name);
|
|
14049
|
+
};
|
|
13856
14050
|
return { BlockStatement(node) {
|
|
13857
14051
|
const perIterationBody = findEnclosingPerIterationBody(node);
|
|
13858
14052
|
if (!perIterationBody) return;
|
|
13859
14053
|
const statements = node.body ?? [];
|
|
13860
14054
|
let layoutReads = null;
|
|
13861
14055
|
for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
|
|
13862
|
-
if (
|
|
14056
|
+
if (getStyleAssignment(statements[statementIndex]) === null || getStyleAssignment(statements[statementIndex - 1]) === null) continue;
|
|
13863
14057
|
if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
|
|
13864
14058
|
layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
|
|
13865
14059
|
if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
|
|
@@ -14555,6 +14749,18 @@ const jsHoistRegexp = defineRule({
|
|
|
14555
14749
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14556
14750
|
});
|
|
14557
14751
|
//#endregion
|
|
14752
|
+
//#region src/plugin/utils/resolve-first-argument-binding.ts
|
|
14753
|
+
const resolveFirstArgumentBinding = (firstParameter) => {
|
|
14754
|
+
if (!firstParameter) return null;
|
|
14755
|
+
if (!isNodeOfType(firstParameter, "RestElement")) return firstParameter;
|
|
14756
|
+
if (!isNodeOfType(firstParameter.argument, "ArrayPattern")) return null;
|
|
14757
|
+
const elements = firstParameter.argument.elements ?? [];
|
|
14758
|
+
if (elements.length !== 1) return null;
|
|
14759
|
+
const firstBinding = elements[0];
|
|
14760
|
+
if (!firstBinding || isNodeOfType(firstBinding, "RestElement")) return null;
|
|
14761
|
+
return firstBinding;
|
|
14762
|
+
};
|
|
14763
|
+
//#endregion
|
|
14558
14764
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14559
14765
|
const referencesParameter = (expression, parameterName) => {
|
|
14560
14766
|
if (!expression) return false;
|
|
@@ -14565,7 +14771,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14565
14771
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14566
14772
|
const callback = node.arguments?.[0];
|
|
14567
14773
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14568
|
-
const firstParameter = callback.params?.[0];
|
|
14774
|
+
const firstParameter = resolveFirstArgumentBinding(callback.params?.[0]);
|
|
14569
14775
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14570
14776
|
let predicate = null;
|
|
14571
14777
|
const body = callback.body;
|
|
@@ -15257,9 +15463,21 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
15257
15463
|
};
|
|
15258
15464
|
const getResolvedInitializer = (receiver) => {
|
|
15259
15465
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
15260
|
-
const
|
|
15261
|
-
|
|
15262
|
-
return
|
|
15466
|
+
const binding = findVariableInitializer(receiver, receiver.name);
|
|
15467
|
+
const initializer = binding?.initializer ?? null;
|
|
15468
|
+
if (!binding || !initializer) return null;
|
|
15469
|
+
const isDefault = isNodeOfType(binding.bindingIdentifier.parent, "AssignmentPattern");
|
|
15470
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
15471
|
+
const aliased = findVariableInitializer(initializer, initializer.name);
|
|
15472
|
+
if (aliased?.initializer) return {
|
|
15473
|
+
initializer: aliased.initializer,
|
|
15474
|
+
isDefault: isDefault || isNodeOfType(aliased.bindingIdentifier.parent, "AssignmentPattern")
|
|
15475
|
+
};
|
|
15476
|
+
}
|
|
15477
|
+
return {
|
|
15478
|
+
initializer,
|
|
15479
|
+
isDefault
|
|
15480
|
+
};
|
|
15263
15481
|
};
|
|
15264
15482
|
const isSplitCall = (expression) => {
|
|
15265
15483
|
if (!expression) return false;
|
|
@@ -15425,8 +15643,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
15425
15643
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
15426
15644
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
15427
15645
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
15428
|
-
const
|
|
15429
|
-
if (
|
|
15646
|
+
const resolved = getResolvedInitializer(stripped);
|
|
15647
|
+
if (resolved && !resolved.isDefault && isSmallInlineLiteralArray(resolved.initializer)) return true;
|
|
15430
15648
|
}
|
|
15431
15649
|
return false;
|
|
15432
15650
|
};
|
|
@@ -15478,8 +15696,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15478
15696
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15479
15697
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15480
15698
|
if (resolvedInitializer) {
|
|
15481
|
-
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15482
|
-
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15699
|
+
if (isLikelyStringReceiver(resolvedInitializer.initializer)) return;
|
|
15700
|
+
if (!resolvedInitializer.isDefault && isSmallInlineLiteralArray(resolvedInitializer.initializer)) return;
|
|
15483
15701
|
}
|
|
15484
15702
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15485
15703
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -15892,14 +16110,21 @@ const jsxFilenameExtension = defineRule({
|
|
|
15892
16110
|
});
|
|
15893
16111
|
//#endregion
|
|
15894
16112
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
15895
|
-
const isJsxFragmentElement = (node) => {
|
|
16113
|
+
const isJsxFragmentElement = (node, scopes) => {
|
|
15896
16114
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
15897
16115
|
const elementName = node.name;
|
|
15898
|
-
if (isNodeOfType(elementName, "JSXIdentifier"))
|
|
16116
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
16117
|
+
if (!scopes) return elementName.name === "Fragment";
|
|
16118
|
+
const symbol = resolveConstIdentifierAlias(elementName, scopes);
|
|
16119
|
+
if (!symbol) return elementName.name === "Fragment" && scopes.isGlobalReference(elementName);
|
|
16120
|
+
return isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "Fragment";
|
|
16121
|
+
}
|
|
15899
16122
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
15900
16123
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
15901
|
-
if (elementName.
|
|
15902
|
-
return elementName.
|
|
16124
|
+
if (elementName.property.name !== "Fragment") return false;
|
|
16125
|
+
if (!scopes) return elementName.object.name === "React";
|
|
16126
|
+
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
16127
|
+
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15903
16128
|
}
|
|
15904
16129
|
return false;
|
|
15905
16130
|
};
|
|
@@ -15925,7 +16150,7 @@ const jsxFragments = defineRule({
|
|
|
15925
16150
|
if (mode !== "syntax") return;
|
|
15926
16151
|
if (!node.closingElement) return;
|
|
15927
16152
|
const openingElement = node.openingElement;
|
|
15928
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
16153
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
15929
16154
|
if (openingElement.attributes.length > 0) return;
|
|
15930
16155
|
context.report({
|
|
15931
16156
|
node: openingElement,
|
|
@@ -18606,10 +18831,6 @@ const resolveSettings$29 = (settings) => {
|
|
|
18606
18831
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18607
18832
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18608
18833
|
};
|
|
18609
|
-
const getElementName = (node) => {
|
|
18610
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18611
|
-
return null;
|
|
18612
|
-
};
|
|
18613
18834
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18614
18835
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18615
18836
|
const explicit = options.components?.[elementName];
|
|
@@ -18629,7 +18850,8 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18629
18850
|
create: (context) => {
|
|
18630
18851
|
const options = resolveSettings$29(context.settings);
|
|
18631
18852
|
return { JSXOpeningElement(node) {
|
|
18632
|
-
|
|
18853
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18854
|
+
const elementName = resolveJsxElementType(node);
|
|
18633
18855
|
if (!elementName) return;
|
|
18634
18856
|
for (const attribute of node.attributes) {
|
|
18635
18857
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -18816,11 +19038,6 @@ const checkTarget = (attributeValue) => {
|
|
|
18816
19038
|
alternate: false
|
|
18817
19039
|
};
|
|
18818
19040
|
};
|
|
18819
|
-
const getOpeningElementName = (node) => {
|
|
18820
|
-
const name = node.name;
|
|
18821
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18822
|
-
return null;
|
|
18823
|
-
};
|
|
18824
19041
|
const jsxNoTargetBlank = defineRule({
|
|
18825
19042
|
id: "jsx-no-target-blank",
|
|
18826
19043
|
title: "Unsafe target=_blank link",
|
|
@@ -18840,7 +19057,8 @@ const jsxNoTargetBlank = defineRule({
|
|
|
18840
19057
|
return settings.formComponents.has(tagName);
|
|
18841
19058
|
};
|
|
18842
19059
|
return { JSXOpeningElement(node) {
|
|
18843
|
-
|
|
19060
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
19061
|
+
const tagName = resolveJsxElementType(node);
|
|
18844
19062
|
if (!tagName) return;
|
|
18845
19063
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
18846
19064
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -19073,7 +19291,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19073
19291
|
return {
|
|
19074
19292
|
JSXElement(node) {
|
|
19075
19293
|
const openingElement = node.openingElement;
|
|
19076
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
19294
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
19077
19295
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19078
19296
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19079
19297
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20315,11 +20533,24 @@ const hasDirective = (programNode, directive) => {
|
|
|
20315
20533
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20316
20534
|
};
|
|
20317
20535
|
//#endregion
|
|
20318
|
-
//#region src/plugin/utils/
|
|
20319
|
-
const
|
|
20320
|
-
|
|
20321
|
-
|
|
20322
|
-
|
|
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
|
+
};
|
|
20323
20554
|
//#endregion
|
|
20324
20555
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
20325
20556
|
const nextjsAsyncClientComponent = defineRule({
|
|
@@ -20345,10 +20576,10 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
20345
20576
|
},
|
|
20346
20577
|
VariableDeclarator(node) {
|
|
20347
20578
|
if (!fileHasUseClient) return;
|
|
20348
|
-
if (!isComponentAssignment(node)) return;
|
|
20349
|
-
if (!isInlineFunctionExpression(node.init)) return;
|
|
20350
|
-
if (!node.init.async) return;
|
|
20351
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;
|
|
20352
20583
|
context.report({
|
|
20353
20584
|
node,
|
|
20354
20585
|
message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
|
|
@@ -20650,7 +20881,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20650
20881
|
severity: "warn",
|
|
20651
20882
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20652
20883
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20653
|
-
if (
|
|
20884
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
20654
20885
|
const attributes = node.attributes ?? [];
|
|
20655
20886
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20656
20887
|
if (downloadAttribute) {
|
|
@@ -20846,7 +21077,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
20846
21077
|
severity: "warn",
|
|
20847
21078
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
20848
21079
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20849
|
-
if (
|
|
21080
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20850
21081
|
const attributes = node.attributes ?? [];
|
|
20851
21082
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
20852
21083
|
if (!relAttribute?.value) return;
|
|
@@ -20954,7 +21185,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
20954
21185
|
severity: "warn",
|
|
20955
21186
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
20956
21187
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20957
|
-
if (
|
|
21188
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20958
21189
|
const attributes = node.attributes ?? [];
|
|
20959
21190
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
20960
21191
|
if (!hrefAttribute?.value) return;
|
|
@@ -21129,7 +21360,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21129
21360
|
create: (context) => {
|
|
21130
21361
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21131
21362
|
return { JSXOpeningElement(node) {
|
|
21132
|
-
if (
|
|
21363
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
21133
21364
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21134
21365
|
const programRoot = findProgramRoot(node);
|
|
21135
21366
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21176,8 +21407,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21176
21407
|
severity: "warn",
|
|
21177
21408
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21178
21409
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21179
|
-
|
|
21180
|
-
if (
|
|
21410
|
+
const elementName = resolveJsxElementType(node);
|
|
21411
|
+
if (elementName !== "script" && elementName !== "Script") return;
|
|
21181
21412
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21182
21413
|
if (!srcAttribute?.value) return;
|
|
21183
21414
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -22985,10 +23216,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
22985
23216
|
if (!declaringNode) return false;
|
|
22986
23217
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
22987
23218
|
}));
|
|
23219
|
+
const isWholePropsParameterBinding = (bindingNode) => {
|
|
23220
|
+
if (isFunctionLike$1(bindingNode.parent)) return true;
|
|
23221
|
+
let declaringFunction = bindingNode.parent;
|
|
23222
|
+
while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
|
|
23223
|
+
return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
|
|
23224
|
+
};
|
|
22988
23225
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
22989
23226
|
if (def.type !== "Parameter") return false;
|
|
22990
|
-
|
|
22991
|
-
return isFunctionLike$1(bindingParent);
|
|
23227
|
+
return isWholePropsParameterBinding(def.name);
|
|
22992
23228
|
}));
|
|
22993
23229
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
22994
23230
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -25985,6 +26221,56 @@ const noBarrelImport = defineRule({
|
|
|
25985
26221
|
}
|
|
25986
26222
|
});
|
|
25987
26223
|
//#endregion
|
|
26224
|
+
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
26225
|
+
const collectReturnedExpressions = (functionNode) => {
|
|
26226
|
+
if (!isFunctionLike$1(functionNode)) return [];
|
|
26227
|
+
const body = functionNode.body;
|
|
26228
|
+
if (!body) return [];
|
|
26229
|
+
if (!isNodeOfType(body, "BlockStatement")) return [body];
|
|
26230
|
+
const returnedExpressions = [];
|
|
26231
|
+
walkAst(body, (node) => {
|
|
26232
|
+
if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
26233
|
+
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
|
|
26234
|
+
});
|
|
26235
|
+
return returnedExpressions;
|
|
26236
|
+
};
|
|
26237
|
+
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
|
|
26238
|
+
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
26239
|
+
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
26240
|
+
const functionMatches = (candidateFunction) => {
|
|
26241
|
+
if (visitedFunctions.has(candidateFunction)) return false;
|
|
26242
|
+
visitedFunctions.add(candidateFunction);
|
|
26243
|
+
return collectReturnedExpressions(candidateFunction).some(expressionMatches);
|
|
26244
|
+
};
|
|
26245
|
+
const expressionMatches = (expression) => {
|
|
26246
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
26247
|
+
if (visitedExpressions.has(unwrappedExpression)) return false;
|
|
26248
|
+
visitedExpressions.add(unwrappedExpression);
|
|
26249
|
+
if (matchesExpression(unwrappedExpression)) return true;
|
|
26250
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
26251
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
26252
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
|
|
26253
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
26254
|
+
if (isFunctionLike$1(initializer)) return false;
|
|
26255
|
+
return expressionMatches(initializer);
|
|
26256
|
+
}
|
|
26257
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
26258
|
+
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
26259
|
+
if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
26260
|
+
const symbol = scopes.symbolFor(unwrappedExpression.callee);
|
|
26261
|
+
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
|
|
26262
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
26263
|
+
const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
|
|
26264
|
+
if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
|
|
26265
|
+
return functionMatches(candidateFunction);
|
|
26266
|
+
}
|
|
26267
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
|
|
26268
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
|
|
26269
|
+
return false;
|
|
26270
|
+
};
|
|
26271
|
+
return functionMatches(functionNode);
|
|
26272
|
+
};
|
|
26273
|
+
//#endregion
|
|
25988
26274
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
25989
26275
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
25990
26276
|
"FunctionDeclaration",
|
|
@@ -26004,12 +26290,13 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
26004
26290
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
26005
26291
|
};
|
|
26006
26292
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
26293
|
+
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
26007
26294
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26008
26295
|
let hasRenderOutput = false;
|
|
26009
26296
|
walkAst(rootNode, (node) => {
|
|
26010
26297
|
if (hasRenderOutput) return false;
|
|
26011
26298
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26012
|
-
if (
|
|
26299
|
+
if (isRenderOutputExpression(node, scopes)) {
|
|
26013
26300
|
hasRenderOutput = true;
|
|
26014
26301
|
return false;
|
|
26015
26302
|
}
|
|
@@ -26020,7 +26307,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26020
26307
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26021
26308
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26022
26309
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26023
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26310
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
|
|
26024
26311
|
renderOutputCache.set(functionNode, {
|
|
26025
26312
|
scopes,
|
|
26026
26313
|
hasRenderOutput
|
|
@@ -26028,6 +26315,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
|
26028
26315
|
return hasRenderOutput;
|
|
26029
26316
|
};
|
|
26030
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
|
|
26031
26321
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
26032
26322
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
26033
26323
|
//#endregion
|
|
@@ -28485,7 +28775,7 @@ const noDisabledZoom = defineRule({
|
|
|
28485
28775
|
category: "Accessibility",
|
|
28486
28776
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28487
28777
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28488
|
-
if (
|
|
28778
|
+
if (resolveJsxElementType(node) !== "meta") return;
|
|
28489
28779
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28490
28780
|
if (!nameAttr?.value) return;
|
|
28491
28781
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -28875,7 +29165,7 @@ const findTopLevelEffectCalls = (componentBody) => {
|
|
|
28875
29165
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
28876
29166
|
for (const statement of componentBody.body ?? []) {
|
|
28877
29167
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
28878
|
-
const expression = statement
|
|
29168
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
28879
29169
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
28880
29170
|
if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
|
|
28881
29171
|
effectCalls.push(expression);
|
|
@@ -31657,7 +31947,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31657
31947
|
severity: "warn",
|
|
31658
31948
|
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
31659
31949
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31660
|
-
if (
|
|
31950
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
31661
31951
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31662
31952
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31663
31953
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -31898,7 +32188,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
31898
32188
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
31899
32189
|
return {
|
|
31900
32190
|
JSXOpeningElement(node) {
|
|
31901
|
-
if (
|
|
32191
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
31902
32192
|
let typeAttribute = null;
|
|
31903
32193
|
let typeAttributeIndex = null;
|
|
31904
32194
|
let indeterminateAttribute = null;
|
|
@@ -32077,11 +32367,12 @@ const isMemoCall = (node) => {
|
|
|
32077
32367
|
};
|
|
32078
32368
|
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
32079
32369
|
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
32080
|
-
const isInlineReference = (node) => {
|
|
32081
|
-
|
|
32082
|
-
if (isNodeOfType(
|
|
32083
|
-
if (isNodeOfType(
|
|
32084
|
-
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";
|
|
32085
32376
|
return null;
|
|
32086
32377
|
};
|
|
32087
32378
|
const noInlinePropOnMemoComponent = defineRule({
|
|
@@ -32111,7 +32402,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
32111
32402
|
let elementName = null;
|
|
32112
32403
|
if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
|
|
32113
32404
|
if (!elementName || !memoizedComponentNames.has(elementName)) return;
|
|
32114
|
-
const propType = isInlineReference(node.value.expression);
|
|
32405
|
+
const propType = isInlineReference(node.value.expression, context.scopes);
|
|
32115
32406
|
if (propType) context.report({
|
|
32116
32407
|
node: node.value.expression,
|
|
32117
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`
|
|
@@ -32970,11 +33261,13 @@ const noManyBooleanProps = defineRule({
|
|
|
32970
33261
|
};
|
|
32971
33262
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
32972
33263
|
if (!param) return;
|
|
33264
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33265
|
+
if (!propsBinding) return;
|
|
32973
33266
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
32974
|
-
if (isNodeOfType(
|
|
33267
|
+
if (isNodeOfType(propsBinding, "ObjectPattern")) {
|
|
32975
33268
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
32976
33269
|
const booleanLikePropNames = [];
|
|
32977
|
-
for (const property of
|
|
33270
|
+
for (const property of propsBinding.properties ?? []) {
|
|
32978
33271
|
if (!isNodeOfType(property, "Property")) continue;
|
|
32979
33272
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
32980
33273
|
if (!keyName) continue;
|
|
@@ -32985,7 +33278,7 @@ const noManyBooleanProps = defineRule({
|
|
|
32985
33278
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
32986
33279
|
return;
|
|
32987
33280
|
}
|
|
32988
|
-
if (isNodeOfType(
|
|
33281
|
+
if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
|
|
32989
33282
|
};
|
|
32990
33283
|
return {
|
|
32991
33284
|
FunctionDeclaration(node) {
|
|
@@ -33107,7 +33400,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33107
33400
|
if (mirrorBindings.length === 0) return;
|
|
33108
33401
|
for (const statement of componentBody.body ?? []) {
|
|
33109
33402
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
33110
|
-
const effectCall = statement
|
|
33403
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
33111
33404
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
33112
33405
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
33113
33406
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -33121,7 +33414,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
33121
33414
|
const bodyStatements = getCallbackStatements(callback);
|
|
33122
33415
|
if (bodyStatements.length !== 1) continue;
|
|
33123
33416
|
const onlyStatement = bodyStatements[0];
|
|
33124
|
-
const expression =
|
|
33417
|
+
const expression = unwrapDiscardedExpression(onlyStatement);
|
|
33125
33418
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
33126
33419
|
if (!isNodeOfType(expression.callee, "Identifier")) continue;
|
|
33127
33420
|
if (!isSetterIdentifier(expression.callee.name)) continue;
|
|
@@ -35737,7 +36030,7 @@ const noPreventDefault = defineRule({
|
|
|
35737
36030
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
35738
36031
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
35739
36032
|
return { JSXOpeningElement(node) {
|
|
35740
|
-
const elementName =
|
|
36033
|
+
const elementName = resolveJsxElementType(node);
|
|
35741
36034
|
if (!elementName) return;
|
|
35742
36035
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
35743
36036
|
if (!targetEventProps) return;
|
|
@@ -37222,7 +37515,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
|
37222
37515
|
return expressionReadsStateValue(argument, stateName);
|
|
37223
37516
|
};
|
|
37224
37517
|
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
37225
|
-
const expression =
|
|
37518
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
37226
37519
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
37227
37520
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
37228
37521
|
if (!setterNames.has(expression.callee.name)) return null;
|
|
@@ -37571,7 +37864,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
37571
37864
|
const setterNames = new Set(setterNameToStateName.keys());
|
|
37572
37865
|
for (const statement of functionBody.body ?? []) {
|
|
37573
37866
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
37574
|
-
const effectCall = statement
|
|
37867
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
37575
37868
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
37576
37869
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
37577
37870
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -38201,9 +38494,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38201
38494
|
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
38202
38495
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38203
38496
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38204
|
-
const
|
|
38497
|
+
const elementName = resolveJsxElementType(node);
|
|
38498
|
+
const firstCharacter = elementName.charCodeAt(0);
|
|
38205
38499
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38206
|
-
if (
|
|
38500
|
+
if (elementName.includes("-")) return;
|
|
38207
38501
|
for (const attribute of node.attributes) {
|
|
38208
38502
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38209
38503
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38591,8 +38885,7 @@ const noUncontrolledInput = defineRule({
|
|
|
38591
38885
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38592
38886
|
walkAst(componentBody, (child) => {
|
|
38593
38887
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38594
|
-
|
|
38595
|
-
const tagName = child.name.name;
|
|
38888
|
+
const tagName = resolveJsxElementType(child);
|
|
38596
38889
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38597
38890
|
const attributes = child.attributes ?? [];
|
|
38598
38891
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38653,7 +38946,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38653
38946
|
severity: "warn",
|
|
38654
38947
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38655
38948
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38656
|
-
if (
|
|
38949
|
+
if (resolveJsxElementType(node) !== "script") return;
|
|
38657
38950
|
const attributes = node.attributes ?? [];
|
|
38658
38951
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38659
38952
|
if (!srcAttribute) return;
|
|
@@ -39868,7 +40161,7 @@ const noUnknownProperty = defineRule({
|
|
|
39868
40161
|
}
|
|
39869
40162
|
if (fileIsNonReactJsx) return;
|
|
39870
40163
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
39871
|
-
const elementType = node
|
|
40164
|
+
const elementType = resolveJsxElementType(node);
|
|
39872
40165
|
const firstCharacter = elementType.charCodeAt(0);
|
|
39873
40166
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
39874
40167
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40046,20 +40339,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40046
40339
|
});
|
|
40047
40340
|
return found;
|
|
40048
40341
|
};
|
|
40342
|
+
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40049
40343
|
const isReactClassComponent = (classNode) => {
|
|
40050
40344
|
if (isEs6Component(classNode)) return true;
|
|
40051
40345
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40052
40346
|
};
|
|
40053
|
-
const findEnclosingComponent = (node) => {
|
|
40347
|
+
const findEnclosingComponent = (node, scopes) => {
|
|
40054
40348
|
let walker = node.parent;
|
|
40055
40349
|
while (walker) {
|
|
40056
40350
|
if (isFunctionLike$1(walker)) {
|
|
40057
40351
|
const componentName = inferFunctionLikeName(walker);
|
|
40058
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40352
|
+
if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
|
|
40059
40353
|
component: walker,
|
|
40060
40354
|
name: componentName
|
|
40061
40355
|
};
|
|
40062
|
-
if (!componentName &&
|
|
40356
|
+
if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40063
40357
|
component: walker,
|
|
40064
40358
|
name: null
|
|
40065
40359
|
};
|
|
@@ -40298,7 +40592,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40298
40592
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40299
40593
|
if (settings.allowAsProps) return;
|
|
40300
40594
|
}
|
|
40301
|
-
const enclosing = findEnclosingComponent(candidateNode);
|
|
40595
|
+
const enclosing = findEnclosingComponent(candidateNode, context.scopes);
|
|
40302
40596
|
if (!enclosing) return;
|
|
40303
40597
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40304
40598
|
queuedReports.push({
|
|
@@ -40309,7 +40603,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40309
40603
|
});
|
|
40310
40604
|
};
|
|
40311
40605
|
const checkFunctionLike = (node) => {
|
|
40312
|
-
if (!
|
|
40606
|
+
if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
|
|
40313
40607
|
const inferredName = inferFunctionLikeName(node);
|
|
40314
40608
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40315
40609
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41569,7 +41863,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41569
41863
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41570
41864
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41571
41865
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41572
|
-
const tagName = node
|
|
41866
|
+
const tagName = resolveJsxElementType(node);
|
|
41573
41867
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41574
41868
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41575
41869
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41589,7 +41883,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41589
41883
|
]);
|
|
41590
41884
|
const isTextLikeInput = (openingElement) => {
|
|
41591
41885
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41592
|
-
const tagName = openingElement
|
|
41886
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
41593
41887
|
if (tagName === "textarea") return true;
|
|
41594
41888
|
if (tagName !== "input") return false;
|
|
41595
41889
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -41746,8 +42040,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
41746
42040
|
]);
|
|
41747
42041
|
const collectBooleanPropBindings = (param) => {
|
|
41748
42042
|
const bindings = /* @__PURE__ */ new Set();
|
|
41749
|
-
|
|
41750
|
-
|
|
42043
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
42044
|
+
if (!isNodeOfType(propsBinding, "ObjectPattern")) return bindings;
|
|
42045
|
+
for (const property of propsBinding.properties ?? []) {
|
|
41751
42046
|
if (!isNodeOfType(property, "Property")) continue;
|
|
41752
42047
|
if (property.computed) continue;
|
|
41753
42048
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -42008,7 +42303,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42008
42303
|
},
|
|
42009
42304
|
JSXOpeningElement(node) {
|
|
42010
42305
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42011
|
-
const tagName = node
|
|
42306
|
+
const tagName = resolveJsxElementType(node);
|
|
42012
42307
|
if (tagName === "dialog") return;
|
|
42013
42308
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42014
42309
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -43555,13 +43850,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
43555
43850
|
}
|
|
43556
43851
|
return null;
|
|
43557
43852
|
};
|
|
43558
|
-
const resolveHookNameFromInitializer = (initializer) => {
|
|
43853
|
+
const resolveHookNameFromInitializer = (initializer, scopes) => {
|
|
43559
43854
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
43560
43855
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
43561
|
-
const
|
|
43562
|
-
if (
|
|
43563
|
-
if (!isNodeOfType(
|
|
43564
|
-
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);
|
|
43565
43860
|
};
|
|
43566
43861
|
const queryNoRestDestructuring = defineRule({
|
|
43567
43862
|
id: "query-no-rest-destructuring",
|
|
@@ -43574,7 +43869,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
43574
43869
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
43575
43870
|
if (!node.init) return;
|
|
43576
43871
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
43577
|
-
const hookName = resolveHookNameFromInitializer(node.init);
|
|
43872
|
+
const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
|
|
43578
43873
|
if (!hookName) return;
|
|
43579
43874
|
context.report({
|
|
43580
43875
|
node: node.id,
|
|
@@ -44045,7 +44340,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44045
44340
|
severity: "warn",
|
|
44046
44341
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44047
44342
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44048
|
-
if (
|
|
44343
|
+
if (resolveJsxElementType(node) !== "svg") return;
|
|
44049
44344
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44050
44345
|
node,
|
|
44051
44346
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -44929,7 +45224,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
|
|
|
44929
45224
|
"useParams",
|
|
44930
45225
|
"usePathname"
|
|
44931
45226
|
]);
|
|
44932
|
-
const findHookCallBindings = (componentBody) => {
|
|
45227
|
+
const findHookCallBindings = (componentBody, scopes) => {
|
|
44933
45228
|
const bindings = [];
|
|
44934
45229
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
44935
45230
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -44940,8 +45235,10 @@ const findHookCallBindings = (componentBody) => {
|
|
|
44940
45235
|
const callee = declarator.init.callee;
|
|
44941
45236
|
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
44942
45237
|
if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
|
|
45238
|
+
const valueSymbol = scopes.symbolFor(declarator.id);
|
|
45239
|
+
if (!valueSymbol) continue;
|
|
44943
45240
|
bindings.push({
|
|
44944
|
-
|
|
45241
|
+
valueSymbol,
|
|
44945
45242
|
hookName: callee.name,
|
|
44946
45243
|
declarator
|
|
44947
45244
|
});
|
|
@@ -44949,6 +45246,49 @@ const findHookCallBindings = (componentBody) => {
|
|
|
44949
45246
|
}
|
|
44950
45247
|
return bindings;
|
|
44951
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
|
+
};
|
|
44952
45292
|
const rerenderDeferReadsHook = defineRule({
|
|
44953
45293
|
id: "rerender-defer-reads-hook",
|
|
44954
45294
|
title: "URL hook value only read in handlers",
|
|
@@ -44959,15 +45299,13 @@ const rerenderDeferReadsHook = defineRule({
|
|
|
44959
45299
|
create: (context) => {
|
|
44960
45300
|
const checkComponent = (componentBody) => {
|
|
44961
45301
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
44962
|
-
const bindings = findHookCallBindings(componentBody);
|
|
45302
|
+
const bindings = findHookCallBindings(componentBody, context.scopes);
|
|
44963
45303
|
if (bindings.length === 0) return;
|
|
44964
45304
|
const handlerBindingNames = collectHandlerBindingNames(componentBody);
|
|
44965
45305
|
for (const binding of bindings) {
|
|
45306
|
+
const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
|
|
44966
45307
|
const referenceLocations = [];
|
|
44967
|
-
|
|
44968
|
-
if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
|
|
44969
|
-
if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
|
|
44970
|
-
});
|
|
45308
|
+
for (const symbol of symbols) for (const reference of symbol.references) if (!aliasSourceIdentifiers.has(reference.identifier)) referenceLocations.push(reference.identifier);
|
|
44971
45309
|
if (referenceLocations.length === 0) continue;
|
|
44972
45310
|
if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
|
|
44973
45311
|
context.report({
|
|
@@ -45338,14 +45676,10 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45338
45676
|
//#endregion
|
|
45339
45677
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45340
45678
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45341
|
-
const callbackReturnsJsx = (callback) => {
|
|
45679
|
+
const callbackReturnsJsx = (callback, scopes) => {
|
|
45342
45680
|
if (!callback) return false;
|
|
45343
45681
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45344
|
-
|
|
45345
|
-
if (isJsxExpression(body)) return true;
|
|
45346
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
45347
|
-
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
|
|
45348
|
-
return false;
|
|
45682
|
+
return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
|
|
45349
45683
|
};
|
|
45350
45684
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45351
45685
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45423,7 +45757,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45423
45757
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45424
45758
|
for (const declarator of stmt.declarations ?? []) {
|
|
45425
45759
|
const init = declarator.init;
|
|
45426
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
45760
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
|
|
45427
45761
|
memoNode = declarator;
|
|
45428
45762
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45429
45763
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45489,8 +45823,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45489
45823
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45490
45824
|
const bindings = /* @__PURE__ */ new Map();
|
|
45491
45825
|
const params = functionNode.params ?? [];
|
|
45492
|
-
for (const param of params)
|
|
45493
|
-
|
|
45826
|
+
for (const [parameterIndex, param] of params.entries()) {
|
|
45827
|
+
const parameterBinding = parameterIndex === 0 ? resolveFirstArgumentBinding(param) : param;
|
|
45828
|
+
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45829
|
+
}
|
|
45830
|
+
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45494
45831
|
const body = functionNode.body;
|
|
45495
45832
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45496
45833
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -45996,8 +46333,8 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
45996
46333
|
if (statements.length !== 1) return;
|
|
45997
46334
|
const onlyStatement = statements[0];
|
|
45998
46335
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
45999
|
-
singleAssignment = onlyStatement
|
|
46000
|
-
} else if (body) singleAssignment = body;
|
|
46336
|
+
singleAssignment = unwrapDiscardedExpression(onlyStatement);
|
|
46337
|
+
} else if (body) singleAssignment = unwrapDiscardedExpression(body);
|
|
46001
46338
|
if (!singleAssignment) return;
|
|
46002
46339
|
const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
|
|
46003
46340
|
const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
|
|
@@ -52917,7 +53254,8 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
52917
53254
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
52918
53255
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
52919
53256
|
const firstArg = node.arguments?.[0];
|
|
52920
|
-
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;
|
|
52921
53259
|
context.report({
|
|
52922
53260
|
node,
|
|
52923
53261
|
message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
|
|
@@ -53604,10 +53942,6 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53604
53942
|
}
|
|
53605
53943
|
return false;
|
|
53606
53944
|
};
|
|
53607
|
-
const getJsxOpeningElementName = (node) => {
|
|
53608
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53609
|
-
return null;
|
|
53610
|
-
};
|
|
53611
53945
|
const stylePropObject = defineRule({
|
|
53612
53946
|
id: "style-prop-object",
|
|
53613
53947
|
title: "Style prop is not an object",
|
|
@@ -53619,7 +53953,8 @@ const stylePropObject = defineRule({
|
|
|
53619
53953
|
const allowSet = new Set(allow);
|
|
53620
53954
|
return {
|
|
53621
53955
|
JSXOpeningElement(node) {
|
|
53622
|
-
|
|
53956
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
53957
|
+
const elementName = resolveJsxElementType(node);
|
|
53623
53958
|
if (elementName && allowSet.has(elementName)) return;
|
|
53624
53959
|
if (elementName) {
|
|
53625
53960
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54324,7 +54659,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54324
54659
|
create: (context) => {
|
|
54325
54660
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54326
54661
|
return { JSXOpeningElement(node) {
|
|
54327
|
-
if (
|
|
54662
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
54328
54663
|
const attributes = node.attributes ?? [];
|
|
54329
54664
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54330
54665
|
if (!hrefAttribute?.value) return;
|
|
@@ -54945,8 +55280,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
54945
55280
|
create: (context) => ({
|
|
54946
55281
|
JSXElement(node) {
|
|
54947
55282
|
const openingElement = node.openingElement;
|
|
54948
|
-
|
|
54949
|
-
const tagName = openingElement.name.name;
|
|
55283
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
54950
55284
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
54951
55285
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
54952
55286
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|