oxlint-plugin-react-doctor 0.7.6-dev.c6f996e → 0.7.6-dev.e026a23
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 +125 -349
- 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;
|
|
@@ -16110,21 +15938,14 @@ const jsxFilenameExtension = defineRule({
|
|
|
16110
15938
|
});
|
|
16111
15939
|
//#endregion
|
|
16112
15940
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
16113
|
-
const isJsxFragmentElement = (node
|
|
15941
|
+
const isJsxFragmentElement = (node) => {
|
|
16114
15942
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
16115
15943
|
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
|
-
}
|
|
15944
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) return elementName.name === "Fragment";
|
|
16122
15945
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
16123
15946
|
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);
|
|
15947
|
+
if (elementName.object.name !== "React") return false;
|
|
15948
|
+
return elementName.property.name === "Fragment";
|
|
16128
15949
|
}
|
|
16129
15950
|
return false;
|
|
16130
15951
|
};
|
|
@@ -16150,7 +15971,7 @@ const jsxFragments = defineRule({
|
|
|
16150
15971
|
if (mode !== "syntax") return;
|
|
16151
15972
|
if (!node.closingElement) return;
|
|
16152
15973
|
const openingElement = node.openingElement;
|
|
16153
|
-
if (!isJsxFragmentElement(openingElement
|
|
15974
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
16154
15975
|
if (openingElement.attributes.length > 0) return;
|
|
16155
15976
|
context.report({
|
|
16156
15977
|
node: openingElement,
|
|
@@ -18831,6 +18652,10 @@ const resolveSettings$29 = (settings) => {
|
|
|
18831
18652
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18832
18653
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18833
18654
|
};
|
|
18655
|
+
const getElementName = (node) => {
|
|
18656
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18657
|
+
return null;
|
|
18658
|
+
};
|
|
18834
18659
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18835
18660
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18836
18661
|
const explicit = options.components?.[elementName];
|
|
@@ -18850,8 +18675,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18850
18675
|
create: (context) => {
|
|
18851
18676
|
const options = resolveSettings$29(context.settings);
|
|
18852
18677
|
return { JSXOpeningElement(node) {
|
|
18853
|
-
|
|
18854
|
-
const elementName = resolveJsxElementType(node);
|
|
18678
|
+
const elementName = getElementName(node);
|
|
18855
18679
|
if (!elementName) return;
|
|
18856
18680
|
for (const attribute of node.attributes) {
|
|
18857
18681
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -19038,6 +18862,11 @@ const checkTarget = (attributeValue) => {
|
|
|
19038
18862
|
alternate: false
|
|
19039
18863
|
};
|
|
19040
18864
|
};
|
|
18865
|
+
const getOpeningElementName = (node) => {
|
|
18866
|
+
const name = node.name;
|
|
18867
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18868
|
+
return null;
|
|
18869
|
+
};
|
|
19041
18870
|
const jsxNoTargetBlank = defineRule({
|
|
19042
18871
|
id: "jsx-no-target-blank",
|
|
19043
18872
|
title: "Unsafe target=_blank link",
|
|
@@ -19057,8 +18886,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
19057
18886
|
return settings.formComponents.has(tagName);
|
|
19058
18887
|
};
|
|
19059
18888
|
return { JSXOpeningElement(node) {
|
|
19060
|
-
|
|
19061
|
-
const tagName = resolveJsxElementType(node);
|
|
18889
|
+
const tagName = getOpeningElementName(node);
|
|
19062
18890
|
if (!tagName) return;
|
|
19063
18891
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
19064
18892
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -19291,7 +19119,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19291
19119
|
return {
|
|
19292
19120
|
JSXElement(node) {
|
|
19293
19121
|
const openingElement = node.openingElement;
|
|
19294
|
-
if (!isJsxFragmentElement(openingElement
|
|
19122
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
19295
19123
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19296
19124
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19297
19125
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20533,6 +20361,9 @@ const hasDirective = (programNode, directive) => {
|
|
|
20533
20361
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20534
20362
|
};
|
|
20535
20363
|
//#endregion
|
|
20364
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20365
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20366
|
+
//#endregion
|
|
20536
20367
|
//#region src/plugin/utils/is-component-assignment.ts
|
|
20537
20368
|
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"));
|
|
20538
20369
|
//#endregion
|
|
@@ -20865,7 +20696,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20865
20696
|
severity: "warn",
|
|
20866
20697
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20867
20698
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20868
|
-
if (
|
|
20699
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
20869
20700
|
const attributes = node.attributes ?? [];
|
|
20870
20701
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20871
20702
|
if (downloadAttribute) {
|
|
@@ -21061,7 +20892,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
21061
20892
|
severity: "warn",
|
|
21062
20893
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
21063
20894
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21064
|
-
if (
|
|
20895
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21065
20896
|
const attributes = node.attributes ?? [];
|
|
21066
20897
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
21067
20898
|
if (!relAttribute?.value) return;
|
|
@@ -21169,7 +21000,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
21169
21000
|
severity: "warn",
|
|
21170
21001
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
21171
21002
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21172
|
-
if (
|
|
21003
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21173
21004
|
const attributes = node.attributes ?? [];
|
|
21174
21005
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
21175
21006
|
if (!hrefAttribute?.value) return;
|
|
@@ -21344,7 +21175,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21344
21175
|
create: (context) => {
|
|
21345
21176
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21346
21177
|
return { JSXOpeningElement(node) {
|
|
21347
|
-
if (
|
|
21178
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
21348
21179
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21349
21180
|
const programRoot = findProgramRoot(node);
|
|
21350
21181
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21391,8 +21222,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21391
21222
|
severity: "warn",
|
|
21392
21223
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21393
21224
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21394
|
-
|
|
21395
|
-
if (
|
|
21225
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
21226
|
+
if (node.name.name !== "script" && node.name.name !== "Script") return;
|
|
21396
21227
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21397
21228
|
if (!srcAttribute?.value) return;
|
|
21398
21229
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -23200,15 +23031,10 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
23200
23031
|
if (!declaringNode) return false;
|
|
23201
23032
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
23202
23033
|
}));
|
|
23203
|
-
const isWholePropsParameterBinding = (bindingNode) => {
|
|
23204
|
-
if (isFunctionLike$1(bindingNode.parent)) return true;
|
|
23205
|
-
let declaringFunction = bindingNode.parent;
|
|
23206
|
-
while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
|
|
23207
|
-
return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
|
|
23208
|
-
};
|
|
23209
23034
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
23210
23035
|
if (def.type !== "Parameter") return false;
|
|
23211
|
-
|
|
23036
|
+
const bindingParent = def.name.parent;
|
|
23037
|
+
return isFunctionLike$1(bindingParent);
|
|
23212
23038
|
}));
|
|
23213
23039
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
23214
23040
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -26205,56 +26031,6 @@ const noBarrelImport = defineRule({
|
|
|
26205
26031
|
}
|
|
26206
26032
|
});
|
|
26207
26033
|
//#endregion
|
|
26208
|
-
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
26209
|
-
const collectReturnedExpressions = (functionNode) => {
|
|
26210
|
-
if (!isFunctionLike$1(functionNode)) return [];
|
|
26211
|
-
const body = functionNode.body;
|
|
26212
|
-
if (!body) return [];
|
|
26213
|
-
if (!isNodeOfType(body, "BlockStatement")) return [body];
|
|
26214
|
-
const returnedExpressions = [];
|
|
26215
|
-
walkAst(body, (node) => {
|
|
26216
|
-
if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
26217
|
-
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
|
|
26218
|
-
});
|
|
26219
|
-
return returnedExpressions;
|
|
26220
|
-
};
|
|
26221
|
-
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
|
|
26222
|
-
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
26223
|
-
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
26224
|
-
const functionMatches = (candidateFunction) => {
|
|
26225
|
-
if (visitedFunctions.has(candidateFunction)) return false;
|
|
26226
|
-
visitedFunctions.add(candidateFunction);
|
|
26227
|
-
return collectReturnedExpressions(candidateFunction).some(expressionMatches);
|
|
26228
|
-
};
|
|
26229
|
-
const expressionMatches = (expression) => {
|
|
26230
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
26231
|
-
if (visitedExpressions.has(unwrappedExpression)) return false;
|
|
26232
|
-
visitedExpressions.add(unwrappedExpression);
|
|
26233
|
-
if (matchesExpression(unwrappedExpression)) return true;
|
|
26234
|
-
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
26235
|
-
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
26236
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
|
|
26237
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
26238
|
-
if (isFunctionLike$1(initializer)) return false;
|
|
26239
|
-
return expressionMatches(initializer);
|
|
26240
|
-
}
|
|
26241
|
-
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
26242
|
-
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
26243
|
-
if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
26244
|
-
const symbol = scopes.symbolFor(unwrappedExpression.callee);
|
|
26245
|
-
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
|
|
26246
|
-
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
26247
|
-
const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
|
|
26248
|
-
if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
|
|
26249
|
-
return functionMatches(candidateFunction);
|
|
26250
|
-
}
|
|
26251
|
-
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
|
|
26252
|
-
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
|
|
26253
|
-
return false;
|
|
26254
|
-
};
|
|
26255
|
-
return functionMatches(functionNode);
|
|
26256
|
-
};
|
|
26257
|
-
//#endregion
|
|
26258
26034
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
26259
26035
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
26260
26036
|
"FunctionDeclaration",
|
|
@@ -26274,13 +26050,12 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
26274
26050
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
26275
26051
|
};
|
|
26276
26052
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
26277
|
-
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
26278
26053
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26279
26054
|
let hasRenderOutput = false;
|
|
26280
26055
|
walkAst(rootNode, (node) => {
|
|
26281
26056
|
if (hasRenderOutput) return false;
|
|
26282
26057
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26283
|
-
if (
|
|
26058
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
|
|
26284
26059
|
hasRenderOutput = true;
|
|
26285
26060
|
return false;
|
|
26286
26061
|
}
|
|
@@ -26291,7 +26066,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26291
26066
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26292
26067
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26293
26068
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26294
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes)
|
|
26069
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26295
26070
|
renderOutputCache.set(functionNode, {
|
|
26296
26071
|
scopes,
|
|
26297
26072
|
hasRenderOutput
|
|
@@ -28756,7 +28531,7 @@ const noDisabledZoom = defineRule({
|
|
|
28756
28531
|
category: "Accessibility",
|
|
28757
28532
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28758
28533
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28759
|
-
if (
|
|
28534
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
|
|
28760
28535
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28761
28536
|
if (!nameAttr?.value) return;
|
|
28762
28537
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -31928,7 +31703,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31928
31703
|
severity: "warn",
|
|
31929
31704
|
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.",
|
|
31930
31705
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31931
|
-
if (
|
|
31706
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
31932
31707
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31933
31708
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31934
31709
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -32169,7 +31944,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
32169
31944
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
32170
31945
|
return {
|
|
32171
31946
|
JSXOpeningElement(node) {
|
|
32172
|
-
if (
|
|
31947
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
32173
31948
|
let typeAttribute = null;
|
|
32174
31949
|
let typeAttributeIndex = null;
|
|
32175
31950
|
let indeterminateAttribute = null;
|
|
@@ -33241,13 +33016,11 @@ const noManyBooleanProps = defineRule({
|
|
|
33241
33016
|
};
|
|
33242
33017
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
33243
33018
|
if (!param) return;
|
|
33244
|
-
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33245
|
-
if (!propsBinding) return;
|
|
33246
33019
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
33247
|
-
if (isNodeOfType(
|
|
33020
|
+
if (isNodeOfType(param, "ObjectPattern")) {
|
|
33248
33021
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
33249
33022
|
const booleanLikePropNames = [];
|
|
33250
|
-
for (const property of
|
|
33023
|
+
for (const property of param.properties ?? []) {
|
|
33251
33024
|
if (!isNodeOfType(property, "Property")) continue;
|
|
33252
33025
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
33253
33026
|
if (!keyName) continue;
|
|
@@ -33258,7 +33031,7 @@ const noManyBooleanProps = defineRule({
|
|
|
33258
33031
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
33259
33032
|
return;
|
|
33260
33033
|
}
|
|
33261
|
-
if (isNodeOfType(
|
|
33034
|
+
if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
|
|
33262
33035
|
};
|
|
33263
33036
|
return {
|
|
33264
33037
|
FunctionDeclaration(node) {
|
|
@@ -36010,7 +35783,7 @@ const noPreventDefault = defineRule({
|
|
|
36010
35783
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
36011
35784
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
36012
35785
|
return { JSXOpeningElement(node) {
|
|
36013
|
-
const elementName =
|
|
35786
|
+
const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
|
|
36014
35787
|
if (!elementName) return;
|
|
36015
35788
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
36016
35789
|
if (!targetEventProps) return;
|
|
@@ -38474,10 +38247,9 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38474
38247
|
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.",
|
|
38475
38248
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38476
38249
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38477
|
-
const
|
|
38478
|
-
const firstCharacter = elementName.charCodeAt(0);
|
|
38250
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
38479
38251
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38480
|
-
if (
|
|
38252
|
+
if (node.name.name.includes("-")) return;
|
|
38481
38253
|
for (const attribute of node.attributes) {
|
|
38482
38254
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38483
38255
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38865,7 +38637,8 @@ const noUncontrolledInput = defineRule({
|
|
|
38865
38637
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38866
38638
|
walkAst(componentBody, (child) => {
|
|
38867
38639
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38868
|
-
|
|
38640
|
+
if (!isNodeOfType(child.name, "JSXIdentifier")) return;
|
|
38641
|
+
const tagName = child.name.name;
|
|
38869
38642
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38870
38643
|
const attributes = child.attributes ?? [];
|
|
38871
38644
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38926,7 +38699,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38926
38699
|
severity: "warn",
|
|
38927
38700
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38928
38701
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38929
|
-
if (
|
|
38702
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
|
|
38930
38703
|
const attributes = node.attributes ?? [];
|
|
38931
38704
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38932
38705
|
if (!srcAttribute) return;
|
|
@@ -40141,7 +39914,7 @@ const noUnknownProperty = defineRule({
|
|
|
40141
39914
|
}
|
|
40142
39915
|
if (fileIsNonReactJsx) return;
|
|
40143
39916
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
40144
|
-
const elementType =
|
|
39917
|
+
const elementType = node.name.name;
|
|
40145
39918
|
const firstCharacter = elementType.charCodeAt(0);
|
|
40146
39919
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
40147
39920
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40319,21 +40092,20 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40319
40092
|
});
|
|
40320
40093
|
return found;
|
|
40321
40094
|
};
|
|
40322
|
-
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40323
40095
|
const isReactClassComponent = (classNode) => {
|
|
40324
40096
|
if (isEs6Component(classNode)) return true;
|
|
40325
40097
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40326
40098
|
};
|
|
40327
|
-
const findEnclosingComponent = (node
|
|
40099
|
+
const findEnclosingComponent = (node) => {
|
|
40328
40100
|
let walker = node.parent;
|
|
40329
40101
|
while (walker) {
|
|
40330
40102
|
if (isFunctionLike$1(walker)) {
|
|
40331
40103
|
const componentName = inferFunctionLikeName(walker);
|
|
40332
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40104
|
+
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
40333
40105
|
component: walker,
|
|
40334
40106
|
name: componentName
|
|
40335
40107
|
};
|
|
40336
|
-
if (!componentName &&
|
|
40108
|
+
if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40337
40109
|
component: walker,
|
|
40338
40110
|
name: null
|
|
40339
40111
|
};
|
|
@@ -40572,7 +40344,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40572
40344
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40573
40345
|
if (settings.allowAsProps) return;
|
|
40574
40346
|
}
|
|
40575
|
-
const enclosing = findEnclosingComponent(candidateNode
|
|
40347
|
+
const enclosing = findEnclosingComponent(candidateNode);
|
|
40576
40348
|
if (!enclosing) return;
|
|
40577
40349
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40578
40350
|
queuedReports.push({
|
|
@@ -40583,7 +40355,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40583
40355
|
});
|
|
40584
40356
|
};
|
|
40585
40357
|
const checkFunctionLike = (node) => {
|
|
40586
|
-
if (!
|
|
40358
|
+
if (!expressionContainsJsxOrCreateElement(node)) return;
|
|
40587
40359
|
const inferredName = inferFunctionLikeName(node);
|
|
40588
40360
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40589
40361
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41843,7 +41615,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41843
41615
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41844
41616
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41845
41617
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41846
|
-
const tagName =
|
|
41618
|
+
const tagName = node.name.name;
|
|
41847
41619
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41848
41620
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41849
41621
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41863,7 +41635,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41863
41635
|
]);
|
|
41864
41636
|
const isTextLikeInput = (openingElement) => {
|
|
41865
41637
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41866
|
-
const tagName =
|
|
41638
|
+
const tagName = openingElement.name.name;
|
|
41867
41639
|
if (tagName === "textarea") return true;
|
|
41868
41640
|
if (tagName !== "input") return false;
|
|
41869
41641
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -42020,9 +41792,8 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
42020
41792
|
]);
|
|
42021
41793
|
const collectBooleanPropBindings = (param) => {
|
|
42022
41794
|
const bindings = /* @__PURE__ */ new Set();
|
|
42023
|
-
|
|
42024
|
-
|
|
42025
|
-
for (const property of propsBinding.properties ?? []) {
|
|
41795
|
+
if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
|
|
41796
|
+
for (const property of param.properties ?? []) {
|
|
42026
41797
|
if (!isNodeOfType(property, "Property")) continue;
|
|
42027
41798
|
if (property.computed) continue;
|
|
42028
41799
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -42283,7 +42054,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42283
42054
|
},
|
|
42284
42055
|
JSXOpeningElement(node) {
|
|
42285
42056
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42286
|
-
const tagName =
|
|
42057
|
+
const tagName = node.name.name;
|
|
42287
42058
|
if (tagName === "dialog") return;
|
|
42288
42059
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42289
42060
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -43830,13 +43601,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
43830
43601
|
}
|
|
43831
43602
|
return null;
|
|
43832
43603
|
};
|
|
43833
|
-
const resolveHookNameFromInitializer = (initializer
|
|
43604
|
+
const resolveHookNameFromInitializer = (initializer) => {
|
|
43834
43605
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
43835
43606
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
43836
|
-
const
|
|
43837
|
-
if (
|
|
43838
|
-
if (!isNodeOfType(
|
|
43839
|
-
return resolveTanstackQueryHookName(
|
|
43607
|
+
const binding = findVariableInitializer(initializer, initializer.name);
|
|
43608
|
+
if (!binding?.initializer || !isConstDeclaredBinding(binding)) return null;
|
|
43609
|
+
if (!isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
43610
|
+
return resolveTanstackQueryHookName(binding.initializer);
|
|
43840
43611
|
};
|
|
43841
43612
|
const queryNoRestDestructuring = defineRule({
|
|
43842
43613
|
id: "query-no-rest-destructuring",
|
|
@@ -43849,7 +43620,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
43849
43620
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
43850
43621
|
if (!node.init) return;
|
|
43851
43622
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
43852
|
-
const hookName = resolveHookNameFromInitializer(node.init
|
|
43623
|
+
const hookName = resolveHookNameFromInitializer(node.init);
|
|
43853
43624
|
if (!hookName) return;
|
|
43854
43625
|
context.report({
|
|
43855
43626
|
node: node.id,
|
|
@@ -44320,7 +44091,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44320
44091
|
severity: "warn",
|
|
44321
44092
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44322
44093
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44323
|
-
if (
|
|
44094
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
|
|
44324
44095
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44325
44096
|
node,
|
|
44326
44097
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -45613,10 +45384,14 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45613
45384
|
//#endregion
|
|
45614
45385
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45615
45386
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45616
|
-
const callbackReturnsJsx = (callback
|
|
45387
|
+
const callbackReturnsJsx = (callback) => {
|
|
45617
45388
|
if (!callback) return false;
|
|
45618
45389
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45619
|
-
|
|
45390
|
+
const body = callback.body;
|
|
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;
|
|
45620
45395
|
};
|
|
45621
45396
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45622
45397
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45694,7 +45469,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45694
45469
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45695
45470
|
for (const declarator of stmt.declarations ?? []) {
|
|
45696
45471
|
const init = declarator.init;
|
|
45697
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0]
|
|
45472
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
45698
45473
|
memoNode = declarator;
|
|
45699
45474
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45700
45475
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45760,11 +45535,8 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45760
45535
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45761
45536
|
const bindings = /* @__PURE__ */ new Map();
|
|
45762
45537
|
const params = functionNode.params ?? [];
|
|
45763
|
-
for (const
|
|
45764
|
-
|
|
45765
|
-
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45766
|
-
}
|
|
45767
|
-
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45538
|
+
for (const param of params) collectFromObjectPattern(param, bindings);
|
|
45539
|
+
const propsParam = params[0];
|
|
45768
45540
|
const body = functionNode.body;
|
|
45769
45541
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45770
45542
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -53878,6 +53650,10 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53878
53650
|
}
|
|
53879
53651
|
return false;
|
|
53880
53652
|
};
|
|
53653
|
+
const getJsxOpeningElementName = (node) => {
|
|
53654
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53655
|
+
return null;
|
|
53656
|
+
};
|
|
53881
53657
|
const stylePropObject = defineRule({
|
|
53882
53658
|
id: "style-prop-object",
|
|
53883
53659
|
title: "Style prop is not an object",
|
|
@@ -53889,8 +53665,7 @@ const stylePropObject = defineRule({
|
|
|
53889
53665
|
const allowSet = new Set(allow);
|
|
53890
53666
|
return {
|
|
53891
53667
|
JSXOpeningElement(node) {
|
|
53892
|
-
|
|
53893
|
-
const elementName = resolveJsxElementType(node);
|
|
53668
|
+
const elementName = getJsxOpeningElementName(node);
|
|
53894
53669
|
if (elementName && allowSet.has(elementName)) return;
|
|
53895
53670
|
if (elementName) {
|
|
53896
53671
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54595,7 +54370,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54595
54370
|
create: (context) => {
|
|
54596
54371
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54597
54372
|
return { JSXOpeningElement(node) {
|
|
54598
|
-
if (
|
|
54373
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
54599
54374
|
const attributes = node.attributes ?? [];
|
|
54600
54375
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54601
54376
|
if (!hrefAttribute?.value) return;
|
|
@@ -55216,7 +54991,8 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
55216
54991
|
create: (context) => ({
|
|
55217
54992
|
JSXElement(node) {
|
|
55218
54993
|
const openingElement = node.openingElement;
|
|
55219
|
-
|
|
54994
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
|
|
54995
|
+
const tagName = openingElement.name.name;
|
|
55220
54996
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
55221
54997
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
55222
54998
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|