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 +56 -0
- package/README.md +12 -8
- package/index.d.ts +6 -0
- package/index.js +812 -87
- package/package.json +1 -1
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
|
-
*
|
|
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 **
|
|
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** —
|
|
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
|
|
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
|
-
**
|
|
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
|
-
> **
|
|
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` |
|
|
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
|
-
|
|
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
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
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
|
-
|
|
3992
|
-
if (test.type !== "LogicalExpression") return;
|
|
3992
|
+
if (test.type === "MemberExpression") return true;
|
|
3993
3993
|
|
|
3994
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4007
|
-
(op) => isBinaryExpressionSplitHandler(op),
|
|
4008
|
-
);
|
|
3998
|
+
if (test.type === "CallExpression") return true;
|
|
4009
3999
|
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
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
|
-
|
|
4019
|
-
|
|
4005
|
+
return false;
|
|
4006
|
+
};
|
|
4020
4007
|
|
|
4021
|
-
|
|
4022
|
-
|
|
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
|
-
|
|
4026
|
-
|
|
4014
|
+
return `${testText} ? ${consequentText} : ${alternateText}`;
|
|
4015
|
+
};
|
|
4027
4016
|
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
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
|
-
|
|
4036
|
-
|
|
4021
|
+
return lineText.match(/^\s*/)[0].length;
|
|
4022
|
+
};
|
|
4037
4023
|
|
|
4038
|
-
|
|
4039
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
4067
|
-
const rightText =
|
|
4108
|
+
const leftText = buildSameLineHandler(n.left);
|
|
4109
|
+
const rightText = buildSameLineHandler(n.right);
|
|
4068
4110
|
|
|
4069
|
-
return `${leftText}
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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 } =
|
|
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 } =
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
13314
|
-
const
|
|
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
|
|
13322
|
-
const
|
|
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