eslint-plugin-code-style 1.3.2 → 1.3.8
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/README.md +3 -2
- package/index.js +294 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -300,8 +300,9 @@ rules: {
|
|
|
300
300
|
| `module-index-exports` | Index files must export all folder contents (files and subfolders) ⚙️ |
|
|
301
301
|
| **JSX Rules** | |
|
|
302
302
|
| `classname-dynamic-at-end` | Dynamic expressions (`${className}`) must be at the end of class strings (JSX and variables) |
|
|
303
|
-
| `classname-multiline` | Long className strings broken into multiple lines
|
|
304
|
-
| `classname-no-extra-spaces` | No extra/leading/trailing spaces in class strings
|
|
303
|
+
| `classname-multiline` | Long className strings broken into multiple lines; smart detection for objects/returns with Tailwind values ⚙️ |
|
|
304
|
+
| `classname-no-extra-spaces` | No extra/leading/trailing spaces in class strings; smart detection for objects/returns with Tailwind values |
|
|
305
|
+
| `classname-order` | Tailwind class ordering in variables/objects/returns; smart detection for Tailwind values |
|
|
305
306
|
| `jsx-children-on-new-line` | Multiple JSX children: each on own line with proper indentation |
|
|
306
307
|
| `jsx-closing-bracket-spacing` | No space before `>` or `/>` in JSX tags |
|
|
307
308
|
| `jsx-element-child-new-line` | Nested JSX elements on new lines; text/expression children can stay inline |
|
package/index.js
CHANGED
|
@@ -410,7 +410,7 @@ const arrayItemsPerLine = {
|
|
|
410
410
|
node,
|
|
411
411
|
singleLine,
|
|
412
412
|
),
|
|
413
|
-
message: `Array with
|
|
413
|
+
message: `Array with ≤${maxItems} simple items should be single line: [a, b, c]. Multi-line only for >${maxItems} items or complex values`,
|
|
414
414
|
node,
|
|
415
415
|
});
|
|
416
416
|
}
|
|
@@ -1114,7 +1114,7 @@ const arrowFunctionSimplify = {
|
|
|
1114
1114
|
node.body,
|
|
1115
1115
|
expressionText,
|
|
1116
1116
|
),
|
|
1117
|
-
message: "Arrow function with single
|
|
1117
|
+
message: "Arrow function with single return should use expression body: () => value instead of () => { return value }",
|
|
1118
1118
|
node: node.body,
|
|
1119
1119
|
});
|
|
1120
1120
|
|
|
@@ -1148,7 +1148,7 @@ const arrowFunctionSimplify = {
|
|
|
1148
1148
|
node.body,
|
|
1149
1149
|
expressionText,
|
|
1150
1150
|
),
|
|
1151
|
-
message: "Arrow function with single statement should use expression body",
|
|
1151
|
+
message: "Arrow function with single statement should use expression body: () => expression instead of () => { return expression }",
|
|
1152
1152
|
node: node.body,
|
|
1153
1153
|
});
|
|
1154
1154
|
}
|
|
@@ -1773,6 +1773,7 @@ const functionDeclarationStyle = {
|
|
|
1773
1773
|
* Description:
|
|
1774
1774
|
* Function names should follow naming conventions: camelCase,
|
|
1775
1775
|
* starting with a verb, and handlers ending with "Handler".
|
|
1776
|
+
* Auto-fixes PascalCase functions that start with verbs to camelCase.
|
|
1776
1777
|
*
|
|
1777
1778
|
* ✓ Good:
|
|
1778
1779
|
* function getUserData() {}
|
|
@@ -1780,10 +1781,10 @@ const functionDeclarationStyle = {
|
|
|
1780
1781
|
* function isValidEmail() {}
|
|
1781
1782
|
* const submitHandler = () => {}
|
|
1782
1783
|
*
|
|
1783
|
-
* ✗ Bad:
|
|
1784
|
-
* function GetUserData() {}
|
|
1784
|
+
* ✗ Bad (auto-fixed):
|
|
1785
|
+
* function GetUserData() {} // → getUserData
|
|
1786
|
+
* const FetchStatus = () => {} // → fetchStatus
|
|
1785
1787
|
* function user_data() {}
|
|
1786
|
-
* function click() {}
|
|
1787
1788
|
*/
|
|
1788
1789
|
const functionNamingConvention = {
|
|
1789
1790
|
create(context) {
|
|
@@ -1803,7 +1804,7 @@ const functionNamingConvention = {
|
|
|
1803
1804
|
"render", "display", "show", "hide", "toggle", "enable", "disable",
|
|
1804
1805
|
"open", "close", "start", "stop", "init", "setup", "reset", "clear",
|
|
1805
1806
|
"connect", "disconnect", "subscribe", "unsubscribe", "listen", "emit",
|
|
1806
|
-
"send", "receive", "request", "respond", "submit", "cancel", "abort",
|
|
1807
|
+
"send", "receive", "request", "respond", "submit", "cancel", "abort", "poll",
|
|
1807
1808
|
"read", "write", "copy", "move", "clone", "extract", "insert", "append", "prepend",
|
|
1808
1809
|
"build", "make", "generate", "compute", "calculate", "process", "execute", "run",
|
|
1809
1810
|
"apply", "call", "invoke", "trigger", "fire", "dispatch",
|
|
@@ -1823,6 +1824,16 @@ const functionNamingConvention = {
|
|
|
1823
1824
|
|
|
1824
1825
|
const startsWithVerbHandler = (name) => verbPrefixes.some((verb) => name.startsWith(verb));
|
|
1825
1826
|
|
|
1827
|
+
// Case-insensitive check for verb prefix (to catch PascalCase like "GetForStatus")
|
|
1828
|
+
const startsWithVerbCaseInsensitiveHandler = (name) => {
|
|
1829
|
+
const lowerName = name.toLowerCase();
|
|
1830
|
+
|
|
1831
|
+
return verbPrefixes.some((verb) => lowerName.startsWith(verb));
|
|
1832
|
+
};
|
|
1833
|
+
|
|
1834
|
+
// Convert PascalCase to camelCase
|
|
1835
|
+
const toCamelCaseHandler = (name) => name[0].toLowerCase() + name.slice(1);
|
|
1836
|
+
|
|
1826
1837
|
const endsWithHandler = (name) => handlerRegex.test(name);
|
|
1827
1838
|
|
|
1828
1839
|
const checkFunctionHandler = (node) => {
|
|
@@ -1838,12 +1849,50 @@ const functionNamingConvention = {
|
|
|
1838
1849
|
|
|
1839
1850
|
if (!name) return;
|
|
1840
1851
|
|
|
1841
|
-
// Skip React components (PascalCase)
|
|
1842
|
-
if (/^[A-Z]/.test(name)) return;
|
|
1843
|
-
|
|
1844
1852
|
// Skip hooks
|
|
1845
1853
|
if (/^use[A-Z]/.test(name)) return;
|
|
1846
1854
|
|
|
1855
|
+
// Check PascalCase functions
|
|
1856
|
+
if (/^[A-Z]/.test(name)) {
|
|
1857
|
+
// If starts with a verb (case-insensitive), it should be camelCase
|
|
1858
|
+
if (startsWithVerbCaseInsensitiveHandler(name)) {
|
|
1859
|
+
const camelCaseName = toCamelCaseHandler(name);
|
|
1860
|
+
const identifierNode = node.id || node.parent.id;
|
|
1861
|
+
|
|
1862
|
+
context.report({
|
|
1863
|
+
fix(fixer) {
|
|
1864
|
+
const scope = context.sourceCode
|
|
1865
|
+
? context.sourceCode.getScope(node)
|
|
1866
|
+
: context.getScope();
|
|
1867
|
+
|
|
1868
|
+
const variable = scope.variables.find((v) => v.name === name)
|
|
1869
|
+
|| (scope.upper && scope.upper.variables.find((v) => v.name === name));
|
|
1870
|
+
|
|
1871
|
+
if (!variable) return fixer.replaceText(identifierNode, camelCaseName);
|
|
1872
|
+
|
|
1873
|
+
const fixes = [];
|
|
1874
|
+
const fixedRanges = new Set();
|
|
1875
|
+
|
|
1876
|
+
variable.references.forEach((ref) => {
|
|
1877
|
+
const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
|
|
1878
|
+
|
|
1879
|
+
if (!fixedRanges.has(rangeKey)) {
|
|
1880
|
+
fixedRanges.add(rangeKey);
|
|
1881
|
+
fixes.push(fixer.replaceText(ref.identifier, camelCaseName));
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
return fixes;
|
|
1886
|
+
},
|
|
1887
|
+
message: `Function "${name}" should be camelCase. Use "${camelCaseName}" instead of "${name}"`,
|
|
1888
|
+
node: node.id || node.parent.id,
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// Skip other PascalCase names (likely React components)
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1847
1896
|
const hasVerbPrefix = startsWithVerbHandler(name);
|
|
1848
1897
|
const hasHandlerSuffix = endsWithHandler(name);
|
|
1849
1898
|
|
|
@@ -1874,15 +1923,26 @@ const functionNamingConvention = {
|
|
|
1874
1923
|
if (!variable) return fixer.replaceText(identifierNode, newName);
|
|
1875
1924
|
|
|
1876
1925
|
const fixes = [];
|
|
1926
|
+
const fixedRanges = new Set();
|
|
1927
|
+
|
|
1928
|
+
// Helper to add fix only if not already fixed (avoid overlapping fixes)
|
|
1929
|
+
const addFixHandler = (nodeToFix) => {
|
|
1930
|
+
const rangeKey = `${nodeToFix.range[0]}-${nodeToFix.range[1]}`;
|
|
1931
|
+
|
|
1932
|
+
if (!fixedRanges.has(rangeKey)) {
|
|
1933
|
+
fixedRanges.add(rangeKey);
|
|
1934
|
+
fixes.push(fixer.replaceText(nodeToFix, newName));
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1877
1937
|
|
|
1878
1938
|
// Fix the definition
|
|
1879
1939
|
variable.defs.forEach((def) => {
|
|
1880
|
-
|
|
1940
|
+
addFixHandler(def.name);
|
|
1881
1941
|
});
|
|
1882
1942
|
|
|
1883
1943
|
// Fix all references
|
|
1884
1944
|
variable.references.forEach((ref) => {
|
|
1885
|
-
|
|
1945
|
+
addFixHandler(ref.identifier);
|
|
1886
1946
|
});
|
|
1887
1947
|
|
|
1888
1948
|
return fixes;
|
|
@@ -2593,7 +2653,7 @@ const hookDepsPerLine = {
|
|
|
2593
2653
|
[openBracket.range[1], closeBracket.range[0]],
|
|
2594
2654
|
elementsText,
|
|
2595
2655
|
),
|
|
2596
|
-
message: `
|
|
2656
|
+
message: `Hook dependencies with ≤${maxDeps} items should be single line: [dep1, dep2]. Multi-line only for >${maxDeps} dependencies`,
|
|
2597
2657
|
node: depsArg,
|
|
2598
2658
|
});
|
|
2599
2659
|
}
|
|
@@ -2984,7 +3044,7 @@ const multilineIfConditions = {
|
|
|
2984
3044
|
`(${buildSameLineHandler(test)})`,
|
|
2985
3045
|
);
|
|
2986
3046
|
},
|
|
2987
|
-
message: `If conditions with
|
|
3047
|
+
message: `If conditions with ≤${maxOperands} operands should be single line: if (a && b && c). Multi-line only for >${maxOperands} operands`,
|
|
2988
3048
|
node: test,
|
|
2989
3049
|
});
|
|
2990
3050
|
}
|
|
@@ -3144,7 +3204,7 @@ const multilineIfConditions = {
|
|
|
3144
3204
|
|
|
3145
3205
|
return fixer.replaceText(value, buildSameLineHandler(value));
|
|
3146
3206
|
},
|
|
3147
|
-
message: `Property conditions with
|
|
3207
|
+
message: `Property conditions with ≤${maxOperands} operands should be single line: condition: a && b && c. Multi-line only for >${maxOperands} operands`,
|
|
3148
3208
|
node: value,
|
|
3149
3209
|
});
|
|
3150
3210
|
}
|
|
@@ -3574,7 +3634,7 @@ const exportFormat = {
|
|
|
3574
3634
|
[openBrace.range[0], closeBrace.range[1]],
|
|
3575
3635
|
`{ ${specifiersText} }`,
|
|
3576
3636
|
),
|
|
3577
|
-
message: `Exports with
|
|
3637
|
+
message: `Exports with ≤${maxSpecifiers} specifiers should be single line: export { a, b, c }`,
|
|
3578
3638
|
node,
|
|
3579
3639
|
});
|
|
3580
3640
|
}
|
|
@@ -3768,7 +3828,7 @@ const importFormat = {
|
|
|
3768
3828
|
[openBrace.range[0], closeBrace.range[1]],
|
|
3769
3829
|
`{ ${specifiersText} }`,
|
|
3770
3830
|
),
|
|
3771
|
-
message: `Imports with
|
|
3831
|
+
message: `Imports with ≤${maxSpecifiers} specifiers should be single line: import { a, b, c } from "module"`,
|
|
3772
3832
|
node,
|
|
3773
3833
|
});
|
|
3774
3834
|
}
|
|
@@ -5618,7 +5678,7 @@ const classNameDynamicAtEnd = {
|
|
|
5618
5678
|
|
|
5619
5679
|
return fixer.replaceText(templateLiteral, newValue);
|
|
5620
5680
|
},
|
|
5621
|
-
message: "Dynamic expressions
|
|
5681
|
+
message: "Dynamic expressions (${...}) must be at the end of class strings. Use: `static-class ${dynamic}` not `${dynamic} static-class`",
|
|
5622
5682
|
node: reportNode || expressions[i],
|
|
5623
5683
|
});
|
|
5624
5684
|
|
|
@@ -5665,17 +5725,18 @@ const classNameDynamicAtEnd = {
|
|
|
5665
5725
|
*
|
|
5666
5726
|
* Description:
|
|
5667
5727
|
* Disallow multiple consecutive spaces and leading/trailing spaces
|
|
5668
|
-
* in className values. Uses smart detection
|
|
5728
|
+
* in className values. Uses smart detection: checks objects/returns
|
|
5729
|
+
* if variable name contains "class" OR if values look like Tailwind.
|
|
5669
5730
|
*
|
|
5670
5731
|
* ✓ Good:
|
|
5671
5732
|
* className="flex items-center gap-4"
|
|
5672
|
-
* const
|
|
5673
|
-
*
|
|
5733
|
+
* const variants = { primary: "bg-blue-500 text-white" };
|
|
5734
|
+
* return "border-error text-error focus:border-error";
|
|
5674
5735
|
*
|
|
5675
5736
|
* ✗ Bad:
|
|
5676
5737
|
* className="flex items-center gap-4"
|
|
5677
|
-
* const
|
|
5678
|
-
*
|
|
5738
|
+
* const variants = { primary: "bg-blue-500 text-white" };
|
|
5739
|
+
* return "border-error text-error focus:border-error";
|
|
5679
5740
|
*/
|
|
5680
5741
|
const classNameNoExtraSpaces = {
|
|
5681
5742
|
create(context) {
|
|
@@ -5805,15 +5866,15 @@ const classNameNoExtraSpaces = {
|
|
|
5805
5866
|
|
|
5806
5867
|
// Check object with class values
|
|
5807
5868
|
if (node.init && node.init.type === "ObjectExpression") {
|
|
5808
|
-
// Only check objects if variable name suggests classes
|
|
5809
|
-
if (!isClassRelatedName(varName)) return;
|
|
5810
|
-
|
|
5811
5869
|
node.init.properties.forEach((prop) => {
|
|
5812
5870
|
if (prop.type !== "Property") return;
|
|
5813
5871
|
|
|
5814
5872
|
if (prop.value && prop.value.type === "Literal" && typeof prop.value.value === "string") {
|
|
5815
|
-
// For object properties, always check since parent is class-related
|
|
5816
5873
|
const value = prop.value.value;
|
|
5874
|
+
|
|
5875
|
+
// Check if variable name suggests classes OR value looks like Tailwind
|
|
5876
|
+
if (!isClassRelated(varName, value)) return;
|
|
5877
|
+
|
|
5817
5878
|
const raw = sourceCode.getText(prop.value);
|
|
5818
5879
|
const quote = raw[0];
|
|
5819
5880
|
|
|
@@ -5856,15 +5917,33 @@ const classNameNoExtraSpaces = {
|
|
|
5856
5917
|
}
|
|
5857
5918
|
|
|
5858
5919
|
if (prop.value && prop.value.type === "TemplateLiteral") {
|
|
5859
|
-
|
|
5920
|
+
// For template literals, extract static content to check for Tailwind classes
|
|
5921
|
+
const staticContent = prop.value.quasis.map((q) => q.value.raw).join(" ").trim();
|
|
5922
|
+
|
|
5923
|
+
if (isClassRelated(varName, staticContent)) {
|
|
5924
|
+
checkTemplateLiteralHandler(prop.value, varName);
|
|
5925
|
+
}
|
|
5860
5926
|
}
|
|
5861
5927
|
});
|
|
5862
5928
|
}
|
|
5863
5929
|
},
|
|
5930
|
+
|
|
5931
|
+
// Check return statements with Tailwind class values
|
|
5932
|
+
ReturnStatement(node) {
|
|
5933
|
+
if (!node.argument) return;
|
|
5934
|
+
|
|
5935
|
+
if (node.argument.type === "Literal" && typeof node.argument.value === "string") {
|
|
5936
|
+
checkStringLiteralHandler(node.argument, node.argument.value, "return");
|
|
5937
|
+
}
|
|
5938
|
+
|
|
5939
|
+
if (node.argument.type === "TemplateLiteral") {
|
|
5940
|
+
checkTemplateLiteralHandler(node.argument, "return");
|
|
5941
|
+
}
|
|
5942
|
+
},
|
|
5864
5943
|
};
|
|
5865
5944
|
},
|
|
5866
5945
|
meta: {
|
|
5867
|
-
docs: { description: "Disallow extra/leading/trailing spaces in className values" },
|
|
5946
|
+
docs: { description: "Disallow extra/leading/trailing spaces in className values; smart detection for objects with Tailwind values and return statements" },
|
|
5868
5947
|
fixable: "code",
|
|
5869
5948
|
schema: [],
|
|
5870
5949
|
type: "layout",
|
|
@@ -5877,21 +5956,21 @@ const classNameNoExtraSpaces = {
|
|
|
5877
5956
|
* ───────────────────────────────────────────────────────────────
|
|
5878
5957
|
*
|
|
5879
5958
|
* Description:
|
|
5880
|
-
* Enforce Tailwind CSS class ordering in class string variables
|
|
5881
|
-
*
|
|
5882
|
-
* by handling cases it doesn't cover
|
|
5883
|
-
* Uses smart detection
|
|
5959
|
+
* Enforce Tailwind CSS class ordering in class string variables,
|
|
5960
|
+
* object properties, and return statements. Complements
|
|
5961
|
+
* tailwindcss/classnames-order by handling cases it doesn't cover.
|
|
5962
|
+
* Uses smart detection: checks if values look like Tailwind classes.
|
|
5884
5963
|
*
|
|
5885
5964
|
* Note: This rule does NOT check JSX className attributes directly,
|
|
5886
5965
|
* as those should be handled by tailwindcss/classnames-order.
|
|
5887
5966
|
*
|
|
5888
5967
|
* ✓ Good:
|
|
5889
|
-
* const
|
|
5890
|
-
*
|
|
5968
|
+
* const variants = { primary: "bg-blue-500 hover:bg-blue-600" };
|
|
5969
|
+
* return "border-error text-error focus:border-error";
|
|
5891
5970
|
*
|
|
5892
5971
|
* ✗ Bad:
|
|
5893
|
-
* const
|
|
5894
|
-
*
|
|
5972
|
+
* const variants = { primary: "hover:bg-blue-600 bg-blue-500" };
|
|
5973
|
+
* return "focus:border-error text-error border-error";
|
|
5895
5974
|
*/
|
|
5896
5975
|
const classNameOrder = {
|
|
5897
5976
|
create(context) {
|
|
@@ -5910,7 +5989,7 @@ const classNameOrder = {
|
|
|
5910
5989
|
|
|
5911
5990
|
context.report({
|
|
5912
5991
|
fix: (fixer) => fixer.replaceText(node, `${quote}${sorted}${quote}`),
|
|
5913
|
-
message: "Tailwind classes should
|
|
5992
|
+
message: "Tailwind classes should follow recommended order: layout (flex, grid) → sizing (w, h) → spacing (p, m) → typography (text, font) → colors (bg, text) → effects (shadow, opacity) → states (hover, focus)",
|
|
5914
5993
|
node,
|
|
5915
5994
|
});
|
|
5916
5995
|
};
|
|
@@ -5992,7 +6071,7 @@ const classNameOrder = {
|
|
|
5992
6071
|
|
|
5993
6072
|
return fixer.replaceText(templateLiteral, result);
|
|
5994
6073
|
},
|
|
5995
|
-
message: "Tailwind classes should
|
|
6074
|
+
message: "Tailwind classes should follow recommended order: layout (flex, grid) → sizing (w, h) → spacing (p, m) → typography (text, font) → colors (bg, text) → effects (shadow, opacity) → states (hover, focus)",
|
|
5996
6075
|
node: templateLiteral,
|
|
5997
6076
|
});
|
|
5998
6077
|
};
|
|
@@ -6019,14 +6098,15 @@ const classNameOrder = {
|
|
|
6019
6098
|
|
|
6020
6099
|
// Check object with class values
|
|
6021
6100
|
if (node.init && node.init.type === "ObjectExpression") {
|
|
6022
|
-
if (!isClassRelatedName(varName)) return;
|
|
6023
|
-
|
|
6024
6101
|
node.init.properties.forEach((prop) => {
|
|
6025
6102
|
if (prop.type !== "Property") return;
|
|
6026
6103
|
|
|
6027
6104
|
if (prop.value && prop.value.type === "Literal" && typeof prop.value.value === "string") {
|
|
6028
6105
|
const value = prop.value.value;
|
|
6029
6106
|
|
|
6107
|
+
// Check if variable name suggests classes OR value looks like Tailwind
|
|
6108
|
+
if (!isClassRelated(varName, value)) return;
|
|
6109
|
+
|
|
6030
6110
|
if (needsReordering(value)) {
|
|
6031
6111
|
const sorted = sortTailwindClasses(value);
|
|
6032
6112
|
const raw = sourceCode.getText(prop.value);
|
|
@@ -6034,22 +6114,52 @@ const classNameOrder = {
|
|
|
6034
6114
|
|
|
6035
6115
|
context.report({
|
|
6036
6116
|
fix: (fixer) => fixer.replaceText(prop.value, `${quote}${sorted}${quote}`),
|
|
6037
|
-
message: "Tailwind classes should
|
|
6117
|
+
message: "Tailwind classes should follow recommended order: layout (flex, grid) → sizing (w, h) → spacing (p, m) → typography (text, font) → colors (bg, text) → effects (shadow, opacity) → states (hover, focus)",
|
|
6038
6118
|
node: prop.value,
|
|
6039
6119
|
});
|
|
6040
6120
|
}
|
|
6041
6121
|
}
|
|
6042
6122
|
|
|
6043
6123
|
if (prop.value && prop.value.type === "TemplateLiteral") {
|
|
6044
|
-
|
|
6124
|
+
// For template literals, extract static content to check for Tailwind classes
|
|
6125
|
+
const staticContent = prop.value.quasis.map((q) => q.value.raw).join(" ").trim();
|
|
6126
|
+
|
|
6127
|
+
if (isClassRelated(varName, staticContent)) {
|
|
6128
|
+
checkTemplateLiteralOrderHandler(prop.value, varName);
|
|
6129
|
+
}
|
|
6045
6130
|
}
|
|
6046
6131
|
});
|
|
6047
6132
|
}
|
|
6048
6133
|
},
|
|
6134
|
+
|
|
6135
|
+
// Check return statements with Tailwind class values
|
|
6136
|
+
ReturnStatement(node) {
|
|
6137
|
+
if (!node.argument) return;
|
|
6138
|
+
|
|
6139
|
+
if (node.argument.type === "Literal" && typeof node.argument.value === "string") {
|
|
6140
|
+
const value = node.argument.value;
|
|
6141
|
+
|
|
6142
|
+
if (looksLikeTailwindClasses(value) && needsReordering(value)) {
|
|
6143
|
+
const sorted = sortTailwindClasses(value);
|
|
6144
|
+
const raw = sourceCode.getText(node.argument);
|
|
6145
|
+
const quote = raw[0];
|
|
6146
|
+
|
|
6147
|
+
context.report({
|
|
6148
|
+
fix: (fixer) => fixer.replaceText(node.argument, `${quote}${sorted}${quote}`),
|
|
6149
|
+
message: "Tailwind classes should follow recommended order: layout (flex, grid) → sizing (w, h) → spacing (p, m) → typography (text, font) → colors (bg, text) → effects (shadow, opacity) → states (hover, focus)",
|
|
6150
|
+
node: node.argument,
|
|
6151
|
+
});
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6154
|
+
|
|
6155
|
+
if (node.argument.type === "TemplateLiteral") {
|
|
6156
|
+
checkTemplateLiteralOrderHandler(node.argument, "return");
|
|
6157
|
+
}
|
|
6158
|
+
},
|
|
6049
6159
|
};
|
|
6050
6160
|
},
|
|
6051
6161
|
meta: {
|
|
6052
|
-
docs: { description: "Enforce Tailwind CSS class ordering in class strings" },
|
|
6162
|
+
docs: { description: "Enforce Tailwind CSS class ordering in class strings; smart detection for objects with Tailwind values and return statements" },
|
|
6053
6163
|
fixable: "code",
|
|
6054
6164
|
schema: [],
|
|
6055
6165
|
type: "layout",
|
|
@@ -6065,20 +6175,26 @@ const classNameOrder = {
|
|
|
6065
6175
|
* Enforce that long className strings are broken into multiple
|
|
6066
6176
|
* lines, with each class on its own line. Triggers when either
|
|
6067
6177
|
* the class count or string length exceeds the threshold.
|
|
6068
|
-
*
|
|
6178
|
+
* Uses smart detection: checks objects/returns if values look
|
|
6179
|
+
* like Tailwind classes.
|
|
6069
6180
|
*
|
|
6070
6181
|
* ✓ Good:
|
|
6071
|
-
*
|
|
6072
|
-
*
|
|
6073
|
-
*
|
|
6074
|
-
*
|
|
6075
|
-
*
|
|
6076
|
-
*
|
|
6077
|
-
*
|
|
6182
|
+
* const variants = {
|
|
6183
|
+
* primary: `
|
|
6184
|
+
* bg-primary
|
|
6185
|
+
* text-white
|
|
6186
|
+
* hover:bg-primary-dark
|
|
6187
|
+
* `,
|
|
6188
|
+
* };
|
|
6189
|
+
* return `
|
|
6190
|
+
* border-error
|
|
6191
|
+
* text-error
|
|
6192
|
+
* focus:border-error
|
|
6193
|
+
* `;
|
|
6078
6194
|
*
|
|
6079
6195
|
* ✗ Bad:
|
|
6080
|
-
*
|
|
6081
|
-
*
|
|
6196
|
+
* const variants = { primary: "bg-primary text-white hover:bg-primary-dark focus:ring-2" };
|
|
6197
|
+
* return "border-error text-error placeholder-error/50 focus:border-error";
|
|
6082
6198
|
*/
|
|
6083
6199
|
const classNameMultiline = {
|
|
6084
6200
|
create(context) {
|
|
@@ -6208,7 +6324,7 @@ const classNameMultiline = {
|
|
|
6208
6324
|
// Variables/objects: multiline "..." is invalid JS, use template literal
|
|
6209
6325
|
return fixer.replaceText(node, buildMultilineTemplate(classes, [], baseIndent));
|
|
6210
6326
|
},
|
|
6211
|
-
message:
|
|
6327
|
+
message: `Class strings with >${maxClassCount} classes or >${maxLength} chars should be multiline with one class per line. Example: className="\\n flex\\n items-center\\n"`,
|
|
6212
6328
|
node,
|
|
6213
6329
|
});
|
|
6214
6330
|
};
|
|
@@ -6294,7 +6410,7 @@ const classNameMultiline = {
|
|
|
6294
6410
|
|
|
6295
6411
|
context.report({
|
|
6296
6412
|
fix: (fixer) => fixer.replaceText(templateLiteral, multiline),
|
|
6297
|
-
message:
|
|
6413
|
+
message: `Class strings with >${maxClassCount} classes or >${maxLength} chars should be multiline with one class per line. Example: className="\\n flex\\n items-center\\n"`,
|
|
6298
6414
|
node: templateLiteral,
|
|
6299
6415
|
});
|
|
6300
6416
|
};
|
|
@@ -6330,25 +6446,53 @@ const classNameMultiline = {
|
|
|
6330
6446
|
}
|
|
6331
6447
|
|
|
6332
6448
|
if (node.init && node.init.type === "ObjectExpression") {
|
|
6333
|
-
if
|
|
6334
|
-
|
|
6449
|
+
// Check each property - apply rule if name suggests classes OR value looks like Tailwind
|
|
6335
6450
|
node.init.properties.forEach((prop) => {
|
|
6336
6451
|
if (prop.type !== "Property") return;
|
|
6337
6452
|
|
|
6338
6453
|
if (prop.value && prop.value.type === "Literal" && typeof prop.value.value === "string") {
|
|
6339
|
-
|
|
6454
|
+
// Check if variable name suggests classes OR if the value looks like Tailwind classes
|
|
6455
|
+
if (isClassRelated(varName, prop.value.value)) {
|
|
6456
|
+
checkStringLiteralHandler(prop.value, prop.value.value, varName);
|
|
6457
|
+
}
|
|
6340
6458
|
}
|
|
6341
6459
|
|
|
6342
6460
|
if (prop.value && prop.value.type === "TemplateLiteral") {
|
|
6343
|
-
|
|
6461
|
+
// For template literals, extract static content to check for Tailwind classes
|
|
6462
|
+
const staticContent = prop.value.quasis.map((q) => q.value.raw).join(" ").trim();
|
|
6463
|
+
|
|
6464
|
+
if (isClassRelated(varName, staticContent)) {
|
|
6465
|
+
checkTemplateLiteralHandler(prop.value, varName);
|
|
6466
|
+
}
|
|
6344
6467
|
}
|
|
6345
6468
|
});
|
|
6346
6469
|
}
|
|
6347
6470
|
},
|
|
6471
|
+
|
|
6472
|
+
// Check return statements with Tailwind class values
|
|
6473
|
+
ReturnStatement(node) {
|
|
6474
|
+
if (!node.argument) return;
|
|
6475
|
+
|
|
6476
|
+
if (node.argument.type === "Literal" && typeof node.argument.value === "string") {
|
|
6477
|
+
const value = node.argument.value;
|
|
6478
|
+
|
|
6479
|
+
if (looksLikeTailwindClasses(value)) {
|
|
6480
|
+
checkStringLiteralHandler(node.argument, value, "return");
|
|
6481
|
+
}
|
|
6482
|
+
}
|
|
6483
|
+
|
|
6484
|
+
if (node.argument.type === "TemplateLiteral") {
|
|
6485
|
+
const staticContent = node.argument.quasis.map((q) => q.value.raw).join(" ").trim();
|
|
6486
|
+
|
|
6487
|
+
if (looksLikeTailwindClasses(staticContent)) {
|
|
6488
|
+
checkTemplateLiteralHandler(node.argument, "return");
|
|
6489
|
+
}
|
|
6490
|
+
}
|
|
6491
|
+
},
|
|
6348
6492
|
};
|
|
6349
6493
|
},
|
|
6350
6494
|
meta: {
|
|
6351
|
-
docs: { description: "Enforce multiline formatting for long className strings" },
|
|
6495
|
+
docs: { description: "Enforce multiline formatting for long className strings; smart detection for objects with Tailwind values and return statements" },
|
|
6352
6496
|
fixable: "code",
|
|
6353
6497
|
schema: [
|
|
6354
6498
|
{
|
|
@@ -7408,7 +7552,7 @@ const functionArgumentsFormat = {
|
|
|
7408
7552
|
[openParen.range[1], firstArg.range[0]],
|
|
7409
7553
|
"\n" + argIndent,
|
|
7410
7554
|
),
|
|
7411
|
-
message: "
|
|
7555
|
+
message: "With multiple arguments, first argument should be on its own line: fn(\\n arg1,\\n arg2,\\n)",
|
|
7412
7556
|
node: firstArg,
|
|
7413
7557
|
});
|
|
7414
7558
|
}
|
|
@@ -8708,7 +8852,7 @@ const objectPropertyPerLine = {
|
|
|
8708
8852
|
[openBrace.range[0], closeBrace.range[1]],
|
|
8709
8853
|
`{ ${propertiesText} }`,
|
|
8710
8854
|
),
|
|
8711
|
-
message: `Objects with
|
|
8855
|
+
message: `Objects with <${minProperties} properties should be single line: { key: value }. Multi-line only for ${minProperties}+ properties`,
|
|
8712
8856
|
node,
|
|
8713
8857
|
});
|
|
8714
8858
|
|
|
@@ -10733,22 +10877,25 @@ const stringPropertySpacing = {
|
|
|
10733
10877
|
*
|
|
10734
10878
|
* Description:
|
|
10735
10879
|
* Variable names should follow naming conventions: camelCase
|
|
10736
|
-
* for regular variables
|
|
10737
|
-
*
|
|
10880
|
+
* for regular variables and PascalCase for React components.
|
|
10881
|
+
* Auto-fixes SCREAMING_SNAKE_CASE and snake_case to camelCase.
|
|
10738
10882
|
*
|
|
10739
10883
|
* ✓ Good:
|
|
10740
10884
|
* const userName = "John";
|
|
10741
|
-
* const
|
|
10885
|
+
* const maxRetries = 3;
|
|
10886
|
+
* const codeLength = 8;
|
|
10742
10887
|
* const UserProfile = () => <div />;
|
|
10743
10888
|
* const useCustomHook = () => {};
|
|
10744
10889
|
*
|
|
10745
|
-
* ✗ Bad:
|
|
10746
|
-
* const user_name = "John";
|
|
10747
|
-
* const
|
|
10748
|
-
* const
|
|
10890
|
+
* ✗ Bad (auto-fixed):
|
|
10891
|
+
* const user_name = "John"; // → userName
|
|
10892
|
+
* const CODE_LENGTH = 8; // → codeLength
|
|
10893
|
+
* const MAX_RETRIES = 3; // → maxRetries
|
|
10749
10894
|
*/
|
|
10750
10895
|
const variableNamingConvention = {
|
|
10751
10896
|
create(context) {
|
|
10897
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
10898
|
+
|
|
10752
10899
|
const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
|
|
10753
10900
|
|
|
10754
10901
|
const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
|
|
@@ -10757,6 +10904,36 @@ const variableNamingConvention = {
|
|
|
10757
10904
|
|
|
10758
10905
|
const constantRegex = /^[A-Z][A-Z0-9_]*$/;
|
|
10759
10906
|
|
|
10907
|
+
// Convert any naming convention to camelCase
|
|
10908
|
+
const toCamelCaseHandler = (name) => {
|
|
10909
|
+
// Handle SCREAMING_SNAKE_CASE (e.g., CODE_LENGTH -> codeLength)
|
|
10910
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(name)) {
|
|
10911
|
+
return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
10912
|
+
}
|
|
10913
|
+
|
|
10914
|
+
// Handle snake_case (e.g., user_name -> userName)
|
|
10915
|
+
if (/_/.test(name)) {
|
|
10916
|
+
return name.toLowerCase().replace(/_([a-z0-9])/g, (_, char) => char.toUpperCase());
|
|
10917
|
+
}
|
|
10918
|
+
|
|
10919
|
+
// Handle PascalCase (e.g., UserName -> userName)
|
|
10920
|
+
if (/^[A-Z]/.test(name)) {
|
|
10921
|
+
return name[0].toLowerCase() + name.slice(1);
|
|
10922
|
+
}
|
|
10923
|
+
|
|
10924
|
+
return name;
|
|
10925
|
+
};
|
|
10926
|
+
|
|
10927
|
+
// Get all references to a variable in the current scope
|
|
10928
|
+
const getVariableReferencesHandler = (node) => {
|
|
10929
|
+
const scope = sourceCode.getScope ? sourceCode.getScope(node) : context.getScope();
|
|
10930
|
+
const variable = scope.variables.find((v) => v.name === node.name);
|
|
10931
|
+
|
|
10932
|
+
if (!variable) return [];
|
|
10933
|
+
|
|
10934
|
+
return variable.references.map((ref) => ref.identifier);
|
|
10935
|
+
};
|
|
10936
|
+
|
|
10760
10937
|
const allowedIdentifiers = [
|
|
10761
10938
|
"ArrowFunctionExpression", "CallExpression", "FunctionDeclaration", "FunctionExpression",
|
|
10762
10939
|
"Property", "VariableDeclarator", "JSXElement", "JSXOpeningElement", "ReturnStatement",
|
|
@@ -10967,8 +11144,21 @@ const variableNamingConvention = {
|
|
|
10967
11144
|
}
|
|
10968
11145
|
|
|
10969
11146
|
if (!camelCaseRegex.test(name)) {
|
|
11147
|
+
const camelCaseName = toCamelCaseHandler(name);
|
|
11148
|
+
const references = getVariableReferencesHandler(node.id);
|
|
11149
|
+
|
|
10970
11150
|
context.report({
|
|
10971
|
-
|
|
11151
|
+
fix: (fixer) => {
|
|
11152
|
+
const fixes = [];
|
|
11153
|
+
|
|
11154
|
+
// Fix all references to this variable
|
|
11155
|
+
references.forEach((ref) => {
|
|
11156
|
+
fixes.push(fixer.replaceText(ref, camelCaseName));
|
|
11157
|
+
});
|
|
11158
|
+
|
|
11159
|
+
return fixes;
|
|
11160
|
+
},
|
|
11161
|
+
message: `Variable "${name}" should be camelCase (e.g., ${camelCaseName} instead of ${name})`,
|
|
10972
11162
|
node: node.id,
|
|
10973
11163
|
});
|
|
10974
11164
|
}
|
|
@@ -11026,9 +11216,36 @@ const variableNamingConvention = {
|
|
|
11026
11216
|
// Allow component property names as arguments (e.g., Icon, Component)
|
|
11027
11217
|
if (componentPropertyNames.includes(name)) return;
|
|
11028
11218
|
|
|
11219
|
+
// Skip PascalCase that doesn't look like a misnamed function
|
|
11220
|
+
// (function-naming-convention handles verb-prefixed PascalCase)
|
|
11221
|
+
if (pascalCaseRegex.test(name)) return;
|
|
11222
|
+
|
|
11029
11223
|
if (!camelCaseRegex.test(name)) {
|
|
11224
|
+
const camelCaseName = toCamelCaseHandler(name);
|
|
11225
|
+
|
|
11030
11226
|
context.report({
|
|
11031
|
-
|
|
11227
|
+
fix(fixer) {
|
|
11228
|
+
const scope = sourceCode.getScope ? sourceCode.getScope(arg) : context.getScope();
|
|
11229
|
+
const variable = scope.variables.find((v) => v.name === name)
|
|
11230
|
+
|| (scope.upper && scope.upper.variables.find((v) => v.name === name));
|
|
11231
|
+
|
|
11232
|
+
if (!variable) return fixer.replaceText(arg, camelCaseName);
|
|
11233
|
+
|
|
11234
|
+
const fixes = [];
|
|
11235
|
+
const fixedRanges = new Set();
|
|
11236
|
+
|
|
11237
|
+
variable.references.forEach((ref) => {
|
|
11238
|
+
const rangeKey = `${ref.identifier.range[0]}-${ref.identifier.range[1]}`;
|
|
11239
|
+
|
|
11240
|
+
if (!fixedRanges.has(rangeKey)) {
|
|
11241
|
+
fixedRanges.add(rangeKey);
|
|
11242
|
+
fixes.push(fixer.replaceText(ref.identifier, camelCaseName));
|
|
11243
|
+
}
|
|
11244
|
+
});
|
|
11245
|
+
|
|
11246
|
+
return fixes;
|
|
11247
|
+
},
|
|
11248
|
+
message: `Argument "${name}" should be camelCase (e.g., ${camelCaseName} instead of ${name})`,
|
|
11032
11249
|
node: arg,
|
|
11033
11250
|
});
|
|
11034
11251
|
}
|
|
@@ -11047,6 +11264,7 @@ const variableNamingConvention = {
|
|
|
11047
11264
|
},
|
|
11048
11265
|
meta: {
|
|
11049
11266
|
docs: { description: "Enforce naming conventions: camelCase for variables/properties/params/arguments, PascalCase for components, useXxx for hooks" },
|
|
11267
|
+
fixable: "code",
|
|
11050
11268
|
schema: [],
|
|
11051
11269
|
type: "suggestion",
|
|
11052
11270
|
},
|
|
@@ -12754,7 +12972,7 @@ const typeFormat = {
|
|
|
12754
12972
|
fix(fixer) {
|
|
12755
12973
|
return fixer.replaceText(node.id, `${typeName}Type`);
|
|
12756
12974
|
},
|
|
12757
|
-
message: `Type name "${typeName}" must end with "Type" suffix`,
|
|
12975
|
+
message: `Type name "${typeName}" must end with "Type" suffix. Use "${typeName}Type" instead of "${typeName}"`,
|
|
12758
12976
|
node: node.id,
|
|
12759
12977
|
});
|
|
12760
12978
|
}
|
|
@@ -13703,7 +13921,7 @@ const reactCodeOrder = {
|
|
|
13703
13921
|
previous: ORDER_NAMES[lastCategory],
|
|
13704
13922
|
type: isHook ? "hook" : "component",
|
|
13705
13923
|
},
|
|
13706
|
-
message: "\"{{current}}\" should come before \"{{previous}}\" in {{type}}
|
|
13924
|
+
message: "\"{{current}}\" should come before \"{{previous}}\" in {{type}}. Order: refs → state → redux → router → context → custom hooks → derived → useMemo → useCallback → handlers → useEffect → return",
|
|
13707
13925
|
node: statement,
|
|
13708
13926
|
});
|
|
13709
13927
|
|
|
@@ -13765,7 +13983,7 @@ const enumFormat = {
|
|
|
13765
13983
|
fix(fixer) {
|
|
13766
13984
|
return fixer.replaceText(node.id, `${enumName}Enum`);
|
|
13767
13985
|
},
|
|
13768
|
-
message: `Enum name "${enumName}" must end with "Enum" suffix`,
|
|
13986
|
+
message: `Enum name "${enumName}" must end with "Enum" suffix. Use "${enumName}Enum" instead of "${enumName}"`,
|
|
13769
13987
|
node: node.id,
|
|
13770
13988
|
});
|
|
13771
13989
|
}
|
|
@@ -13987,7 +14205,7 @@ const interfaceFormat = {
|
|
|
13987
14205
|
fix(fixer) {
|
|
13988
14206
|
return fixer.replaceText(node.id, `${interfaceName}Interface`);
|
|
13989
14207
|
},
|
|
13990
|
-
message: `Interface name "${interfaceName}" must end with "Interface" suffix`,
|
|
14208
|
+
message: `Interface name "${interfaceName}" must end with "Interface" suffix. Use "${interfaceName}Interface" instead of "${interfaceName}"`,
|
|
13991
14209
|
node: node.id,
|
|
13992
14210
|
});
|
|
13993
14211
|
}
|
package/package.json
CHANGED