oxlint-plugin-react-doctor 0.7.6-dev.82e10cd → 0.7.6-dev.8528def
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 +95 -75
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1518,6 +1518,37 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1518
1518
|
}
|
|
1519
1519
|
};
|
|
1520
1520
|
//#endregion
|
|
1521
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
1522
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
1523
|
+
//#endregion
|
|
1524
|
+
//#region src/plugin/utils/resolve-jsx-element-type.ts
|
|
1525
|
+
const resolveConstantStringBinding = (name) => {
|
|
1526
|
+
const binding = findVariableInitializer(name, name.name);
|
|
1527
|
+
if (!binding?.initializer) return null;
|
|
1528
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
1529
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
1530
|
+
if (declarator.id !== binding.bindingIdentifier) return null;
|
|
1531
|
+
const declaration = declarator.parent;
|
|
1532
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
1533
|
+
if (declaration.kind !== "const") return null;
|
|
1534
|
+
const initializer = stripParenExpression(binding.initializer);
|
|
1535
|
+
return isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
|
|
1536
|
+
};
|
|
1537
|
+
const flattenJsxName$2 = (name) => {
|
|
1538
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1539
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1540
|
+
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1541
|
+
return "";
|
|
1542
|
+
};
|
|
1543
|
+
const resolveJsxElementType = (openingElement) => {
|
|
1544
|
+
const name = openingElement.name;
|
|
1545
|
+
if (isNodeOfType(name, "JSXIdentifier")) {
|
|
1546
|
+
if (!isUppercaseName(name.name)) return name.name;
|
|
1547
|
+
return resolveConstantStringBinding(name) ?? name.name;
|
|
1548
|
+
}
|
|
1549
|
+
return flattenJsxName$2(name);
|
|
1550
|
+
};
|
|
1551
|
+
//#endregion
|
|
1521
1552
|
//#region src/plugin/utils/get-element-type.ts
|
|
1522
1553
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1523
1554
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1530,14 +1561,8 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1530
1561
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1531
1562
|
return a11ySettings;
|
|
1532
1563
|
};
|
|
1533
|
-
const flattenJsxName$2 = (name) => {
|
|
1534
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1535
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1536
|
-
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1537
|
-
return "";
|
|
1538
|
-
};
|
|
1539
1564
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1540
|
-
const baseName =
|
|
1565
|
+
const baseName = resolveJsxElementType(openingElement);
|
|
1541
1566
|
if (a11ySettings.polymorphicPropName) {
|
|
1542
1567
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1543
1568
|
if (polymorphicAttribute) {
|
|
@@ -2372,8 +2397,9 @@ const anchorIsValid = defineRule({
|
|
|
2372
2397
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2373
2398
|
return { JSXOpeningElement(node) {
|
|
2374
2399
|
if (isTestlikeFile) return;
|
|
2375
|
-
|
|
2376
|
-
if (
|
|
2400
|
+
const tag = getElementType(node, context.settings);
|
|
2401
|
+
if (!fileHasJsxA11ySettings && tag !== "a") return;
|
|
2402
|
+
if (tag !== "a") return;
|
|
2377
2403
|
let hrefAttribute;
|
|
2378
2404
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2379
2405
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5728,7 +5754,7 @@ const buttonHasType = defineRule({
|
|
|
5728
5754
|
return {
|
|
5729
5755
|
JSXOpeningElement(node) {
|
|
5730
5756
|
if (isTestlikeFile) return;
|
|
5731
|
-
if (
|
|
5757
|
+
if (resolveJsxElementType(node) !== "button") return;
|
|
5732
5758
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5733
5759
|
if (!typeAttr) {
|
|
5734
5760
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5918,7 +5944,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5918
5944
|
};
|
|
5919
5945
|
return {
|
|
5920
5946
|
JSXOpeningElement(node) {
|
|
5921
|
-
if (
|
|
5947
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
5922
5948
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5923
5949
|
},
|
|
5924
5950
|
CallExpression(node) {
|
|
@@ -7433,7 +7459,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7433
7459
|
const getOpeningElementTagName = (openingElement) => {
|
|
7434
7460
|
if (!openingElement) return null;
|
|
7435
7461
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7436
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement
|
|
7462
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return resolveJsxElementType(openingElement);
|
|
7437
7463
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7438
7464
|
let cursor = openingElement.name;
|
|
7439
7465
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7685,7 +7711,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7685
7711
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7686
7712
|
return { JSXOpeningElement(node) {
|
|
7687
7713
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7688
|
-
const tagName = node
|
|
7714
|
+
const tagName = resolveJsxElementType(node);
|
|
7689
7715
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7690
7716
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7691
7717
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -11917,7 +11943,7 @@ const forbidDomProps = defineRule({
|
|
|
11917
11943
|
return { JSXOpeningElement(node) {
|
|
11918
11944
|
if (forbidMap.size === 0) return;
|
|
11919
11945
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
11920
|
-
const elementName = node
|
|
11946
|
+
const elementName = resolveJsxElementType(node);
|
|
11921
11947
|
if (isReactComponentName(elementName)) return;
|
|
11922
11948
|
for (const attribute of node.attributes) {
|
|
11923
11949
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11995,7 +12021,7 @@ const forbidElements = defineRule({
|
|
|
11995
12021
|
return {
|
|
11996
12022
|
JSXOpeningElement(node) {
|
|
11997
12023
|
if (forbidMap.size === 0) return;
|
|
11998
|
-
const fullName =
|
|
12024
|
+
const fullName = resolveJsxElementType(node);
|
|
11999
12025
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
12000
12026
|
context.report({
|
|
12001
12027
|
node: node.name,
|
|
@@ -12357,8 +12383,7 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
12357
12383
|
const isParagraphElement = (candidate) => {
|
|
12358
12384
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12359
12385
|
const opening = candidate.openingElement;
|
|
12360
|
-
|
|
12361
|
-
return opening.name.name === "p";
|
|
12386
|
+
return resolveJsxElementType(opening) === "p";
|
|
12362
12387
|
};
|
|
12363
12388
|
const findEnclosingParagraph = (openingElement) => {
|
|
12364
12389
|
const owningElement = openingElement.parent;
|
|
@@ -12379,8 +12404,7 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
12379
12404
|
severity: "warn",
|
|
12380
12405
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
12381
12406
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12382
|
-
|
|
12383
|
-
const childTagName = node.name.name;
|
|
12407
|
+
const childTagName = resolveJsxElementType(node);
|
|
12384
12408
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
12385
12409
|
if (!findEnclosingParagraph(node)) return;
|
|
12386
12410
|
context.report({
|
|
@@ -12411,7 +12435,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
12411
12435
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
12412
12436
|
const opening = jsxElement.openingElement;
|
|
12413
12437
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
12414
|
-
const tagName = opening
|
|
12438
|
+
const tagName = resolveJsxElementType(opening);
|
|
12415
12439
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
12416
12440
|
return tagName;
|
|
12417
12441
|
};
|
|
@@ -12441,7 +12465,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
12441
12465
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
12442
12466
|
const opening = ancestor.openingElement;
|
|
12443
12467
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
12444
|
-
const ancestorTag = opening
|
|
12468
|
+
const ancestorTag = resolveJsxElementType(opening);
|
|
12445
12469
|
if (ancestorTag.length === 0) {
|
|
12446
12470
|
previous = ancestor;
|
|
12447
12471
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12523,8 +12547,7 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12523
12547
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12524
12548
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12525
12549
|
const opening = candidate.openingElement;
|
|
12526
|
-
|
|
12527
|
-
return opening.name.name === tagName;
|
|
12550
|
+
return resolveJsxElementType(opening) === tagName;
|
|
12528
12551
|
};
|
|
12529
12552
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12530
12553
|
const owningElement = openingElement.parent;
|
|
@@ -12545,8 +12568,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12545
12568
|
severity: "warn",
|
|
12546
12569
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12547
12570
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12548
|
-
|
|
12549
|
-
const tagName = node.name.name;
|
|
12571
|
+
const tagName = resolveJsxElementType(node);
|
|
12550
12572
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12551
12573
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12552
12574
|
context.report({
|
|
@@ -12661,7 +12683,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12661
12683
|
matchByOccurrence: true,
|
|
12662
12684
|
create: skipNonProductionFiles((context) => ({
|
|
12663
12685
|
JSXOpeningElement(node) {
|
|
12664
|
-
if (
|
|
12686
|
+
if (resolveJsxElementType(node) !== "iframe") return;
|
|
12665
12687
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12666
12688
|
if (!sandboxAttr) {
|
|
12667
12689
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -15291,9 +15313,21 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
15291
15313
|
};
|
|
15292
15314
|
const getResolvedInitializer = (receiver) => {
|
|
15293
15315
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
15294
|
-
const
|
|
15295
|
-
|
|
15296
|
-
return
|
|
15316
|
+
const binding = findVariableInitializer(receiver, receiver.name);
|
|
15317
|
+
const initializer = binding?.initializer ?? null;
|
|
15318
|
+
if (!binding || !initializer) return null;
|
|
15319
|
+
const isDefault = isNodeOfType(binding.bindingIdentifier.parent, "AssignmentPattern");
|
|
15320
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
15321
|
+
const aliased = findVariableInitializer(initializer, initializer.name);
|
|
15322
|
+
if (aliased?.initializer) return {
|
|
15323
|
+
initializer: aliased.initializer,
|
|
15324
|
+
isDefault: isDefault || isNodeOfType(aliased.bindingIdentifier.parent, "AssignmentPattern")
|
|
15325
|
+
};
|
|
15326
|
+
}
|
|
15327
|
+
return {
|
|
15328
|
+
initializer,
|
|
15329
|
+
isDefault
|
|
15330
|
+
};
|
|
15297
15331
|
};
|
|
15298
15332
|
const isSplitCall = (expression) => {
|
|
15299
15333
|
if (!expression) return false;
|
|
@@ -15459,8 +15493,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
15459
15493
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
15460
15494
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
15461
15495
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
15462
|
-
const
|
|
15463
|
-
if (
|
|
15496
|
+
const resolved = getResolvedInitializer(stripped);
|
|
15497
|
+
if (resolved && !resolved.isDefault && isSmallInlineLiteralArray(resolved.initializer)) return true;
|
|
15464
15498
|
}
|
|
15465
15499
|
return false;
|
|
15466
15500
|
};
|
|
@@ -15512,8 +15546,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15512
15546
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15513
15547
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15514
15548
|
if (resolvedInitializer) {
|
|
15515
|
-
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15516
|
-
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15549
|
+
if (isLikelyStringReceiver(resolvedInitializer.initializer)) return;
|
|
15550
|
+
if (!resolvedInitializer.isDefault && isSmallInlineLiteralArray(resolvedInitializer.initializer)) return;
|
|
15517
15551
|
}
|
|
15518
15552
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15519
15553
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -18640,10 +18674,6 @@ const resolveSettings$29 = (settings) => {
|
|
|
18640
18674
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18641
18675
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18642
18676
|
};
|
|
18643
|
-
const getElementName = (node) => {
|
|
18644
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18645
|
-
return null;
|
|
18646
|
-
};
|
|
18647
18677
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18648
18678
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18649
18679
|
const explicit = options.components?.[elementName];
|
|
@@ -18663,7 +18693,8 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18663
18693
|
create: (context) => {
|
|
18664
18694
|
const options = resolveSettings$29(context.settings);
|
|
18665
18695
|
return { JSXOpeningElement(node) {
|
|
18666
|
-
|
|
18696
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18697
|
+
const elementName = resolveJsxElementType(node);
|
|
18667
18698
|
if (!elementName) return;
|
|
18668
18699
|
for (const attribute of node.attributes) {
|
|
18669
18700
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -18850,11 +18881,6 @@ const checkTarget = (attributeValue) => {
|
|
|
18850
18881
|
alternate: false
|
|
18851
18882
|
};
|
|
18852
18883
|
};
|
|
18853
|
-
const getOpeningElementName = (node) => {
|
|
18854
|
-
const name = node.name;
|
|
18855
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18856
|
-
return null;
|
|
18857
|
-
};
|
|
18858
18884
|
const jsxNoTargetBlank = defineRule({
|
|
18859
18885
|
id: "jsx-no-target-blank",
|
|
18860
18886
|
title: "Unsafe target=_blank link",
|
|
@@ -18874,7 +18900,8 @@ const jsxNoTargetBlank = defineRule({
|
|
|
18874
18900
|
return settings.formComponents.has(tagName);
|
|
18875
18901
|
};
|
|
18876
18902
|
return { JSXOpeningElement(node) {
|
|
18877
|
-
|
|
18903
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18904
|
+
const tagName = resolveJsxElementType(node);
|
|
18878
18905
|
if (!tagName) return;
|
|
18879
18906
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
18880
18907
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -20349,9 +20376,6 @@ const hasDirective = (programNode, directive) => {
|
|
|
20349
20376
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20350
20377
|
};
|
|
20351
20378
|
//#endregion
|
|
20352
|
-
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20353
|
-
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20354
|
-
//#endregion
|
|
20355
20379
|
//#region src/plugin/utils/is-component-assignment.ts
|
|
20356
20380
|
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"));
|
|
20357
20381
|
//#endregion
|
|
@@ -20684,7 +20708,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20684
20708
|
severity: "warn",
|
|
20685
20709
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20686
20710
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20687
|
-
if (
|
|
20711
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
20688
20712
|
const attributes = node.attributes ?? [];
|
|
20689
20713
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20690
20714
|
if (downloadAttribute) {
|
|
@@ -20880,7 +20904,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
20880
20904
|
severity: "warn",
|
|
20881
20905
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
20882
20906
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20883
|
-
if (
|
|
20907
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20884
20908
|
const attributes = node.attributes ?? [];
|
|
20885
20909
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
20886
20910
|
if (!relAttribute?.value) return;
|
|
@@ -20988,7 +21012,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
20988
21012
|
severity: "warn",
|
|
20989
21013
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
20990
21014
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20991
|
-
if (
|
|
21015
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20992
21016
|
const attributes = node.attributes ?? [];
|
|
20993
21017
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
20994
21018
|
if (!hrefAttribute?.value) return;
|
|
@@ -21163,7 +21187,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21163
21187
|
create: (context) => {
|
|
21164
21188
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21165
21189
|
return { JSXOpeningElement(node) {
|
|
21166
|
-
if (
|
|
21190
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
21167
21191
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21168
21192
|
const programRoot = findProgramRoot(node);
|
|
21169
21193
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21210,8 +21234,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21210
21234
|
severity: "warn",
|
|
21211
21235
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21212
21236
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21213
|
-
|
|
21214
|
-
if (
|
|
21237
|
+
const elementName = resolveJsxElementType(node);
|
|
21238
|
+
if (elementName !== "script" && elementName !== "Script") return;
|
|
21215
21239
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21216
21240
|
if (!srcAttribute?.value) return;
|
|
21217
21241
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -28519,7 +28543,7 @@ const noDisabledZoom = defineRule({
|
|
|
28519
28543
|
category: "Accessibility",
|
|
28520
28544
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28521
28545
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28522
|
-
if (
|
|
28546
|
+
if (resolveJsxElementType(node) !== "meta") return;
|
|
28523
28547
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28524
28548
|
if (!nameAttr?.value) return;
|
|
28525
28549
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -31691,7 +31715,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31691
31715
|
severity: "warn",
|
|
31692
31716
|
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.",
|
|
31693
31717
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31694
|
-
if (
|
|
31718
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
31695
31719
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31696
31720
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31697
31721
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -31932,7 +31956,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
31932
31956
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
31933
31957
|
return {
|
|
31934
31958
|
JSXOpeningElement(node) {
|
|
31935
|
-
if (
|
|
31959
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
31936
31960
|
let typeAttribute = null;
|
|
31937
31961
|
let typeAttributeIndex = null;
|
|
31938
31962
|
let indeterminateAttribute = null;
|
|
@@ -35771,7 +35795,7 @@ const noPreventDefault = defineRule({
|
|
|
35771
35795
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
35772
35796
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
35773
35797
|
return { JSXOpeningElement(node) {
|
|
35774
|
-
const elementName =
|
|
35798
|
+
const elementName = resolveJsxElementType(node);
|
|
35775
35799
|
if (!elementName) return;
|
|
35776
35800
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
35777
35801
|
if (!targetEventProps) return;
|
|
@@ -38235,9 +38259,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38235
38259
|
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.",
|
|
38236
38260
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38237
38261
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38238
|
-
const
|
|
38262
|
+
const elementName = resolveJsxElementType(node);
|
|
38263
|
+
const firstCharacter = elementName.charCodeAt(0);
|
|
38239
38264
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38240
|
-
if (
|
|
38265
|
+
if (elementName.includes("-")) return;
|
|
38241
38266
|
for (const attribute of node.attributes) {
|
|
38242
38267
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38243
38268
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38625,8 +38650,7 @@ const noUncontrolledInput = defineRule({
|
|
|
38625
38650
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38626
38651
|
walkAst(componentBody, (child) => {
|
|
38627
38652
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38628
|
-
|
|
38629
|
-
const tagName = child.name.name;
|
|
38653
|
+
const tagName = resolveJsxElementType(child);
|
|
38630
38654
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38631
38655
|
const attributes = child.attributes ?? [];
|
|
38632
38656
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38687,7 +38711,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38687
38711
|
severity: "warn",
|
|
38688
38712
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38689
38713
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38690
|
-
if (
|
|
38714
|
+
if (resolveJsxElementType(node) !== "script") return;
|
|
38691
38715
|
const attributes = node.attributes ?? [];
|
|
38692
38716
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38693
38717
|
if (!srcAttribute) return;
|
|
@@ -39902,7 +39926,7 @@ const noUnknownProperty = defineRule({
|
|
|
39902
39926
|
}
|
|
39903
39927
|
if (fileIsNonReactJsx) return;
|
|
39904
39928
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
39905
|
-
const elementType = node
|
|
39929
|
+
const elementType = resolveJsxElementType(node);
|
|
39906
39930
|
const firstCharacter = elementType.charCodeAt(0);
|
|
39907
39931
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
39908
39932
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -41603,7 +41627,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41603
41627
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41604
41628
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41605
41629
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41606
|
-
const tagName = node
|
|
41630
|
+
const tagName = resolveJsxElementType(node);
|
|
41607
41631
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41608
41632
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41609
41633
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41623,7 +41647,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41623
41647
|
]);
|
|
41624
41648
|
const isTextLikeInput = (openingElement) => {
|
|
41625
41649
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41626
|
-
const tagName = openingElement
|
|
41650
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
41627
41651
|
if (tagName === "textarea") return true;
|
|
41628
41652
|
if (tagName !== "input") return false;
|
|
41629
41653
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -42042,7 +42066,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42042
42066
|
},
|
|
42043
42067
|
JSXOpeningElement(node) {
|
|
42044
42068
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42045
|
-
const tagName = node
|
|
42069
|
+
const tagName = resolveJsxElementType(node);
|
|
42046
42070
|
if (tagName === "dialog") return;
|
|
42047
42071
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42048
42072
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -44079,7 +44103,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44079
44103
|
severity: "warn",
|
|
44080
44104
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44081
44105
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44082
|
-
if (
|
|
44106
|
+
if (resolveJsxElementType(node) !== "svg") return;
|
|
44083
44107
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44084
44108
|
node,
|
|
44085
44109
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -53638,10 +53662,6 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53638
53662
|
}
|
|
53639
53663
|
return false;
|
|
53640
53664
|
};
|
|
53641
|
-
const getJsxOpeningElementName = (node) => {
|
|
53642
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53643
|
-
return null;
|
|
53644
|
-
};
|
|
53645
53665
|
const stylePropObject = defineRule({
|
|
53646
53666
|
id: "style-prop-object",
|
|
53647
53667
|
title: "Style prop is not an object",
|
|
@@ -53653,7 +53673,8 @@ const stylePropObject = defineRule({
|
|
|
53653
53673
|
const allowSet = new Set(allow);
|
|
53654
53674
|
return {
|
|
53655
53675
|
JSXOpeningElement(node) {
|
|
53656
|
-
|
|
53676
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
53677
|
+
const elementName = resolveJsxElementType(node);
|
|
53657
53678
|
if (elementName && allowSet.has(elementName)) return;
|
|
53658
53679
|
if (elementName) {
|
|
53659
53680
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54358,7 +54379,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54358
54379
|
create: (context) => {
|
|
54359
54380
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54360
54381
|
return { JSXOpeningElement(node) {
|
|
54361
|
-
if (
|
|
54382
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
54362
54383
|
const attributes = node.attributes ?? [];
|
|
54363
54384
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54364
54385
|
if (!hrefAttribute?.value) return;
|
|
@@ -54979,8 +55000,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
54979
55000
|
create: (context) => ({
|
|
54980
55001
|
JSXElement(node) {
|
|
54981
55002
|
const openingElement = node.openingElement;
|
|
54982
|
-
|
|
54983
|
-
const tagName = openingElement.name.name;
|
|
55003
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
54984
55004
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
54985
55005
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
54986
55006
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|