eslint-plugin-code-style 1.3.3 → 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 +281 -74
- 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
|
|
|
@@ -2604,7 +2653,7 @@ const hookDepsPerLine = {
|
|
|
2604
2653
|
[openBracket.range[1], closeBracket.range[0]],
|
|
2605
2654
|
elementsText,
|
|
2606
2655
|
),
|
|
2607
|
-
message: `
|
|
2656
|
+
message: `Hook dependencies with ≤${maxDeps} items should be single line: [dep1, dep2]. Multi-line only for >${maxDeps} dependencies`,
|
|
2608
2657
|
node: depsArg,
|
|
2609
2658
|
});
|
|
2610
2659
|
}
|
|
@@ -2995,7 +3044,7 @@ const multilineIfConditions = {
|
|
|
2995
3044
|
`(${buildSameLineHandler(test)})`,
|
|
2996
3045
|
);
|
|
2997
3046
|
},
|
|
2998
|
-
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`,
|
|
2999
3048
|
node: test,
|
|
3000
3049
|
});
|
|
3001
3050
|
}
|
|
@@ -3155,7 +3204,7 @@ const multilineIfConditions = {
|
|
|
3155
3204
|
|
|
3156
3205
|
return fixer.replaceText(value, buildSameLineHandler(value));
|
|
3157
3206
|
},
|
|
3158
|
-
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`,
|
|
3159
3208
|
node: value,
|
|
3160
3209
|
});
|
|
3161
3210
|
}
|
|
@@ -3585,7 +3634,7 @@ const exportFormat = {
|
|
|
3585
3634
|
[openBrace.range[0], closeBrace.range[1]],
|
|
3586
3635
|
`{ ${specifiersText} }`,
|
|
3587
3636
|
),
|
|
3588
|
-
message: `Exports with
|
|
3637
|
+
message: `Exports with ≤${maxSpecifiers} specifiers should be single line: export { a, b, c }`,
|
|
3589
3638
|
node,
|
|
3590
3639
|
});
|
|
3591
3640
|
}
|
|
@@ -3779,7 +3828,7 @@ const importFormat = {
|
|
|
3779
3828
|
[openBrace.range[0], closeBrace.range[1]],
|
|
3780
3829
|
`{ ${specifiersText} }`,
|
|
3781
3830
|
),
|
|
3782
|
-
message: `Imports with
|
|
3831
|
+
message: `Imports with ≤${maxSpecifiers} specifiers should be single line: import { a, b, c } from "module"`,
|
|
3783
3832
|
node,
|
|
3784
3833
|
});
|
|
3785
3834
|
}
|
|
@@ -5629,7 +5678,7 @@ const classNameDynamicAtEnd = {
|
|
|
5629
5678
|
|
|
5630
5679
|
return fixer.replaceText(templateLiteral, newValue);
|
|
5631
5680
|
},
|
|
5632
|
-
message: "Dynamic expressions
|
|
5681
|
+
message: "Dynamic expressions (${...}) must be at the end of class strings. Use: `static-class ${dynamic}` not `${dynamic} static-class`",
|
|
5633
5682
|
node: reportNode || expressions[i],
|
|
5634
5683
|
});
|
|
5635
5684
|
|
|
@@ -5676,17 +5725,18 @@ const classNameDynamicAtEnd = {
|
|
|
5676
5725
|
*
|
|
5677
5726
|
* Description:
|
|
5678
5727
|
* Disallow multiple consecutive spaces and leading/trailing spaces
|
|
5679
|
-
* 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.
|
|
5680
5730
|
*
|
|
5681
5731
|
* ✓ Good:
|
|
5682
5732
|
* className="flex items-center gap-4"
|
|
5683
|
-
* const
|
|
5684
|
-
*
|
|
5733
|
+
* const variants = { primary: "bg-blue-500 text-white" };
|
|
5734
|
+
* return "border-error text-error focus:border-error";
|
|
5685
5735
|
*
|
|
5686
5736
|
* ✗ Bad:
|
|
5687
5737
|
* className="flex items-center gap-4"
|
|
5688
|
-
* const
|
|
5689
|
-
*
|
|
5738
|
+
* const variants = { primary: "bg-blue-500 text-white" };
|
|
5739
|
+
* return "border-error text-error focus:border-error";
|
|
5690
5740
|
*/
|
|
5691
5741
|
const classNameNoExtraSpaces = {
|
|
5692
5742
|
create(context) {
|
|
@@ -5816,15 +5866,15 @@ const classNameNoExtraSpaces = {
|
|
|
5816
5866
|
|
|
5817
5867
|
// Check object with class values
|
|
5818
5868
|
if (node.init && node.init.type === "ObjectExpression") {
|
|
5819
|
-
// Only check objects if variable name suggests classes
|
|
5820
|
-
if (!isClassRelatedName(varName)) return;
|
|
5821
|
-
|
|
5822
5869
|
node.init.properties.forEach((prop) => {
|
|
5823
5870
|
if (prop.type !== "Property") return;
|
|
5824
5871
|
|
|
5825
5872
|
if (prop.value && prop.value.type === "Literal" && typeof prop.value.value === "string") {
|
|
5826
|
-
// For object properties, always check since parent is class-related
|
|
5827
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
|
+
|
|
5828
5878
|
const raw = sourceCode.getText(prop.value);
|
|
5829
5879
|
const quote = raw[0];
|
|
5830
5880
|
|
|
@@ -5867,15 +5917,33 @@ const classNameNoExtraSpaces = {
|
|
|
5867
5917
|
}
|
|
5868
5918
|
|
|
5869
5919
|
if (prop.value && prop.value.type === "TemplateLiteral") {
|
|
5870
|
-
|
|
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
|
+
}
|
|
5871
5926
|
}
|
|
5872
5927
|
});
|
|
5873
5928
|
}
|
|
5874
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
|
+
},
|
|
5875
5943
|
};
|
|
5876
5944
|
},
|
|
5877
5945
|
meta: {
|
|
5878
|
-
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" },
|
|
5879
5947
|
fixable: "code",
|
|
5880
5948
|
schema: [],
|
|
5881
5949
|
type: "layout",
|
|
@@ -5888,21 +5956,21 @@ const classNameNoExtraSpaces = {
|
|
|
5888
5956
|
* ───────────────────────────────────────────────────────────────
|
|
5889
5957
|
*
|
|
5890
5958
|
* Description:
|
|
5891
|
-
* Enforce Tailwind CSS class ordering in class string variables
|
|
5892
|
-
*
|
|
5893
|
-
* by handling cases it doesn't cover
|
|
5894
|
-
* 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.
|
|
5895
5963
|
*
|
|
5896
5964
|
* Note: This rule does NOT check JSX className attributes directly,
|
|
5897
5965
|
* as those should be handled by tailwindcss/classnames-order.
|
|
5898
5966
|
*
|
|
5899
5967
|
* ✓ Good:
|
|
5900
|
-
* const
|
|
5901
|
-
*
|
|
5968
|
+
* const variants = { primary: "bg-blue-500 hover:bg-blue-600" };
|
|
5969
|
+
* return "border-error text-error focus:border-error";
|
|
5902
5970
|
*
|
|
5903
5971
|
* ✗ Bad:
|
|
5904
|
-
* const
|
|
5905
|
-
*
|
|
5972
|
+
* const variants = { primary: "hover:bg-blue-600 bg-blue-500" };
|
|
5973
|
+
* return "focus:border-error text-error border-error";
|
|
5906
5974
|
*/
|
|
5907
5975
|
const classNameOrder = {
|
|
5908
5976
|
create(context) {
|
|
@@ -5921,7 +5989,7 @@ const classNameOrder = {
|
|
|
5921
5989
|
|
|
5922
5990
|
context.report({
|
|
5923
5991
|
fix: (fixer) => fixer.replaceText(node, `${quote}${sorted}${quote}`),
|
|
5924
|
-
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)",
|
|
5925
5993
|
node,
|
|
5926
5994
|
});
|
|
5927
5995
|
};
|
|
@@ -6003,7 +6071,7 @@ const classNameOrder = {
|
|
|
6003
6071
|
|
|
6004
6072
|
return fixer.replaceText(templateLiteral, result);
|
|
6005
6073
|
},
|
|
6006
|
-
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)",
|
|
6007
6075
|
node: templateLiteral,
|
|
6008
6076
|
});
|
|
6009
6077
|
};
|
|
@@ -6030,14 +6098,15 @@ const classNameOrder = {
|
|
|
6030
6098
|
|
|
6031
6099
|
// Check object with class values
|
|
6032
6100
|
if (node.init && node.init.type === "ObjectExpression") {
|
|
6033
|
-
if (!isClassRelatedName(varName)) return;
|
|
6034
|
-
|
|
6035
6101
|
node.init.properties.forEach((prop) => {
|
|
6036
6102
|
if (prop.type !== "Property") return;
|
|
6037
6103
|
|
|
6038
6104
|
if (prop.value && prop.value.type === "Literal" && typeof prop.value.value === "string") {
|
|
6039
6105
|
const value = prop.value.value;
|
|
6040
6106
|
|
|
6107
|
+
// Check if variable name suggests classes OR value looks like Tailwind
|
|
6108
|
+
if (!isClassRelated(varName, value)) return;
|
|
6109
|
+
|
|
6041
6110
|
if (needsReordering(value)) {
|
|
6042
6111
|
const sorted = sortTailwindClasses(value);
|
|
6043
6112
|
const raw = sourceCode.getText(prop.value);
|
|
@@ -6045,22 +6114,52 @@ const classNameOrder = {
|
|
|
6045
6114
|
|
|
6046
6115
|
context.report({
|
|
6047
6116
|
fix: (fixer) => fixer.replaceText(prop.value, `${quote}${sorted}${quote}`),
|
|
6048
|
-
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)",
|
|
6049
6118
|
node: prop.value,
|
|
6050
6119
|
});
|
|
6051
6120
|
}
|
|
6052
6121
|
}
|
|
6053
6122
|
|
|
6054
6123
|
if (prop.value && prop.value.type === "TemplateLiteral") {
|
|
6055
|
-
|
|
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
|
+
}
|
|
6056
6130
|
}
|
|
6057
6131
|
});
|
|
6058
6132
|
}
|
|
6059
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
|
+
},
|
|
6060
6159
|
};
|
|
6061
6160
|
},
|
|
6062
6161
|
meta: {
|
|
6063
|
-
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" },
|
|
6064
6163
|
fixable: "code",
|
|
6065
6164
|
schema: [],
|
|
6066
6165
|
type: "layout",
|
|
@@ -6076,20 +6175,26 @@ const classNameOrder = {
|
|
|
6076
6175
|
* Enforce that long className strings are broken into multiple
|
|
6077
6176
|
* lines, with each class on its own line. Triggers when either
|
|
6078
6177
|
* the class count or string length exceeds the threshold.
|
|
6079
|
-
*
|
|
6178
|
+
* Uses smart detection: checks objects/returns if values look
|
|
6179
|
+
* like Tailwind classes.
|
|
6080
6180
|
*
|
|
6081
6181
|
* ✓ Good:
|
|
6082
|
-
*
|
|
6083
|
-
*
|
|
6084
|
-
*
|
|
6085
|
-
*
|
|
6086
|
-
*
|
|
6087
|
-
*
|
|
6088
|
-
*
|
|
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
|
+
* `;
|
|
6089
6194
|
*
|
|
6090
6195
|
* ✗ Bad:
|
|
6091
|
-
*
|
|
6092
|
-
*
|
|
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";
|
|
6093
6198
|
*/
|
|
6094
6199
|
const classNameMultiline = {
|
|
6095
6200
|
create(context) {
|
|
@@ -6219,7 +6324,7 @@ const classNameMultiline = {
|
|
|
6219
6324
|
// Variables/objects: multiline "..." is invalid JS, use template literal
|
|
6220
6325
|
return fixer.replaceText(node, buildMultilineTemplate(classes, [], baseIndent));
|
|
6221
6326
|
},
|
|
6222
|
-
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"`,
|
|
6223
6328
|
node,
|
|
6224
6329
|
});
|
|
6225
6330
|
};
|
|
@@ -6305,7 +6410,7 @@ const classNameMultiline = {
|
|
|
6305
6410
|
|
|
6306
6411
|
context.report({
|
|
6307
6412
|
fix: (fixer) => fixer.replaceText(templateLiteral, multiline),
|
|
6308
|
-
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"`,
|
|
6309
6414
|
node: templateLiteral,
|
|
6310
6415
|
});
|
|
6311
6416
|
};
|
|
@@ -6341,25 +6446,53 @@ const classNameMultiline = {
|
|
|
6341
6446
|
}
|
|
6342
6447
|
|
|
6343
6448
|
if (node.init && node.init.type === "ObjectExpression") {
|
|
6344
|
-
if
|
|
6345
|
-
|
|
6449
|
+
// Check each property - apply rule if name suggests classes OR value looks like Tailwind
|
|
6346
6450
|
node.init.properties.forEach((prop) => {
|
|
6347
6451
|
if (prop.type !== "Property") return;
|
|
6348
6452
|
|
|
6349
6453
|
if (prop.value && prop.value.type === "Literal" && typeof prop.value.value === "string") {
|
|
6350
|
-
|
|
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
|
+
}
|
|
6351
6458
|
}
|
|
6352
6459
|
|
|
6353
6460
|
if (prop.value && prop.value.type === "TemplateLiteral") {
|
|
6354
|
-
|
|
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
|
+
}
|
|
6355
6467
|
}
|
|
6356
6468
|
});
|
|
6357
6469
|
}
|
|
6358
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
|
+
},
|
|
6359
6492
|
};
|
|
6360
6493
|
},
|
|
6361
6494
|
meta: {
|
|
6362
|
-
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" },
|
|
6363
6496
|
fixable: "code",
|
|
6364
6497
|
schema: [
|
|
6365
6498
|
{
|
|
@@ -7419,7 +7552,7 @@ const functionArgumentsFormat = {
|
|
|
7419
7552
|
[openParen.range[1], firstArg.range[0]],
|
|
7420
7553
|
"\n" + argIndent,
|
|
7421
7554
|
),
|
|
7422
|
-
message: "
|
|
7555
|
+
message: "With multiple arguments, first argument should be on its own line: fn(\\n arg1,\\n arg2,\\n)",
|
|
7423
7556
|
node: firstArg,
|
|
7424
7557
|
});
|
|
7425
7558
|
}
|
|
@@ -8719,7 +8852,7 @@ const objectPropertyPerLine = {
|
|
|
8719
8852
|
[openBrace.range[0], closeBrace.range[1]],
|
|
8720
8853
|
`{ ${propertiesText} }`,
|
|
8721
8854
|
),
|
|
8722
|
-
message: `Objects with
|
|
8855
|
+
message: `Objects with <${minProperties} properties should be single line: { key: value }. Multi-line only for ${minProperties}+ properties`,
|
|
8723
8856
|
node,
|
|
8724
8857
|
});
|
|
8725
8858
|
|
|
@@ -10744,22 +10877,25 @@ const stringPropertySpacing = {
|
|
|
10744
10877
|
*
|
|
10745
10878
|
* Description:
|
|
10746
10879
|
* Variable names should follow naming conventions: camelCase
|
|
10747
|
-
* for regular variables
|
|
10748
|
-
*
|
|
10880
|
+
* for regular variables and PascalCase for React components.
|
|
10881
|
+
* Auto-fixes SCREAMING_SNAKE_CASE and snake_case to camelCase.
|
|
10749
10882
|
*
|
|
10750
10883
|
* ✓ Good:
|
|
10751
10884
|
* const userName = "John";
|
|
10752
|
-
* const
|
|
10885
|
+
* const maxRetries = 3;
|
|
10886
|
+
* const codeLength = 8;
|
|
10753
10887
|
* const UserProfile = () => <div />;
|
|
10754
10888
|
* const useCustomHook = () => {};
|
|
10755
10889
|
*
|
|
10756
|
-
* ✗ Bad:
|
|
10757
|
-
* const user_name = "John";
|
|
10758
|
-
* const
|
|
10759
|
-
* const
|
|
10890
|
+
* ✗ Bad (auto-fixed):
|
|
10891
|
+
* const user_name = "John"; // → userName
|
|
10892
|
+
* const CODE_LENGTH = 8; // → codeLength
|
|
10893
|
+
* const MAX_RETRIES = 3; // → maxRetries
|
|
10760
10894
|
*/
|
|
10761
10895
|
const variableNamingConvention = {
|
|
10762
10896
|
create(context) {
|
|
10897
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
10898
|
+
|
|
10763
10899
|
const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
|
|
10764
10900
|
|
|
10765
10901
|
const pascalCaseRegex = /^[A-Z][a-zA-Z0-9]*$/;
|
|
@@ -10768,6 +10904,36 @@ const variableNamingConvention = {
|
|
|
10768
10904
|
|
|
10769
10905
|
const constantRegex = /^[A-Z][A-Z0-9_]*$/;
|
|
10770
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
|
+
|
|
10771
10937
|
const allowedIdentifiers = [
|
|
10772
10938
|
"ArrowFunctionExpression", "CallExpression", "FunctionDeclaration", "FunctionExpression",
|
|
10773
10939
|
"Property", "VariableDeclarator", "JSXElement", "JSXOpeningElement", "ReturnStatement",
|
|
@@ -10978,8 +11144,21 @@ const variableNamingConvention = {
|
|
|
10978
11144
|
}
|
|
10979
11145
|
|
|
10980
11146
|
if (!camelCaseRegex.test(name)) {
|
|
11147
|
+
const camelCaseName = toCamelCaseHandler(name);
|
|
11148
|
+
const references = getVariableReferencesHandler(node.id);
|
|
11149
|
+
|
|
10981
11150
|
context.report({
|
|
10982
|
-
|
|
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})`,
|
|
10983
11162
|
node: node.id,
|
|
10984
11163
|
});
|
|
10985
11164
|
}
|
|
@@ -11037,9 +11216,36 @@ const variableNamingConvention = {
|
|
|
11037
11216
|
// Allow component property names as arguments (e.g., Icon, Component)
|
|
11038
11217
|
if (componentPropertyNames.includes(name)) return;
|
|
11039
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
|
+
|
|
11040
11223
|
if (!camelCaseRegex.test(name)) {
|
|
11224
|
+
const camelCaseName = toCamelCaseHandler(name);
|
|
11225
|
+
|
|
11041
11226
|
context.report({
|
|
11042
|
-
|
|
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})`,
|
|
11043
11249
|
node: arg,
|
|
11044
11250
|
});
|
|
11045
11251
|
}
|
|
@@ -11058,6 +11264,7 @@ const variableNamingConvention = {
|
|
|
11058
11264
|
},
|
|
11059
11265
|
meta: {
|
|
11060
11266
|
docs: { description: "Enforce naming conventions: camelCase for variables/properties/params/arguments, PascalCase for components, useXxx for hooks" },
|
|
11267
|
+
fixable: "code",
|
|
11061
11268
|
schema: [],
|
|
11062
11269
|
type: "suggestion",
|
|
11063
11270
|
},
|
|
@@ -12765,7 +12972,7 @@ const typeFormat = {
|
|
|
12765
12972
|
fix(fixer) {
|
|
12766
12973
|
return fixer.replaceText(node.id, `${typeName}Type`);
|
|
12767
12974
|
},
|
|
12768
|
-
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}"`,
|
|
12769
12976
|
node: node.id,
|
|
12770
12977
|
});
|
|
12771
12978
|
}
|
|
@@ -13714,7 +13921,7 @@ const reactCodeOrder = {
|
|
|
13714
13921
|
previous: ORDER_NAMES[lastCategory],
|
|
13715
13922
|
type: isHook ? "hook" : "component",
|
|
13716
13923
|
},
|
|
13717
|
-
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",
|
|
13718
13925
|
node: statement,
|
|
13719
13926
|
});
|
|
13720
13927
|
|
|
@@ -13776,7 +13983,7 @@ const enumFormat = {
|
|
|
13776
13983
|
fix(fixer) {
|
|
13777
13984
|
return fixer.replaceText(node.id, `${enumName}Enum`);
|
|
13778
13985
|
},
|
|
13779
|
-
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}"`,
|
|
13780
13987
|
node: node.id,
|
|
13781
13988
|
});
|
|
13782
13989
|
}
|
|
@@ -13998,7 +14205,7 @@ const interfaceFormat = {
|
|
|
13998
14205
|
fix(fixer) {
|
|
13999
14206
|
return fixer.replaceText(node.id, `${interfaceName}Interface`);
|
|
14000
14207
|
},
|
|
14001
|
-
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}"`,
|
|
14002
14209
|
node: node.id,
|
|
14003
14210
|
});
|
|
14004
14211
|
}
|
package/package.json
CHANGED