oxlint-plugin-react-doctor 0.7.6-dev.e026a23 → 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 +432 -144
- 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"));
|
|
@@ -14589,6 +14749,18 @@ const jsHoistRegexp = defineRule({
|
|
|
14589
14749
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14590
14750
|
});
|
|
14591
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
|
|
14592
14764
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14593
14765
|
const referencesParameter = (expression, parameterName) => {
|
|
14594
14766
|
if (!expression) return false;
|
|
@@ -14599,7 +14771,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14599
14771
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14600
14772
|
const callback = node.arguments?.[0];
|
|
14601
14773
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14602
|
-
const firstParameter = callback.params?.[0];
|
|
14774
|
+
const firstParameter = resolveFirstArgumentBinding(callback.params?.[0]);
|
|
14603
14775
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14604
14776
|
let predicate = null;
|
|
14605
14777
|
const body = callback.body;
|
|
@@ -15938,14 +16110,21 @@ const jsxFilenameExtension = defineRule({
|
|
|
15938
16110
|
});
|
|
15939
16111
|
//#endregion
|
|
15940
16112
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
15941
|
-
const isJsxFragmentElement = (node) => {
|
|
16113
|
+
const isJsxFragmentElement = (node, scopes) => {
|
|
15942
16114
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
15943
16115
|
const elementName = node.name;
|
|
15944
|
-
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
|
+
}
|
|
15945
16122
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
15946
16123
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
15947
|
-
if (elementName.
|
|
15948
|
-
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);
|
|
15949
16128
|
}
|
|
15950
16129
|
return false;
|
|
15951
16130
|
};
|
|
@@ -15971,7 +16150,7 @@ const jsxFragments = defineRule({
|
|
|
15971
16150
|
if (mode !== "syntax") return;
|
|
15972
16151
|
if (!node.closingElement) return;
|
|
15973
16152
|
const openingElement = node.openingElement;
|
|
15974
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
16153
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
15975
16154
|
if (openingElement.attributes.length > 0) return;
|
|
15976
16155
|
context.report({
|
|
15977
16156
|
node: openingElement,
|
|
@@ -18652,10 +18831,6 @@ const resolveSettings$29 = (settings) => {
|
|
|
18652
18831
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18653
18832
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18654
18833
|
};
|
|
18655
|
-
const getElementName = (node) => {
|
|
18656
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18657
|
-
return null;
|
|
18658
|
-
};
|
|
18659
18834
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18660
18835
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18661
18836
|
const explicit = options.components?.[elementName];
|
|
@@ -18675,7 +18850,8 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18675
18850
|
create: (context) => {
|
|
18676
18851
|
const options = resolveSettings$29(context.settings);
|
|
18677
18852
|
return { JSXOpeningElement(node) {
|
|
18678
|
-
|
|
18853
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18854
|
+
const elementName = resolveJsxElementType(node);
|
|
18679
18855
|
if (!elementName) return;
|
|
18680
18856
|
for (const attribute of node.attributes) {
|
|
18681
18857
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -18862,11 +19038,6 @@ const checkTarget = (attributeValue) => {
|
|
|
18862
19038
|
alternate: false
|
|
18863
19039
|
};
|
|
18864
19040
|
};
|
|
18865
|
-
const getOpeningElementName = (node) => {
|
|
18866
|
-
const name = node.name;
|
|
18867
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18868
|
-
return null;
|
|
18869
|
-
};
|
|
18870
19041
|
const jsxNoTargetBlank = defineRule({
|
|
18871
19042
|
id: "jsx-no-target-blank",
|
|
18872
19043
|
title: "Unsafe target=_blank link",
|
|
@@ -18886,7 +19057,8 @@ const jsxNoTargetBlank = defineRule({
|
|
|
18886
19057
|
return settings.formComponents.has(tagName);
|
|
18887
19058
|
};
|
|
18888
19059
|
return { JSXOpeningElement(node) {
|
|
18889
|
-
|
|
19060
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
19061
|
+
const tagName = resolveJsxElementType(node);
|
|
18890
19062
|
if (!tagName) return;
|
|
18891
19063
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
18892
19064
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -19119,7 +19291,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19119
19291
|
return {
|
|
19120
19292
|
JSXElement(node) {
|
|
19121
19293
|
const openingElement = node.openingElement;
|
|
19122
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
19294
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
19123
19295
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19124
19296
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19125
19297
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20361,11 +20533,24 @@ const hasDirective = (programNode, directive) => {
|
|
|
20361
20533
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20362
20534
|
};
|
|
20363
20535
|
//#endregion
|
|
20364
|
-
//#region src/plugin/utils/
|
|
20365
|
-
const
|
|
20366
|
-
|
|
20367
|
-
|
|
20368
|
-
|
|
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
|
+
};
|
|
20369
20554
|
//#endregion
|
|
20370
20555
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
20371
20556
|
const nextjsAsyncClientComponent = defineRule({
|
|
@@ -20391,10 +20576,10 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
20391
20576
|
},
|
|
20392
20577
|
VariableDeclarator(node) {
|
|
20393
20578
|
if (!fileHasUseClient) return;
|
|
20394
|
-
if (!isComponentAssignment(node)) return;
|
|
20395
|
-
if (!isInlineFunctionExpression(node.init)) return;
|
|
20396
|
-
if (!node.init.async) return;
|
|
20397
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;
|
|
20398
20583
|
context.report({
|
|
20399
20584
|
node,
|
|
20400
20585
|
message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
|
|
@@ -20696,7 +20881,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20696
20881
|
severity: "warn",
|
|
20697
20882
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20698
20883
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20699
|
-
if (
|
|
20884
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
20700
20885
|
const attributes = node.attributes ?? [];
|
|
20701
20886
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20702
20887
|
if (downloadAttribute) {
|
|
@@ -20892,7 +21077,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
20892
21077
|
severity: "warn",
|
|
20893
21078
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
20894
21079
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20895
|
-
if (
|
|
21080
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20896
21081
|
const attributes = node.attributes ?? [];
|
|
20897
21082
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
20898
21083
|
if (!relAttribute?.value) return;
|
|
@@ -21000,7 +21185,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
21000
21185
|
severity: "warn",
|
|
21001
21186
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
21002
21187
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21003
|
-
if (
|
|
21188
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
21004
21189
|
const attributes = node.attributes ?? [];
|
|
21005
21190
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
21006
21191
|
if (!hrefAttribute?.value) return;
|
|
@@ -21175,7 +21360,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21175
21360
|
create: (context) => {
|
|
21176
21361
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21177
21362
|
return { JSXOpeningElement(node) {
|
|
21178
|
-
if (
|
|
21363
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
21179
21364
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21180
21365
|
const programRoot = findProgramRoot(node);
|
|
21181
21366
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21222,8 +21407,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21222
21407
|
severity: "warn",
|
|
21223
21408
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21224
21409
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21225
|
-
|
|
21226
|
-
if (
|
|
21410
|
+
const elementName = resolveJsxElementType(node);
|
|
21411
|
+
if (elementName !== "script" && elementName !== "Script") return;
|
|
21227
21412
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21228
21413
|
if (!srcAttribute?.value) return;
|
|
21229
21414
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -23031,10 +23216,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
23031
23216
|
if (!declaringNode) return false;
|
|
23032
23217
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
23033
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
|
+
};
|
|
23034
23225
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
23035
23226
|
if (def.type !== "Parameter") return false;
|
|
23036
|
-
|
|
23037
|
-
return isFunctionLike$1(bindingParent);
|
|
23227
|
+
return isWholePropsParameterBinding(def.name);
|
|
23038
23228
|
}));
|
|
23039
23229
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
23040
23230
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -26031,6 +26221,56 @@ const noBarrelImport = defineRule({
|
|
|
26031
26221
|
}
|
|
26032
26222
|
});
|
|
26033
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
|
|
26034
26274
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
26035
26275
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
26036
26276
|
"FunctionDeclaration",
|
|
@@ -26050,12 +26290,13 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
26050
26290
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
26051
26291
|
};
|
|
26052
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);
|
|
26053
26294
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26054
26295
|
let hasRenderOutput = false;
|
|
26055
26296
|
walkAst(rootNode, (node) => {
|
|
26056
26297
|
if (hasRenderOutput) return false;
|
|
26057
26298
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26058
|
-
if (
|
|
26299
|
+
if (isRenderOutputExpression(node, scopes)) {
|
|
26059
26300
|
hasRenderOutput = true;
|
|
26060
26301
|
return false;
|
|
26061
26302
|
}
|
|
@@ -26066,7 +26307,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26066
26307
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26067
26308
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26068
26309
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26069
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26310
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
|
|
26070
26311
|
renderOutputCache.set(functionNode, {
|
|
26071
26312
|
scopes,
|
|
26072
26313
|
hasRenderOutput
|
|
@@ -26074,6 +26315,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
|
26074
26315
|
return hasRenderOutput;
|
|
26075
26316
|
};
|
|
26076
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
|
|
26077
26321
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
26078
26322
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
26079
26323
|
//#endregion
|
|
@@ -28531,7 +28775,7 @@ const noDisabledZoom = defineRule({
|
|
|
28531
28775
|
category: "Accessibility",
|
|
28532
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.",
|
|
28533
28777
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28534
|
-
if (
|
|
28778
|
+
if (resolveJsxElementType(node) !== "meta") return;
|
|
28535
28779
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28536
28780
|
if (!nameAttr?.value) return;
|
|
28537
28781
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -31703,7 +31947,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31703
31947
|
severity: "warn",
|
|
31704
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.",
|
|
31705
31949
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31706
|
-
if (
|
|
31950
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
31707
31951
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31708
31952
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31709
31953
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -31944,7 +32188,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
31944
32188
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
31945
32189
|
return {
|
|
31946
32190
|
JSXOpeningElement(node) {
|
|
31947
|
-
if (
|
|
32191
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
31948
32192
|
let typeAttribute = null;
|
|
31949
32193
|
let typeAttributeIndex = null;
|
|
31950
32194
|
let indeterminateAttribute = null;
|
|
@@ -32123,11 +32367,12 @@ const isMemoCall = (node) => {
|
|
|
32123
32367
|
};
|
|
32124
32368
|
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
32125
32369
|
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
32126
|
-
const isInlineReference = (node) => {
|
|
32127
|
-
|
|
32128
|
-
if (isNodeOfType(
|
|
32129
|
-
if (isNodeOfType(
|
|
32130
|
-
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";
|
|
32131
32376
|
return null;
|
|
32132
32377
|
};
|
|
32133
32378
|
const noInlinePropOnMemoComponent = defineRule({
|
|
@@ -32157,7 +32402,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
32157
32402
|
let elementName = null;
|
|
32158
32403
|
if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
|
|
32159
32404
|
if (!elementName || !memoizedComponentNames.has(elementName)) return;
|
|
32160
|
-
const propType = isInlineReference(node.value.expression);
|
|
32405
|
+
const propType = isInlineReference(node.value.expression, context.scopes);
|
|
32161
32406
|
if (propType) context.report({
|
|
32162
32407
|
node: node.value.expression,
|
|
32163
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`
|
|
@@ -33016,11 +33261,13 @@ const noManyBooleanProps = defineRule({
|
|
|
33016
33261
|
};
|
|
33017
33262
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
33018
33263
|
if (!param) return;
|
|
33264
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33265
|
+
if (!propsBinding) return;
|
|
33019
33266
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
33020
|
-
if (isNodeOfType(
|
|
33267
|
+
if (isNodeOfType(propsBinding, "ObjectPattern")) {
|
|
33021
33268
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
33022
33269
|
const booleanLikePropNames = [];
|
|
33023
|
-
for (const property of
|
|
33270
|
+
for (const property of propsBinding.properties ?? []) {
|
|
33024
33271
|
if (!isNodeOfType(property, "Property")) continue;
|
|
33025
33272
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
33026
33273
|
if (!keyName) continue;
|
|
@@ -33031,7 +33278,7 @@ const noManyBooleanProps = defineRule({
|
|
|
33031
33278
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
33032
33279
|
return;
|
|
33033
33280
|
}
|
|
33034
|
-
if (isNodeOfType(
|
|
33281
|
+
if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
|
|
33035
33282
|
};
|
|
33036
33283
|
return {
|
|
33037
33284
|
FunctionDeclaration(node) {
|
|
@@ -35783,7 +36030,7 @@ const noPreventDefault = defineRule({
|
|
|
35783
36030
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
35784
36031
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
35785
36032
|
return { JSXOpeningElement(node) {
|
|
35786
|
-
const elementName =
|
|
36033
|
+
const elementName = resolveJsxElementType(node);
|
|
35787
36034
|
if (!elementName) return;
|
|
35788
36035
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
35789
36036
|
if (!targetEventProps) return;
|
|
@@ -38247,9 +38494,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38247
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.",
|
|
38248
38495
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38249
38496
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38250
|
-
const
|
|
38497
|
+
const elementName = resolveJsxElementType(node);
|
|
38498
|
+
const firstCharacter = elementName.charCodeAt(0);
|
|
38251
38499
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38252
|
-
if (
|
|
38500
|
+
if (elementName.includes("-")) return;
|
|
38253
38501
|
for (const attribute of node.attributes) {
|
|
38254
38502
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38255
38503
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38637,8 +38885,7 @@ const noUncontrolledInput = defineRule({
|
|
|
38637
38885
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38638
38886
|
walkAst(componentBody, (child) => {
|
|
38639
38887
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38640
|
-
|
|
38641
|
-
const tagName = child.name.name;
|
|
38888
|
+
const tagName = resolveJsxElementType(child);
|
|
38642
38889
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38643
38890
|
const attributes = child.attributes ?? [];
|
|
38644
38891
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38699,7 +38946,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38699
38946
|
severity: "warn",
|
|
38700
38947
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38701
38948
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38702
|
-
if (
|
|
38949
|
+
if (resolveJsxElementType(node) !== "script") return;
|
|
38703
38950
|
const attributes = node.attributes ?? [];
|
|
38704
38951
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38705
38952
|
if (!srcAttribute) return;
|
|
@@ -39914,7 +40161,7 @@ const noUnknownProperty = defineRule({
|
|
|
39914
40161
|
}
|
|
39915
40162
|
if (fileIsNonReactJsx) return;
|
|
39916
40163
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
39917
|
-
const elementType = node
|
|
40164
|
+
const elementType = resolveJsxElementType(node);
|
|
39918
40165
|
const firstCharacter = elementType.charCodeAt(0);
|
|
39919
40166
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
39920
40167
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40092,20 +40339,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40092
40339
|
});
|
|
40093
40340
|
return found;
|
|
40094
40341
|
};
|
|
40342
|
+
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40095
40343
|
const isReactClassComponent = (classNode) => {
|
|
40096
40344
|
if (isEs6Component(classNode)) return true;
|
|
40097
40345
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40098
40346
|
};
|
|
40099
|
-
const findEnclosingComponent = (node) => {
|
|
40347
|
+
const findEnclosingComponent = (node, scopes) => {
|
|
40100
40348
|
let walker = node.parent;
|
|
40101
40349
|
while (walker) {
|
|
40102
40350
|
if (isFunctionLike$1(walker)) {
|
|
40103
40351
|
const componentName = inferFunctionLikeName(walker);
|
|
40104
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40352
|
+
if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
|
|
40105
40353
|
component: walker,
|
|
40106
40354
|
name: componentName
|
|
40107
40355
|
};
|
|
40108
|
-
if (!componentName &&
|
|
40356
|
+
if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40109
40357
|
component: walker,
|
|
40110
40358
|
name: null
|
|
40111
40359
|
};
|
|
@@ -40344,7 +40592,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40344
40592
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40345
40593
|
if (settings.allowAsProps) return;
|
|
40346
40594
|
}
|
|
40347
|
-
const enclosing = findEnclosingComponent(candidateNode);
|
|
40595
|
+
const enclosing = findEnclosingComponent(candidateNode, context.scopes);
|
|
40348
40596
|
if (!enclosing) return;
|
|
40349
40597
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40350
40598
|
queuedReports.push({
|
|
@@ -40355,7 +40603,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40355
40603
|
});
|
|
40356
40604
|
};
|
|
40357
40605
|
const checkFunctionLike = (node) => {
|
|
40358
|
-
if (!
|
|
40606
|
+
if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
|
|
40359
40607
|
const inferredName = inferFunctionLikeName(node);
|
|
40360
40608
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40361
40609
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41615,7 +41863,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41615
41863
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41616
41864
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41617
41865
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41618
|
-
const tagName = node
|
|
41866
|
+
const tagName = resolveJsxElementType(node);
|
|
41619
41867
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41620
41868
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41621
41869
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41635,7 +41883,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41635
41883
|
]);
|
|
41636
41884
|
const isTextLikeInput = (openingElement) => {
|
|
41637
41885
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41638
|
-
const tagName = openingElement
|
|
41886
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
41639
41887
|
if (tagName === "textarea") return true;
|
|
41640
41888
|
if (tagName !== "input") return false;
|
|
41641
41889
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -41792,8 +42040,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
41792
42040
|
]);
|
|
41793
42041
|
const collectBooleanPropBindings = (param) => {
|
|
41794
42042
|
const bindings = /* @__PURE__ */ new Set();
|
|
41795
|
-
|
|
41796
|
-
|
|
42043
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
42044
|
+
if (!isNodeOfType(propsBinding, "ObjectPattern")) return bindings;
|
|
42045
|
+
for (const property of propsBinding.properties ?? []) {
|
|
41797
42046
|
if (!isNodeOfType(property, "Property")) continue;
|
|
41798
42047
|
if (property.computed) continue;
|
|
41799
42048
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -42054,7 +42303,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42054
42303
|
},
|
|
42055
42304
|
JSXOpeningElement(node) {
|
|
42056
42305
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42057
|
-
const tagName = node
|
|
42306
|
+
const tagName = resolveJsxElementType(node);
|
|
42058
42307
|
if (tagName === "dialog") return;
|
|
42059
42308
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42060
42309
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -43601,13 +43850,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
43601
43850
|
}
|
|
43602
43851
|
return null;
|
|
43603
43852
|
};
|
|
43604
|
-
const resolveHookNameFromInitializer = (initializer) => {
|
|
43853
|
+
const resolveHookNameFromInitializer = (initializer, scopes) => {
|
|
43605
43854
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
43606
43855
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
43607
|
-
const
|
|
43608
|
-
if (
|
|
43609
|
-
if (!isNodeOfType(
|
|
43610
|
-
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);
|
|
43611
43860
|
};
|
|
43612
43861
|
const queryNoRestDestructuring = defineRule({
|
|
43613
43862
|
id: "query-no-rest-destructuring",
|
|
@@ -43620,7 +43869,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
43620
43869
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
43621
43870
|
if (!node.init) return;
|
|
43622
43871
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
43623
|
-
const hookName = resolveHookNameFromInitializer(node.init);
|
|
43872
|
+
const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
|
|
43624
43873
|
if (!hookName) return;
|
|
43625
43874
|
context.report({
|
|
43626
43875
|
node: node.id,
|
|
@@ -44091,7 +44340,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44091
44340
|
severity: "warn",
|
|
44092
44341
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44093
44342
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44094
|
-
if (
|
|
44343
|
+
if (resolveJsxElementType(node) !== "svg") return;
|
|
44095
44344
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44096
44345
|
node,
|
|
44097
44346
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -44975,7 +45224,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
|
|
|
44975
45224
|
"useParams",
|
|
44976
45225
|
"usePathname"
|
|
44977
45226
|
]);
|
|
44978
|
-
const findHookCallBindings = (componentBody) => {
|
|
45227
|
+
const findHookCallBindings = (componentBody, scopes) => {
|
|
44979
45228
|
const bindings = [];
|
|
44980
45229
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
44981
45230
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -44986,8 +45235,10 @@ const findHookCallBindings = (componentBody) => {
|
|
|
44986
45235
|
const callee = declarator.init.callee;
|
|
44987
45236
|
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
44988
45237
|
if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
|
|
45238
|
+
const valueSymbol = scopes.symbolFor(declarator.id);
|
|
45239
|
+
if (!valueSymbol) continue;
|
|
44989
45240
|
bindings.push({
|
|
44990
|
-
|
|
45241
|
+
valueSymbol,
|
|
44991
45242
|
hookName: callee.name,
|
|
44992
45243
|
declarator
|
|
44993
45244
|
});
|
|
@@ -44995,6 +45246,49 @@ const findHookCallBindings = (componentBody) => {
|
|
|
44995
45246
|
}
|
|
44996
45247
|
return bindings;
|
|
44997
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
|
+
};
|
|
44998
45292
|
const rerenderDeferReadsHook = defineRule({
|
|
44999
45293
|
id: "rerender-defer-reads-hook",
|
|
45000
45294
|
title: "URL hook value only read in handlers",
|
|
@@ -45005,15 +45299,13 @@ const rerenderDeferReadsHook = defineRule({
|
|
|
45005
45299
|
create: (context) => {
|
|
45006
45300
|
const checkComponent = (componentBody) => {
|
|
45007
45301
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
45008
|
-
const bindings = findHookCallBindings(componentBody);
|
|
45302
|
+
const bindings = findHookCallBindings(componentBody, context.scopes);
|
|
45009
45303
|
if (bindings.length === 0) return;
|
|
45010
45304
|
const handlerBindingNames = collectHandlerBindingNames(componentBody);
|
|
45011
45305
|
for (const binding of bindings) {
|
|
45306
|
+
const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
|
|
45012
45307
|
const referenceLocations = [];
|
|
45013
|
-
|
|
45014
|
-
if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
|
|
45015
|
-
if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
|
|
45016
|
-
});
|
|
45308
|
+
for (const symbol of symbols) for (const reference of symbol.references) if (!aliasSourceIdentifiers.has(reference.identifier)) referenceLocations.push(reference.identifier);
|
|
45017
45309
|
if (referenceLocations.length === 0) continue;
|
|
45018
45310
|
if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
|
|
45019
45311
|
context.report({
|
|
@@ -45384,14 +45676,10 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45384
45676
|
//#endregion
|
|
45385
45677
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45386
45678
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45387
|
-
const callbackReturnsJsx = (callback) => {
|
|
45679
|
+
const callbackReturnsJsx = (callback, scopes) => {
|
|
45388
45680
|
if (!callback) return false;
|
|
45389
45681
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45390
|
-
|
|
45391
|
-
if (isJsxExpression(body)) return true;
|
|
45392
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
45393
|
-
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
|
|
45394
|
-
return false;
|
|
45682
|
+
return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
|
|
45395
45683
|
};
|
|
45396
45684
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45397
45685
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45469,7 +45757,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45469
45757
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45470
45758
|
for (const declarator of stmt.declarations ?? []) {
|
|
45471
45759
|
const init = declarator.init;
|
|
45472
|
-
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)) {
|
|
45473
45761
|
memoNode = declarator;
|
|
45474
45762
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45475
45763
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45535,8 +45823,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45535
45823
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45536
45824
|
const bindings = /* @__PURE__ */ new Map();
|
|
45537
45825
|
const params = functionNode.params ?? [];
|
|
45538
|
-
for (const param of params)
|
|
45539
|
-
|
|
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]);
|
|
45540
45831
|
const body = functionNode.body;
|
|
45541
45832
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45542
45833
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -52963,7 +53254,8 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
52963
53254
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
52964
53255
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
52965
53256
|
const firstArg = node.arguments?.[0];
|
|
52966
|
-
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;
|
|
52967
53259
|
context.report({
|
|
52968
53260
|
node,
|
|
52969
53261
|
message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
|
|
@@ -53650,10 +53942,6 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53650
53942
|
}
|
|
53651
53943
|
return false;
|
|
53652
53944
|
};
|
|
53653
|
-
const getJsxOpeningElementName = (node) => {
|
|
53654
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53655
|
-
return null;
|
|
53656
|
-
};
|
|
53657
53945
|
const stylePropObject = defineRule({
|
|
53658
53946
|
id: "style-prop-object",
|
|
53659
53947
|
title: "Style prop is not an object",
|
|
@@ -53665,7 +53953,8 @@ const stylePropObject = defineRule({
|
|
|
53665
53953
|
const allowSet = new Set(allow);
|
|
53666
53954
|
return {
|
|
53667
53955
|
JSXOpeningElement(node) {
|
|
53668
|
-
|
|
53956
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
53957
|
+
const elementName = resolveJsxElementType(node);
|
|
53669
53958
|
if (elementName && allowSet.has(elementName)) return;
|
|
53670
53959
|
if (elementName) {
|
|
53671
53960
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54370,7 +54659,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54370
54659
|
create: (context) => {
|
|
54371
54660
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54372
54661
|
return { JSXOpeningElement(node) {
|
|
54373
|
-
if (
|
|
54662
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
54374
54663
|
const attributes = node.attributes ?? [];
|
|
54375
54664
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54376
54665
|
if (!hrefAttribute?.value) return;
|
|
@@ -54991,8 +55280,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
54991
55280
|
create: (context) => ({
|
|
54992
55281
|
JSXElement(node) {
|
|
54993
55282
|
const openingElement = node.openingElement;
|
|
54994
|
-
|
|
54995
|
-
const tagName = openingElement.name.name;
|
|
55283
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
54996
55284
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
54997
55285
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
54998
55286
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|