eslint-plugin-code-style 1.6.6 → 1.7.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.7.0] - 2026-02-02
11
+
12
+ **New Rules for Blocks, Classes & Enum Enforcement + Multiple Enhancements**
13
+
14
+ **Version Range:** v1.6.1 → v1.7.0
15
+
16
+ ### Added
17
+
18
+ **New Rules (3)**
19
+ - `empty-line-after-block` - Require empty line between closing `}` of block statement and next statement 🔧
20
+ - `class-naming-convention` - Enforce class declarations end with "Class" suffix 🔧
21
+ - `enum-type-enforcement` - Enforce using enum values instead of string literals for typed variables (e.g., `ButtonVariantEnum.PRIMARY` instead of `"primary"`) 🔧
22
+
23
+ ### Enhanced
24
+
25
+ - **`ternary-condition-multiline`** - Now also collapses simple ternaries to single line when they fit within max line length (default: 120 chars). Added `maxLineLength` option.
26
+ - **`function-object-destructure`** - Add auto-fix (replaces destructured usages with dot notation), expand module paths (services, constants, config, api, utils, helpers, lib, apis, configs, utilities, routes)
27
+ - **`function-params-per-line`** - Handle callbacks with mixed params (destructured + simple like `({ item }, index)`)
28
+ - **`array-callback-destructure`** - Fix closing brace on same line as last property
29
+ - **`simple-call-single-line`** - Skip callbacks with 2+ params to avoid conflicts
30
+ - **`jsx-simple-element-one-line`**, **`jsx-children-on-new-line`**, **`jsx-element-child-new-line`** - Treat simple function calls (0-1 args) as simple expressions
31
+
32
+ ### Fixed
33
+
34
+ - **`component-props-destructure`** - Detect body destructuring patterns even without type annotations, add auto-fix for body destructuring, preserve TypeScript type annotation when auto-fixing
35
+
36
+ ### Stats
37
+
38
+ - Total Rules: 69 (was 66)
39
+ - Auto-fixable: 63 rules 🔧
40
+ - Report-only: 6 rules
41
+
42
+ **Full Changelog:** [v1.6.1...v1.7.0](https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.1...v1.7.0)
43
+
44
+ ---
45
+
10
46
  ## [1.6.6] - 2026-02-01
11
47
 
12
48
  ### Fixed
@@ -1028,6 +1064,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1028
1064
 
1029
1065
  ---
1030
1066
 
