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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/index.js +294 -76
  3. 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, one class per line; uses `"..."` in JSX (no expressions) or `` `...` `` (with expressions/variables) ⚙️ |
304
- | `classname-no-extra-spaces` | No extra/leading/trailing spaces in class strings (JSX, variables, and objects) |
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 ${maxItems} or fewer items should be on one line`,
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 simple statement should use expression body",
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
- fixes.push(fixer.replaceText(def.name, newName));
1940
+ addFixHandler(def.name);
1881
1941
  });
1882
1942
 
1883
1943
  // Fix all references
1884
1944
  variable.references.forEach((ref) => {
1885
- fixes.push(fixer.replaceText(ref.identifier, newName));
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: `Dependencies should be on same line when ${maxDeps} or fewer`,
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 ${maxOperands} or fewer operands should have all operands start on the same line`,
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 ${maxOperands} or fewer operands should have all operands start on the same line`,
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 ${maxSpecifiers} or fewer specifiers should be on a single line`,
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 ${maxSpecifiers} or fewer specifiers should be on a single line`,
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 in class string should be at the end, after static classes",
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 to identify class strings.
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 buttonClasses = `flex items-center ${className}`;
5673
- * const variantClasses = { primary: "bg-blue-500 text-white" };
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 buttonClasses = ` flex items-center ${className} `;
5678
- * const variantClasses = { primary: "bg-blue-500 text-white" };
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
- checkTemplateLiteralHandler(prop.value, varName);
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
- * and object properties. This rule complements tailwindcss/classnames-order
5882
- * by handling cases it doesn't cover (variables and objects).
5883
- * Uses smart detection to identify class strings by name or content.
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 classes = "flex items-center p-4 text-white";
5890
- * const variantClasses = { primary: "bg-blue-500 hover:bg-blue-600" };
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 classes = "text-white p-4 flex items-center";
5894
- * const variantClasses = { primary: "hover:bg-blue-600 bg-blue-500" };
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 be ordered according to recommended order",
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 be ordered according to recommended order",
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 be ordered according to recommended order",
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
- checkTemplateLiteralOrderHandler(prop.value, varName);
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
- * Dynamic expressions must remain at the end.
6178
+ * Uses smart detection: checks objects/returns if values look
6179
+ * like Tailwind classes.
6069
6180
  *
6070
6181
  * ✓ Good:
6071
- * className={`
6072
- * flex
6073
- * items-center
6074
- * justify-center
6075
- * rounded-lg
6076
- * ${className}
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
- * className="flex items-center justify-center rounded-lg"
6081
- * className={`flex items-center justify-center rounded-lg ${className}`}
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: "Class string with many classes should be broken into multiple lines",
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: "Class string with many classes should be broken into multiple lines",
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 (!isClassRelatedName(varName)) return;
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
- checkStringLiteralHandler(prop.value, prop.value.value, varName);
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
- checkTemplateLiteralHandler(prop.value, varName);
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: "First argument should be on its own line",
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 fewer than ${minProperties} properties should be on a single line`,
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, UPPER_CASE for constants, and
10737
- * PascalCase for React components.
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 MAX_RETRIES = 3;
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 maxretries = 3;
10748
- * const userProfile = () => <div />;
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
- message: `Variable "${name}" should be camelCase (e.g., userCookie instead of UserCookie)`,
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
- message: `Argument "${name}" should be camelCase`,
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}} code order",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.3.2",
3
+ "version": "1.3.8",
4
4
  "description": "A custom ESLint plugin for enforcing consistent code formatting and style rules in React/JSX projects",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",