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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/index.js +281 -74
  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
 
@@ -2604,7 +2653,7 @@ const hookDepsPerLine = {
2604
2653
  [openBracket.range[1], closeBracket.range[0]],
2605
2654
  elementsText,
2606
2655
  ),
2607
- 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`,
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 ${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`,
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 ${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`,
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 ${maxSpecifiers} or fewer specifiers should be on a single line`,
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 ${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"`,
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 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`",
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 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.
5680
5730
  *
5681
5731
  * ✓ Good:
5682
5732
  * className="flex items-center gap-4"
5683
- * const buttonClasses = `flex items-center ${className}`;
5684
- * 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";
5685
5735
  *
5686
5736
  * ✗ Bad:
5687
5737
  * className="flex items-center gap-4"
5688
- * const buttonClasses = ` flex items-center ${className} `;
5689
- * 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";
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
- 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
+ }
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
- * and object properties. This rule complements tailwindcss/classnames-order
5893
- * by handling cases it doesn't cover (variables and objects).
5894
- * 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.
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 classes = "flex items-center p-4 text-white";
5901
- * 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";
5902
5970
  *
5903
5971
  * ✗ Bad:
5904
- * const classes = "text-white p-4 flex items-center";
5905
- * 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";
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 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)",
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 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)",
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 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)",
6049
6118
  node: prop.value,
6050
6119
  });
6051
6120
  }
6052
6121
  }
6053
6122
 
6054
6123
  if (prop.value && prop.value.type === "TemplateLiteral") {
6055
- 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
+ }
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
- * Dynamic expressions must remain at the end.
6178
+ * Uses smart detection: checks objects/returns if values look
6179
+ * like Tailwind classes.
6080
6180
  *
6081
6181
  * ✓ Good:
6082
- * className={`
6083
- * flex
6084
- * items-center
6085
- * justify-center
6086
- * rounded-lg
6087
- * ${className}
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
- * className="flex items-center justify-center rounded-lg"
6092
- * 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";
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: "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"`,
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: "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"`,
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 (!isClassRelatedName(varName)) return;
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
- 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
+ }
6351
6458
  }
6352
6459
 
6353
6460
  if (prop.value && prop.value.type === "TemplateLiteral") {
6354
- 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
+ }
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: "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)",
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 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`,
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, UPPER_CASE for constants, and
10748
- * PascalCase for React components.
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 MAX_RETRIES = 3;
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 maxretries = 3;
10759
- * 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
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
- 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})`,
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
- 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})`,
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}} 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",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.3.3",
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",