1067
+ [1.7.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.6...v1.7.0
1068
+ [1.6.6]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.5...v1.6.6
1031
1069
  [1.6.5]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.4...v1.6.5
1032
1070
  [1.6.4]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.3...v1.6.4
1033
1071
  [1.6.3]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.2...v1.6.3
package/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
 
20
20
  **A powerful ESLint plugin for enforcing consistent code formatting and style rules in React/JSX projects.**
21
21
 
22
- *66 rules (60 auto-fixable) to keep your codebase clean and consistent*
22
+ *69 rules (63 auto-fixable) to keep your codebase clean and consistent*
23
23
 
24
24
  </div>
25
25
 
@@ -27,7 +27,7 @@
27
27
 
28
28
  ## 🎯 Why This Plugin?
29
29
 
30
- This plugin provides **66 custom rules** (60 auto-fixable) for code formatting. Built for **ESLint v9 flat configs**.
30
+ This plugin provides **69 custom rules** (63 auto-fixable) for code formatting. Built for **ESLint v9 flat configs**.
31
31
 
32
32
  > **Note:** ESLint [deprecated 79 formatting rules](https://eslint.org/blog/2023/10/deprecating-formatting-rules/) in v8.53.0. Our recommended configs use `@stylistic/eslint-plugin` as the replacement for these deprecated rules.
33
33
 
@@ -36,7 +36,7 @@ This plugin provides **66 custom rules** (60 auto-fixable) for code formatting.
36
36
  - **Works alongside existing tools** — Complements ESLint's built-in rules and packages like eslint-plugin-react, eslint-plugin-import, etc
37
37
  - **Self-sufficient rules** — Each rule handles complete formatting independently
38
38
  - **Consistency at scale** — Reduces code-style differences between team members by enforcing uniform formatting across your projects
39
- - **Highly automated** — 60 of 66 rules support auto-fix with `eslint --fix`
39
+ - **Highly automated** — 63 of 69 rules support auto-fix with `eslint --fix`
40
40
 
41
41
  When combined with ESLint's native rules and other popular plugins, this package helps create a complete code style solution that keeps your codebase clean and consistent.
42
42
 
@@ -60,7 +60,7 @@ We provide **ready-to-use ESLint flat configuration files** that combine `eslint
60
60
 
61
61
  ### 💡 Why Use These Configs?
62
62
 
63
- - **Complete Coverage** — Combines ESLint built-in rules, third-party plugins, and all 64 code-style rules
63
+ - **Complete Coverage** — Combines ESLint built-in rules, third-party plugins, and all 69 code-style rules
64
64
  - **Ready-to-Use** — Copy the config file and start linting immediately
65
65
  - **Battle-Tested** — These configurations have been refined through real-world usage
66
66
  - **Fully Documented** — Each config includes detailed instructions and explanations
@@ -97,7 +97,7 @@ We provide **ready-to-use ESLint flat configuration files** that combine `eslint
97
97
  <td width="50%">
98
98
 
99
99
  ### 🔧 Auto-Fixable Rules
100
- **60 rules** support automatic fixing with `eslint --fix`. 6 rules are report-only (require manual changes).
100
+ **63 rules** support automatic fixing with `eslint --fix`. 6 rules are report-only (require manual changes).
101
101
 
102
102
  </td>
103
103
  <td width="50%">
@@ -252,7 +252,7 @@ rules: {
252
252
 
253
253
  ## 📖 Rules Categories
254
254
 
255
- > **66 rules total** — 60 with auto-fix 🔧, 6 report-only. See detailed examples in [Rules Reference](#-rules-reference) below.
255
+ > **69 rules total** — 63 with auto-fix 🔧, 6 report-only. See detailed examples in [Rules Reference](#-rules-reference) below.
256
256
  >
257
257
  > **Legend:** 🔧 Auto-fixable with `eslint --fix` • ⚙️ Customizable options
258
258
 
@@ -279,13 +279,16 @@ rules: {
279
279
  | **Component Rules** | |
280
280
  | `component-props-destructure` | Component props must be destructured `({ prop })` not received as `(props)` 🔧 |
281
281
  | `component-props-inline-type` | Inline type annotation `} : {` with matching props, proper spacing, commas, no interface reference 🔧 |
282
+ | **Class Rules** | |
283
+ | `class-naming-convention` | Class declarations must end with "Class" suffix (e.g., `ApiServiceClass`) 🔧 |
282
284
  | **Control Flow Rules** | |
283
285
  | `block-statement-newlines` | Newline after `{` and before `}` in if/for/while/function blocks 🔧 |
286
+ | `empty-line-after-block` | Empty line required between closing `}` of block and next statement 🔧 |
284
287
  | `if-else-spacing` | Empty line between consecutive if blocks, no empty line between single-line if/else 🔧 |
285
288
  | `if-statement-format` | `{` on same line as `if`/`else if`, `else` on same line as `}`, proper spacing 🔧 |
286
289
  | `multiline-if-conditions` | Conditions exceeding threshold get one operand per line with proper indentation (default: >3) 🔧 ⚙️ |
287
290
  | `no-empty-lines-in-switch-cases` | No empty line after `case X:` before code, no empty lines between cases 🔧 |
288
- | `ternary-condition-multiline` | Ternary conditions exceeding threshold get one operand per line (default: >3) 🔧 ⚙️ |
291
+ | `ternary-condition-multiline` | Collapse simple ternaries to single line; expand complex conditions (>3 operands) to multiline 🔧 ⚙️ |
289
292
  | **Function Rules** | |
290
293
  | `function-call-spacing` | No space between function name and `(`: `fn()` not `fn ()` 🔧 |
291
294
  | `function-declaration-style` | Auto-fix for `func-style`: converts function declarations to arrow expressions 🔧 |
@@ -330,6 +333,7 @@ rules: {
330
333
  | `member-expression-bracket-spacing` | No spaces inside brackets in computed member expressions: `arr[0]` not `arr[ 0 ]` 🔧 |
331
334
  | **TypeScript Rules** | |
332
335
  | `enum-format` | Enforce enum naming (PascalCase + Enum suffix), UPPER_CASE members, no empty lines, and trailing commas 🔧 |
336
+ | `enum-type-enforcement` | Enforce using enum values instead of string literals for variables typed with `*Type` (e.g., use `ButtonVariantEnum.PRIMARY` not `"primary"`) 🔧 |
333
337
  | `interface-format` | Enforce interface naming (PascalCase + Interface suffix), camelCase properties, no empty lines, and trailing commas 🔧 |
334
338
  | `no-inline-type-definitions` | Inline union types in function params should be extracted to named types ⚙️ |
335
339
  | `type-annotation-spacing` | Enforce consistent spacing in type annotations: no space before colon/generic/array brackets, one space after colon 🔧 |
@@ -3066,7 +3070,7 @@ const UseAuth = () => {}; // hooks should be camelCase
3066
3070
 
3067
3071
  ## 🔧 Auto-fixing
3068
3072
 
3069
- 60 of 66 rules support auto-fixing. Run ESLint with the `--fix` flag:
3073
+ 63 of 69 rules support auto-fixing. Run ESLint with the `--fix` flag:
3070
3074
 
3071
3075
  ```bash
3072
3076
  # Fix all files in src directory
package/index.d.ts CHANGED
@@ -13,11 +13,14 @@ export type RuleNames =
13
13
  | "code-style/arrow-function-simplify"
14
14
  | "code-style/assignment-value-same-line"
15
15
  | "code-style/block-statement-newlines"
16
+ | "code-style/class-naming-convention"
16
17
  | "code-style/comment-format"
17
18
  | "code-style/component-props-destructure"
18
19
  | "code-style/react-code-order"
19
20
  | "code-style/component-props-inline-type"
20
21
  | "code-style/curried-arrow-same-line"
22
+ | "code-style/empty-line-after-block"
23
+ | "code-style/enum-type-enforcement"
21
24
  | "code-style/export-format"
22
25
  | "code-style/function-arguments-format"
23
26
  | "code-style/function-call-spacing"
@@ -101,11 +104,14 @@ interface PluginRules {
101
104
  "arrow-function-simplify": Rule.RuleModule;
102
105
  "assignment-value-same-line": Rule.RuleModule;
103
106
  "block-statement-newlines": Rule.RuleModule;
107
+ "class-naming-convention": Rule.RuleModule;
104
108
  "comment-format": Rule.RuleModule;
105
109
  "component-props-destructure": Rule.RuleModule;
106
110
  "react-code-order": Rule.RuleModule;
107
111
  "component-props-inline-type": Rule.RuleModule;
108
112
  "curried-arrow-same-line": Rule.RuleModule;
113
+ "empty-line-after-block": Rule.RuleModule;
114
+ "enum-type-enforcement": Rule.RuleModule;
109
115
  "export-format": Rule.RuleModule;
110
116
  "function-arguments-format": Rule.RuleModule;
111
117
  "function-call-spacing": Rule.RuleModule;
package/index.js CHANGED
@@ -3890,6 +3890,7 @@ const ternaryConditionMultiline = {
3890
3890
  const sourceCode = context.sourceCode || context.getSourceCode();
3891
3891
  const options = context.options[0] || {};
3892
3892
  const maxOperands = options.maxOperands ?? 3;
3893
+ const maxLineLength = options.maxLineLength ?? 120;
3893
3894
 
3894
3895
  // Check if node is wrapped in parentheses
3895
3896
  const isParenthesizedHandler = (node) => {
@@ -3984,115 +3985,228 @@ const ternaryConditionMultiline = {
3984
3985
  return false;
3985
3986
  };
3986
3987
 
3987
- return {
3988
- ConditionalExpression(node) {
3989
- const { test } = node;
3988
+ // Check if the test is a simple condition (not a complex logical expression)
3989
+ const isSimpleConditionHandler = (test) => {
3990
+ if (test.type === "Identifier") return true;
3990
3991
 
3991
- // Only handle ternaries with logical expression conditions
3992
- if (test.type !== "LogicalExpression") return;
3992
+ if (test.type === "MemberExpression") return true;
3993
3993
 
3994
- const operands = collectOperandsHandler(test);
3995
- const testStartLine = test.loc.start.line;
3996
- const testEndLine = test.loc.end.line;
3997
- const isMultiLine = testStartLine !== testEndLine;
3994
+ if (test.type === "UnaryExpression") return true;
3998
3995
 
3999
- // ≤maxOperands operands: keep on single line
4000
- if (operands.length <= maxOperands) {
4001
- const firstOperandStartLine = operands[0].loc.start.line;
4002
- const allOperandsStartOnSameLine = operands.every(
4003
- (op) => op.loc.start.line === firstOperandStartLine,
4004
- );
3996
+ if (test.type === "BinaryExpression") return true;
4005
3997
 
4006
- const hasSplitBinaryExpression = operands.some(
4007
- (op) => isBinaryExpressionSplitHandler(op),
4008
- );
3998
+ if (test.type === "CallExpression") return true;
4009
3999
 
4010
- if (!allOperandsStartOnSameLine || hasSplitBinaryExpression) {
4011
- context.report({
4012
- fix: (fixer) => {
4013
- const buildSameLineHandler = (n) => {
4014
- if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
4015
- const leftText = buildSameLineHandler(n.left);
4016
- const rightText = buildSameLineHandler(n.right);
4000
+ // Logical expression with ≤maxOperands is still simple
4001
+ if (test.type === "LogicalExpression") {
4002
+ return collectOperandsHandler(test).length <= maxOperands;
4003
+ }
4017
4004
 
4018
- return `${leftText} ${n.operator} ${rightText}`;
4019
- }
4005
+ return false;
4006
+ };
4020
4007
 
4021
- if (n.type === "BinaryExpression" && isBinaryExpressionSplitHandler(n)) {
4022
- return buildBinaryExpressionSingleLineHandler(n);
4023
- }
4008
+ // Get the full ternary as single line text
4009
+ const getTernarySingleLineHandler = (node) => {
4010
+ const testText = sourceCode.getText(node.test).replace(/\s+/g, " ").trim();
4011
+ const consequentText = sourceCode.getText(node.consequent).replace(/\s+/g, " ").trim();
4012
+ const alternateText = sourceCode.getText(node.alternate).replace(/\s+/g, " ").trim();
4024
4013
 
4025
- return getSourceTextWithGroupsHandler(n);
4026
- };
4014
+ return `${testText} ? ${consequentText} : ${alternateText}`;
4015
+ };
4027
4016
 
4028
- return fixer.replaceText(test, buildSameLineHandler(test));
4029
- },
4030
- message: `Ternary conditions with ≤${maxOperands} operands should be single line`,
4031
- node: test,
4032
- });
4033
- }
4017
+ // Get the indentation level for the line
4018
+ const getLineIndentHandler = (node) => {
4019
+ const lineText = sourceCode.lines[node.loc.start.line - 1];
4034
4020
 
4035
- return;
4036
- }
4021
+ return lineText.match(/^\s*/)[0].length;
4022
+ };
4037
4023
 
4038
- // More than maxOperands: each on its own line
4039
- let isCorrectionNeeded = !isMultiLine;
4024
+ // Check if branches have complex objects (should stay multiline)
4025
+ const hasComplexObjectHandler = (n) => {
4026
+ if (n.type === "ObjectExpression" && n.properties.length >= 2) return true;
4040
4027
 
4041
- if (isMultiLine) {
4042
- for (let i = 0; i < operands.length - 1; i += 1) {
4043
- if (operands[i].loc.end.line === operands[i + 1].loc.start.line) {
4044
- isCorrectionNeeded = true;
4045
- break;
4046
- }
4047
- }
4028
+ if (n.type === "ArrayExpression" && n.elements.length >= 3) return true;
4048
4029
 
4049
- // Check if any operator is at end of line (should be at beginning)
4050
- if (!isCorrectionNeeded && hasOperatorAtEndOfLineHandler(test)) {
4051
- isCorrectionNeeded = true;
4052
- }
4053
- }
4030
+ return false;
4031
+ };
4032
+
4033
+ // Handle simple ternaries - collapse to single line if they fit
4034
+ const handleSimpleTernaryHandler = (node) => {
4035
+ // Skip if already on single line
4036
+ if (node.loc.start.line === node.loc.end.line) return false;
4037
+
4038
+ // Skip nested ternaries
4039
+ if (node.consequent.type === "ConditionalExpression" || node.alternate.type === "ConditionalExpression") {
4040
+ return false;
4041
+ }
4042
+
4043
+ // Skip if branches have complex objects
4044
+ if (hasComplexObjectHandler(node.consequent) || hasComplexObjectHandler(node.alternate)) {
4045
+ return false;
4046
+ }
4047
+
4048
+ // Calculate what the single line would look like
4049
+ const singleLineText = getTernarySingleLineHandler(node);
4050
+ const indent = getLineIndentHandler(node);
4051
+
4052
+ // Check if the parent needs prefix text (like "const x = ")
4053
+ let prefixLength = 0;
4054
+ const parent = node.parent;
4055
+
4056
+ if (parent && parent.type === "VariableDeclarator" && parent.init === node) {
4057
+ const declarationLine = sourceCode.lines[parent.loc.start.line - 1];
4058
+ const beforeTernary = declarationLine.slice(0, node.loc.start.column);
4059
+
4060
+ prefixLength = beforeTernary.length - indent;
4061
+ } else if (parent && parent.type === "AssignmentExpression" && parent.right === node) {
4062
+ const assignmentLine = sourceCode.lines[parent.loc.start.line - 1];
4063
+ const beforeTernary = assignmentLine.slice(0, node.loc.start.column);
4064
+
4065
+ prefixLength = beforeTernary.length - indent;
4066
+ }
4054
4067
 
4055
- if (isCorrectionNeeded) {
4068
+ // Check if single line would fit
4069
+ const totalLength = indent + prefixLength + singleLineText.length + 1;
4070
+
4071
+ if (totalLength <= maxLineLength) {
4072
+ context.report({
4073
+ fix: (fixer) => fixer.replaceText(node, singleLineText),
4074
+ message: "Simple ternary should be on a single line",
4075
+ node,
4076
+ });
4077
+
4078
+ return true;
4079
+ }
4080
+
4081
+ return false;
4082
+ };
4083
+
4084
+ // Handle complex logical expressions - format multiline
4085
+ const handleComplexLogicalTernaryHandler = (node) => {
4086
+ const { test } = node;
4087
+ const operands = collectOperandsHandler(test);
4088
+ const testStartLine = test.loc.start.line;
4089
+ const testEndLine = test.loc.end.line;
4090
+ const isMultiLine = testStartLine !== testEndLine;
4091
+
4092
+ // ≤maxOperands operands: keep condition on single line
4093
+ if (operands.length <= maxOperands) {
4094
+ const firstOperandStartLine = operands[0].loc.start.line;
4095
+ const allOperandsStartOnSameLine = operands.every(
4096
+ (op) => op.loc.start.line === firstOperandStartLine,
4097
+ );
4098
+
4099
+ const hasSplitBinaryExpression = operands.some(
4100
+ (op) => isBinaryExpressionSplitHandler(op),
4101
+ );
4102
+
4103
+ if (!allOperandsStartOnSameLine || hasSplitBinaryExpression) {
4056
4104
  context.report({
4057
4105
  fix: (fixer) => {
4058
- // Get the indentation based on where the ternary starts
4059
- const lineText = sourceCode.lines[node.loc.start.line - 1];
4060
- const baseIndent = lineText.match(/^\s*/)[0];
4061
- const conditionIndent = baseIndent + " ";
4062
- const branchIndent = baseIndent + " ";
4063
-
4064
- const buildMultilineHandler = (n) => {
4106
+ const buildSameLineHandler = (n) => {
4065
4107
  if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
4066
- const leftText = buildMultilineHandler(n.left);
4067
- const rightText = buildMultilineHandler(n.right);
4108
+ const leftText = buildSameLineHandler(n.left);
4109
+ const rightText = buildSameLineHandler(n.right);
4068
4110
 
4069
- return `${leftText}\n${conditionIndent}${n.operator} ${rightText}`;
4111
+ return `${leftText} ${n.operator} ${rightText}`;
4112
+ }
4113
+
4114
+ if (n.type === "BinaryExpression" && isBinaryExpressionSplitHandler(n)) {
4115
+ return buildBinaryExpressionSingleLineHandler(n);
4070
4116
  }
4071
4117
 
4072
4118
  return getSourceTextWithGroupsHandler(n);
4073
4119
  };
4074
4120
 
4075
- const consequentText = sourceCode.getText(node.consequent);
4076
- const alternateText = sourceCode.getText(node.alternate);
4077
-
4078
- const newText = `\n${conditionIndent}${buildMultilineHandler(test)}\n${branchIndent}? ${consequentText}\n${branchIndent}: ${alternateText}`;
4079
-
4080
- return fixer.replaceText(node, newText);
4121
+ return fixer.replaceText(test, buildSameLineHandler(test));
4081
4122
  },
4082
- message: `Ternary conditions with more than ${maxOperands} operands should be multiline, with each operand on its own line`,
4123
+ message: `Ternary conditions with ≤${maxOperands} operands should be single line`,
4083
4124
  node: test,
4084
4125
  });
4085
4126
  }
4127
+
4128
+ return;
4129
+ }
4130
+
4131
+ // More than maxOperands: each on its own line
4132
+ let isCorrectionNeeded = !isMultiLine;
4133
+
4134
+ if (isMultiLine) {
4135
+ for (let i = 0; i < operands.length - 1; i += 1) {
4136
+ if (operands[i].loc.end.line === operands[i + 1].loc.start.line) {
4137
+ isCorrectionNeeded = true;
4138
+ break;
4139
+ }
4140
+ }
4141
+
4142
+ // Check if any operator is at end of line (should be at beginning)
4143
+ if (!isCorrectionNeeded && hasOperatorAtEndOfLineHandler(test)) {
4144
+ isCorrectionNeeded = true;
4145
+ }
4146
+ }
4147
+
4148
+ if (isCorrectionNeeded) {
4149
+ context.report({
4150
+ fix: (fixer) => {
4151
+ // Get the indentation based on where the ternary starts
4152
+ const lineText = sourceCode.lines[node.loc.start.line - 1];
4153
+ const baseIndent = lineText.match(/^\s*/)[0];
4154
+ const conditionIndent = baseIndent + " ";
4155
+ const branchIndent = baseIndent + " ";
4156
+
4157
+ const buildMultilineHandler = (n) => {
4158
+ if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
4159
+ const leftText = buildMultilineHandler(n.left);
4160
+ const rightText = buildMultilineHandler(n.right);
4161
+
4162
+ return `${leftText}\n${conditionIndent}${n.operator} ${rightText}`;
4163
+ }
4164
+
4165
+ return getSourceTextWithGroupsHandler(n);
4166
+ };
4167
+
4168
+ const consequentText = sourceCode.getText(node.consequent);
4169
+ const alternateText = sourceCode.getText(node.alternate);
4170
+
4171
+ const newText = `\n${conditionIndent}${buildMultilineHandler(test)}\n${branchIndent}? ${consequentText}\n${branchIndent}: ${alternateText}`;
4172
+
4173
+ return fixer.replaceText(node, newText);
4174
+ },
4175
+ message: `Ternary conditions with more than ${maxOperands} operands should be multiline, with each operand on its own line`,
4176
+ node: test,
4177
+ });
4178
+ }
4179
+ };
4180
+
4181
+ return {
4182
+ ConditionalExpression(node) {
4183
+ const { test } = node;
4184
+
4185
+ // First, try to collapse simple ternaries to single line
4186
+ if (isSimpleConditionHandler(test)) {
4187
+ if (handleSimpleTernaryHandler(node)) return;
4188
+ }
4189
+
4190
+ // For complex logical expressions, handle multiline formatting
4191
+ if (test.type === "LogicalExpression") {
4192
+ handleComplexLogicalTernaryHandler(node);
4193
+ }
4086
4194
  },
4087
4195
  };
4088
4196
  },
4089
4197
  meta: {
4090
- docs: { description: "Enforce multiline formatting for ternary expressions with complex conditions" },
4198
+ docs: { description: "Enforce consistent ternary formatting: collapse simple ternaries to single line, expand complex conditions to multiline" },
4091
4199
  fixable: "code",
4092
4200
  schema: [
4093
4201
  {
4094
4202
  additionalProperties: false,
4095
4203
  properties: {
4204
+ maxLineLength: {
4205
+ default: 120,
4206
+ description: "Maximum line length for single-line ternaries (default: 120)",
4207
+ minimum: 80,
4208
+ type: "integer",
4209
+ },
4096
4210
  maxOperands: {
4097
4211
  default: 3,
4098
4212
  description: "Maximum operands to keep on single line (default: 3)",
@@ -4107,6 +4221,372 @@ const ternaryConditionMultiline = {
4107
4221
  },
4108
4222
  };
4109
4223
 
4224
+ /**
4225
+ * ───────────────────────────────────────────────────────────────
4226
+ * Rule: Empty Line After Block
4227
+ * ───────────────────────────────────────────────────────────────
4228
+ *
4229
+ * Description:
4230
+ * Require an empty line between a closing brace `}` of a block
4231
+ * statement (if, try, for, while, etc.) and the next statement,
4232
+ * unless the next statement is part of the same construct (else, catch, finally).
4233
+ *
4234
+ * ✓ Good:
4235
+ * if (condition) {
4236
+ * doSomething();
4237
+ * }
4238
+ *
4239
+ * const x = 1;
4240
+ *
4241
+ * ✗ Bad:
4242
+ * if (condition) {
4243
+ * doSomething();
4244
+ * }
4245
+ * const x = 1;
4246
+ */
4247
+ const emptyLineAfterBlock = {
4248
+ create(context) {
4249
+ const sourceCode = context.sourceCode || context.getSourceCode();
4250
+
4251
+ // Check if a node is a block-containing statement
4252
+ const isBlockStatementHandler = (node) => {
4253
+ const blockTypes = [
4254
+ "IfStatement",
4255
+ "ForStatement",
4256
+ "ForInStatement",
4257
+ "ForOfStatement",
4258
+ "WhileStatement",
4259
+ "DoWhileStatement",
4260
+ "TryStatement",
4261
+ "SwitchStatement",
4262
+ "WithStatement",
4263
+ ];
4264
+
4265
+ return blockTypes.includes(node.type);
4266
+ };
4267
+
4268
+ // Get the actual end line of a statement (including else, catch, finally)
4269
+ const getStatementEndLineHandler = (node) => {
4270
+ if (node.type === "IfStatement" && node.alternate) {
4271
+ return getStatementEndLineHandler(node.alternate);
4272
+ }
4273
+
4274
+ if (node.type === "TryStatement") {
4275
+ if (node.finalizer) return node.finalizer.loc.end.line;
4276
+
4277
+ if (node.handler) return node.handler.loc.end.line;
4278
+ }
4279
+
4280
+ return node.loc.end.line;
4281
+ };
4282
+
4283
+ return {
4284
+ "BlockStatement:exit"(node) {
4285
+ const parent = node.parent;
4286
+
4287
+ // Only check for block-containing statements
4288
+ if (!parent || !isBlockStatementHandler(parent)) return;
4289
+
4290
+ // Skip if this block is followed by else, catch, or finally
4291
+ if (parent.type === "IfStatement" && parent.consequent === node && parent.alternate) {
4292
+ return;
4293
+ }
4294
+
4295
+ if (parent.type === "TryStatement" && (parent.block === node || parent.handler?.body === node) && (parent.handler || parent.finalizer)) {
4296
+ if (parent.block === node && (parent.handler || parent.finalizer)) return;
4297
+
4298
+ if (parent.handler?.body === node && parent.finalizer) return;
4299
+ }
4300
+
4301
+ // Get the parent's container (the block that contains the parent statement)
4302
+ const grandparent = parent.parent;
4303
+
4304
+ if (!grandparent || grandparent.type !== "BlockStatement") return;
4305
+
4306
+ // Find the index of the parent statement in the grandparent's body
4307
+ const stmtIndex = grandparent.body.indexOf(parent);
4308
+
4309
+ if (stmtIndex === -1 || stmtIndex === grandparent.body.length - 1) return;
4310
+
4311
+ // Get the next statement
4312
+ const nextStmt = grandparent.body[stmtIndex + 1];
4313
+
4314
+ // Get the actual end of the current statement
4315
+ const currentEndLine = getStatementEndLineHandler(parent);
4316
+ const nextStartLine = nextStmt.loc.start.line;
4317
+
4318
+ // Check if there's an empty line between them
4319
+ if (nextStartLine - currentEndLine < 2) {
4320
+ context.report({
4321
+ fix: (fixer) => {
4322
+ const endToken = sourceCode.getLastToken(parent);
4323
+
4324
+ return fixer.insertTextAfter(endToken, "\n");
4325
+ },
4326
+ message: "Expected empty line after block statement",
4327
+ node: nextStmt,
4328
+ });
4329
+ }
4330
+ },
4331
+ };
4332
+ },
4333
+ meta: {
4334
+ docs: { description: "Require empty line between block statement closing brace and next statement" },
4335
+ fixable: "whitespace",
4336
+ schema: [],
4337
+ type: "layout",
4338
+ },
4339
+ };
4340
+
4341
+ /**
4342
+ * ───────────────────────────────────────────────────────────────
4343
+ * Rule: Class Naming Convention
4344
+ * ───────────────────────────────────────────────────────────────
4345
+ *
4346
+ * Description:
4347
+ * Enforce that class declarations must end with "Class" suffix.
4348
+ * This distinguishes class definitions from other PascalCase names
4349
+ * like React components or type definitions.
4350
+ *
4351
+ * ✓ Good:
4352
+ * class ApiServiceClass { ... }
4353
+ * class UserRepositoryClass { ... }
4354
+ *
4355
+ * ✗ Bad:
4356
+ * class ApiService { ... }
4357
+ * class UserRepository { ... }
4358
+ */
4359
+ const classNamingConvention = {
4360
+ create(context) {
4361
+ const sourceCode = context.sourceCode || context.getSourceCode();
4362
+
4363
+ return {
4364
+ ClassDeclaration(node) {
4365
+ if (!node.id || !node.id.name) return;
4366
+
4367
+ const className = node.id.name;
4368
+
4369
+ if (!className.endsWith("Class")) {
4370
+ context.report({
4371
+ fix: (fixer) => {
4372
+ const newName = `${className}Class`;
4373
+
4374
+ // Find all references to this class and rename them
4375
+ const scope = context.sourceCode.getScope
4376
+ ? context.sourceCode.getScope(node)
4377
+ : context.getScope();
4378
+ const variable = scope.set.get(className);
4379
+ const fixes = [fixer.replaceText(node.id, newName)];
4380
+
4381
+ if (variable && variable.references) {
4382
+ variable.references.forEach((ref) => {
4383
+ if (ref.identifier !== node.id) {
4384
+ fixes.push(fixer.replaceText(ref.identifier, newName));
4385
+ }
4386
+ });
4387
+ }
4388
+
4389
+ return fixes;
4390
+ },
4391
+ message: `Class name "${className}" should end with "Class" suffix`,
4392
+ node: node.id,
4393
+ });
4394
+ }
4395
+ },
4396
+ };
4397
+ },
4398
+ meta: {
4399
+ docs: { description: "Enforce class names end with 'Class' suffix" },
4400
+ fixable: "code",
4401
+ schema: [],
4402
+ type: "suggestion",
4403
+ },
4404
+ };
4405
+
4406
+ /**
4407
+ * ───────────────────────────────────────────────────────────────
4408
+ * Rule: Enum Type Enforcement
4409
+ * ───────────────────────────────────────────────────────────────
4410
+ *
4411
+ * Description:
4412
+ * When a variable/parameter has a type like "ButtonVariantType",
4413
+ * enforce using the corresponding enum "ButtonVariantEnum.VALUE"
4414
+ * instead of string literals like "primary" or "ghost".
4415
+ *
4416
+ * The rule detects:
4417
+ * - Default values in destructuring: `variant = "primary"` → `variant = ButtonVariantEnum.PRIMARY`
4418
+ * - Comparisons: `variant === "ghost"` → `variant === ButtonVariantEnum.GHOST`
4419
+ * - Object property values matching the type
4420
+ *
4421
+ * ✓ Good:
4422
+ * const Button = ({ variant = ButtonVariantEnum.PRIMARY }: { variant?: ButtonVariantType }) => ...
4423
+ * if (variant === ButtonVariantEnum.GHOST) { ... }
4424
+ *
4425
+ * ✗ Bad:
4426
+ * const Button = ({ variant = "primary" }: { variant?: ButtonVariantType }) => ...
4427
+ * if (variant === "ghost") { ... }
4428
+ */
4429
+ const enumTypeEnforcement = {
4430
+ create(context) {
4431
+ const sourceCode = context.sourceCode || context.getSourceCode();
4432
+
4433
+ // Map to track variables with Type annotations and their corresponding Enum
4434
+ // e.g., "variant" -> { typeName: "ButtonVariantType", enumName: "ButtonVariantEnum" }
4435
+ const typeAnnotatedVars = new Map();
4436
+
4437
+ // Convert type name to enum name: ButtonVariantType -> ButtonVariantEnum
4438
+ const getEnumNameFromTypeHandler = (typeName) => {
4439
+ if (typeName.endsWith("Type")) {
4440
+ return typeName.slice(0, -4) + "Enum";
4441
+ }
4442
+
4443
+ return null;
4444
+ };
4445
+
4446
+ // Convert string literal to enum member: "primary" -> "PRIMARY", "ghost-danger" -> "GHOST_DANGER"
4447
+ const toEnumMemberHandler = (str) => str.toUpperCase().replace(/-/g, "_");
4448
+
4449
+ // Check if a type annotation references a Type that has a corresponding Enum
4450
+ const extractTypeInfoHandler = (typeAnnotation) => {
4451
+ if (!typeAnnotation) return null;
4452
+
4453
+ const annotation = typeAnnotation.typeAnnotation;
4454
+
4455
+ if (!annotation) return null;
4456
+
4457
+ // Handle direct type reference: : ButtonVariantType
4458
+ if (annotation.type === "TSTypeReference" && annotation.typeName?.type === "Identifier") {
4459
+ const typeName = annotation.typeName.name;
4460
+
4461
+ if (typeName.endsWith("Type")) {
4462
+ return {
4463
+ enumName: getEnumNameFromTypeHandler(typeName),
4464
+ typeName,
4465
+ };
4466
+ }
4467
+ }
4468
+
4469
+ return null;
4470
+ };
4471
+
4472
+ // Track type-annotated parameters in function/component definitions
4473
+ const trackTypedParamsHandler = (params) => {
4474
+ params.forEach((param) => {
4475
+ // Handle destructured params: ({ variant }: { variant?: ButtonVariantType })
4476
+ if (param.type === "ObjectPattern" && param.typeAnnotation) {
4477
+ const annotation = param.typeAnnotation.typeAnnotation;
4478
+
4479
+ if (annotation && annotation.type === "TSTypeLiteral") {
4480
+ annotation.members.forEach((member) => {
4481
+ if (member.type === "TSPropertySignature" && member.key?.type === "Identifier") {
4482
+ const propName = member.key.name;
4483
+ const typeInfo = extractTypeInfoHandler(member);
4484
+
4485
+ if (typeInfo) {
4486
+ typeAnnotatedVars.set(propName, typeInfo);
4487
+ }
4488
+ }
4489
+ });
4490
+ }
4491
+ }
4492
+
4493
+ // Handle simple typed param: (variant: ButtonVariantType)
4494
+ if (param.type === "Identifier" && param.typeAnnotation) {
4495
+ const typeInfo = extractTypeInfoHandler(param);
4496
+
4497
+ if (typeInfo) {
4498
+ typeAnnotatedVars.set(param.name, typeInfo);
4499
+ }
4500
+ }
4501
+ });
4502
+ };
4503
+
4504
+ return {
4505
+ // Track function parameters
4506
+ "ArrowFunctionExpression, FunctionDeclaration, FunctionExpression"(node) {
4507
+ trackTypedParamsHandler(node.params);
4508
+ },
4509
+
4510
+ // Check default values in destructuring patterns
4511
+ AssignmentPattern(node) {
4512
+ // Pattern like: variant = "primary"
4513
+ if (node.left.type !== "Identifier") return;
4514
+
4515
+ const varName = node.left.name;
4516
+ const typeInfo = typeAnnotatedVars.get(varName);
4517
+
4518
+ if (!typeInfo) return;
4519
+
4520
+ // Check if the default is a string literal
4521
+ if (node.right.type === "Literal" && typeof node.right.value === "string") {
4522
+ const stringValue = node.right.value;
4523
+ const enumMember = toEnumMemberHandler(stringValue);
4524
+ const replacement = `${typeInfo.enumName}.${enumMember}`;
4525
+
4526
+ context.report({
4527
+ fix: (fixer) => fixer.replaceText(node.right, replacement),
4528
+ message: `Use "${replacement}" instead of string literal "${stringValue}"`,
4529
+ node: node.right,
4530
+ });
4531
+ }
4532
+ },
4533
+
4534
+ // Check comparisons: variant === "ghost"
4535
+ BinaryExpression(node) {
4536
+ if (node.operator !== "===" && node.operator !== "!==") return;
4537
+
4538
+ let varNode = null;
4539
+ let literalNode = null;
4540
+
4541
+ if (node.left.type === "Identifier" && node.right.type === "Literal") {
4542
+ varNode = node.left;
4543
+ literalNode = node.right;
4544
+ } else if (node.right.type === "Identifier" && node.left.type === "Literal") {
4545
+ varNode = node.right;
4546
+ literalNode = node.left;
4547
+ }
4548
+
4549
+ if (!varNode || !literalNode) return;
4550
+
4551
+ if (typeof literalNode.value !== "string") return;
4552
+
4553
+ const typeInfo = typeAnnotatedVars.get(varNode.name);
4554
+
4555
+ if (!typeInfo) return;
4556
+
4557
+ const stringValue = literalNode.value;
4558
+ const enumMember = toEnumMemberHandler(stringValue);
4559
+ const replacement = `${typeInfo.enumName}.${enumMember}`;
4560
+
4561
+ context.report({
4562
+ fix: (fixer) => fixer.replaceText(literalNode, replacement),
4563
+ message: `Use "${replacement}" instead of string literal "${stringValue}"`,
4564
+ node: literalNode,
4565
+ });
4566
+ },
4567
+
4568
+ // Clear tracked vars when exiting function scope
4569
+ "ArrowFunctionExpression:exit"() {
4570
+ typeAnnotatedVars.clear();
4571
+ },
4572
+
4573
+ "FunctionDeclaration:exit"() {
4574
+ typeAnnotatedVars.clear();
4575
+ },
4576
+
4577
+ "FunctionExpression:exit"() {
4578
+ typeAnnotatedVars.clear();
4579
+ },
4580
+ };
4581
+ },
4582
+ meta: {
4583
+ docs: { description: "Enforce using enum values instead of string literals for typed variables" },
4584
+ fixable: "code",
4585
+ schema: [],
4586
+ type: "suggestion",
4587
+ },
4588
+ };
4589
+
4110
4590
  /**
4111
4591
  * ───────────────────────────────────────────────────────────────
4112
4592
  * Rule: Absolute Imports Only
@@ -16694,12 +17174,16 @@ export default {
16694
17174
 
16695
17175
  // Control flow rules
16696
17176
  "block-statement-newlines": blockStatementNewlines,
17177
+ "empty-line-after-block": emptyLineAfterBlock,
16697
17178
  "if-else-spacing": ifElseSpacing,
16698
17179
  "if-statement-format": ifStatementFormat,
16699
17180
  "multiline-if-conditions": multilineIfConditions,
16700
17181
  "no-empty-lines-in-switch-cases": noEmptyLinesInSwitchCases,
16701
17182
  "ternary-condition-multiline": ternaryConditionMultiline,
16702
17183
 
17184
+ // Class rules
17185
+ "class-naming-convention": classNamingConvention,
17186
+
16703
17187
  // Function rules
16704
17188
  "function-call-spacing": functionCallSpacing,
16705
17189
  "function-declaration-style": functionDeclarationStyle,
@@ -16756,6 +17240,9 @@ export default {
16756
17240
  "type-format": typeFormat,
16757
17241
  "typescript-definition-location": typescriptDefinitionLocation,
16758
17242
 
17243
+ // Type/Enum rules
17244
+ "enum-type-enforcement": enumTypeEnforcement,
17245
+
16759
17246
  // Variable rules
16760
17247
  "variable-naming-convention": variableNamingConvention,
16761
17248
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.6.6",
3
+ "version": "1.7.0",
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",