oxlint-plugin-react-doctor 0.7.6-dev.f215ace → 0.7.6-dev.fbd85e3
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 +99 -190
- 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) {
|
|
@@ -7459,7 +7433,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7459
7433
|
const getOpeningElementTagName = (openingElement) => {
|
|
7460
7434
|
if (!openingElement) return null;
|
|
7461
7435
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7462
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return
|
|
7436
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement.name.name;
|
|
7463
7437
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7464
7438
|
let cursor = openingElement.name;
|
|
7465
7439
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7711,7 +7685,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7711
7685
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7712
7686
|
return { JSXOpeningElement(node) {
|
|
7713
7687
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7714
|
-
const tagName =
|
|
7688
|
+
const tagName = node.name.name;
|
|
7715
7689
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7716
7690
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7717
7691
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -11943,7 +11917,7 @@ const forbidDomProps = defineRule({
|
|
|
11943
11917
|
return { JSXOpeningElement(node) {
|
|
11944
11918
|
if (forbidMap.size === 0) return;
|
|
11945
11919
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
11946
|
-
const elementName =
|
|
11920
|
+
const elementName = node.name.name;
|
|
11947
11921
|
if (isReactComponentName(elementName)) return;
|
|
11948
11922
|
for (const attribute of node.attributes) {
|
|
11949
11923
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -12021,7 +11995,7 @@ const forbidElements = defineRule({
|
|
|
12021
11995
|
return {
|
|
12022
11996
|
JSXOpeningElement(node) {
|
|
12023
11997
|
if (forbidMap.size === 0) return;
|
|
12024
|
-
const fullName =
|
|
11998
|
+
const fullName = flattenJsxName$1(node.name);
|
|
12025
11999
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
12026
12000
|
context.report({
|
|
12027
12001
|
node: node.name,
|
|
@@ -12383,7 +12357,8 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
12383
12357
|
const isParagraphElement = (candidate) => {
|
|
12384
12358
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12385
12359
|
const opening = candidate.openingElement;
|
|
12386
|
-
|
|
12360
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12361
|
+
return opening.name.name === "p";
|
|
12387
12362
|
};
|
|
12388
12363
|
const findEnclosingParagraph = (openingElement) => {
|
|
12389
12364
|
const owningElement = openingElement.parent;
|
|
@@ -12404,7 +12379,8 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
12404
12379
|
severity: "warn",
|
|
12405
12380
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
12406
12381
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12407
|
-
|
|
12382
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12383
|
+
const childTagName = node.name.name;
|
|
12408
12384
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
12409
12385
|
if (!findEnclosingParagraph(node)) return;
|
|
12410
12386
|
context.report({
|
|
@@ -12435,7 +12411,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
12435
12411
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
12436
12412
|
const opening = jsxElement.openingElement;
|
|
12437
12413
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
12438
|
-
const tagName =
|
|
12414
|
+
const tagName = opening.name.name;
|
|
12439
12415
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
12440
12416
|
return tagName;
|
|
12441
12417
|
};
|
|
@@ -12465,7 +12441,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
12465
12441
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
12466
12442
|
const opening = ancestor.openingElement;
|
|
12467
12443
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
12468
|
-
const ancestorTag =
|
|
12444
|
+
const ancestorTag = opening.name.name;
|
|
12469
12445
|
if (ancestorTag.length === 0) {
|
|
12470
12446
|
previous = ancestor;
|
|
12471
12447
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12547,7 +12523,8 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12547
12523
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12548
12524
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12549
12525
|
const opening = candidate.openingElement;
|
|
12550
|
-
|
|
12526
|
+
if (!isNodeOfType(opening.name, "JSXIdentifier")) return false;
|
|
12527
|
+
return opening.name.name === tagName;
|
|
12551
12528
|
};
|
|
12552
12529
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12553
12530
|
const owningElement = openingElement.parent;
|
|
@@ -12568,7 +12545,8 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12568
12545
|
severity: "warn",
|
|
12569
12546
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12570
12547
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12571
|
-
|
|
12548
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
12549
|
+
const tagName = node.name.name;
|
|
12572
12550
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12573
12551
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12574
12552
|
context.report({
|
|
@@ -12683,7 +12661,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12683
12661
|
matchByOccurrence: true,
|
|
12684
12662
|
create: skipNonProductionFiles((context) => ({
|
|
12685
12663
|
JSXOpeningElement(node) {
|
|
12686
|
-
if (
|
|
12664
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "iframe") return;
|
|
12687
12665
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12688
12666
|
if (!sandboxAttr) {
|
|
12689
12667
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -14611,18 +14589,6 @@ const jsHoistRegexp = defineRule({
|
|
|
14611
14589
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14612
14590
|
});
|
|
14613
14591
|
//#endregion
|
|
14614
|
-
//#region src/plugin/utils/resolve-first-argument-binding.ts
|
|
14615
|
-
const resolveFirstArgumentBinding = (firstParameter) => {
|
|
14616
|
-
if (!firstParameter) return null;
|
|
14617
|
-
if (!isNodeOfType(firstParameter, "RestElement")) return firstParameter;
|
|
14618
|
-
if (!isNodeOfType(firstParameter.argument, "ArrayPattern")) return null;
|
|
14619
|
-
const elements = firstParameter.argument.elements ?? [];
|
|
14620
|
-
if (elements.length !== 1) return null;
|
|
14621
|
-
const firstBinding = elements[0];
|
|
14622
|
-
if (!firstBinding || isNodeOfType(firstBinding, "RestElement")) return null;
|
|
14623
|
-
return firstBinding;
|
|
14624
|
-
};
|
|
14625
|
-
//#endregion
|
|
14626
14592
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14627
14593
|
const referencesParameter = (expression, parameterName) => {
|
|
14628
14594
|
if (!expression) return false;
|
|
@@ -14633,7 +14599,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14633
14599
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14634
14600
|
const callback = node.arguments?.[0];
|
|
14635
14601
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14636
|
-
const firstParameter =
|
|
14602
|
+
const firstParameter = callback.params?.[0];
|
|
14637
14603
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14638
14604
|
let predicate = null;
|
|
14639
14605
|
const body = callback.body;
|
|
@@ -15325,21 +15291,9 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
15325
15291
|
};
|
|
15326
15292
|
const getResolvedInitializer = (receiver) => {
|
|
15327
15293
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
15328
|
-
const
|
|
15329
|
-
|
|
15330
|
-
|
|
15331
|
-
const isDefault = isNodeOfType(binding.bindingIdentifier.parent, "AssignmentPattern");
|
|
15332
|
-
if (isNodeOfType(initializer, "Identifier")) {
|
|
15333
|
-
const aliased = findVariableInitializer(initializer, initializer.name);
|
|
15334
|
-
if (aliased?.initializer) return {
|
|
15335
|
-
initializer: aliased.initializer,
|
|
15336
|
-
isDefault: isDefault || isNodeOfType(aliased.bindingIdentifier.parent, "AssignmentPattern")
|
|
15337
|
-
};
|
|
15338
|
-
}
|
|
15339
|
-
return {
|
|
15340
|
-
initializer,
|
|
15341
|
-
isDefault
|
|
15342
|
-
};
|
|
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;
|
|
15343
15297
|
};
|
|
15344
15298
|
const isSplitCall = (expression) => {
|
|
15345
15299
|
if (!expression) return false;
|
|
@@ -15505,8 +15459,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
15505
15459
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
15506
15460
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
15507
15461
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
15508
|
-
const
|
|
15509
|
-
if (
|
|
15462
|
+
const initializer = getResolvedInitializer(stripped);
|
|
15463
|
+
if (initializer && isSmallInlineLiteralArray(initializer)) return true;
|
|
15510
15464
|
}
|
|
15511
15465
|
return false;
|
|
15512
15466
|
};
|
|
@@ -15558,8 +15512,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15558
15512
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15559
15513
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15560
15514
|
if (resolvedInitializer) {
|
|
15561
|
-
if (isLikelyStringReceiver(resolvedInitializer
|
|
15562
|
-
if (
|
|
15515
|
+
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15516
|
+
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15563
15517
|
}
|
|
15564
15518
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15565
15519
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -18686,6 +18640,10 @@ const resolveSettings$29 = (settings) => {
|
|
|
18686
18640
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18687
18641
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18688
18642
|
};
|
|
18643
|
+
const getElementName = (node) => {
|
|
18644
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18645
|
+
return null;
|
|
18646
|
+
};
|
|
18689
18647
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18690
18648
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18691
18649
|
const explicit = options.components?.[elementName];
|
|
@@ -18705,8 +18663,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18705
18663
|
create: (context) => {
|
|
18706
18664
|
const options = resolveSettings$29(context.settings);
|
|
18707
18665
|
return { JSXOpeningElement(node) {
|
|
18708
|
-
|
|
18709
|
-
const elementName = resolveJsxElementType(node);
|
|
18666
|
+
const elementName = getElementName(node);
|
|
18710
18667
|
if (!elementName) return;
|
|
18711
18668
|
for (const attribute of node.attributes) {
|
|
18712
18669
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -18893,6 +18850,11 @@ const checkTarget = (attributeValue) => {
|
|
|
18893
18850
|
alternate: false
|
|
18894
18851
|
};
|
|
18895
18852
|
};
|
|
18853
|
+
const getOpeningElementName = (node) => {
|
|
18854
|
+
const name = node.name;
|
|
18855
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18856
|
+
return null;
|
|
18857
|
+
};
|
|
18896
18858
|
const jsxNoTargetBlank = defineRule({
|
|
18897
18859
|
id: "jsx-no-target-blank",
|
|
18898
18860
|
title: "Unsafe target=_blank link",
|
|
@@ -18912,8 +18874,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
18912
18874
|
return settings.formComponents.has(tagName);
|
|
18913
18875
|
};
|
|
18914
18876
|
return { JSXOpeningElement(node) {
|
|
18915
|
-
|
|
18916
|
-
const tagName = resolveJsxElementType(node);
|
|
18877
|
+
const tagName = getOpeningElementName(node);
|
|
18917
18878
|
if (!tagName) return;
|
|
18918
18879
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
18919
18880
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -20388,6 +20349,9 @@ const hasDirective = (programNode, directive) => {
|
|
|
20388
20349
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20389
20350
|
};
|
|
20390
20351
|
//#endregion
|
|
20352
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20353
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20354
|
+
//#endregion
|
|
20391
20355
|
//#region src/plugin/utils/is-component-assignment.ts
|
|
20392
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"));
|
|
20393
20357
|
//#endregion
|
|
@@ -20720,7 +20684,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20720
20684
|
severity: "warn",
|
|
20721
20685
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20722
20686
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20723
|
-
if (
|
|
20687
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
20724
20688
|
const attributes = node.attributes ?? [];
|
|
20725
20689
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20726
20690
|
if (downloadAttribute) {
|
|
@@ -20916,7 +20880,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
20916
20880
|
severity: "warn",
|
|
20917
20881
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
20918
20882
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20919
|
-
if (
|
|
20883
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
20920
20884
|
const attributes = node.attributes ?? [];
|
|
20921
20885
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
20922
20886
|
if (!relAttribute?.value) return;
|
|
@@ -21024,7 +20988,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
21024
20988
|
severity: "warn",
|
|
21025
20989
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
21026
20990
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21027
|
-
if (
|
|
20991
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21028
20992
|
const attributes = node.attributes ?? [];
|
|
21029
20993
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
21030
20994
|
if (!hrefAttribute?.value) return;
|
|
@@ -21199,7 +21163,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21199
21163
|
create: (context) => {
|
|
21200
21164
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21201
21165
|
return { JSXOpeningElement(node) {
|
|
21202
|
-
if (
|
|
21166
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
21203
21167
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21204
21168
|
const programRoot = findProgramRoot(node);
|
|
21205
21169
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21246,8 +21210,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21246
21210
|
severity: "warn",
|
|
21247
21211
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21248
21212
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21249
|
-
|
|
21250
|
-
if (
|
|
21213
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
21214
|
+
if (node.name.name !== "script" && node.name.name !== "Script") return;
|
|
21251
21215
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21252
21216
|
if (!srcAttribute?.value) return;
|
|
21253
21217
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -23055,15 +23019,10 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
23055
23019
|
if (!declaringNode) return false;
|
|
23056
23020
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
23057
23021
|
}));
|
|
23058
|
-
const isWholePropsParameterBinding = (bindingNode) => {
|
|
23059
|
-
if (isFunctionLike$1(bindingNode.parent)) return true;
|
|
23060
|
-
let declaringFunction = bindingNode.parent;
|
|
23061
|
-
while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
|
|
23062
|
-
return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
|
|
23063
|
-
};
|
|
23064
23022
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
23065
23023
|
if (def.type !== "Parameter") return false;
|
|
23066
|
-
|
|
23024
|
+
const bindingParent = def.name.parent;
|
|
23025
|
+
return isFunctionLike$1(bindingParent);
|
|
23067
23026
|
}));
|
|
23068
23027
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
23069
23028
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -26060,56 +26019,6 @@ const noBarrelImport = defineRule({
|
|
|
26060
26019
|
}
|
|
26061
26020
|
});
|
|
26062
26021
|
//#endregion
|
|
26063
|
-
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
26064
|
-
const collectReturnedExpressions = (functionNode) => {
|
|
26065
|
-
if (!isFunctionLike$1(functionNode)) return [];
|
|
26066
|
-
const body = functionNode.body;
|
|
26067
|
-
if (!body) return [];
|
|
26068
|
-
if (!isNodeOfType(body, "BlockStatement")) return [body];
|
|
26069
|
-
const returnedExpressions = [];
|
|
26070
|
-
walkAst(body, (node) => {
|
|
26071
|
-
if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
26072
|
-
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
|
|
26073
|
-
});
|
|
26074
|
-
return returnedExpressions;
|
|
26075
|
-
};
|
|
26076
|
-
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
|
|
26077
|
-
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
26078
|
-
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
26079
|
-
const functionMatches = (candidateFunction) => {
|
|
26080
|
-
if (visitedFunctions.has(candidateFunction)) return false;
|
|
26081
|
-
visitedFunctions.add(candidateFunction);
|
|
26082
|
-
return collectReturnedExpressions(candidateFunction).some(expressionMatches);
|
|
26083
|
-
};
|
|
26084
|
-
const expressionMatches = (expression) => {
|
|
26085
|
-
const unwrappedExpression = stripParenExpression(expression);
|
|
26086
|
-
if (visitedExpressions.has(unwrappedExpression)) return false;
|
|
26087
|
-
visitedExpressions.add(unwrappedExpression);
|
|
26088
|
-
if (matchesExpression(unwrappedExpression)) return true;
|
|
26089
|
-
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
26090
|
-
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
26091
|
-
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
|
|
26092
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
26093
|
-
if (isFunctionLike$1(initializer)) return false;
|
|
26094
|
-
return expressionMatches(initializer);
|
|
26095
|
-
}
|
|
26096
|
-
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
26097
|
-
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
26098
|
-
if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
26099
|
-
const symbol = scopes.symbolFor(unwrappedExpression.callee);
|
|
26100
|
-
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
|
|
26101
|
-
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
26102
|
-
const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
|
|
26103
|
-
if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
|
|
26104
|
-
return functionMatches(candidateFunction);
|
|
26105
|
-
}
|
|
26106
|
-
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
|
|
26107
|
-
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
|
|
26108
|
-
return false;
|
|
26109
|
-
};
|
|
26110
|
-
return functionMatches(functionNode);
|
|
26111
|
-
};
|
|
26112
|
-
//#endregion
|
|
26113
26022
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
26114
26023
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
26115
26024
|
"FunctionDeclaration",
|
|
@@ -26129,13 +26038,12 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
26129
26038
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
26130
26039
|
};
|
|
26131
26040
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
26132
|
-
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
26133
26041
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26134
26042
|
let hasRenderOutput = false;
|
|
26135
26043
|
walkAst(rootNode, (node) => {
|
|
26136
26044
|
if (hasRenderOutput) return false;
|
|
26137
26045
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26138
|
-
if (
|
|
26046
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
|
|
26139
26047
|
hasRenderOutput = true;
|
|
26140
26048
|
return false;
|
|
26141
26049
|
}
|
|
@@ -26146,7 +26054,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26146
26054
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26147
26055
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26148
26056
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26149
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes)
|
|
26057
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26150
26058
|
renderOutputCache.set(functionNode, {
|
|
26151
26059
|
scopes,
|
|
26152
26060
|
hasRenderOutput
|
|
@@ -28611,7 +28519,7 @@ const noDisabledZoom = defineRule({
|
|
|
28611
28519
|
category: "Accessibility",
|
|
28612
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.",
|
|
28613
28521
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28614
|
-
if (
|
|
28522
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
|
|
28615
28523
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28616
28524
|
if (!nameAttr?.value) return;
|
|
28617
28525
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -31783,7 +31691,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31783
31691
|
severity: "warn",
|
|
31784
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.",
|
|
31785
31693
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31786
|
-
if (
|
|
31694
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
31787
31695
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31788
31696
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31789
31697
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -32024,7 +31932,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
32024
31932
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
32025
31933
|
return {
|
|
32026
31934
|
JSXOpeningElement(node) {
|
|
32027
|
-
if (
|
|
31935
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
32028
31936
|
let typeAttribute = null;
|
|
32029
31937
|
let typeAttributeIndex = null;
|
|
32030
31938
|
let indeterminateAttribute = null;
|
|
@@ -33096,13 +33004,11 @@ const noManyBooleanProps = defineRule({
|
|
|
33096
33004
|
};
|
|
33097
33005
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
33098
33006
|
if (!param) return;
|
|
33099
|
-
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33100
|
-
if (!propsBinding) return;
|
|
33101
33007
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
33102
|
-
if (isNodeOfType(
|
|
33008
|
+
if (isNodeOfType(param, "ObjectPattern")) {
|
|
33103
33009
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
33104
33010
|
const booleanLikePropNames = [];
|
|
33105
|
-
for (const property of
|
|
33011
|
+
for (const property of param.properties ?? []) {
|
|
33106
33012
|
if (!isNodeOfType(property, "Property")) continue;
|
|
33107
33013
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
33108
33014
|
if (!keyName) continue;
|
|
@@ -33113,7 +33019,7 @@ const noManyBooleanProps = defineRule({
|
|
|
33113
33019
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
33114
33020
|
return;
|
|
33115
33021
|
}
|
|
33116
|
-
if (isNodeOfType(
|
|
33022
|
+
if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
|
|
33117
33023
|
};
|
|
33118
33024
|
return {
|
|
33119
33025
|
FunctionDeclaration(node) {
|
|
@@ -35865,7 +35771,7 @@ const noPreventDefault = defineRule({
|
|
|
35865
35771
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
35866
35772
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
35867
35773
|
return { JSXOpeningElement(node) {
|
|
35868
|
-
const elementName =
|
|
35774
|
+
const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
|
|
35869
35775
|
if (!elementName) return;
|
|
35870
35776
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
35871
35777
|
if (!targetEventProps) return;
|
|
@@ -38329,10 +38235,9 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38329
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.",
|
|
38330
38236
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38331
38237
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38332
|
-
const
|
|
38333
|
-
const firstCharacter = elementName.charCodeAt(0);
|
|
38238
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
38334
38239
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38335
|
-
if (
|
|
38240
|
+
if (node.name.name.includes("-")) return;
|
|
38336
38241
|
for (const attribute of node.attributes) {
|
|
38337
38242
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38338
38243
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38720,7 +38625,8 @@ const noUncontrolledInput = defineRule({
|
|
|
38720
38625
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38721
38626
|
walkAst(componentBody, (child) => {
|
|
38722
38627
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38723
|
-
|
|
38628
|
+
if (!isNodeOfType(child.name, "JSXIdentifier")) return;
|
|
38629
|
+
const tagName = child.name.name;
|
|
38724
38630
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38725
38631
|
const attributes = child.attributes ?? [];
|
|
38726
38632
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38781,7 +38687,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38781
38687
|
severity: "warn",
|
|
38782
38688
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38783
38689
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38784
|
-
if (
|
|
38690
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
|
|
38785
38691
|
const attributes = node.attributes ?? [];
|
|
38786
38692
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38787
38693
|
if (!srcAttribute) return;
|
|
@@ -39996,7 +39902,7 @@ const noUnknownProperty = defineRule({
|
|
|
39996
39902
|
}
|
|
39997
39903
|
if (fileIsNonReactJsx) return;
|
|
39998
39904
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
39999
|
-
const elementType =
|
|
39905
|
+
const elementType = node.name.name;
|
|
40000
39906
|
const firstCharacter = elementType.charCodeAt(0);
|
|
40001
39907
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
40002
39908
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40174,21 +40080,20 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40174
40080
|
});
|
|
40175
40081
|
return found;
|
|
40176
40082
|
};
|
|
40177
|
-
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40178
40083
|
const isReactClassComponent = (classNode) => {
|
|
40179
40084
|
if (isEs6Component(classNode)) return true;
|
|
40180
40085
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40181
40086
|
};
|
|
40182
|
-
const findEnclosingComponent = (node
|
|
40087
|
+
const findEnclosingComponent = (node) => {
|
|
40183
40088
|
let walker = node.parent;
|
|
40184
40089
|
while (walker) {
|
|
40185
40090
|
if (isFunctionLike$1(walker)) {
|
|
40186
40091
|
const componentName = inferFunctionLikeName(walker);
|
|
40187
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40092
|
+
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
40188
40093
|
component: walker,
|
|
40189
40094
|
name: componentName
|
|
40190
40095
|
};
|
|
40191
|
-
if (!componentName &&
|
|
40096
|
+
if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40192
40097
|
component: walker,
|
|
40193
40098
|
name: null
|
|
40194
40099
|
};
|
|
@@ -40427,7 +40332,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40427
40332
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40428
40333
|
if (settings.allowAsProps) return;
|
|
40429
40334
|
}
|
|
40430
|
-
const enclosing = findEnclosingComponent(candidateNode
|
|
40335
|
+
const enclosing = findEnclosingComponent(candidateNode);
|
|
40431
40336
|
if (!enclosing) return;
|
|
40432
40337
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40433
40338
|
queuedReports.push({
|
|
@@ -40438,7 +40343,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40438
40343
|
});
|
|
40439
40344
|
};
|
|
40440
40345
|
const checkFunctionLike = (node) => {
|
|
40441
|
-
if (!
|
|
40346
|
+
if (!expressionContainsJsxOrCreateElement(node)) return;
|
|
40442
40347
|
const inferredName = inferFunctionLikeName(node);
|
|
40443
40348
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40444
40349
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41698,7 +41603,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41698
41603
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41699
41604
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41700
41605
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41701
|
-
const tagName =
|
|
41606
|
+
const tagName = node.name.name;
|
|
41702
41607
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41703
41608
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41704
41609
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41718,7 +41623,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41718
41623
|
]);
|
|
41719
41624
|
const isTextLikeInput = (openingElement) => {
|
|
41720
41625
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41721
|
-
const tagName =
|
|
41626
|
+
const tagName = openingElement.name.name;
|
|
41722
41627
|
if (tagName === "textarea") return true;
|
|
41723
41628
|
if (tagName !== "input") return false;
|
|
41724
41629
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -41875,9 +41780,8 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
41875
41780
|
]);
|
|
41876
41781
|
const collectBooleanPropBindings = (param) => {
|
|
41877
41782
|
const bindings = /* @__PURE__ */ new Set();
|
|
41878
|
-
|
|
41879
|
-
|
|
41880
|
-
for (const property of propsBinding.properties ?? []) {
|
|
41783
|
+
if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
|
|
41784
|
+
for (const property of param.properties ?? []) {
|
|
41881
41785
|
if (!isNodeOfType(property, "Property")) continue;
|
|
41882
41786
|
if (property.computed) continue;
|
|
41883
41787
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -42138,7 +42042,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42138
42042
|
},
|
|
42139
42043
|
JSXOpeningElement(node) {
|
|
42140
42044
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42141
|
-
const tagName =
|
|
42045
|
+
const tagName = node.name.name;
|
|
42142
42046
|
if (tagName === "dialog") return;
|
|
42143
42047
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42144
42048
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -44175,7 +44079,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44175
44079
|
severity: "warn",
|
|
44176
44080
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44177
44081
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44178
|
-
if (
|
|
44082
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
|
|
44179
44083
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44180
44084
|
node,
|
|
44181
44085
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -45468,10 +45372,14 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45468
45372
|
//#endregion
|
|
45469
45373
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45470
45374
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45471
|
-
const callbackReturnsJsx = (callback
|
|
45375
|
+
const callbackReturnsJsx = (callback) => {
|
|
45472
45376
|
if (!callback) return false;
|
|
45473
45377
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45474
|
-
|
|
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;
|
|
45475
45383
|
};
|
|
45476
45384
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45477
45385
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45549,7 +45457,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45549
45457
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45550
45458
|
for (const declarator of stmt.declarations ?? []) {
|
|
45551
45459
|
const init = declarator.init;
|
|
45552
|
-
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])) {
|
|
45553
45461
|
memoNode = declarator;
|
|
45554
45462
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45555
45463
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45615,11 +45523,8 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45615
45523
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45616
45524
|
const bindings = /* @__PURE__ */ new Map();
|
|
45617
45525
|
const params = functionNode.params ?? [];
|
|
45618
|
-
for (const
|
|
45619
|
-
|
|
45620
|
-
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45621
|
-
}
|
|
45622
|
-
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45526
|
+
for (const param of params) collectFromObjectPattern(param, bindings);
|
|
45527
|
+
const propsParam = params[0];
|
|
45623
45528
|
const body = functionNode.body;
|
|
45624
45529
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45625
45530
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -53733,6 +53638,10 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53733
53638
|
}
|
|
53734
53639
|
return false;
|
|
53735
53640
|
};
|
|
53641
|
+
const getJsxOpeningElementName = (node) => {
|
|
53642
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53643
|
+
return null;
|
|
53644
|
+
};
|
|
53736
53645
|
const stylePropObject = defineRule({
|
|
53737
53646
|
id: "style-prop-object",
|
|
53738
53647
|
title: "Style prop is not an object",
|
|
@@ -53744,8 +53653,7 @@ const stylePropObject = defineRule({
|
|
|
53744
53653
|
const allowSet = new Set(allow);
|
|
53745
53654
|
return {
|
|
53746
53655
|
JSXOpeningElement(node) {
|
|
53747
|
-
|
|
53748
|
-
const elementName = resolveJsxElementType(node);
|
|
53656
|
+
const elementName = getJsxOpeningElementName(node);
|
|
53749
53657
|
if (elementName && allowSet.has(elementName)) return;
|
|
53750
53658
|
if (elementName) {
|
|
53751
53659
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54450,7 +54358,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54450
54358
|
create: (context) => {
|
|
54451
54359
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54452
54360
|
return { JSXOpeningElement(node) {
|
|
54453
|
-
if (
|
|
54361
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
54454
54362
|
const attributes = node.attributes ?? [];
|
|
54455
54363
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54456
54364
|
if (!hrefAttribute?.value) return;
|
|
@@ -55071,7 +54979,8 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
55071
54979
|
create: (context) => ({
|
|
55072
54980
|
JSXElement(node) {
|
|
55073
54981
|
const openingElement = node.openingElement;
|
|
55074
|
-
|
|
54982
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
|
|
54983
|
+
const tagName = openingElement.name.name;
|
|
55075
54984
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
55076
54985
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
55077
54986
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|