eslint-plugin-code-style 1.6.4 → 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,59 @@ 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
+
46
+ ## [1.6.6] - 2026-02-01
47
+
48
+ ### Fixed
49
+
50
+ - **`component-props-destructure`** - Detect body destructuring patterns (e.g., `const { name } = data;`) even without type annotations
51
+ - **`component-props-destructure`** - Add auto-fix for body destructuring: moves props to parameter and removes body declaration
52
+
53
+ ---
54
+
55
+ ## [1.6.5] - 2026-02-01
56
+
57
+ ### Added
58
+
59
+ - **`function-object-destructure`** - Add auto-fix: replaces destructured usages with dot notation and removes declaration
60
+
61
+ ---
62
+
10
63
  ## [1.6.4] - 2026-02-01
11
64
 
12
65
  ### Enhanced
@@ -1011,6 +1064,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1011
1064
 
1012
1065
  ---
1013
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
1069
+ [1.6.5]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.4...v1.6.5
1014
1070
  [1.6.4]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.3...v1.6.4
1015
1071
  [1.6.3]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.2...v1.6.3
1016
1072
  [1.6.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.1...v1.6.2
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
+ }
4067
+
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
+ };
4054
4083
 
4055
- if (isCorrectionNeeded) {
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
@@ -12459,19 +12939,60 @@ const functionObjectDestructure = {
12459
12939
  }
12460
12940
  };
12461
12941
 
