oxlint-plugin-react-doctor 0.7.6-dev.81e6647 → 0.7.6-dev.82e10cd
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 +151 -451
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1518,37 +1518,6 @@ 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
|
|
1552
1521
|
//#region src/plugin/utils/get-element-type.ts
|
|
1553
1522
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1554
1523
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1561,8 +1530,14 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1561
1530
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1562
1531
|
return a11ySettings;
|
|
1563
1532
|
};
|
|
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
|
+
};
|
|
1564
1539
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1565
|
-
const baseName =
|
|
1540
|
+
const baseName = flattenJsxName$2(openingElement.name);
|
|
1566
1541
|
if (a11ySettings.polymorphicPropName) {
|
|
1567
1542
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1568
1543
|
if (polymorphicAttribute) {
|
|
@@ -2397,9 +2372,8 @@ const anchorIsValid = defineRule({
|
|
|
2397
2372
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2398
2373
|
return { JSXOpeningElement(node) {
|
|
2399
2374
|
if (isTestlikeFile) return;
|
|
2400
|
-
|
|
2401
|
-
if (
|
|
2402
|
-
if (tag !== "a") return;
|
|
2375
|
+
if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
|
|
2376
|
+
if (getElementType(node, context.settings) !== "a") return;
|
|
2403
2377
|
let hrefAttribute;
|
|
2404
2378
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2405
2379
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5754,7 +5728,7 @@ const buttonHasType = defineRule({
|
|
|
5754
5728
|
return {
|
|
5755
5729
|
JSXOpeningElement(node) {
|
|
5756
5730
|
if (isTestlikeFile) return;
|
|
5757
|
-
if (
|
|
5731
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "button") return;
|
|
5758
5732
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5759
5733
|
if (!typeAttr) {
|
|
5760
5734
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5944,7 +5918,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5944
5918
|
};
|
|
5945
5919
|
return {
|
|
5946
5920
|
JSXOpeningElement(node) {
|
|
5947
|
-
if (
|
|
5921
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
5948
5922
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5949
5923
|
},
|
|
5950
5924
|
CallExpression(node) {
|
|
@@ -6021,7 +5995,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
6021
5995
|
//#endregion
|
|
6022
5996
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
6023
5997
|
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
6024
|
-
if (!isNodeOfType(identifier, "Identifier")
|
|
5998
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
6025
5999
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6026
6000
|
let symbol = scopes.symbolFor(identifier);
|
|
6027
6001
|
while (symbol?.kind === "const") {
|
|
@@ -6296,131 +6270,6 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6296
6270
|
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6297
6271
|
};
|
|
6298
6272
|
//#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
|
|
6424
6273
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6425
6274
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6426
6275
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
@@ -6442,39 +6291,26 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6442
6291
|
severity: "warn",
|
|
6443
6292
|
category: "Correctness",
|
|
6444
6293
|
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.",
|
|
6445
|
-
create: (context) => {
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
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
|
-
}
|
|
6294
|
+
create: (context) => ({ CallExpression(node) {
|
|
6295
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6296
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6297
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6298
|
+
if (receiver.name !== "localStorage") return;
|
|
6299
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6300
|
+
if (node.callee.property.name !== "setItem") return;
|
|
6301
|
+
const keyArg = node.arguments?.[0];
|
|
6302
|
+
if (!keyArg) return;
|
|
6303
|
+
const keyValue = resolveStringKey(keyArg, context);
|
|
6304
|
+
if (keyValue === null) return;
|
|
6305
|
+
if (isVersionedKey(keyValue)) return;
|
|
6306
|
+
const valueArg = node.arguments?.[1];
|
|
6307
|
+
if (!valueArg) return;
|
|
6308
|
+
if (!isJsonStringifyCall(valueArg)) return;
|
|
6309
|
+
context.report({
|
|
6310
|
+
node: keyArg,
|
|
6311
|
+
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").`
|
|
6312
|
+
});
|
|
6313
|
+
} })
|
|
6478
6314
|
});
|
|
6479
6315
|
//#endregion
|
|
6480
6316
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
@@ -7597,7 +7433,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7597
7433
|
const getOpeningElementTagName = (openingElement) => {
|
|
7598
7434
|
if (!openingElement) return null;
|
|
7599
7435
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7600
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return
|
|
7436
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement.name.name;
|
|
7601
7437
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7602
7438
|
let cursor = openingElement.name;
|
|
7603
7439
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7849,7 +7685,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7849
7685
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7850
7686
|
return { JSXOpeningElement(node) {
|
|
7851
7687
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7852
|
-
const tagName =
|
|
7688
|
+
const tagName = node.name.name;
|
|
7853
7689
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7854
7690
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7855
7691
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -12081,7 +11917,7 @@ const forbidDomProps = defineRule({
|
|
|
12081
11917
|
return { JSXOpeningElement(node) {
|
|
12082
11918
|
if (forbidMap.size === 0) return;
|
|
12083
11919
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12084
|
-
const elementName =
|
|
11920
|
+
const elementName = node.name.name;
|
|
12085
11921
|
if (isReactComponentName(elementName)) return;
|
|
12086
11922
|
for (const attribute of node.attributes) {
|
|
12087
11923
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -12159,7 +11995,7 @@ const forbidElements = defineRule({
|
|
|
12159
11995
|
return {
|
|
12160
11996
|
JSXOpeningElement(node) {
|
|
12161
11997
|
if (forbidMap.size === 0) return;
|
|
12162
|
-
const fullName =
|
|
11998
|
+
const fullName = flattenJsxName$1(node.name);
|
|
12163
11999
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
12164
12000
|
context.report({
|
|
12165
12001
|
node: node.name,
|
|
@@ -12521,7 +12357,8 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
12521
12357
|
const isParagraphElement = (candidate) => {
|
|
12522
12358
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12523
12359
|
const opening = candidate.openingElement;
|
|
12524
|
-
|
|
12360
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12361
|
+
return opening.name.name === "p";
|
|
12525
12362
|
};
|
|
12526
12363
|
const findEnclosingParagraph = (openingElement) => {
|
|
12527
12364
|
const owningElement = openingElement.parent;
|
|
@@ -12542,7 +12379,8 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
12542
12379
|
severity: "warn",
|
|
12543
12380
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
12544
12381
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12545
|
-
|
|
12382
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12383
|
+
const childTagName = node.name.name;
|
|
12546
12384
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
12547
12385
|
if (!findEnclosingParagraph(node)) return;
|
|
12548
12386
|
context.report({
|
|
@@ -12573,7 +12411,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
12573
12411
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
12574
12412
|
const opening = jsxElement.openingElement;
|
|
12575
12413
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
12576
|
-
const tagName =
|
|
12414
|
+
const tagName = opening.name.name;
|
|
12577
12415
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
12578
12416
|
return tagName;
|
|
12579
12417
|
};
|
|
@@ -12603,7 +12441,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
12603
12441
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
12604
12442
|
const opening = ancestor.openingElement;
|
|
12605
12443
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
12606
|
-
const ancestorTag =
|
|
12444
|
+
const ancestorTag = opening.name.name;
|
|
12607
12445
|
if (ancestorTag.length === 0) {
|
|
12608
12446
|
previous = ancestor;
|
|
12609
12447
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12685,7 +12523,8 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12685
12523
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12686
12524
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12687
12525
|
const opening = candidate.openingElement;
|
|
12688
|
-
|
|
12526
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12527
|
+
return opening.name.name === tagName;
|
|
12689
12528
|
};
|
|
12690
12529
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12691
12530
|
const owningElement = openingElement.parent;
|
|
@@ -12706,7 +12545,8 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12706
12545
|
severity: "warn",
|
|
12707
12546
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12708
12547
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12709
|
-
|
|
12548
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12549
|
+
const tagName = node.name.name;
|
|
12710
12550
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12711
12551
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12712
12552
|
context.report({
|
|
@@ -12821,7 +12661,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12821
12661
|
matchByOccurrence: true,
|
|
12822
12662
|
create: skipNonProductionFiles((context) => ({
|
|
12823
12663
|
JSXOpeningElement(node) {
|
|
12824
|
-
if (
|
|
12664
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "iframe") return;
|
|
12825
12665
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12826
12666
|
if (!sandboxAttr) {
|
|
12827
12667
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -14749,18 +14589,6 @@ const jsHoistRegexp = defineRule({
|
|
|
14749
14589
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14750
14590
|
});
|
|
14751
14591
|
//#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
|
|
14764
14592
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14765
14593
|
const referencesParameter = (expression, parameterName) => {
|
|
14766
14594
|
if (!expression) return false;
|
|
@@ -14771,7 +14599,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14771
14599
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14772
14600
|
const callback = node.arguments?.[0];
|
|
14773
14601
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14774
|
-
const firstParameter =
|
|
14602
|
+
const firstParameter = callback.params?.[0];
|
|
14775
14603
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14776
14604
|
let predicate = null;
|
|
14777
14605
|
const body = callback.body;
|
|
@@ -15463,21 +15291,9 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
15463
15291
|
};
|
|
15464
15292
|
const getResolvedInitializer = (receiver) => {
|
|
15465
15293
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
15466
|
-
const
|
|
15467
|
-
|
|
15468
|
-
|
|
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
|
-
};
|
|
15294
|
+
const initializer = findVariableInitializer(receiver, receiver.name)?.initializer ?? null;
|
|
15295
|
+
if (initializer && isNodeOfType(initializer, "Identifier")) return findVariableInitializer(initializer, initializer.name)?.initializer ?? initializer;
|
|
15296
|
+
return initializer;
|
|
15481
15297
|
};
|
|
15482
15298
|
const isSplitCall = (expression) => {
|
|
15483
15299
|
if (!expression) return false;
|
|
@@ -15643,8 +15459,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
15643
15459
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
15644
15460
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
15645
15461
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
15646
|
-
const
|
|
15647
|
-
if (
|
|
15462
|
+
const initializer = getResolvedInitializer(stripped);
|
|
15463
|
+
if (initializer && isSmallInlineLiteralArray(initializer)) return true;
|
|
15648
15464
|
}
|
|
15649
15465
|
return false;
|
|
15650
15466
|
};
|
|
@@ -15696,8 +15512,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15696
15512
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15697
15513
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15698
15514
|
if (resolvedInitializer) {
|
|
15699
|
-
if (isLikelyStringReceiver(resolvedInitializer
|
|
15700
|
-
if (
|
|
15515
|
+
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15516
|
+
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15701
15517
|
}
|
|
15702
15518
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15703
15519
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -16110,21 +15926,14 @@ const jsxFilenameExtension = defineRule({
|
|
|
16110
15926
|
});
|
|
16111
15927
|
//#endregion
|
|
16112
15928
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
16113
|
-
const isJsxFragmentElement = (node
|
|
15929
|
+
const isJsxFragmentElement = (node) => {
|
|
16114
15930
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
16115
15931
|
const elementName = node.name;
|
|
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
|
-
}
|
|
15932
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) return elementName.name === "Fragment";
|
|
16122
15933
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
16123
15934
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
16124
|
-
if (elementName.
|
|
16125
|
-
|
|
16126
|
-
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
16127
|
-
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15935
|
+
if (elementName.object.name !== "React") return false;
|
|
15936
|
+
return elementName.property.name === "Fragment";
|
|
16128
15937
|
}
|
|
16129
15938
|
return false;
|
|
16130
15939
|
};
|
|
@@ -16150,7 +15959,7 @@ const jsxFragments = defineRule({
|
|
|
16150
15959
|
if (mode !== "syntax") return;
|
|
16151
15960
|
if (!node.closingElement) return;
|
|
16152
15961
|
const openingElement = node.openingElement;
|
|
16153
|
-
if (!isJsxFragmentElement(openingElement
|
|
15962
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
16154
15963
|
if (openingElement.attributes.length > 0) return;
|
|
16155
15964
|
context.report({
|
|
16156
15965
|
node: openingElement,
|
|
@@ -18831,6 +18640,10 @@ const resolveSettings$29 = (settings) => {
|
|
|
18831
18640
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18832
18641
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18833
18642
|
};
|
|
18643
|
+
const getElementName = (node) => {
|
|
18644
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18645
|
+
return null;
|
|
18646
|
+
};
|
|
18834
18647
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18835
18648
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18836
18649
|
const explicit = options.components?.[elementName];
|
|
@@ -18850,8 +18663,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18850
18663
|
create: (context) => {
|
|
18851
18664
|
const options = resolveSettings$29(context.settings);
|
|
18852
18665
|
return { JSXOpeningElement(node) {
|
|
18853
|
-
|
|
18854
|
-
const elementName = resolveJsxElementType(node);
|
|
18666
|
+
const elementName = getElementName(node);
|
|
18855
18667
|
if (!elementName) return;
|
|
18856
18668
|
for (const attribute of node.attributes) {
|
|
18857
18669
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -19038,6 +18850,11 @@ const checkTarget = (attributeValue) => {
|
|
|
19038
18850
|
alternate: false
|
|
19039
18851
|
};
|
|
19040
18852
|
};
|
|
18853
|
+
const getOpeningElementName = (node) => {
|
|
18854
|
+
const name = node.name;
|
|
18855
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18856
|
+
return null;
|
|
18857
|
+
};
|
|
19041
18858
|
const jsxNoTargetBlank = defineRule({
|
|
19042
18859
|
id: "jsx-no-target-blank",
|
|
19043
18860
|
title: "Unsafe target=_blank link",
|
|
@@ -19057,8 +18874,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
19057
18874
|
return settings.formComponents.has(tagName);
|
|
19058
18875
|
};
|
|
19059
18876
|
return { JSXOpeningElement(node) {
|
|
19060
|
-
|
|
19061
|
-
const tagName = resolveJsxElementType(node);
|
|
18877
|
+
const tagName = getOpeningElementName(node);
|
|
19062
18878
|
if (!tagName) return;
|
|
19063
18879
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
19064
18880
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -19291,7 +19107,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19291
19107
|
return {
|
|
19292
19108
|
JSXElement(node) {
|
|
19293
19109
|
const openingElement = node.openingElement;
|
|
19294
|
-
if (!isJsxFragmentElement(openingElement
|
|
19110
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
19295
19111
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19296
19112
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19297
19113
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20533,24 +20349,11 @@ const hasDirective = (programNode, directive) => {
|
|
|
20533
20349
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20534
20350
|
};
|
|
20535
20351
|
//#endregion
|
|
20536
|
-
//#region src/plugin/utils/
|
|
20537
|
-
const
|
|
20538
|
-
|
|
20539
|
-
|
|
20540
|
-
|
|
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
|
-
};
|
|
20352
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20353
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20354
|
+
//#endregion
|
|
20355
|
+
//#region src/plugin/utils/is-component-assignment.ts
|
|
20356
|
+
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"));
|
|
20554
20357
|
//#endregion
|
|
20555
20358
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
20556
20359
|
const nextjsAsyncClientComponent = defineRule({
|
|
@@ -20576,10 +20379,10 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
20576
20379
|
},
|
|
20577
20380
|
VariableDeclarator(node) {
|
|
20578
20381
|
if (!fileHasUseClient) return;
|
|
20382
|
+
if (!isComponentAssignment(node)) return;
|
|
20383
|
+
if (!isInlineFunctionExpression(node.init)) return;
|
|
20384
|
+
if (!node.init.async) return;
|
|
20579
20385
|
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;
|
|
20583
20386
|
context.report({
|
|
20584
20387
|
node,
|
|
20585
20388
|
message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
|
|
@@ -20881,7 +20684,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20881
20684
|
severity: "warn",
|
|
20882
20685
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20883
20686
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20884
|
-
if (
|
|
20687
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
20885
20688
|
const attributes = node.attributes ?? [];
|
|
20886
20689
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20887
20690
|
if (downloadAttribute) {
|
|
@@ -21077,7 +20880,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
21077
20880
|
severity: "warn",
|
|
21078
20881
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
21079
20882
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21080
|
-
if (
|
|
20883
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21081
20884
|
const attributes = node.attributes ?? [];
|
|
21082
20885
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
21083
20886
|
if (!relAttribute?.value) return;
|
|
@@ -21185,7 +20988,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
21185
20988
|
severity: "warn",
|
|
21186
20989
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
21187
20990
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21188
|
-
if (
|
|
20991
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21189
20992
|
const attributes = node.attributes ?? [];
|
|
21190
20993
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
21191
20994
|
if (!hrefAttribute?.value) return;
|
|
@@ -21360,7 +21163,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21360
21163
|
create: (context) => {
|
|
21361
21164
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21362
21165
|
return { JSXOpeningElement(node) {
|
|
21363
|
-
if (
|
|
21166
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
21364
21167
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21365
21168
|
const programRoot = findProgramRoot(node);
|
|
21366
21169
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21407,8 +21210,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21407
21210
|
severity: "warn",
|
|
21408
21211
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21409
21212
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21410
|
-
|
|
21411
|
-
if (
|
|
21213
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
21214
|
+
if (node.name.name !== "script" && node.name.name !== "Script") return;
|
|
21412
21215
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21413
21216
|
if (!srcAttribute?.value) return;
|
|
21414
21217
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -23216,15 +23019,10 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
23216
23019
|
if (!declaringNode) return false;
|
|
23217
23020
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
23218
23021
|
}));
|
|
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
|
-
};
|
|
23225
23022
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
23226
23023
|
if (def.type !== "Parameter") return false;
|
|
23227
|
-
|
|
23024
|
+
const bindingParent = def.name.parent;
|
|
23025
|
+
return isFunctionLike$1(bindingParent);
|
|
23228
23026
|
}));
|
|
23229
23027
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
23230
23028
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -26221,56 +26019,6 @@ const noBarrelImport = defineRule({
|
|
|
26221
26019
|
}
|
|
26222
26020
|
});
|
|
26223
26021
|
//#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
|
|
26274
26022
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
26275
26023
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
26276
26024
|
"FunctionDeclaration",
|
|
@@ -26290,13 +26038,12 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
26290
26038
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
26291
26039
|
};
|
|
26292
26040
|
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);
|
|
26294
26041
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26295
26042
|
let hasRenderOutput = false;
|
|
26296
26043
|
walkAst(rootNode, (node) => {
|
|
26297
26044
|
if (hasRenderOutput) return false;
|
|
26298
26045
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26299
|
-
if (
|
|
26046
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
|
|
26300
26047
|
hasRenderOutput = true;
|
|
26301
26048
|
return false;
|
|
26302
26049
|
}
|
|
@@ -26307,7 +26054,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26307
26054
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26308
26055
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26309
26056
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26310
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes)
|
|
26057
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26311
26058
|
renderOutputCache.set(functionNode, {
|
|
26312
26059
|
scopes,
|
|
26313
26060
|
hasRenderOutput
|
|
@@ -26315,9 +26062,6 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
|
26315
26062
|
return hasRenderOutput;
|
|
26316
26063
|
};
|
|
26317
26064
|
//#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
|
|
26321
26065
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
26322
26066
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
26323
26067
|
//#endregion
|
|
@@ -28775,7 +28519,7 @@ const noDisabledZoom = defineRule({
|
|
|
28775
28519
|
category: "Accessibility",
|
|
28776
28520
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28777
28521
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28778
|
-
if (
|
|
28522
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
|
|
28779
28523
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28780
28524
|
if (!nameAttr?.value) return;
|
|
28781
28525
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -31947,7 +31691,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31947
31691
|
severity: "warn",
|
|
31948
31692
|
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.",
|
|
31949
31693
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31950
|
-
if (
|
|
31694
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
31951
31695
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31952
31696
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31953
31697
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -32188,7 +31932,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
32188
31932
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
32189
31933
|
return {
|
|
32190
31934
|
JSXOpeningElement(node) {
|
|
32191
|
-
if (
|
|
31935
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
32192
31936
|
let typeAttribute = null;
|
|
32193
31937
|
let typeAttributeIndex = null;
|
|
32194
31938
|
let indeterminateAttribute = null;
|
|
@@ -32367,12 +32111,11 @@ const isMemoCall = (node) => {
|
|
|
32367
32111
|
};
|
|
32368
32112
|
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
32369
32113
|
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
32370
|
-
const isInlineReference = (node
|
|
32371
|
-
|
|
32372
|
-
if (isNodeOfType(
|
|
32373
|
-
if (isNodeOfType(
|
|
32374
|
-
if (isNodeOfType(
|
|
32375
|
-
if (isNodeOfType(referenceNode, "JSXElement") || isNodeOfType(referenceNode, "JSXFragment")) return "JSX";
|
|
32114
|
+
const isInlineReference = (node) => {
|
|
32115
|
+
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "bind") return "functions";
|
|
32116
|
+
if (isNodeOfType(node, "ObjectExpression")) return "objects";
|
|
32117
|
+
if (isNodeOfType(node, "ArrayExpression")) return "Arrays";
|
|
32118
|
+
if (isNodeOfType(node, "JSXElement") || isNodeOfType(node, "JSXFragment")) return "JSX";
|
|
32376
32119
|
return null;
|
|
32377
32120
|
};
|
|
32378
32121
|
const noInlinePropOnMemoComponent = defineRule({
|
|
@@ -32402,7 +32145,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
32402
32145
|
let elementName = null;
|
|
32403
32146
|
if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
|
|
32404
32147
|
if (!elementName || !memoizedComponentNames.has(elementName)) return;
|
|
32405
|
-
const propType = isInlineReference(node.value.expression
|
|
32148
|
+
const propType = isInlineReference(node.value.expression);
|
|
32406
32149
|
if (propType) context.report({
|
|
32407
32150
|
node: node.value.expression,
|
|
32408
32151
|
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`
|
|
@@ -33261,13 +33004,11 @@ const noManyBooleanProps = defineRule({
|
|
|
33261
33004
|
};
|
|
33262
33005
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
33263
33006
|
if (!param) return;
|
|
33264
|
-
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33265
|
-
if (!propsBinding) return;
|
|
33266
33007
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
33267
|
-
if (isNodeOfType(
|
|
33008
|
+
if (isNodeOfType(param, "ObjectPattern")) {
|
|
33268
33009
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
33269
33010
|
const booleanLikePropNames = [];
|
|
33270
|
-
for (const property of
|
|
33011
|
+
for (const property of param.properties ?? []) {
|
|
33271
33012
|
if (!isNodeOfType(property, "Property")) continue;
|
|
33272
33013
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
33273
33014
|
if (!keyName) continue;
|
|
@@ -33278,7 +33019,7 @@ const noManyBooleanProps = defineRule({
|
|
|
33278
33019
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
33279
33020
|
return;
|
|
33280
33021
|
}
|
|
33281
|
-
if (isNodeOfType(
|
|
33022
|
+
if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
|
|
33282
33023
|
};
|
|
33283
33024
|
return {
|
|
33284
33025
|
FunctionDeclaration(node) {
|
|
@@ -36030,7 +35771,7 @@ const noPreventDefault = defineRule({
|
|
|
36030
35771
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
36031
35772
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
36032
35773
|
return { JSXOpeningElement(node) {
|
|
36033
|
-
const elementName =
|
|
35774
|
+
const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
|
|
36034
35775
|
if (!elementName) return;
|
|
36035
35776
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
36036
35777
|
if (!targetEventProps) return;
|
|
@@ -38494,10 +38235,9 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38494
38235
|
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.",
|
|
38495
38236
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38496
38237
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38497
|
-
const
|
|
38498
|
-
const firstCharacter = elementName.charCodeAt(0);
|
|
38238
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
38499
38239
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38500
|
-
if (
|
|
38240
|
+
if (node.name.name.includes("-")) return;
|
|
38501
38241
|
for (const attribute of node.attributes) {
|
|
38502
38242
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38503
38243
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38885,7 +38625,8 @@ const noUncontrolledInput = defineRule({
|
|
|
38885
38625
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38886
38626
|
walkAst(componentBody, (child) => {
|
|
38887
38627
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38888
|
-
|
|
38628
|
+
if (!isNodeOfType(child.name, "JSXIdentifier")) return;
|
|
38629
|
+
const tagName = child.name.name;
|
|
38889
38630
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38890
38631
|
const attributes = child.attributes ?? [];
|
|
38891
38632
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38946,7 +38687,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38946
38687
|
severity: "warn",
|
|
38947
38688
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38948
38689
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38949
|
-
if (
|
|
38690
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
|
|
38950
38691
|
const attributes = node.attributes ?? [];
|
|
38951
38692
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38952
38693
|
if (!srcAttribute) return;
|
|
@@ -40161,7 +39902,7 @@ const noUnknownProperty = defineRule({
|
|
|
40161
39902
|
}
|
|
40162
39903
|
if (fileIsNonReactJsx) return;
|
|
40163
39904
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
40164
|
-
const elementType =
|
|
39905
|
+
const elementType = node.name.name;
|
|
40165
39906
|
const firstCharacter = elementType.charCodeAt(0);
|
|
40166
39907
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
40167
39908
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40339,21 +40080,20 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40339
40080
|
});
|
|
40340
40081
|
return found;
|
|
40341
40082
|
};
|
|
40342
|
-
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40343
40083
|
const isReactClassComponent = (classNode) => {
|
|
40344
40084
|
if (isEs6Component(classNode)) return true;
|
|
40345
40085
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40346
40086
|
};
|
|
40347
|
-
const findEnclosingComponent = (node
|
|
40087
|
+
const findEnclosingComponent = (node) => {
|
|
40348
40088
|
let walker = node.parent;
|
|
40349
40089
|
while (walker) {
|
|
40350
40090
|
if (isFunctionLike$1(walker)) {
|
|
40351
40091
|
const componentName = inferFunctionLikeName(walker);
|
|
40352
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40092
|
+
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
40353
40093
|
component: walker,
|
|
40354
40094
|
name: componentName
|
|
40355
40095
|
};
|
|
40356
|
-
if (!componentName &&
|
|
40096
|
+
if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40357
40097
|
component: walker,
|
|
40358
40098
|
name: null
|
|
40359
40099
|
};
|
|
@@ -40592,7 +40332,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40592
40332
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40593
40333
|
if (settings.allowAsProps) return;
|
|
40594
40334
|
}
|
|
40595
|
-
const enclosing = findEnclosingComponent(candidateNode
|
|
40335
|
+
const enclosing = findEnclosingComponent(candidateNode);
|
|
40596
40336
|
if (!enclosing) return;
|
|
40597
40337
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40598
40338
|
queuedReports.push({
|
|
@@ -40603,7 +40343,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40603
40343
|
});
|
|
40604
40344
|
};
|
|
40605
40345
|
const checkFunctionLike = (node) => {
|
|
40606
|
-
if (!
|
|
40346
|
+
if (!expressionContainsJsxOrCreateElement(node)) return;
|
|
40607
40347
|
const inferredName = inferFunctionLikeName(node);
|
|
40608
40348
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40609
40349
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41863,7 +41603,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41863
41603
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41864
41604
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41865
41605
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41866
|
-
const tagName =
|
|
41606
|
+
const tagName = node.name.name;
|
|
41867
41607
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41868
41608
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41869
41609
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41883,7 +41623,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41883
41623
|
]);
|
|
41884
41624
|
const isTextLikeInput = (openingElement) => {
|
|
41885
41625
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41886
|
-
const tagName =
|
|
41626
|
+
const tagName = openingElement.name.name;
|
|
41887
41627
|
if (tagName === "textarea") return true;
|
|
41888
41628
|
if (tagName !== "input") return false;
|
|
41889
41629
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -42040,9 +41780,8 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
42040
41780
|
]);
|
|
42041
41781
|
const collectBooleanPropBindings = (param) => {
|
|
42042
41782
|
const bindings = /* @__PURE__ */ new Set();
|
|
42043
|
-
|
|
42044
|
-
|
|
42045
|
-
for (const property of propsBinding.properties ?? []) {
|
|
41783
|
+
if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
|
|
41784
|
+
for (const property of param.properties ?? []) {
|
|
42046
41785
|
if (!isNodeOfType(property, "Property")) continue;
|
|
42047
41786
|
if (property.computed) continue;
|
|
42048
41787
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -42303,7 +42042,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42303
42042
|
},
|
|
42304
42043
|
JSXOpeningElement(node) {
|
|
42305
42044
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42306
|
-
const tagName =
|
|
42045
|
+
const tagName = node.name.name;
|
|
42307
42046
|
if (tagName === "dialog") return;
|
|
42308
42047
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42309
42048
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -43850,13 +43589,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
43850
43589
|
}
|
|
43851
43590
|
return null;
|
|
43852
43591
|
};
|
|
43853
|
-
const resolveHookNameFromInitializer = (initializer
|
|
43592
|
+
const resolveHookNameFromInitializer = (initializer) => {
|
|
43854
43593
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
43855
43594
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
43856
|
-
const
|
|
43857
|
-
if (
|
|
43858
|
-
if (!isNodeOfType(
|
|
43859
|
-
return resolveTanstackQueryHookName(
|
|
43595
|
+
const binding = findVariableInitializer(initializer, initializer.name);
|
|
43596
|
+
if (!binding?.initializer || !isConstDeclaredBinding(binding)) return null;
|
|
43597
|
+
if (!isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
43598
|
+
return resolveTanstackQueryHookName(binding.initializer);
|
|
43860
43599
|
};
|
|
43861
43600
|
const queryNoRestDestructuring = defineRule({
|
|
43862
43601
|
id: "query-no-rest-destructuring",
|
|
@@ -43869,7 +43608,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
43869
43608
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
43870
43609
|
if (!node.init) return;
|
|
43871
43610
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
43872
|
-
const hookName = resolveHookNameFromInitializer(node.init
|
|
43611
|
+
const hookName = resolveHookNameFromInitializer(node.init);
|
|
43873
43612
|
if (!hookName) return;
|
|
43874
43613
|
context.report({
|
|
43875
43614
|
node: node.id,
|
|
@@ -44340,7 +44079,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44340
44079
|
severity: "warn",
|
|
44341
44080
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44342
44081
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44343
|
-
if (
|
|
44082
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
|
|
44344
44083
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44345
44084
|
node,
|
|
44346
44085
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -45224,7 +44963,7 @@ const DEFERRABLE_HOOK_NAMES = new Set([
|
|
|
45224
44963
|
"useParams",
|
|
45225
44964
|
"usePathname"
|
|
45226
44965
|
]);
|
|
45227
|
-
const findHookCallBindings = (componentBody
|
|
44966
|
+
const findHookCallBindings = (componentBody) => {
|
|
45228
44967
|
const bindings = [];
|
|
45229
44968
|
if (!isNodeOfType(componentBody, "BlockStatement")) return bindings;
|
|
45230
44969
|
for (const statement of componentBody.body ?? []) {
|
|
@@ -45235,10 +44974,8 @@ const findHookCallBindings = (componentBody, scopes) => {
|
|
|
45235
44974
|
const callee = declarator.init.callee;
|
|
45236
44975
|
if (!isNodeOfType(callee, "Identifier")) continue;
|
|
45237
44976
|
if (!DEFERRABLE_HOOK_NAMES.has(callee.name)) continue;
|
|
45238
|
-
const valueSymbol = scopes.symbolFor(declarator.id);
|
|
45239
|
-
if (!valueSymbol) continue;
|
|
45240
44977
|
bindings.push({
|
|
45241
|
-
|
|
44978
|
+
valueName: declarator.id.name,
|
|
45242
44979
|
hookName: callee.name,
|
|
45243
44980
|
declarator
|
|
45244
44981
|
});
|
|
@@ -45246,49 +44983,6 @@ const findHookCallBindings = (componentBody, scopes) => {
|
|
|
45246
44983
|
}
|
|
45247
44984
|
return bindings;
|
|
45248
44985
|
};
|
|
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
|
-
};
|
|
45292
44986
|
const rerenderDeferReadsHook = defineRule({
|
|
45293
44987
|
id: "rerender-defer-reads-hook",
|
|
45294
44988
|
title: "URL hook value only read in handlers",
|
|
@@ -45299,13 +44993,15 @@ const rerenderDeferReadsHook = defineRule({
|
|
|
45299
44993
|
create: (context) => {
|
|
45300
44994
|
const checkComponent = (componentBody) => {
|
|
45301
44995
|
if (!componentBody || !isNodeOfType(componentBody, "BlockStatement")) return;
|
|
45302
|
-
const bindings = findHookCallBindings(componentBody
|
|
44996
|
+
const bindings = findHookCallBindings(componentBody);
|
|
45303
44997
|
if (bindings.length === 0) return;
|
|
45304
44998
|
const handlerBindingNames = collectHandlerBindingNames(componentBody);
|
|
45305
44999
|
for (const binding of bindings) {
|
|
45306
|
-
const { symbols, aliasSourceIdentifiers } = collectExactAliasSymbols(componentBody, binding.valueSymbol, context.scopes);
|
|
45307
45000
|
const referenceLocations = [];
|
|
45308
|
-
|
|
45001
|
+
walkAst(componentBody, (child) => {
|
|
45002
|
+
if (isNodeOfType(binding.declarator, "VariableDeclarator") && child === binding.declarator.id) return;
|
|
45003
|
+
if (isNodeOfType(child, "Identifier") && child.name === binding.valueName) referenceLocations.push(child);
|
|
45004
|
+
});
|
|
45309
45005
|
if (referenceLocations.length === 0) continue;
|
|
45310
45006
|
if (!referenceLocations.every((ref) => isInsideEventHandler(ref, handlerBindingNames))) continue;
|
|
45311
45007
|
context.report({
|
|
@@ -45676,10 +45372,14 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45676
45372
|
//#endregion
|
|
45677
45373
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45678
45374
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45679
|
-
const callbackReturnsJsx = (callback
|
|
45375
|
+
const callbackReturnsJsx = (callback) => {
|
|
45680
45376
|
if (!callback) return false;
|
|
45681
45377
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45682
|
-
|
|
45378
|
+
const body = callback.body;
|
|
45379
|
+
if (isJsxExpression(body)) return true;
|
|
45380
|
+
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
45381
|
+
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
|
|
45382
|
+
return false;
|
|
45683
45383
|
};
|
|
45684
45384
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45685
45385
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45757,7 +45457,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45757
45457
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45758
45458
|
for (const declarator of stmt.declarations ?? []) {
|
|
45759
45459
|
const init = declarator.init;
|
|
45760
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0]
|
|
45460
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
45761
45461
|
memoNode = declarator;
|
|
45762
45462
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45763
45463
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45823,11 +45523,8 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45823
45523
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45824
45524
|
const bindings = /* @__PURE__ */ new Map();
|
|
45825
45525
|
const params = functionNode.params ?? [];
|
|
45826
|
-
for (const
|
|
45827
|
-
|
|
45828
|
-
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45829
|
-
}
|
|
45830
|
-
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45526
|
+
for (const param of params) collectFromObjectPattern(param, bindings);
|
|
45527
|
+
const propsParam = params[0];
|
|
45831
45528
|
const body = functionNode.body;
|
|
45832
45529
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45833
45530
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -53254,8 +52951,7 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
53254
52951
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
53255
52952
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
53256
52953
|
const firstArg = node.arguments?.[0];
|
|
53257
|
-
if (!
|
|
53258
|
-
if (!isNodeOfType(unwrapObjectIntegrityExpression(firstArg, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES), "ObjectExpression")) return;
|
|
52954
|
+
if (!isNodeOfType(firstArg, "ObjectExpression")) return;
|
|
53259
52955
|
context.report({
|
|
53260
52956
|
node,
|
|
53261
52957
|
message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
|
|
@@ -53942,6 +53638,10 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53942
53638
|
}
|
|
53943
53639
|
return false;
|
|
53944
53640
|
};
|
|
53641
|
+
const getJsxOpeningElementName = (node) => {
|
|
53642
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53643
|
+
return null;
|
|
53644
|
+
};
|
|
53945
53645
|
const stylePropObject = defineRule({
|
|
53946
53646
|
id: "style-prop-object",
|
|
53947
53647
|
title: "Style prop is not an object",
|
|
@@ -53953,8 +53653,7 @@ const stylePropObject = defineRule({
|
|
|
53953
53653
|
const allowSet = new Set(allow);
|
|
53954
53654
|
return {
|
|
53955
53655
|
JSXOpeningElement(node) {
|
|
53956
|
-
|
|
53957
|
-
const elementName = resolveJsxElementType(node);
|
|
53656
|
+
const elementName = getJsxOpeningElementName(node);
|
|
53958
53657
|
if (elementName && allowSet.has(elementName)) return;
|
|
53959
53658
|
if (elementName) {
|
|
53960
53659
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54659,7 +54358,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54659
54358
|
create: (context) => {
|
|
54660
54359
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54661
54360
|
return { JSXOpeningElement(node) {
|
|
54662
|
-
if (
|
|
54361
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
54663
54362
|
const attributes = node.attributes ?? [];
|
|
54664
54363
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54665
54364
|
if (!hrefAttribute?.value) return;
|
|
@@ -55280,7 +54979,8 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
55280
54979
|
create: (context) => ({
|
|
55281
54980
|
JSXElement(node) {
|
|
55282
54981
|
const openingElement = node.openingElement;
|
|
55283
|
-
|
|
54982
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
|
|
54983
|
+
const tagName = openingElement.name.name;
|
|
55284
54984
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
55285
54985
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
55286
54986
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|