oxlint-plugin-react-doctor 0.7.6-dev.eb09902 → 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 +106 -204
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1518,37 +1518,6 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1518
1518
|
}
|
|
1519
1519
|
};
|
|
1520
1520
|
//#endregion
|
|
1521
|
-
//#region src/plugin/utils/is-uppercase-name.ts
|
|
1522
|
-
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
1523
|
-
//#endregion
|
|
1524
|
-
//#region src/plugin/utils/resolve-jsx-element-type.ts
|
|
1525
|
-
const resolveConstantStringBinding = (name) => {
|
|
1526
|
-
const binding = findVariableInitializer(name, name.name);
|
|
1527
|
-
if (!binding?.initializer) return null;
|
|
1528
|
-
const declarator = binding.bindingIdentifier.parent;
|
|
1529
|
-
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
1530
|
-
if (declarator.id !== binding.bindingIdentifier) return null;
|
|
1531
|
-
const declaration = declarator.parent;
|
|
1532
|
-
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
1533
|
-
if (declaration.kind !== "const") return null;
|
|
1534
|
-
const initializer = stripParenExpression(binding.initializer);
|
|
1535
|
-
return isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
|
|
1536
|
-
};
|
|
1537
|
-
const flattenJsxName$2 = (name) => {
|
|
1538
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1539
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1540
|
-
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1541
|
-
return "";
|
|
1542
|
-
};
|
|
1543
|
-
const resolveJsxElementType = (openingElement) => {
|
|
1544
|
-
const name = openingElement.name;
|
|
1545
|
-
if (isNodeOfType(name, "JSXIdentifier")) {
|
|
1546
|
-
if (!isUppercaseName(name.name)) return name.name;
|
|
1547
|
-
return resolveConstantStringBinding(name) ?? name.name;
|
|
1548
|
-
}
|
|
1549
|
-
return flattenJsxName$2(name);
|
|
1550
|
-
};
|
|
1551
|
-
//#endregion
|
|
1552
1521
|
//#region src/plugin/utils/get-element-type.ts
|
|
1553
1522
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1554
1523
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1561,8 +1530,14 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1561
1530
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1562
1531
|
return a11ySettings;
|
|
1563
1532
|
};
|
|
1533
|
+
const flattenJsxName$2 = (name) => {
|
|
1534
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1535
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1536
|
+
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1537
|
+
return "";
|
|
1538
|
+
};
|
|
1564
1539
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1565
|
-
const baseName =
|
|
1540
|
+
const baseName = flattenJsxName$2(openingElement.name);
|
|
1566
1541
|
if (a11ySettings.polymorphicPropName) {
|
|
1567
1542
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1568
1543
|
if (polymorphicAttribute) {
|
|
@@ -2397,9 +2372,8 @@ const anchorIsValid = defineRule({
|
|
|
2397
2372
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2398
2373
|
return { JSXOpeningElement(node) {
|
|
2399
2374
|
if (isTestlikeFile) return;
|
|
2400
|
-
|
|
2401
|
-
if (
|
|
2402
|
-
if (tag !== "a") return;
|
|
2375
|
+
if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
|
|
2376
|
+
if (getElementType(node, context.settings) !== "a") return;
|
|
2403
2377
|
let hrefAttribute;
|
|
2404
2378
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2405
2379
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5754,7 +5728,7 @@ const buttonHasType = defineRule({
|
|
|
5754
5728
|
return {
|
|
5755
5729
|
JSXOpeningElement(node) {
|
|
5756
5730
|
if (isTestlikeFile) return;
|
|
5757
|
-
if (
|
|
5731
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "button") return;
|
|
5758
5732
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5759
5733
|
if (!typeAttr) {
|
|
5760
5734
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5944,7 +5918,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5944
5918
|
};
|
|
5945
5919
|
return {
|
|
5946
5920
|
JSXOpeningElement(node) {
|
|
5947
|
-
if (
|
|
5921
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
5948
5922
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5949
5923
|
},
|
|
5950
5924
|
CallExpression(node) {
|
|
@@ -6021,7 +5995,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
6021
5995
|
//#endregion
|
|
6022
5996
|
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
6023
5997
|
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
6024
|
-
if (!isNodeOfType(identifier, "Identifier")
|
|
5998
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
6025
5999
|
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6026
6000
|
let symbol = scopes.symbolFor(identifier);
|
|
6027
6001
|
while (symbol?.kind === "const") {
|
|
@@ -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;
|
|
@@ -15972,21 +15926,14 @@ const jsxFilenameExtension = defineRule({
|
|
|
15972
15926
|
});
|
|
15973
15927
|
//#endregion
|
|
15974
15928
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
15975
|
-
const isJsxFragmentElement = (node
|
|
15929
|
+
const isJsxFragmentElement = (node) => {
|
|
15976
15930
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
15977
15931
|
const elementName = node.name;
|
|
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
|
-
}
|
|
15932
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) return elementName.name === "Fragment";
|
|
15984
15933
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
15985
15934
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
15986
|
-
if (elementName.
|
|
15987
|
-
|
|
15988
|
-
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
15989
|
-
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15935
|
+
if (elementName.object.name !== "React") return false;
|
|
15936
|
+
return elementName.property.name === "Fragment";
|
|
15990
15937
|
}
|
|
15991
15938
|
return false;
|
|
15992
15939
|
};
|
|
@@ -16012,7 +15959,7 @@ const jsxFragments = defineRule({
|
|
|
16012
15959
|
if (mode !== "syntax") return;
|
|
16013
15960
|
if (!node.closingElement) return;
|
|
16014
15961
|
const openingElement = node.openingElement;
|
|
16015
|
-
if (!isJsxFragmentElement(openingElement
|
|
15962
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
16016
15963
|
if (openingElement.attributes.length > 0) return;
|
|
16017
15964
|
context.report({
|
|
16018
15965
|
node: openingElement,
|
|
@@ -18693,6 +18640,10 @@ const resolveSettings$29 = (settings) => {
|
|
|
18693
18640
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18694
18641
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18695
18642
|
};
|
|
18643
|
+
const getElementName = (node) => {
|
|
18644
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18645
|
+
return null;
|
|
18646
|
+
};
|
|
18696
18647
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18697
18648
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18698
18649
|
const explicit = options.components?.[elementName];
|
|
@@ -18712,8 +18663,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18712
18663
|
create: (context) => {
|
|
18713
18664
|
const options = resolveSettings$29(context.settings);
|
|
18714
18665
|
return { JSXOpeningElement(node) {
|
|
18715
|
-
|
|
18716
|
-
const elementName = resolveJsxElementType(node);
|
|
18666
|
+
const elementName = getElementName(node);
|
|
18717
18667
|
if (!elementName) return;
|
|
18718
18668
|
for (const attribute of node.attributes) {
|
|
18719
18669
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -18900,6 +18850,11 @@ const checkTarget = (attributeValue) => {
|
|
|
18900
18850
|
alternate: false
|
|
18901
18851
|
};
|
|
18902
18852
|
};
|
|
18853
|
+
const getOpeningElementName = (node) => {
|
|
18854
|
+
const name = node.name;
|
|
18855
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18856
|
+
return null;
|
|
18857
|
+
};
|
|
18903
18858
|
const jsxNoTargetBlank = defineRule({
|
|
18904
18859
|
id: "jsx-no-target-blank",
|
|
18905
18860
|
title: "Unsafe target=_blank link",
|
|
@@ -18919,8 +18874,7 @@ const jsxNoTargetBlank = defineRule({
|
|
|
18919
18874
|
return settings.formComponents.has(tagName);
|
|
18920
18875
|
};
|
|
18921
18876
|
return { JSXOpeningElement(node) {
|
|
18922
|
-
|
|
18923
|
-
const tagName = resolveJsxElementType(node);
|
|
18877
|
+
const tagName = getOpeningElementName(node);
|
|
18924
18878
|
if (!tagName) return;
|
|
18925
18879
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
18926
18880
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -19153,7 +19107,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
19153
19107
|
return {
|
|
19154
19108
|
JSXElement(node) {
|
|
19155
19109
|
const openingElement = node.openingElement;
|
|
19156
|
-
if (!isJsxFragmentElement(openingElement
|
|
19110
|
+
if (!isJsxFragmentElement(openingElement)) return;
|
|
19157
19111
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
19158
19112
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
19159
19113
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -20395,6 +20349,9 @@ const hasDirective = (programNode, directive) => {
|
|
|
20395
20349
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
20396
20350
|
};
|
|
20397
20351
|
//#endregion
|
|
20352
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
20353
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
20354
|
+
//#endregion
|
|
20398
20355
|
//#region src/plugin/utils/is-component-assignment.ts
|
|
20399
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"));
|
|
20400
20357
|
//#endregion
|
|
@@ -20727,7 +20684,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20727
20684
|
severity: "warn",
|
|
20728
20685
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20729
20686
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20730
|
-
if (
|
|
20687
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
20731
20688
|
const attributes = node.attributes ?? [];
|
|
20732
20689
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20733
20690
|
if (downloadAttribute) {
|
|
@@ -20923,7 +20880,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
20923
20880
|
severity: "warn",
|
|
20924
20881
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
20925
20882
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20926
|
-
if (
|
|
20883
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
20927
20884
|
const attributes = node.attributes ?? [];
|
|
20928
20885
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
20929
20886
|
if (!relAttribute?.value) return;
|
|
@@ -21031,7 +20988,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
21031
20988
|
severity: "warn",
|
|
21032
20989
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
21033
20990
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21034
|
-
if (
|
|
20991
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "link") return;
|
|
21035
20992
|
const attributes = node.attributes ?? [];
|
|
21036
20993
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
21037
20994
|
if (!hrefAttribute?.value) return;
|
|
@@ -21206,7 +21163,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
21206
21163
|
create: (context) => {
|
|
21207
21164
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
21208
21165
|
return { JSXOpeningElement(node) {
|
|
21209
|
-
if (
|
|
21166
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
21210
21167
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
21211
21168
|
const programRoot = findProgramRoot(node);
|
|
21212
21169
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -21253,8 +21210,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
21253
21210
|
severity: "warn",
|
|
21254
21211
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
21255
21212
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
21256
|
-
|
|
21257
|
-
if (
|
|
21213
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
21214
|
+
if (node.name.name !== "script" && node.name.name !== "Script") return;
|
|
21258
21215
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
21259
21216
|
if (!srcAttribute?.value) return;
|
|
21260
21217
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -23062,15 +23019,10 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
23062
23019
|
if (!declaringNode) return false;
|
|
23063
23020
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
23064
23021
|
}));
|
|
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
|
-
};
|
|
23071
23022
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
23072
23023
|
if (def.type !== "Parameter") return false;
|
|
23073
|
-
|
|
23024
|
+
const bindingParent = def.name.parent;
|
|
23025
|
+
return isFunctionLike$1(bindingParent);
|
|
23074
23026
|
}));
|
|
23075
23027
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
23076
23028
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -26067,56 +26019,6 @@ const noBarrelImport = defineRule({
|
|
|
26067
26019
|
}
|
|
26068
26020
|
});
|
|
26069
26021
|
//#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
|
|
26120
26022
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
26121
26023
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
26122
26024
|
"FunctionDeclaration",
|
|
@@ -26136,13 +26038,12 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
26136
26038
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
26137
26039
|
};
|
|
26138
26040
|
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);
|
|
26140
26041
|
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26141
26042
|
let hasRenderOutput = false;
|
|
26142
26043
|
walkAst(rootNode, (node) => {
|
|
26143
26044
|
if (hasRenderOutput) return false;
|
|
26144
26045
|
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26145
|
-
if (
|
|
26046
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS)) {
|
|
26146
26047
|
hasRenderOutput = true;
|
|
26147
26048
|
return false;
|
|
26148
26049
|
}
|
|
@@ -26153,7 +26054,7 @@ const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
|
26153
26054
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
26154
26055
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
26155
26056
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
26156
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes)
|
|
26057
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes);
|
|
26157
26058
|
renderOutputCache.set(functionNode, {
|
|
26158
26059
|
scopes,
|
|
26159
26060
|
hasRenderOutput
|
|
@@ -28618,7 +28519,7 @@ const noDisabledZoom = defineRule({
|
|
|
28618
28519
|
category: "Accessibility",
|
|
28619
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.",
|
|
28620
28521
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
28621
|
-
if (
|
|
28522
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "meta") return;
|
|
28622
28523
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
28623
28524
|
if (!nameAttr?.value) return;
|
|
28624
28525
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -31790,7 +31691,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31790
31691
|
severity: "warn",
|
|
31791
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.",
|
|
31792
31693
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31793
|
-
if (
|
|
31694
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
31794
31695
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31795
31696
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31796
31697
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -32031,7 +31932,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
32031
31932
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
32032
31933
|
return {
|
|
32033
31934
|
JSXOpeningElement(node) {
|
|
32034
|
-
if (
|
|
31935
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "input") return;
|
|
32035
31936
|
let typeAttribute = null;
|
|
32036
31937
|
let typeAttributeIndex = null;
|
|
32037
31938
|
let indeterminateAttribute = null;
|
|
@@ -33103,13 +33004,11 @@ const noManyBooleanProps = defineRule({
|
|
|
33103
33004
|
};
|
|
33104
33005
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
33105
33006
|
if (!param) return;
|
|
33106
|
-
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33107
|
-
if (!propsBinding) return;
|
|
33108
33007
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
33109
|
-
if (isNodeOfType(
|
|
33008
|
+
if (isNodeOfType(param, "ObjectPattern")) {
|
|
33110
33009
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
33111
33010
|
const booleanLikePropNames = [];
|
|
33112
|
-
for (const property of
|
|
33011
|
+
for (const property of param.properties ?? []) {
|
|
33113
33012
|
if (!isNodeOfType(property, "Property")) continue;
|
|
33114
33013
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
33115
33014
|
if (!keyName) continue;
|
|
@@ -33120,7 +33019,7 @@ const noManyBooleanProps = defineRule({
|
|
|
33120
33019
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
33121
33020
|
return;
|
|
33122
33021
|
}
|
|
33123
|
-
if (isNodeOfType(
|
|
33022
|
+
if (isNodeOfType(param, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, param.name)], componentName, reportNode);
|
|
33124
33023
|
};
|
|
33125
33024
|
return {
|
|
33126
33025
|
FunctionDeclaration(node) {
|
|
@@ -35872,7 +35771,7 @@ const noPreventDefault = defineRule({
|
|
|
35872
35771
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
35873
35772
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
35874
35773
|
return { JSXOpeningElement(node) {
|
|
35875
|
-
const elementName =
|
|
35774
|
+
const elementName = isNodeOfType(node.name, "JSXIdentifier") ? node.name.name : null;
|
|
35876
35775
|
if (!elementName) return;
|
|
35877
35776
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
35878
35777
|
if (!targetEventProps) return;
|
|
@@ -38336,10 +38235,9 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
38336
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.",
|
|
38337
38236
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38338
38237
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
38339
|
-
const
|
|
38340
|
-
const firstCharacter = elementName.charCodeAt(0);
|
|
38238
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
38341
38239
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
38342
|
-
if (
|
|
38240
|
+
if (node.name.name.includes("-")) return;
|
|
38343
38241
|
for (const attribute of node.attributes) {
|
|
38344
38242
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38345
38243
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -38727,7 +38625,8 @@ const noUncontrolledInput = defineRule({
|
|
|
38727
38625
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
38728
38626
|
walkAst(componentBody, (child) => {
|
|
38729
38627
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
38730
|
-
|
|
38628
|
+
if (!isNodeOfType(child.name, "JSXIdentifier")) return;
|
|
38629
|
+
const tagName = child.name.name;
|
|
38731
38630
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
38732
38631
|
const attributes = child.attributes ?? [];
|
|
38733
38632
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -38788,7 +38687,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
38788
38687
|
severity: "warn",
|
|
38789
38688
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
38790
38689
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38791
|
-
if (
|
|
38690
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "script") return;
|
|
38792
38691
|
const attributes = node.attributes ?? [];
|
|
38793
38692
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
38794
38693
|
if (!srcAttribute) return;
|
|
@@ -40003,7 +39902,7 @@ const noUnknownProperty = defineRule({
|
|
|
40003
39902
|
}
|
|
40004
39903
|
if (fileIsNonReactJsx) return;
|
|
40005
39904
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
40006
|
-
const elementType =
|
|
39905
|
+
const elementType = node.name.name;
|
|
40007
39906
|
const firstCharacter = elementType.charCodeAt(0);
|
|
40008
39907
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
40009
39908
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -40181,21 +40080,20 @@ const expressionContainsJsxOrCreateElement = (root) => {
|
|
|
40181
40080
|
});
|
|
40182
40081
|
return found;
|
|
40183
40082
|
};
|
|
40184
|
-
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
40185
40083
|
const isReactClassComponent = (classNode) => {
|
|
40186
40084
|
if (isEs6Component(classNode)) return true;
|
|
40187
40085
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
40188
40086
|
};
|
|
40189
|
-
const findEnclosingComponent = (node
|
|
40087
|
+
const findEnclosingComponent = (node) => {
|
|
40190
40088
|
let walker = node.parent;
|
|
40191
40089
|
while (walker) {
|
|
40192
40090
|
if (isFunctionLike$1(walker)) {
|
|
40193
40091
|
const componentName = inferFunctionLikeName(walker);
|
|
40194
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40092
|
+
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
40195
40093
|
component: walker,
|
|
40196
40094
|
name: componentName
|
|
40197
40095
|
};
|
|
40198
|
-
if (!componentName &&
|
|
40096
|
+
if (!componentName && expressionContainsJsxOrCreateElement(walker) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
40199
40097
|
component: walker,
|
|
40200
40098
|
name: null
|
|
40201
40099
|
};
|
|
@@ -40434,7 +40332,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40434
40332
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
40435
40333
|
if (settings.allowAsProps) return;
|
|
40436
40334
|
}
|
|
40437
|
-
const enclosing = findEnclosingComponent(candidateNode
|
|
40335
|
+
const enclosing = findEnclosingComponent(candidateNode);
|
|
40438
40336
|
if (!enclosing) return;
|
|
40439
40337
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
40440
40338
|
queuedReports.push({
|
|
@@ -40445,7 +40343,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
40445
40343
|
});
|
|
40446
40344
|
};
|
|
40447
40345
|
const checkFunctionLike = (node) => {
|
|
40448
|
-
if (!
|
|
40346
|
+
if (!expressionContainsJsxOrCreateElement(node)) return;
|
|
40449
40347
|
const inferredName = inferFunctionLikeName(node);
|
|
40450
40348
|
const propInfo = isComponentDeclaredInProp(node);
|
|
40451
40349
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -41705,7 +41603,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
41705
41603
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
41706
41604
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
41707
41605
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41708
|
-
const tagName =
|
|
41606
|
+
const tagName = node.name.name;
|
|
41709
41607
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41710
41608
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
41711
41609
|
if (!onDoubleClickAttribute) return;
|
|
@@ -41725,7 +41623,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
41725
41623
|
]);
|
|
41726
41624
|
const isTextLikeInput = (openingElement) => {
|
|
41727
41625
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
41728
|
-
const tagName =
|
|
41626
|
+
const tagName = openingElement.name.name;
|
|
41729
41627
|
if (tagName === "textarea") return true;
|
|
41730
41628
|
if (tagName !== "input") return false;
|
|
41731
41629
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -41882,9 +41780,8 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
41882
41780
|
]);
|
|
41883
41781
|
const collectBooleanPropBindings = (param) => {
|
|
41884
41782
|
const bindings = /* @__PURE__ */ new Set();
|
|
41885
|
-
|
|
41886
|
-
|
|
41887
|
-
for (const property of propsBinding.properties ?? []) {
|
|
41783
|
+
if (!param || !isNodeOfType(param, "ObjectPattern")) return bindings;
|
|
41784
|
+
for (const property of param.properties ?? []) {
|
|
41888
41785
|
if (!isNodeOfType(property, "Property")) continue;
|
|
41889
41786
|
if (property.computed) continue;
|
|
41890
41787
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -42145,7 +42042,7 @@ const preferHtmlDialog = defineRule({
|
|
|
42145
42042
|
},
|
|
42146
42043
|
JSXOpeningElement(node) {
|
|
42147
42044
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
42148
|
-
const tagName =
|
|
42045
|
+
const tagName = node.name.name;
|
|
42149
42046
|
if (tagName === "dialog") return;
|
|
42150
42047
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
42151
42048
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -44182,7 +44079,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
44182
44079
|
severity: "warn",
|
|
44183
44080
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
44184
44081
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
44185
|
-
if (
|
|
44082
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "svg") return;
|
|
44186
44083
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
44187
44084
|
node,
|
|
44188
44085
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -45475,10 +45372,14 @@ const rerenderLazyStateInit = defineRule({
|
|
|
45475
45372
|
//#endregion
|
|
45476
45373
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
45477
45374
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
45478
|
-
const callbackReturnsJsx = (callback
|
|
45375
|
+
const callbackReturnsJsx = (callback) => {
|
|
45479
45376
|
if (!callback) return false;
|
|
45480
45377
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
45481
|
-
|
|
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;
|
|
45482
45383
|
};
|
|
45483
45384
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
45484
45385
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -45556,7 +45457,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
45556
45457
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
45557
45458
|
for (const declarator of stmt.declarations ?? []) {
|
|
45558
45459
|
const init = declarator.init;
|
|
45559
|
-
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])) {
|
|
45560
45461
|
memoNode = declarator;
|
|
45561
45462
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
45562
45463
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -45622,11 +45523,8 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
45622
45523
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
45623
45524
|
const bindings = /* @__PURE__ */ new Map();
|
|
45624
45525
|
const params = functionNode.params ?? [];
|
|
45625
|
-
for (const
|
|
45626
|
-
|
|
45627
|
-
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45628
|
-
}
|
|
45629
|
-
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
45526
|
+
for (const param of params) collectFromObjectPattern(param, bindings);
|
|
45527
|
+
const propsParam = params[0];
|
|
45630
45528
|
const body = functionNode.body;
|
|
45631
45529
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
45632
45530
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -53740,6 +53638,10 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
53740
53638
|
}
|
|
53741
53639
|
return false;
|
|
53742
53640
|
};
|
|
53641
|
+
const getJsxOpeningElementName = (node) => {
|
|
53642
|
+
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
53643
|
+
return null;
|
|
53644
|
+
};
|
|
53743
53645
|
const stylePropObject = defineRule({
|
|
53744
53646
|
id: "style-prop-object",
|
|
53745
53647
|
title: "Style prop is not an object",
|
|
@@ -53751,8 +53653,7 @@ const stylePropObject = defineRule({
|
|
|
53751
53653
|
const allowSet = new Set(allow);
|
|
53752
53654
|
return {
|
|
53753
53655
|
JSXOpeningElement(node) {
|
|
53754
|
-
|
|
53755
|
-
const elementName = resolveJsxElementType(node);
|
|
53656
|
+
const elementName = getJsxOpeningElementName(node);
|
|
53756
53657
|
if (elementName && allowSet.has(elementName)) return;
|
|
53757
53658
|
if (elementName) {
|
|
53758
53659
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -54457,7 +54358,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
54457
54358
|
create: (context) => {
|
|
54458
54359
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
54459
54360
|
return { JSXOpeningElement(node) {
|
|
54460
|
-
if (
|
|
54361
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
54461
54362
|
const attributes = node.attributes ?? [];
|
|
54462
54363
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
54463
54364
|
if (!hrefAttribute?.value) return;
|
|
@@ -55078,7 +54979,8 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
55078
54979
|
create: (context) => ({
|
|
55079
54980
|
JSXElement(node) {
|
|
55080
54981
|
const openingElement = node.openingElement;
|
|
55081
|
-
|
|
54982
|
+
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return;
|
|
54983
|
+
const tagName = openingElement.name.name;
|
|
55082
54984
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
55083
54985
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
55084
54986
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|