12462
- // Check for destructuring of data imports (not allowed)
12942
+ // Find all references to a variable name in a scope (with parent tracking)
12943
+ const findAllReferencesHandler = (scope, varName, declNode) => {
12944
+ const references = [];
12945
+
12946
+ const visitNode = (n, parent) => {
12947
+ if (!n || typeof n !== "object") return;
12948
+
12949
+ // Skip the declaration itself
12950
+ if (n === declNode) return;
12951
+
12952
+ // Found a reference
12953
+ if (n.type === "Identifier" && n.name === varName) {
12954
+ // Make sure it's not a property key or part of a member expression property
12955
+ const isMemberProp = parent && parent.type === "MemberExpression" && parent.property === n && !parent.computed;
12956
+ const isObjectKey = parent && parent.type === "Property" && parent.key === n && !parent.computed;
12957
+ const isShorthandValue = parent && parent.type === "Property" && parent.shorthand && parent.value === n;
12958
+
12959
+ // Include shorthand properties as references (they use the variable)
12960
+ if (!isMemberProp && !isObjectKey) {
12961
+ references.push(n);
12962
+ }
12963
+ }
12964
+
12965
+ for (const key of Object.keys(n)) {
12966
+ if (key === "parent" || key === "range" || key === "loc") continue;
12967
+
12968
+ const child = n[key];
12969
+
12970
+ if (Array.isArray(child)) {
12971
+ child.forEach((c) => visitNode(c, n));
12972
+ } else if (child && typeof child === "object" && child.type) {
12973
+ visitNode(child, n);
12974
+ }
12975
+ }
12976
+ };
12977
+
12978
+ visitNode(scope, null);
12979
+
12980
+ return references;
12981
+ };
12982
+
12983
+ // Check for destructuring of module imports (not allowed)
12463
12984
  const checkVariableDeclarationHandler = (node) => {
12464
12985
  for (const decl of node.declarations) {
12465
12986
  // Check for ObjectPattern destructuring
12466
12987
  if (decl.id.type === "ObjectPattern" && decl.init) {
12467
12988
  let sourceVarName = null;
12468
12989
 
12469
- // Direct destructuring: const { x } = dataImport
12990
+ // Direct destructuring: const { x } = moduleImport
12470
12991
  if (decl.init.type === "Identifier") {
12471
12992
  sourceVarName = decl.init.name;
12472
12993
  }
12473
12994
 
12474
- // Nested destructuring: const { x } = dataImport.nested
12995
+ // Nested destructuring: const { x } = moduleImport.nested
12475
12996
  if (decl.init.type === "MemberExpression") {
12476
12997
  let obj = decl.init;
12477
12998
 
@@ -12485,14 +13006,76 @@ const functionObjectDestructure = {
12485
13006
  }
12486
13007
 
12487
13008
  if (sourceVarName && moduleImports.has(sourceVarName)) {
13009
+ // Get destructured properties with their local names
12488
13010
  const destructuredProps = decl.id.properties
12489
13011
  .filter((p) => p.type === "Property" && p.key && p.key.name)
12490
- .map((p) => p.key.name);
13012
+ .map((p) => ({
13013
+ key: p.key.name,
13014
+ local: p.value && p.value.type === "Identifier" ? p.value.name : p.key.name,
13015
+ }));
12491
13016
 
12492
13017
  const sourceText = sourceCode.getText(decl.init);
12493
13018
 
13019
+ // Find the containing function/program to search for references
13020
+ let scope = node.parent;
13021
+
13022
+ while (scope && scope.type !== "BlockStatement" && scope.type !== "Program") {
13023
+ scope = scope.parent;
13024
+ }
13025
+
12494
13026
  context.report({
12495
- message: `Do not destructure module imports. Use dot notation for searchability: "${sourceText}.${destructuredProps[0]}" instead of destructuring`,
13027
+ fix: scope
13028
+ ? (fixer) => {
13029
+ const fixes = [];
13030
+
13031
+ // Replace all references with dot notation
13032
+ destructuredProps.forEach(({ key, local }) => {
13033
+ const refs = findAllReferencesHandler(scope, local, decl);
13034
+
13035
+ refs.forEach((ref) => {
13036
+ fixes.push(fixer.replaceText(ref, `${sourceText}.${key}`));
13037
+ });
13038
+ });
13039
+
13040
+ // Remove the entire declaration statement
13041
+ // If it's the only declaration in the statement, remove the whole statement
13042
+ if (node.declarations.length === 1) {
13043
+ // Find the full statement including newline
13044
+ const tokenBefore = sourceCode.getTokenBefore(node);
13045
+ const tokenAfter = sourceCode.getTokenAfter(node);
13046
+ let start = node.range[0];
13047
+ let end = node.range[1];
13048
+
13049
+ // Include leading whitespace/newline
13050
+ if (tokenBefore) {
13051
+ const textBetween = sourceCode.text.slice(tokenBefore.range[1], node.range[0]);
13052
+ const newlineIndex = textBetween.lastIndexOf("\n");
13053
+
13054
+ if (newlineIndex !== -1) {
13055
+ start = tokenBefore.range[1] + newlineIndex;
13056
+ }
13057
+ }
13058
+
13059
+ // Include trailing newline
13060
+ if (tokenAfter) {
13061
+ const textBetween = sourceCode.text.slice(node.range[1], tokenAfter.range[0]);
13062
+ const newlineIndex = textBetween.indexOf("\n");
13063
+
13064
+ if (newlineIndex !== -1) {
13065
+ end = node.range[1] + newlineIndex + 1;
13066
+ }
13067
+ }
13068
+
13069
+ fixes.push(fixer.removeRange([start, end]));
13070
+ } else {
13071
+ // Remove just this declarator
13072
+ fixes.push(fixer.remove(decl));
13073
+ }
13074
+
13075
+ return fixes;
13076
+ }
13077
+ : undefined,
13078
+ message: `Do not destructure module imports. Use dot notation for searchability: "${sourceText}.${destructuredProps[0].key}" instead of destructuring`,
12496
13079
  node: decl.id,
12497
13080
  });
12498
13081
  }
@@ -13275,6 +13858,66 @@ const componentPropsDestructure = {
13275
13858
  return accesses;
13276
13859
  };
13277
13860
 
13861
+ // Find body destructuring like: const { name } = data; or const { name, age } = data;
13862
+ const findBodyDestructuringHandler = (body, paramName) => {
13863
+ const results = [];
13864
+
13865
+ if (body.type !== "BlockStatement") return results;
13866
+
13867
+ for (const statement of body.body) {
13868
+ if (statement.type === "VariableDeclaration") {
13869
+ for (const declarator of statement.declarations) {
13870
+ // Check if it's destructuring from the param: const { x } = paramName
13871
+ if (
13872
+ declarator.id.type === "ObjectPattern" &&
13873
+ declarator.init &&
13874
+ declarator.init.type === "Identifier" &&
13875
+ declarator.init.name === paramName
13876
+ ) {
13877
+ const props = [];
13878
+
13879
+ for (const prop of declarator.id.properties) {
13880
+ if (prop.type === "Property" && prop.key.type === "Identifier") {
13881
+ // Handle both { name } and { name: alias }
13882
+ const keyName = prop.key.name;
13883
+ const valueName = prop.value.type === "Identifier" ? prop.value.name : null;
13884
+ const hasDefault = prop.value.type === "AssignmentPattern";
13885
+ let defaultValue = null;
13886
+
13887
+ if (hasDefault && prop.value.right) {
13888
+ defaultValue = sourceCode.getText(prop.value.right);
13889
+ }
13890
+
13891
+ props.push({
13892
+ default: defaultValue,
13893
+ hasAlias: keyName !== valueName && !hasDefault,
13894
+ key: keyName,
13895
+ value: hasDefault ? prop.value.left.name : valueName,
13896
+ });
13897
+ } else if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
13898
+ props.push({
13899
+ isRest: true,
13900
+ key: prop.argument.name,
13901
+ value: prop.argument.name,
13902
+ });
13903
+ }
13904
+ }
13905
+
13906
+ results.push({
13907
+ declarator,
13908
+ props,
13909
+ statement,
13910
+ // Track if this is the only declarator in the statement
13911
+ statementHasOnlyThisDeclarator: statement.declarations.length === 1,
13912
+ });
13913
+ }
13914
+ }
13915
+ }
13916
+ }
13917
+
13918
+ return results;
13919
+ };
13920
+
13278
13921
  const checkComponentPropsHandler = (node) => {
13279
13922
  if (!isReactComponentHandler(node)) return;
13280
13923
 
@@ -13288,14 +13931,33 @@ const componentPropsDestructure = {
13288
13931
 
13289
13932
  if (firstParam.type === "Identifier") {
13290
13933
  const paramName = firstParam.name;
13934
+
13935
+ // Find dot notation accesses: props.name
13291
13936
  const accesses = findPropAccessesHandler(node.body, paramName);
13292
- const accessedProps = [...new Set(accesses.map((a) => a.property))];
13293
13937
 
13294
- // Check if param is used directly (not just via dot notation)
13938
+ // Find body destructuring: const { name } = props
13939
+ const bodyDestructures = findBodyDestructuringHandler(node.body, paramName);
13940
+
13941
+ // Collect all accessed props from dot notation
13942
+ const dotNotationProps = [...new Set(accesses.map((a) => a.property))];
13943
+
13944
+ // Collect all props from body destructuring
13945
+ const bodyDestructuredProps = [];
13946
+
13947
+ bodyDestructures.forEach((bd) => {
13948
+ bd.props.forEach((p) => {
13949
+ bodyDestructuredProps.push(p);
13950
+ });
13951
+ });
13952
+
13953
+ // Check if param is used anywhere that we can't handle
13295
13954
  const allRefs = [];
13296
- const countRefs = (n) => {
13955
+ const countRefs = (n, skipNodes = []) => {
13297
13956
  if (!n || typeof n !== "object") return;
13298
13957
 
13958
+ // Skip nodes we're already accounting for
13959
+ if (skipNodes.includes(n)) return;
13960
+
13299
13961
  if (n.type === "Identifier" && n.name === paramName) allRefs.push(n);
13300
13962
 
13301
13963
  for (const key of Object.keys(n)) {
@@ -13303,23 +13965,58 @@ const componentPropsDestructure = {
13303
13965
 
13304
13966
  const child = n[key];
13305
13967
 
13306
- if (Array.isArray(child)) child.forEach(countRefs);
13307
- else if (child && typeof child === "object" && child.type) countRefs(child);
13968
+ if (Array.isArray(child)) child.forEach((c) => countRefs(c, skipNodes));
13969
+ else if (child && typeof child === "object" && child.type) countRefs(child, skipNodes);
13308
13970
  }
13309
13971
  };
13310
13972
 
13311
13973
  countRefs(node.body);
13312
13974
 
13313
- // Can only auto-fix if all references are covered by dot notation accesses
13314
- const canAutoFix = accessedProps.length > 0 && allRefs.length === accesses.length;
13975
+ // Count expected refs: dot notation accesses + body destructuring init nodes
13976
+ const expectedRefCount = accesses.length + bodyDestructures.length;
13977
+
13978
+ // Can auto-fix if:
13979
+ // 1. We have either dot notation props OR body destructured props
13980
+ // 2. All references to the param are accounted for
13981
+ const hasSomeProps = dotNotationProps.length > 0 || bodyDestructuredProps.length > 0;
13982
+ const canAutoFix = hasSomeProps && allRefs.length === expectedRefCount;
13315
13983
 
13316
13984
  context.report({
13317
13985
  fix: canAutoFix
13318
13986
  ? (fixer) => {
13319
13987
  const fixes = [];
13320
13988
 
13321
- // Build destructured pattern, preserving type annotation if present
13322
- const destructuredPattern = `{ ${accessedProps.join(", ")} }`;
13989
+ // Build the destructured props list for the parameter
13990
+ const allProps = [];
13991
+
13992
+ // Add dot notation props (simple names)
13993
+ dotNotationProps.forEach((p) => {
13994
+ if (!allProps.some((ap) => ap.key === p)) {
13995
+ allProps.push({ key: p, simple: true });
13996
+ }
13997
+ });
13998
+
13999
+ // Add body destructured props (may have aliases, defaults, rest)
14000
+ bodyDestructuredProps.forEach((p) => {
14001
+ // Don't duplicate if already in dot notation
14002
+ if (!allProps.some((ap) => ap.key === p.key)) {
14003
+ allProps.push(p);
14004
+ }
14005
+ });
14006
+
14007
+ // Build the destructured pattern string
14008
+ const propStrings = allProps.map((p) => {
14009
+ if (p.isRest) return `...${p.key}`;
14010
+
14011
+ if (p.simple) return p.key;
14012
+
14013
+ if (p.default) return `${p.key} = ${p.default}`;
14014
+
14015
+ if (p.hasAlias) return `${p.key}: ${p.value}`;
14016
+
14017
+ return p.key;
14018
+ });
14019
+ const destructuredPattern = `{ ${propStrings.join(", ")} }`;
13323
14020
  let replacement = destructuredPattern;
13324
14021
 
13325
14022
  // Preserve TypeScript type annotation if present
@@ -13337,6 +14034,27 @@ const componentPropsDestructure = {
13337
14034
  fixes.push(fixer.replaceText(access.node, access.property));
13338
14035
  });
13339
14036
 
14037
+ // Remove body destructuring statements
14038
+ bodyDestructures.forEach((bd) => {
14039
+ if (bd.statementHasOnlyThisDeclarator) {
14040
+ // Remove the entire statement including newline
14041
+ const statementStart = bd.statement.range[0];
14042
+ let statementEnd = bd.statement.range[1];
14043
+
14044
+ // Try to also remove trailing newline/whitespace
14045
+ const textAfter = sourceCode.getText().slice(statementEnd, statementEnd + 2);
14046
+
14047
+ if (textAfter.startsWith("\n")) statementEnd += 1;
14048
+ else if (textAfter.startsWith("\r\n")) statementEnd += 2;
14049
+
14050
+ fixes.push(fixer.removeRange([statementStart, statementEnd]));
14051
+ } else {
14052
+ // Only remove this declarator from a multi-declarator statement
14053
+ // This is more complex - for now just remove the declarator text
14054
+ fixes.push(fixer.remove(bd.declarator));
14055
+ }
14056
+ });
14057
+
13340
14058
  return fixes;
13341
14059
  }
13342
14060
  : undefined,
@@ -16456,12 +17174,16 @@ export default {
16456
17174
 
16457
17175
  // Control flow rules
16458
17176
  "block-statement-newlines": blockStatementNewlines,
17177
+ "empty-line-after-block": emptyLineAfterBlock,
16459
17178
  "if-else-spacing": ifElseSpacing,
16460
17179
  "if-statement-format": ifStatementFormat,
16461
17180
  "multiline-if-conditions": multilineIfConditions,
16462
17181
  "no-empty-lines-in-switch-cases": noEmptyLinesInSwitchCases,
16463
17182
  "ternary-condition-multiline": ternaryConditionMultiline,
16464
17183
 
17184
+ // Class rules
17185
+ "class-naming-convention": classNamingConvention,
17186
+
16465
17187
  // Function rules
16466
17188
  "function-call-spacing": functionCallSpacing,
16467
17189
  "function-declaration-style": functionDeclarationStyle,
@@ -16518,6 +17240,9 @@ export default {
16518
17240
  "type-format": typeFormat,
16519
17241
  "typescript-definition-location": typescriptDefinitionLocation,
16520
17242
 
17243
+ // Type/Enum rules
17244
+ "enum-type-enforcement": enumTypeEnforcement,
17245
+
16521
17246
  // Variable rules
16522
17247
  "variable-naming-convention": variableNamingConvention,
16523
17248
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.6.4",
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",