oxlint-plugin-react-doctor 0.7.6-dev.64950ba → 0.7.6-dev.778f1a2
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 +185 -99
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1518,6 +1518,37 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1518
1518
|
}
|
|
1519
1519
|
};
|
|
1520
1520
|
//#endregion
|
|
1521
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
1522
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
1523
|
+
//#endregion
|
|
1524
|
+
//#region src/plugin/utils/resolve-jsx-element-type.ts
|
|
1525
|
+
const resolveConstantStringBinding = (name) => {
|
|
1526
|
+
const binding = findVariableInitializer(name, name.name);
|
|
1527
|
+
if (!binding?.initializer) return null;
|
|
1528
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
1529
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
1530
|
+
if (declarator.id !== binding.bindingIdentifier) return null;
|
|
1531
|
+
const declaration = declarator.parent;
|
|
1532
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
1533
|
+
if (declaration.kind !== "const") return null;
|
|
1534
|
+
const initializer = stripParenExpression(binding.initializer);
|
|
1535
|
+
return isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
|
|
1536
|
+
};
|
|
1537
|
+
const flattenJsxName$2 = (name) => {
|
|
1538
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1539
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1540
|
+
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1541
|
+
return "";
|
|
1542
|
+
};
|
|
1543
|
+
const resolveJsxElementType = (openingElement) => {
|
|
1544
|
+
const name = openingElement.name;
|
|
1545
|
+
if (isNodeOfType(name, "JSXIdentifier")) {
|
|
1546
|
+
if (!isUppercaseName(name.name)) return name.name;
|
|
1547
|
+
return resolveConstantStringBinding(name) ?? name.name;
|
|
1548
|
+
}
|
|
1549
|
+
return flattenJsxName$2(name);
|
|
1550
|
+
};
|
|
1551
|
+
//#endregion
|
|
1521
1552
|
//#region src/plugin/utils/get-element-type.ts
|
|
1522
1553
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1523
1554
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1530,14 +1561,8 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1530
1561
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1531
1562
|
return a11ySettings;
|
|
1532
1563
|
};
|
|
1533
|
-
const flattenJsxName$2 = (name) => {
|
|
1534
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1535
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1536
|
-
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1537
|
-
return "";
|
|
1538
|
-
};
|
|
1539
1564
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1540
|
-
const baseName =
|
|
1565
|
+
const baseName = resolveJsxElementType(openingElement);
|
|
1541
1566
|
if (a11ySettings.polymorphicPropName) {
|
|
1542
1567
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1543
1568
|
if (polymorphicAttribute) {
|
|
@@ -2372,8 +2397,9 @@ const anchorIsValid = defineRule({
|
|
|
2372
2397
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2373
2398
|
return { JSXOpeningElement(node) {
|
|
2374
2399
|
if (isTestlikeFile) return;
|
|
2375
|
-
|
|
2376
|
-
if (
|
|
2400
|
+
const tag = getElementType(node, context.settings);
|
|
2401
|
+
if (!fileHasJsxA11ySettings && tag !== "a") return;
|
|
2402
|
+
if (tag !== "a") return;
|
|
2377
2403
|
let hrefAttribute;
|
|
2378
2404
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2379
2405
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5728,7 +5754,7 @@ const buttonHasType = defineRule({
|
|
|
5728
5754
|
return {
|
|
5729
5755
|
JSXOpeningElement(node) {
|
|
5730
5756
|
if (isTestlikeFile) return;
|
|
5731
|
-
if (
|
|
5757
|
+
if (resolveJsxElementType(node) !== "button") return;
|
|
5732
5758
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5733
5759
|
if (!typeAttr) {
|
|
5734
5760
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5918,7 +5944,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5918
5944
|
};
|
|
5919
5945
|
return {
|
|
5920
5946
|
JSXOpeningElement(node) {
|
|
5921
|
-
if (
|
|
5947
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
5922
5948
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5923
5949
|
},
|
|
5924
5950
|
CallExpression(node) {
|
|
@@ -5995,7 +6021,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
5995
6021
|
//#endregion
|
|
5996
6022
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
5997
6023
|
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
5998
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
6024
|
+
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
5999
6025
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6000
6026
|
let symbol = scopes.symbolFor(identifier);
|
|
6001
6027
|
while (symbol?.kind === "const") {
|
|
@@ -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"));
|
|
@@ -14589,6 +14611,18 @@ const jsHoistRegexp = defineRule({
|
|
|
14589
14611
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14590
14612
|
});
|
|
14591
14613
|
//#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
|
|
14592
14626
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14593
14627
|
const referencesParameter = (expression, parameterName) => {
|
|
14594
14628
|
if (!expression) return false;
|
|
@@ -14599,7 +14633,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14599
14633
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14600
14634
|
const callback = node.arguments?.[0];
|
|
14601
14635
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14602
|
-
const firstParameter = callback.params?.[0];
|
|
14636
|
+
const firstParameter = resolveFirstArgumentBinding(callback.params?.[0]);
|
|
14603
14637
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14604
14638
|
let predicate = null;
|
|
14605
14639
|
const body = callback.body;
|
|
@@ -15938,14 +15972,21 @@ const jsxFilenameExtension = defineRule({
|
|
|
15938
15972
|
});
|
|
15939
15973
|
//#endregion
|
|
15940
15974
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
15941
|
-
const isJsxFragmentElement = (node) => {
|
|
15975
|
+
const isJsxFragmentElement = (node, scopes) => {
|
|
15942
15976
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
15943
15977
|
const elementName = node.name;
|
|
15944
|
-
if (isNodeOfType(elementName, "JSXIdentifier"))
|
|
15978
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
15979
|
+
if (!scopes) return elementName.name === "Fragment";
|
|
15980
|
+
const symbol = resolveConstIdentifierAlias(elementName, scopes);
|
|
15981
|
+
if (!symbol) return elementName.name === "Fragment" && scopes.isGlobalReference(elementName);
|
|
15982
|
+
return isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "Fragment";
|
|
15983
|
+
}
|
|
15945
15984
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
15946
15985
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
15947
|
-
if (elementName.
|
|
15948
|
-
return elementName.
|
|
15986
|
+
if (elementName.property.name !== "Fragment") return false;
|
|
15987
|
+
if (!scopes) return elementName.object.name === "React";
|
|
15988
|
+
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
15989
|
+
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15949
15990
|
}
|
|
15950
15991
|
return false;
|
|
15951
15992
|
};
|
|
@@ -15971,7 +16012,7 @@ const jsxFragments = defineRule({
|
|
|
15971
16012
|
if (mode !== "syntax") return;
|
|
15972
16013
|
if (!node.closingElement) return;
|
|
15973
16014
|
const openingElement = node.openingElement;
|
|
15974
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
16015
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
15975
16016
|
if (openingElement.attributes.length > 0) return;
|
|
15976
16017
|
context.report({
|
|
15977
16018
|
node: openingElement,
|
|
@@ -18652,10 +18693,6 @@ const resolveSettings$29 = (settings) => {
|
|
|
18652
18693
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18653
18694
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18654
18695
|
};
|
|
18655
|
-
const getElementName = (node) => {
|
|
18656
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18657
|
-
return null;
|
|
18658
|
-
};
|
|
18659
18696
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18660
18697
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18661
18698
|
const explicit = options.components?.[elementName];
|
|
@@ -18675,7 +18712,8 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18675
18712
|
create: (context) => {
|
|
18676
18713
|
const options = resolveSettings$29(context.settings);
|
|
18677
18714
|
return { JSXOpeningElement(node) {
|
|
18678
|
-
|
|
18715
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18716
|
+
const elementName = resolveJsxElementType(node);
|
|
18679
18717
|
if (!elementName) return;
|
|
18680
18718
|
for (const attribute of node.attributes) {
|
|
18681
18719
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -18862,11 +18900,6 @@ const checkTarget = (attributeValue) => {
|
|
|
18862
18900
|
alternate: false
|
|
18863
18901
|
};
|
|
18864
18902
|
};
|
|
18865
|
-
const getOpeningElementName = (node) => {
|
|
18866
|
-
const name = node.name;
|
|
18867
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18868
|
-
return null;
|
|
18869
|
-
};
|
|
18870
18903
|
const jsxNoTargetBlank = defineRule({
|
|
18871
18904
|
id: "jsx-no-target-blank",
|
|
18872
18905
|
title: "Unsafe target=_blank link",
|
|
@@ -18886,7 +18919,8 @@ const jsxNoTargetBlank = defineRule({
|
|
|
18886
18919
|
return settings.formComponents.has(tagName);
|
|
18887
18920
|
};
|
|
18888
18921
|
return { JSXOpeningElement(node) {
|
|
18889
|
-
|
|
18922
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18923
|
+
const tagName = resolveJsxElementType(node);
|
|
18890
18924
|
if (!tagName) return;
|
|
18891
18925
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
18892
18926
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -19119,7 +19153,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19119
19153
|
return {
|
|
19120
19154
|
JSXElement(node) {
|
|
19121
19155
|
const openingElement = node.openingElement;
|
|
19122
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
19156
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
19123
19157
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19124
19158
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19125
19159
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20361,9 +20395,6 @@ const hasDirective = (programNode, directive) => {
|
|
|
20361
20395
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20362
20396
|
};
|
|
20363
20397
|
//#endregion
|
|
20364
|
-
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20365
|
-
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20366
|
-
//#endregion
|
|
20367
20398
|
//#region src/plugin/utils/is-component-assignment.ts
|
|
20368
20399
|
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"));
|
|
20369
20400
|
//#endregion
|
|
@@ -20696,7 +20727,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20696
20727
|
severity: "warn",
|
|
20697
20728
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20698
20729
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20699
|
-
if (
|
|
20730
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
20700
20731
|
const attributes = node.attributes ?? [];
|
|
20701
20732
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20702
20733
|
if (downloadAttribute) {
|
|
@@ -20892,7 +20923,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
20892
20923
|
severity: "warn",
|
|
20893
20924
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
20894
20925
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20895
|
-
if (
|
|
20926
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20896
20927
|
const attributes = node.attributes ?? [];
|
|
20897
20928
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
20898
20929
|
if (!relAttribute?.value) return;
|
|
@@ -21000,7 +21031,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
21000
21031
|
severity: "warn",
|
|
21001
21032
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
21002
21033
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21003
|
-
if (
|
|
21034
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
21004
21035
|
const attributes = node.attributes ?? [];
|
|
21005
21036
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
21006
21037
|
if (!hrefAttribute?.value) return;
|
|
@@ -21175,7 +21206,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21175
21206
|
create: (context) => {
|
|
21176
21207
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21177
21208
|
return { JSXOpeningElement(node) {
|
|
21178
|
-
if (
|
|
21209
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
21179
21210
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21180
21211
|
const programRoot = findProgramRoot(node);
|
|
21181
21212
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21222,8 +21253,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21222
21253
|
severity: "warn",
|
|
21223
21254
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21224
21255
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21225
|
-
|
|
21226
|
-
if (
|
|
21256
|
+
const elementName = resolveJsxElementType(node);
|
|
21257
|
+
if (elementName !== "script" && elementName !== "Script") return;
|
|
21227
21258
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21228
21259
|
if (!srcAttribute?.value) return;
|
|
21229
21260
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -23031,10 +23062,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
23031
23062
|
if (!declaringNode) return false;
|
|
23032
23063
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
23033
23064
|
}));
|
|
23065
|
+
const isWholePropsParameterBinding = (bindingNode) => {
|
|
23066
|
+
if (isFunctionLike$1(bindingNode.parent)) return true;
|
|
23067
|
+
let declaringFunction = bindingNode.parent;
|
|
23068
|
+
while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
|
|
23069
|
+
return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
|
|
23070
|
+
};
|
|
23034
23071
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
23035
23072
|
if (def.type !== "Parameter") return false;
|
|
23036
|
-
|
|
23037
|
-
return isFunctionLike$1(bindingParent);
|
|
23073
|
+
return isWholePropsParameterBinding(def.name);
|
|
23038
23074
|
}));
|
|
23039
23075
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
23040
23076
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -26031,6 +26067,56 @@ const noBarrelImport = defineRule({
|
|
|
26031
26067
|
}
|
|
26032
26068
|
});
|
|
26033
26069
|
//#endregion
|
|
26070
|
+
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
26071
|
+
const collectReturnedExpressions = (functionNode) => {
|
|
26072
|
+
if (!isFunctionLike$1(functionNode)) return [];
|
|
26073
|
+
const body = functionNode.body;
|
|
26074
|
+
if (!body) return [];
|
|
26075
|
+
if (!isNodeOfType(body, "BlockStatement")) return [body];
|
|
26076
|
+
const returnedExpressions = [];
|
|
26077
|
+
walkAst(body, (node) => {
|
|
26078
|
+
if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
26079
|
+
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
|
|
26080
|
+
});
|
|
26081
|
+
return returnedExpressions;
|
|
26082
|
+
};
|
|
26083
|
+
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
|
|
26084
|
+
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
26085
|
+
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
26086
|
+
const functionMatches = (candidateFunction) => {
|
|
26087
|
+
if (visitedFunctions.has(candidateFunction)) return false;
|
|
26088
|
+
visitedFunctions.add(candidateFunction);
|
|
26089
|
+
return collectReturnedExpressions(candidateFunction).some(expressionMatches);
|
|
26090
|
+
};
|
|
26091
|
+
const expressionMatches = (expression) => {
|
|
26092
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
26093
|
+
if (visitedExpressions.has(unwrappedExpression)) return false;
|
|
26094
|
+
visitedExpressions.add(unwrappedExpression);
|
|
26095
|
+
if (matchesExpression(unwrappedExpression)) return true;
|
|
26096
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
26097
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
26098
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
|
|
26099
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
26100
|
+
if (isFunctionLike$1(initializer)) return false;
|
|
26101
|
+
return expressionMatches(initializer);
|
|
26102
|
+
}
|
|
26103
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
26104
|
+
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
26105
|
+
if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
26106
|
+
const symbol = scopes.symbolFor(unwrappedExpression.callee);
|
|
26107
|
+
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
|
|
26108
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
26109
|
+
const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
|
|
26110
|
+
if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
|
|
26111
|
+
return functionMatches(candidateFunction);
|
|
26112
|
+
}
|
|
26113
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
|
|
26114
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
|
|
26115
|
+
return false;
|
|
26116
|
+
};
|
|
26117
|
+
return functionMatches(functionNode);
|
|
26118
|
+
};
|
|
26119
|
+
//#endregion
|
|
26034
26120
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
26035
26121
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
26036
26122
|
"FunctionDeclaration",
|
|
@@ -26050,12 +26136,13 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
26050
26136
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
26051
26137
|
};
|
|
26052
26138
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
26139
|
+
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
26053
26140
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26054
26141
|
let hasRenderOutput = false;
|
|
26055
26142
|
walkAst(rootNode, (node) => {
|
|
26056
26143
|
if (hasRenderOutput) return false;
|
|
26057
26144
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26058
|
-
if (
|
|
26145
|
+
if (isRenderOutputExpression(node, scopes)) {
|
|
26059
26146
|
hasRenderOutput = true;
|
|
26060
26147
|
return false;
|
|
26061
26148
|
}
|
|
@@ -26066,7 +26153,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26066
26153
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26067
26154
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26068
26155
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26069
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26156
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
|
|
26070
26157
|
renderOutputCache.set(functionNode, {
|
|
26071
26158
|
scopes,
|
|
26072
26159
|
hasRenderOutput
|
|
@@ -28531,7 +28618,7 @@ const noDisabledZoom = defineRule({
|
|
|
28531
28618
|
category: "Accessibility",
|
|
28532
28619
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
28533
28620
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28534
|
-
if (
|
|
28621
|
+
if (resolveJsxElementType(node) !== "meta") return;
|
|
28535
28622
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28536
28623
|
if (!nameAttr?.value) return;
|
|
28537
28624
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -31703,7 +31790,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31703
31790
|
severity: "warn",
|
|
31704
31791
|
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
31705
31792
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31706
|
-
if (
|
|
31793
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
31707
31794
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31708
31795
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31709
31796
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -31944,7 +32031,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
31944
32031
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
31945
32032
|
return {
|
|
31946
32033
|
JSXOpeningElement(node) {
|
|
31947
|
-
if (
|
|
32034
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
31948
32035
|
let typeAttribute = null;
|
|
31949
32036
|
let typeAttributeIndex = null;
|
|
31950
32037
|
let indeterminateAttribute = null;
|
|
@@ -33016,11 +33103,13 @@ const noManyBooleanProps = defineRule({
|
|
|
33016
33103
|
};
|
|
33017
33104
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
33018
33105
|
if (!param) return;
|
|
33106
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33107
|
+
if (!propsBinding) return;
|
|
33019
33108
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
33020
|
-
if (isNodeOfType(
|
|
33109
|
+
if (isNodeOfType(propsBinding, "ObjectPattern")) {
|
|
33021
33110
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
33022
33111
|
const booleanLikePropNames = [];
|
|
33023
|
-
for (const property of
|
|
33112
|
+
for (const property of propsBinding.properties ?? []) {
|
|
33024
33113
|
if (!isNodeOfType(property, "Property")) continue;
|
|
33025
33114
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
33026
33115
|
if (!keyName) continue;
|
|
@@ -33031,7 +33120,7 @@ const noManyBooleanProps = defineRule({
|
|
|
33031
33120
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
33032
33121
|
return;
|
|
33033
33122
|
}
|
|
33034
|
-
if (isNodeOfType(
|
|
33123
|
+
if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
|
|
33035
33124
|
};
|
|
33036
33125
|
return {
|
|
33037
33126
|
FunctionDeclaration(node) {
|
|
@@ -35783,7 +35872,7 @@ const noPreventDefault = defineRule({
|
|
|
35783
35872
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
35784
35873
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
35785
35874
|
return { JSXOpeningElement(node) {
|
|
35786
|
-
const elementName =
|
|
35875
|
+
const elementName = resolveJsxElementType(node);
|
|
35787
35876
|
if (!elementName) return;
|
|
35788
35877
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
35789
35878
|
if (!targetEventProps) return;
|
|
@@ -38247,9 +38336,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38247
38336
|
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
38248
38337
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38249
38338
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38250
|
-
const
|
|
38339
|
+
const elementName = resolveJsxElementType(node);
|
|
38340
|
+
const firstCharacter = elementName.charCodeAt(0);
|
|
38251
38341
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38252
|
-
if (
|
|
38342
|
+
if (elementName.includes("-")) return;
|
|
38253
38343
|
for (const attribute of node.attributes) {
|
|
38254
38344
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38255
38345
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38637,8 +38727,7 @@ const noUncontrolledInput = defineRule({
|
|
|
38637
38727
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38638
38728
|
walkAst(componentBody, (child) => {
|
|
38639
38729
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38640
|
-
|
|
38641
|
-
const tagName = child.name.name;
|
|
38730
|
+
const tagName = resolveJsxElementType(child);
|
|
38642
38731
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38643
38732
|
const attributes = child.attributes ?? [];
|
|
38644
38733
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38699,7 +38788,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38699
38788
|
severity: "warn",
|
|
38700
38789
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38701
38790
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38702
|
-
if (
|
|
38791
|
+
if (resolveJsxElementType(node) !== "script") return;
|
|
38703
38792
|
const attributes = node.attributes ?? [];
|
|
38704
38793
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38705
38794
|
if (!srcAttribute) return;
|
|
@@ -39914,7 +40003,7 @@ const noUnknownProperty = defineRule({
|
|
|
39914
40003
|
}
|
|
39915
40004
|
if (fileIsNonReactJsx) return;
|
|
39916
40005
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
39917
|
-
const elementType = node
|
|
40006
|
+
const elementType = resolveJsxElementType(node);
|
|
39918
40007
|
const firstCharacter = elementType.charCodeAt(0);
|
|
39919
40008
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
39920
40009
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40092,20 +40181,21 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40092
40181
|
});
|
|
40093
40182
|
return found;
|
|
40094
40183
|
};
|
|
40184
|
+
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40095
40185
|
const isReactClassComponent = (classNode) => {
|
|
40096
40186
|
if (isEs6Component(classNode)) return true;
|
|
40097
40187
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40098
40188
|
};
|
|
40099
|
-
const findEnclosingComponent = (node) => {
|
|
40189
|
+
const findEnclosingComponent = (node, scopes) => {
|
|
40100
40190
|
let walker = node.parent;
|
|
40101
40191
|
while (walker) {
|
|
40102
40192
|
if (isFunctionLike$1(walker)) {
|
|
40103
40193
|
const componentName = inferFunctionLikeName(walker);
|
|
40104
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40194
|
+
if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
|
|
40105
40195
|
component: walker,
|
|
40106
40196
|
name: componentName
|
|
40107
40197
|
};
|
|
40108
|
-
if (!componentName &&
|
|
40198
|
+
if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40109
40199
|
component: walker,
|
|
40110
40200
|
name: null
|
|
40111
40201
|
};
|
|
@@ -40344,7 +40434,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40344
40434
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40345
40435
|
if (settings.allowAsProps) return;
|
|
40346
40436
|
}
|
|
40347
|
-
const enclosing = findEnclosingComponent(candidateNode);
|
|
40437
|
+
const enclosing = findEnclosingComponent(candidateNode, context.scopes);
|
|
40348
40438
|
if (!enclosing) return;
|
|
40349
40439
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40350
40440
|
queuedReports.push({
|
|
@@ -40355,7 +40445,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40355
40445
|
});
|
|
40356
40446
|
};
|
|
40357
40447
|
const checkFunctionLike = (node) => {
|
|
40358
|
-
if (!
|
|
40448
|
+
if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
|
|
40359
40449
|
const inferredName = inferFunctionLikeName(node);
|
|
40360
40450
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40361
40451
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41615,7 +41705,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41615
41705
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41616
41706
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41617
41707
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41618
|
-
const tagName = node
|
|
41708
|
+
const tagName = resolveJsxElementType(node);
|
|
41619
41709
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41620
41710
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41621
41711
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41635,7 +41725,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41635
41725
|
]);
|
|
41636
41726
|
const isTextLikeInput = (openingElement) => {
|
|
41637
41727
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41638
|
-
const tagName = openingElement
|
|
41728
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
41639
41729
|
if (tagName === "textarea") return true;
|
|
41640
41730
|
if (tagName !== "input") return false;
|
|
41641
41731
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -41792,8 +41882,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
41792
41882
|
]);
|
|
41793
41883
|
const collectBooleanPropBindings = (param) => {
|
|
41794
41884
|
const bindings = /* @__PURE__ */ new Set();
|
|
41795
|
-
|
|
41796
|
-
|
|
41885
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
41886
|
+
if (!isNodeOfType(propsBinding, "ObjectPattern")) return bindings;
|
|
41887
|
+
for (const property of propsBinding.properties ?? []) {
|
|
41797
41888
|
if (!isNodeOfType(property, "Property")) continue;
|
|
41798
41889
|
if (property.computed) continue;
|
|
41799
41890
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -42054,7 +42145,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42054
42145
|
},
|
|
42055
42146
|
JSXOpeningElement(node) {
|
|
42056
42147
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42057
|
-
const tagName = node
|
|
42148
|
+
const tagName = resolveJsxElementType(node);
|
|
42058
42149
|
if (tagName === "dialog") return;
|
|
42059
42150
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42060
42151
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -44091,7 +44182,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44091
44182
|
severity: "warn",
|
|
44092
44183
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44093
44184
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44094
|
-
if (
|
|
44185
|
+
if (resolveJsxElementType(node) !== "svg") return;
|
|
44095
44186
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44096
44187
|
node,
|
|
44097
44188
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -45384,14 +45475,10 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45384
45475
|
//#endregion
|
|
45385
45476
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45386
45477
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45387
|
-
const callbackReturnsJsx = (callback) => {
|
|
45478
|
+
const callbackReturnsJsx = (callback, scopes) => {
|
|
45388
45479
|
if (!callback) return false;
|
|
45389
45480
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45390
|
-
|
|
45391
|
-
if (isJsxExpression(body)) return true;
|
|
45392
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
45393
|
-
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
|
|
45394
|
-
return false;
|
|
45481
|
+
return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
|
|
45395
45482
|
};
|
|
45396
45483
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45397
45484
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45469,7 +45556,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45469
45556
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45470
45557
|
for (const declarator of stmt.declarations ?? []) {
|
|
45471
45558
|
const init = declarator.init;
|
|
45472
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
45559
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
|
|
45473
45560
|
memoNode = declarator;
|
|
45474
45561
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45475
45562
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45535,8 +45622,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45535
45622
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45536
45623
|
const bindings = /* @__PURE__ */ new Map();
|
|
45537
45624
|
const params = functionNode.params ?? [];
|
|
45538
|
-
for (const param of params)
|
|
45539
|
-
|
|
45625
|
+
for (const [parameterIndex, param] of params.entries()) {
|
|
45626
|
+
const parameterBinding = parameterIndex === 0 ? resolveFirstArgumentBinding(param) : param;
|
|
45627
|
+
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45628
|
+
}
|
|
45629
|
+
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45540
45630
|
const body = functionNode.body;
|
|
45541
45631
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45542
45632
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -53650,10 +53740,6 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53650
53740
|
}
|
|
53651
53741
|
return false;
|
|
53652
53742
|
};
|
|
53653
|
-
const getJsxOpeningElementName = (node) => {
|
|
53654
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53655
|
-
return null;
|
|
53656
|
-
};
|
|
53657
53743
|
const stylePropObject = defineRule({
|
|
53658
53744
|
id: "style-prop-object",
|
|
53659
53745
|
title: "Style prop is not an object",
|
|
@@ -53665,7 +53751,8 @@ const stylePropObject = defineRule({
|
|
|
53665
53751
|
const allowSet = new Set(allow);
|
|
53666
53752
|
return {
|
|
53667
53753
|
JSXOpeningElement(node) {
|
|
53668
|
-
|
|
53754
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
53755
|
+
const elementName = resolveJsxElementType(node);
|
|
53669
53756
|
if (elementName && allowSet.has(elementName)) return;
|
|
53670
53757
|
if (elementName) {
|
|
53671
53758
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54370,7 +54457,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54370
54457
|
create: (context) => {
|
|
54371
54458
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54372
54459
|
return { JSXOpeningElement(node) {
|
|
54373
|
-
if (
|
|
54460
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
54374
54461
|
const attributes = node.attributes ?? [];
|
|
54375
54462
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54376
54463
|
if (!hrefAttribute?.value) return;
|
|
@@ -54991,8 +55078,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
54991
55078
|
create: (context) => ({
|
|
54992
55079
|
JSXElement(node) {
|
|
54993
55080
|
const openingElement = node.openingElement;
|
|
54994
|
-
|
|
54995
|
-
const tagName = openingElement.name.name;
|
|
55081
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
54996
55082
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
54997
55083
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
54998
55084
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|