eslint-plugin-code-style 1.7.1 → 1.7.3
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 +35 -2
- package/README.md +236 -0
- package/index.js +430 -68
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,14 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.7.3] - 2026-02-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`ternary-condition-multiline`** - Fix `?`/`:` on own line without value; collapse simple ternaries to single line when they fit
|
|
15
|
+
- **`no-empty-lines-in-function-params`** - Detect empty lines after opening `{` and before closing `}` in ObjectPattern params
|
|
16
|
+
- **`empty-line-after-block`** - Skip consecutive if statements (already handled by `if-else-spacing`)
|
|
17
|
+
- **`classname-multiline`** - Fix closing backtick alignment for return statements
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
- Add 6 missing rules to README detailed documentation
|
|
22
|
+
- Add 7 missing rules to README Quick Start example
|
|
23
|
+
- Update rule counts from 66 to 69 across all documentation files
|
|
24
|
+
- Update AGENTS.md Tailwind section with actual rules and comparison with `tailwindcss/classnames-order`
|
|
25
|
+
- Add README multi-section update warnings to AGENTS.md rule modification checklists
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`manage-rule` skill** - New skill for adding, editing, or removing ESLint rules with complete workflow
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## [1.7.2] - 2026-02-02
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **`enum-format`** - Fix double comma bug when auto-fixing trailing comma and closing brace position; check for comma token after member, not just member text
|
|
38
|
+
- **`interface-format`** - Same fix as enum-format for trailing comma detection
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
10
42
|
## [1.7.1] - 2026-02-02
|
|
11
43
|
|
|
12
44
|
### Fixed
|
|
13
45
|
|
|
14
46
|
- **`no-empty-lines-in-function-params`** - Detect empty lines between destructured properties inside ObjectPattern params
|
|
15
47
|
- **`component-props-inline-type`** - Handle TSIntersectionType (e.g., `ButtonHTMLAttributes & { prop: Type }`): check `&` position, opening brace position, and apply formatting rules to type literals within intersections
|
|
16
|
-
- **`enum-type-enforcement`** -
|
|
17
|
-
- **`ternary-condition-multiline`** - Improve simple ternary prefix calculation
|
|
48
|
+
- **`enum-type-enforcement`** - Handle TSIntersectionType to track typed props; fix extractTypeInfoHandler argument for TSPropertySignature members
|
|
49
|
+
- **`ternary-condition-multiline`** - Improve simple ternary prefix calculation for object properties; add checks for `?` on same line as condition end and empty lines before `?` or `:`; fix multiline formatting to not add leading newline
|
|
18
50
|
|
|
19
51
|
---
|
|
20
52
|
|
|
@@ -1075,6 +1107,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
1075
1107
|
|
|
1076
1108
|
---
|
|
1077
1109
|
|
|
1110
|
+
[1.7.2]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.1...v1.7.2
|
|
1078
1111
|
[1.7.1]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.7.0...v1.7.1
|
|
1079
1112
|
[1.7.0]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.6...v1.7.0
|
|
1080
1113
|
[1.6.6]: https://github.com/Mohamed-Elhawary/eslint-plugin-code-style/compare/v1.6.5...v1.6.6
|
package/README.md
CHANGED
|
@@ -190,14 +190,18 @@ rules: {
|
|
|
190
190
|
"code-style/arrow-function-simplify": "error",
|
|
191
191
|
"code-style/assignment-value-same-line": "error",
|
|
192
192
|
"code-style/block-statement-newlines": "error",
|
|
193
|
+
"code-style/class-naming-convention": "error",
|
|
193
194
|
"code-style/classname-dynamic-at-end": "error",
|
|
194
195
|
"code-style/classname-multiline": "error",
|
|
195
196
|
"code-style/classname-no-extra-spaces": "error",
|
|
197
|
+
"code-style/classname-order": "error",
|
|
196
198
|
"code-style/comment-format": "error",
|
|
197
199
|
"code-style/component-props-destructure": "error",
|
|
198
200
|
"code-style/component-props-inline-type": "error",
|
|
199
201
|
"code-style/curried-arrow-same-line": "error",
|
|
202
|
+
"code-style/empty-line-after-block": "error",
|
|
200
203
|
"code-style/enum-format": "error",
|
|
204
|
+
"code-style/enum-type-enforcement": "error",
|
|
201
205
|
"code-style/export-format": "error",
|
|
202
206
|
"code-style/function-arguments-format": "error",
|
|
203
207
|
"code-style/function-call-spacing": "error",
|
|
@@ -212,6 +216,7 @@ rules: {
|
|
|
212
216
|
"code-style/import-format": "error",
|
|
213
217
|
"code-style/import-source-spacing": "error",
|
|
214
218
|
"code-style/index-export-style": "error",
|
|
219
|
+
"code-style/index-exports-only": "error",
|
|
215
220
|
"code-style/interface-format": "error",
|
|
216
221
|
"code-style/jsx-children-on-new-line": "error",
|
|
217
222
|
"code-style/jsx-closing-bracket-spacing": "error",
|
|
@@ -231,6 +236,7 @@ rules: {
|
|
|
231
236
|
"code-style/no-empty-lines-in-jsx": "error",
|
|
232
237
|
"code-style/no-empty-lines-in-objects": "error",
|
|
233
238
|
"code-style/no-empty-lines-in-switch-cases": "error",
|
|
239
|
+
"code-style/no-inline-type-definitions": "error",
|
|
234
240
|
"code-style/object-property-per-line": "error",
|
|
235
241
|
"code-style/object-property-value-brace": "error",
|
|
236
242
|
"code-style/object-property-value-format": "error",
|
|
@@ -239,6 +245,7 @@ rules: {
|
|
|
239
245
|
"code-style/simple-call-single-line": "error",
|
|
240
246
|
"code-style/single-argument-on-one-line": "error",
|
|
241
247
|
"code-style/string-property-spacing": "error",
|
|
248
|
+
"code-style/ternary-condition-multiline": "error",
|
|
242
249
|
"code-style/type-annotation-spacing": "error",
|
|
243
250
|
"code-style/type-format": "error",
|
|
244
251
|
"code-style/typescript-definition-location": "error",
|
|
@@ -881,6 +888,37 @@ dispatch(
|
|
|
881
888
|
|
|
882
889
|
<br />
|
|
883
890
|
|
|
891
|
+
## 🏛️ Class Rules
|
|
892
|
+
|
|
893
|
+
### `class-naming-convention`
|
|
894
|
+
|
|
895
|
+
**What it does:** Enforces that class declarations must end with "Class" suffix. This distinguishes class definitions from other PascalCase names like React components or type definitions.
|
|
896
|
+
|
|
897
|
+
**Why use it:** Clear naming conventions prevent confusion between classes, components, and types. The "Class" suffix immediately identifies the construct.
|
|
898
|
+
|
|
899
|
+
```javascript
|
|
900
|
+
// ✅ Good — class ends with "Class"
|
|
901
|
+
class ApiServiceClass {
|
|
902
|
+
constructor() {}
|
|
903
|
+
fetch() {}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
class UserRepositoryClass {
|
|
907
|
+
save(user) {}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// ❌ Bad — missing "Class" suffix
|
|
911
|
+
class ApiService {
|
|
912
|
+
constructor() {}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
class UserRepository {
|
|
916
|
+
save(user) {}
|
|
917
|
+
}
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
<br />
|
|
921
|
+
|
|
884
922
|
## 🔀 Control Flow Rules
|
|
885
923
|
|
|
886
924
|
### `block-statement-newlines`
|
|
@@ -916,6 +954,38 @@ for (const item of items) { process(item);
|
|
|
916
954
|
|
|
917
955
|
---
|
|
918
956
|
|
|
957
|
+
### `empty-line-after-block`
|
|
958
|
+
|
|
959
|
+
**What it does:** Requires an empty line between a closing brace `}` of a block statement (if, try, for, while, etc.) and the next statement, unless the next statement is part of the same construct (else, catch, finally).
|
|
960
|
+
|
|
961
|
+
**Why use it:** Visual separation between logical blocks improves code readability and makes the structure clearer.
|
|
962
|
+
|
|
963
|
+
> **Note:** Consecutive if statements are handled by `if-else-spacing` rule.
|
|
964
|
+
|
|
965
|
+
```javascript
|
|
966
|
+
// ✅ Good — empty line after block
|
|
967
|
+
if (condition) {
|
|
968
|
+
doSomething();
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const x = 1;
|
|
972
|
+
|
|
973
|
+
// ✅ Good — else is part of same construct (no empty line needed)
|
|
974
|
+
if (condition) {
|
|
975
|
+
doSomething();
|
|
976
|
+
} else {
|
|
977
|
+
doOther();
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ❌ Bad — no empty line after block
|
|
981
|
+
if (condition) {
|
|
982
|
+
doSomething();
|
|
983
|
+
}
|
|
984
|
+
const x = 1;
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
---
|
|
988
|
+
|
|
919
989
|
### `if-else-spacing`
|
|
920
990
|
|
|
921
991
|
**What it does:** Enforces proper spacing between if statements and if-else chains:
|
|
@@ -1137,6 +1207,39 @@ switch (status) {
|
|
|
1137
1207
|
}
|
|
1138
1208
|
```
|
|
1139
1209
|
|
|
1210
|
+
---
|
|
1211
|
+
|
|
1212
|
+
### `ternary-condition-multiline`
|
|
1213
|
+
|
|
1214
|
+
**What it does:** Enforces consistent ternary formatting:
|
|
1215
|
+
- Simple ternaries (≤3 operands in condition) collapse to single line if they fit
|
|
1216
|
+
- Complex ternaries (>3 operands) expand to multiline with each operand on its own line
|
|
1217
|
+
|
|
1218
|
+
**Why use it:** Long ternary conditions on a single line are hard to read. Breaking complex conditions into multiple lines makes them scannable.
|
|
1219
|
+
|
|
1220
|
+
**Options:**
|
|
1221
|
+
|
|
1222
|
+
| Option | Type | Default | Description |
|
|
1223
|
+
|--------|------|---------|-------------|
|
|
1224
|
+
| `maxOperands` | `integer` | `3` | Maximum operands to keep on single line |
|
|
1225
|
+
| `maxLineLength` | `integer` | `120` | Maximum line length for single-line ternaries |
|
|
1226
|
+
|
|
1227
|
+
```javascript
|
|
1228
|
+
// ✅ Good — simple condition on single line
|
|
1229
|
+
const x = a && b && c ? "yes" : "no";
|
|
1230
|
+
|
|
1231
|
+
// ✅ Good — complex condition multiline (>3 operands)
|
|
1232
|
+
const style = variant === "ghost"
|
|
1233
|
+
|| variant === "ghost-danger"
|
|
1234
|
+
|| variant === "muted"
|
|
1235
|
+
|| variant === "primary"
|
|
1236
|
+
? "transparent"
|
|
1237
|
+
: "solid";
|
|
1238
|
+
|
|
1239
|
+
// ❌ Bad — complex condition crammed on one line
|
|
1240
|
+
const style = variant === "ghost" || variant === "ghost-danger" || variant === "muted" || variant === "primary" ? "transparent" : "solid";
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1140
1243
|
<br />
|
|
1141
1244
|
|
|
1142
1245
|
## ⚡ Function Rules
|
|
@@ -1719,6 +1822,28 @@ export {
|
|
|
1719
1822
|
|
|
1720
1823
|
---
|
|
1721
1824
|
|
|
1825
|
+
### `index-exports-only`
|
|
1826
|
+
|
|
1827
|
+
**What it does:** Index files (`index.ts`, `index.tsx`, `index.js`, `index.jsx`) should only contain imports and re-exports, not any code definitions. All definitions (types, interfaces, functions, variables, classes) should be moved to separate files.
|
|
1828
|
+
|
|
1829
|
+
**Why use it:** Index files should be "barrels" that aggregate exports from a module. Mixing definitions with re-exports makes the codebase harder to navigate and can cause circular dependency issues.
|
|
1830
|
+
|
|
1831
|
+
```javascript
|
|
1832
|
+
// ✅ Good — index.ts with only imports and re-exports
|
|
1833
|
+
export { Button } from "./Button";
|
|
1834
|
+
export { helper } from "./utils";
|
|
1835
|
+
export type { ButtonProps } from "./types";
|
|
1836
|
+
export * from "./constants";
|
|
1837
|
+
|
|
1838
|
+
// ❌ Bad — index.ts with code definitions
|
|
1839
|
+
export type ButtonVariant = "primary" | "secondary"; // Move to types.ts
|
|
1840
|
+
export interface ButtonProps { ... } // Move to types.ts
|
|
1841
|
+
export const CONSTANT = "value"; // Move to constants.ts
|
|
1842
|
+
export function helper() { ... } // Move to utils.ts
|
|
1843
|
+
```
|
|
1844
|
+
|
|
1845
|
+
---
|
|
1846
|
+
|
|
1722
1847
|
### `module-index-exports`
|
|
1723
1848
|
|
|
1724
1849
|
**What it does:** Ensures module folders have index files that export all their contents, creating a proper public API for each module.
|
|
@@ -1897,6 +2022,49 @@ const buttonClasses = ` flex items-center ${className} `;
|
|
|
1897
2022
|
|
|
1898
2023
|
---
|
|
1899
2024
|
|
|
2025
|
+
### `classname-order`
|
|
2026
|
+
|
|
2027
|
+
**What it does:** Enforces Tailwind CSS class ordering in variables, object properties, and return statements. Uses smart detection to identify Tailwind class strings.
|
|
2028
|
+
|
|
2029
|
+
**Why use it:** This rule complements the official `tailwindcss/classnames-order` plugin by handling areas it doesn't cover:
|
|
2030
|
+
- **`tailwindcss/classnames-order`** — Handles JSX `className` attributes directly
|
|
2031
|
+
- **`classname-order`** — Handles class strings in variables, object properties, and return statements
|
|
2032
|
+
|
|
2033
|
+
Both rules should be enabled together for complete Tailwind class ordering coverage.
|
|
2034
|
+
|
|
2035
|
+
**Order enforced:** layout (flex, grid) → positioning → sizing (w, h) → spacing (p, m) → typography (text, font) → colors (bg, text) → effects (shadow, opacity) → transitions → states (hover, focus)
|
|
2036
|
+
|
|
2037
|
+
```javascript
|
|
2038
|
+
// ✅ Good — classes in correct order (variable)
|
|
2039
|
+
const buttonClasses = "flex items-center px-4 py-2 text-white bg-blue-500 hover:bg-blue-600";
|
|
2040
|
+
|
|
2041
|
+
// ✅ Good — classes in correct order (object property)
|
|
2042
|
+
const variants = {
|
|
2043
|
+
primary: "flex items-center bg-blue-500 hover:bg-blue-600",
|
|
2044
|
+
secondary: "flex items-center bg-gray-500 hover:bg-gray-600",
|
|
2045
|
+
};
|
|
2046
|
+
|
|
2047
|
+
// ✅ Good — classes in correct order (return statement)
|
|
2048
|
+
const getInputStyles = () => {
|
|
2049
|
+
return "border-error text-error placeholder-error/50 focus:border-error";
|
|
2050
|
+
};
|
|
2051
|
+
|
|
2052
|
+
// ❌ Bad — hover state before base color (variable)
|
|
2053
|
+
const buttonClasses = "flex items-center hover:bg-blue-600 bg-blue-500";
|
|
2054
|
+
|
|
2055
|
+
// ❌ Bad — unordered classes (object property)
|
|
2056
|
+
const variants = {
|
|
2057
|
+
primary: "hover:bg-blue-600 bg-blue-500 flex items-center",
|
|
2058
|
+
};
|
|
2059
|
+
|
|
2060
|
+
// ❌ Bad — unordered classes (return statement)
|
|
2061
|
+
const getInputStyles = () => {
|
|
2062
|
+
return "focus:border-error text-error border-error";
|
|
2063
|
+
};
|
|
2064
|
+
```
|
|
2065
|
+
|
|
2066
|
+
---
|
|
2067
|
+
|
|
1900
2068
|
### `jsx-children-on-new-line`
|
|
1901
2069
|
|
|
1902
2070
|
**What it does:** When a JSX element has multiple children, ensures each child is on its own line with proper indentation.
|
|
@@ -2696,6 +2864,38 @@ export enum UserStatusEnum {
|
|
|
2696
2864
|
|
|
2697
2865
|
---
|
|
2698
2866
|
|
|
2867
|
+
### `enum-type-enforcement`
|
|
2868
|
+
|
|
2869
|
+
**What it does:** When a variable or parameter has a type ending in `Type` (like `ButtonVariantType`), enforces using the corresponding enum (`ButtonVariantEnum.VALUE`) instead of string literals.
|
|
2870
|
+
|
|
2871
|
+
**Why use it:** Using enum values instead of string literals provides type safety, autocompletion, and prevents typos. Changes to enum values automatically propagate.
|
|
2872
|
+
|
|
2873
|
+
```javascript
|
|
2874
|
+
// ✅ Good — using enum values
|
|
2875
|
+
const Button = ({
|
|
2876
|
+
variant = ButtonVariantEnum.PRIMARY,
|
|
2877
|
+
}: {
|
|
2878
|
+
variant?: ButtonVariantType,
|
|
2879
|
+
}) => { ... };
|
|
2880
|
+
|
|
2881
|
+
if (variant === ButtonVariantEnum.GHOST) {
|
|
2882
|
+
// ...
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
// ❌ Bad — using string literals
|
|
2886
|
+
const Button = ({
|
|
2887
|
+
variant = "primary", // Should use ButtonVariantEnum.PRIMARY
|
|
2888
|
+
}: {
|
|
2889
|
+
variant?: ButtonVariantType,
|
|
2890
|
+
}) => { ... };
|
|
2891
|
+
|
|
2892
|
+
if (variant === "ghost") { // Should use ButtonVariantEnum.GHOST
|
|
2893
|
+
// ...
|
|
2894
|
+
}
|
|
2895
|
+
```
|
|
2896
|
+
|
|
2897
|
+
---
|
|
2898
|
+
|
|
2699
2899
|
### `interface-format`
|
|
2700
2900
|
|
|
2701
2901
|
**What it does:** Enforces consistent formatting for TypeScript interfaces:
|
|
@@ -2739,6 +2939,42 @@ export interface UserInterface {
|
|
|
2739
2939
|
|
|
2740
2940
|
---
|
|
2741
2941
|
|
|
2942
|
+
### `no-inline-type-definitions`
|
|
2943
|
+
|
|
2944
|
+
**What it does:** Reports when function parameters have inline union types that are too complex (too many members or too long). These should be extracted to a named type in a types file.
|
|
2945
|
+
|
|
2946
|
+
**Why use it:** Complex inline types make function signatures hard to read. Named types are reusable, self-documenting, and easier to maintain.
|
|
2947
|
+
|
|
2948
|
+
**Options:**
|
|
2949
|
+
|
|
2950
|
+
| Option | Type | Default | Description |
|
|
2951
|
+
|--------|------|---------|-------------|
|
|
2952
|
+
| `maxUnionMembers` | `integer` | `2` | Maximum union members before requiring extraction |
|
|
2953
|
+
| `maxLength` | `integer` | `50` | Maximum character length before requiring extraction |
|
|
2954
|
+
|
|
2955
|
+
```javascript
|
|
2956
|
+
// ✅ Good — type extracted to separate file
|
|
2957
|
+
// types.ts
|
|
2958
|
+
export type ButtonVariantType = "primary" | "muted" | "danger";
|
|
2959
|
+
|
|
2960
|
+
// Button.tsx
|
|
2961
|
+
import { ButtonVariantType } from "./types";
|
|
2962
|
+
export const Button = ({
|
|
2963
|
+
variant,
|
|
2964
|
+
}: {
|
|
2965
|
+
variant?: ButtonVariantType,
|
|
2966
|
+
}) => { ... };
|
|
2967
|
+
|
|
2968
|
+
// ❌ Bad — complex inline union type
|
|
2969
|
+
export const Button = ({
|
|
2970
|
+
variant,
|
|
2971
|
+
}: {
|
|
2972
|
+
variant?: "primary" | "muted" | "danger", // Extract to named type
|
|
2973
|
+
}) => { ... };
|
|
2974
|
+
```
|
|
2975
|
+
|
|
2976
|
+
---
|
|
2977
|
+
|
|
2742
2978
|
### `type-format`
|
|
2743
2979
|
|
|
2744
2980
|
**What it does:** Enforces consistent formatting for TypeScript type aliases:
|
package/index.js
CHANGED
|
@@ -4030,10 +4030,31 @@ const ternaryConditionMultiline = {
|
|
|
4030
4030
|
return false;
|
|
4031
4031
|
};
|
|
4032
4032
|
|
|
4033
|
+
// Check if ? or : is on its own line without its value
|
|
4034
|
+
const isOperatorOnOwnLineHandler = (node) => {
|
|
4035
|
+
const questionToken = sourceCode.getTokenAfter(node.test, (t) => t.value === "?");
|
|
4036
|
+
const colonToken = sourceCode.getTokenAfter(node.consequent, (t) => t.value === ":");
|
|
4037
|
+
|
|
4038
|
+
// Check if ? is on different line than consequent start
|
|
4039
|
+
if (questionToken && node.consequent.loc.start.line !== questionToken.loc.start.line) {
|
|
4040
|
+
return true;
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
// Check if : is on different line than alternate start
|
|
4044
|
+
if (colonToken && node.alternate.loc.start.line !== colonToken.loc.start.line) {
|
|
4045
|
+
return true;
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
return false;
|
|
4049
|
+
};
|
|
4050
|
+
|
|
4033
4051
|
// Handle simple ternaries - collapse to single line if they fit
|
|
4034
4052
|
const handleSimpleTernaryHandler = (node) => {
|
|
4035
|
-
|
|
4036
|
-
|
|
4053
|
+
const isOnSingleLine = node.loc.start.line === node.loc.end.line;
|
|
4054
|
+
const hasOperatorOnOwnLine = isOperatorOnOwnLineHandler(node);
|
|
4055
|
+
|
|
4056
|
+
// Skip if already on single line and no formatting issues
|
|
4057
|
+
if (isOnSingleLine && !hasOperatorOnOwnLine) return false;
|
|
4037
4058
|
|
|
4038
4059
|
// Skip nested ternaries
|
|
4039
4060
|
if (node.consequent.type === "ConditionalExpression" || node.alternate.type === "ConditionalExpression") {
|
|
@@ -4049,7 +4070,7 @@ const ternaryConditionMultiline = {
|
|
|
4049
4070
|
const singleLineText = getTernarySingleLineHandler(node);
|
|
4050
4071
|
const indent = getLineIndentHandler(node);
|
|
4051
4072
|
|
|
4052
|
-
// Check if the parent needs prefix text (like "const x = ")
|
|
4073
|
+
// Check if the parent needs prefix text (like "const x = " or "key: ")
|
|
4053
4074
|
let prefixLength = 0;
|
|
4054
4075
|
const parent = node.parent;
|
|
4055
4076
|
|
|
@@ -4066,6 +4087,11 @@ const ternaryConditionMultiline = {
|
|
|
4066
4087
|
const leftText = sourceCode.getText(parent.left);
|
|
4067
4088
|
|
|
4068
4089
|
prefixLength = leftText.length + 3; // left + " = "
|
|
4090
|
+
} else if (parent && parent.type === "Property" && parent.value === node) {
|
|
4091
|
+
// Object property: key: ternary
|
|
4092
|
+
const keyText = sourceCode.getText(parent.key);
|
|
4093
|
+
|
|
4094
|
+
prefixLength = keyText.length + 2; // key + ": "
|
|
4069
4095
|
}
|
|
4070
4096
|
|
|
4071
4097
|
// Check if single line would fit
|
|
@@ -4092,7 +4118,7 @@ const ternaryConditionMultiline = {
|
|
|
4092
4118
|
const testEndLine = test.loc.end.line;
|
|
4093
4119
|
const isMultiLine = testStartLine !== testEndLine;
|
|
4094
4120
|
|
|
4095
|
-
// ≤maxOperands operands: keep condition on single line
|
|
4121
|
+
// ≤maxOperands operands: keep condition on single line, and try to collapse whole ternary
|
|
4096
4122
|
if (operands.length <= maxOperands) {
|
|
4097
4123
|
const firstOperandStartLine = operands[0].loc.start.line;
|
|
4098
4124
|
const allOperandsStartOnSameLine = operands.every(
|
|
@@ -4103,29 +4129,82 @@ const ternaryConditionMultiline = {
|
|
|
4103
4129
|
(op) => isBinaryExpressionSplitHandler(op),
|
|
4104
4130
|
);
|
|
4105
4131
|
|
|
4106
|
-
if
|
|
4107
|
-
|
|
4108
|
-
fix: (fixer) => {
|
|
4109
|
-
const buildSameLineHandler = (n) => {
|
|
4110
|
-
if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
|
|
4111
|
-
const leftText = buildSameLineHandler(n.left);
|
|
4112
|
-
const rightText = buildSameLineHandler(n.right);
|
|
4132
|
+
// Check if ? or : is on its own line without its value
|
|
4133
|
+
const hasOperatorOnOwnLine = isOperatorOnOwnLineHandler(node);
|
|
4113
4134
|
|
|
4114
|
-
|
|
4115
|
-
|
|
4135
|
+
// Check if ternary is multiline (could be collapsed)
|
|
4136
|
+
const isTernaryMultiline = node.loc.start.line !== node.loc.end.line;
|
|
4116
4137
|
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4138
|
+
// Helper to build single line condition
|
|
4139
|
+
const buildSameLineHandler = (n) => {
|
|
4140
|
+
if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
|
|
4141
|
+
const leftText = buildSameLineHandler(n.left);
|
|
4142
|
+
const rightText = buildSameLineHandler(n.right);
|
|
4120
4143
|
|
|
4121
|
-
|
|
4122
|
-
|
|
4144
|
+
return `${leftText} ${n.operator} ${rightText}`;
|
|
4145
|
+
}
|
|
4123
4146
|
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4147
|
+
if (n.type === "BinaryExpression" && isBinaryExpressionSplitHandler(n)) {
|
|
4148
|
+
return buildBinaryExpressionSingleLineHandler(n);
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
return getSourceTextWithGroupsHandler(n);
|
|
4152
|
+
};
|
|
4153
|
+
|
|
4154
|
+
// Check if whole ternary can fit on one line
|
|
4155
|
+
const singleLineText = getTernarySingleLineHandler(node);
|
|
4156
|
+
const indent = getLineIndentHandler(node);
|
|
4157
|
+
|
|
4158
|
+
// Calculate prefix length for context
|
|
4159
|
+
let prefixLength = 0;
|
|
4160
|
+
const parent = node.parent;
|
|
4161
|
+
|
|
4162
|
+
if (parent && parent.type === "VariableDeclarator" && parent.init === node) {
|
|
4163
|
+
const varDecl = parent.parent;
|
|
4164
|
+
const declKeyword = varDecl ? sourceCode.getFirstToken(varDecl).value : "const";
|
|
4165
|
+
const varName = parent.id.name || sourceCode.getText(parent.id);
|
|
4166
|
+
|
|
4167
|
+
prefixLength = declKeyword.length + 1 + varName.length + 3;
|
|
4168
|
+
} else if (parent && parent.type === "AssignmentExpression" && parent.right === node) {
|
|
4169
|
+
const leftText = sourceCode.getText(parent.left);
|
|
4170
|
+
|
|
4171
|
+
prefixLength = leftText.length + 3;
|
|
4172
|
+
} else if (parent && parent.type === "Property" && parent.value === node) {
|
|
4173
|
+
const keyText = sourceCode.getText(parent.key);
|
|
4174
|
+
|
|
4175
|
+
prefixLength = keyText.length + 2;
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
const totalLength = indent + prefixLength + singleLineText.length + 1;
|
|
4179
|
+
const canFitOnOneLine = totalLength <= maxLineLength;
|
|
4180
|
+
|
|
4181
|
+
// Skip if branches have complex objects
|
|
4182
|
+
const hasComplexBranches = hasComplexObjectHandler(node.consequent) || hasComplexObjectHandler(node.alternate);
|
|
4183
|
+
|
|
4184
|
+
// Skip nested ternaries
|
|
4185
|
+
const hasNestedTernary = node.consequent.type === "ConditionalExpression" || node.alternate.type === "ConditionalExpression";
|
|
4186
|
+
|
|
4187
|
+
// Determine if we need to fix anything
|
|
4188
|
+
const needsConditionFix = !allOperandsStartOnSameLine || hasSplitBinaryExpression;
|
|
4189
|
+
const needsTernaryCollapse = isTernaryMultiline && canFitOnOneLine && !hasComplexBranches && !hasNestedTernary;
|
|
4190
|
+
const needsOperatorFix = hasOperatorOnOwnLine;
|
|
4191
|
+
|
|
4192
|
+
if (needsConditionFix || needsTernaryCollapse || needsOperatorFix) {
|
|
4193
|
+
// If whole ternary can fit on one line, collapse it
|
|
4194
|
+
if (canFitOnOneLine && !hasComplexBranches && !hasNestedTernary) {
|
|
4195
|
+
context.report({
|
|
4196
|
+
fix: (fixer) => fixer.replaceText(node, singleLineText),
|
|
4197
|
+
message: `Ternary with ≤${maxOperands} operands should be on single line when it fits`,
|
|
4198
|
+
node,
|
|
4199
|
+
});
|
|
4200
|
+
} else if (needsConditionFix) {
|
|
4201
|
+
// Otherwise just fix the condition to be on single line
|
|
4202
|
+
context.report({
|
|
4203
|
+
fix: (fixer) => fixer.replaceText(test, buildSameLineHandler(test)),
|
|
4204
|
+
message: `Ternary conditions with ≤${maxOperands} operands should be single line`,
|
|
4205
|
+
node: test,
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
4129
4208
|
}
|
|
4130
4209
|
|
|
4131
4210
|
return;
|
|
@@ -4155,6 +4234,29 @@ const ternaryConditionMultiline = {
|
|
|
4155
4234
|
isCorrectionNeeded = true;
|
|
4156
4235
|
}
|
|
4157
4236
|
}
|
|
4237
|
+
|
|
4238
|
+
// Check for empty lines before ? (between condition and ?)
|
|
4239
|
+
if (!isCorrectionNeeded) {
|
|
4240
|
+
const questionToken = sourceCode.getTokenAfter(test, (t) => t.value === "?");
|
|
4241
|
+
|
|
4242
|
+
if (questionToken && questionToken.loc.start.line > test.loc.end.line + 1) {
|
|
4243
|
+
isCorrectionNeeded = true;
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
// Check for empty lines before : (between consequent and :)
|
|
4248
|
+
if (!isCorrectionNeeded) {
|
|
4249
|
+
const colonToken = sourceCode.getTokenAfter(node.consequent, (t) => t.value === ":");
|
|
4250
|
+
|
|
4251
|
+
if (colonToken && colonToken.loc.start.line > node.consequent.loc.end.line + 1) {
|
|
4252
|
+
isCorrectionNeeded = true;
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
// Check if ? or : is on its own line without its value
|
|
4257
|
+
if (!isCorrectionNeeded && isOperatorOnOwnLineHandler(node)) {
|
|
4258
|
+
isCorrectionNeeded = true;
|
|
4259
|
+
}
|
|
4158
4260
|
}
|
|
4159
4261
|
|
|
4160
4262
|
if (isCorrectionNeeded) {
|
|
@@ -4164,7 +4266,6 @@ const ternaryConditionMultiline = {
|
|
|
4164
4266
|
const lineText = sourceCode.lines[node.loc.start.line - 1];
|
|
4165
4267
|
const baseIndent = lineText.match(/^\s*/)[0];
|
|
4166
4268
|
const conditionIndent = baseIndent + " ";
|
|
4167
|
-
const branchIndent = baseIndent + " ";
|
|
4168
4269
|
|
|
4169
4270
|
const buildMultilineHandler = (n) => {
|
|
4170
4271
|
if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
|
|
@@ -4180,7 +4281,9 @@ const ternaryConditionMultiline = {
|
|
|
4180
4281
|
const consequentText = sourceCode.getText(node.consequent);
|
|
4181
4282
|
const alternateText = sourceCode.getText(node.alternate);
|
|
4182
4283
|
|
|
4183
|
-
|
|
4284
|
+
// No leading newline - keep first operand on same line as whatever precedes it
|
|
4285
|
+
// Use conditionIndent for ? and : to align with || operators
|
|
4286
|
+
const newText = `${buildMultilineHandler(test)}\n${conditionIndent}? ${consequentText}\n${conditionIndent}: ${alternateText}`;
|
|
4184
4287
|
|
|
4185
4288
|
return fixer.replaceText(node, newText);
|
|
4186
4289
|
},
|
|
@@ -4243,6 +4346,8 @@ const ternaryConditionMultiline = {
|
|
|
4243
4346
|
* statement (if, try, for, while, etc.) and the next statement,
|
|
4244
4347
|
* unless the next statement is part of the same construct (else, catch, finally).
|
|
4245
4348
|
*
|
|
4349
|
+
* Note: Consecutive if statements are handled by if-else-spacing rule.
|
|
4350
|
+
*
|
|
4246
4351
|
* ✓ Good:
|
|
4247
4352
|
* if (condition) {
|
|
4248
4353
|
* doSomething();
|
|
@@ -4323,6 +4428,11 @@ const emptyLineAfterBlock = {
|
|
|
4323
4428
|
// Get the next statement
|
|
4324
4429
|
const nextStmt = grandparent.body[stmtIndex + 1];
|
|
4325
4430
|
|
|
4431
|
+
// Skip consecutive if statements - handled by if-else-spacing rule
|
|
4432
|
+
if (parent.type === "IfStatement" && nextStmt.type === "IfStatement") {
|
|
4433
|
+
return;
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4326
4436
|
// Get the actual end of the current statement
|
|
4327
4437
|
const currentEndLine = getStatementEndLineHandler(parent);
|
|
4328
4438
|
const nextStartLine = nextStmt.loc.start.line;
|
|
@@ -4481,6 +4591,20 @@ const enumTypeEnforcement = {
|
|
|
4481
4591
|
return null;
|
|
4482
4592
|
};
|
|
4483
4593
|
|
|
4594
|
+
// Helper to process TSTypeLiteral members
|
|
4595
|
+
const processTypeLiteralMembersHandler = (members) => {
|
|
4596
|
+
members.forEach((member) => {
|
|
4597
|
+
if (member.type === "TSPropertySignature" && member.key?.type === "Identifier") {
|
|
4598
|
+
const propName = member.key.name;
|
|
4599
|
+
const typeInfo = extractTypeInfoHandler(member.typeAnnotation);
|
|
4600
|
+
|
|
4601
|
+
if (typeInfo) {
|
|
4602
|
+
typeAnnotatedVars.set(propName, typeInfo);
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
});
|
|
4606
|
+
};
|
|
4607
|
+
|
|
4484
4608
|
// Track type-annotated parameters in function/component definitions
|
|
4485
4609
|
const trackTypedParamsHandler = (params) => {
|
|
4486
4610
|
params.forEach((param) => {
|
|
@@ -4489,14 +4613,14 @@ const enumTypeEnforcement = {
|
|
|
4489
4613
|
const annotation = param.typeAnnotation.typeAnnotation;
|
|
4490
4614
|
|
|
4491
4615
|
if (annotation && annotation.type === "TSTypeLiteral") {
|
|
4492
|
-
annotation.members
|
|
4493
|
-
|
|
4494
|
-
const propName = member.key.name;
|
|
4495
|
-
const typeInfo = extractTypeInfoHandler(member.typeAnnotation);
|
|
4616
|
+
processTypeLiteralMembersHandler(annotation.members);
|
|
4617
|
+
}
|
|
4496
4618
|
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4619
|
+
// Handle intersection types: ButtonHTMLAttributes<HTMLButtonElement> & { variant?: ButtonVariantType }
|
|
4620
|
+
if (annotation && annotation.type === "TSIntersectionType") {
|
|
4621
|
+
annotation.types.forEach((intersectionType) => {
|
|
4622
|
+
if (intersectionType.type === "TSTypeLiteral") {
|
|
4623
|
+
processTypeLiteralMembersHandler(intersectionType.members);
|
|
4500
4624
|
}
|
|
4501
4625
|
});
|
|
4502
4626
|
}
|
|
@@ -7474,12 +7598,16 @@ const classNameNoExtraSpaces = {
|
|
|
7474
7598
|
*
|
|
7475
7599
|
* Description:
|
|
7476
7600
|
* Enforce Tailwind CSS class ordering in class string variables,
|
|
7477
|
-
* object properties, and return statements.
|
|
7478
|
-
* tailwindcss/classnames-order by handling
|
|
7601
|
+
* object properties, and return statements. This rule complements
|
|
7602
|
+
* tailwindcss/classnames-order by handling areas it doesn't cover.
|
|
7479
7603
|
* Uses smart detection: checks if values look like Tailwind classes.
|
|
7480
7604
|
*
|
|
7481
|
-
*
|
|
7482
|
-
*
|
|
7605
|
+
* Coverage Division:
|
|
7606
|
+
* - tailwindcss/classnames-order: Handles JSX className attributes
|
|
7607
|
+
* - classname-order (this rule): Handles variables, object properties,
|
|
7608
|
+
* and return statements containing Tailwind class strings
|
|
7609
|
+
*
|
|
7610
|
+
* Both rules should be enabled together for complete coverage.
|
|
7483
7611
|
*
|
|
7484
7612
|
* ✓ Good:
|
|
7485
7613
|
* const variants = { primary: "bg-blue-500 hover:bg-blue-600" };
|
|
@@ -7808,6 +7936,11 @@ const classNameMultiline = {
|
|
|
7808
7936
|
return getLineIndent(current);
|
|
7809
7937
|
}
|
|
7810
7938
|
|
|
7939
|
+
// For return statements, use the return keyword's indentation
|
|
7940
|
+
if (current.type === "ReturnStatement") {
|
|
7941
|
+
return getLineIndent(current);
|
|
7942
|
+
}
|
|
7943
|
+
|
|
7811
7944
|
current = current.parent;
|
|
7812
7945
|
}
|
|
7813
7946
|
|
|
@@ -9496,6 +9629,8 @@ const noEmptyLinesInFunctionCalls = {
|
|
|
9496
9629
|
* Description:
|
|
9497
9630
|
* Function parameter lists should not contain empty lines
|
|
9498
9631
|
* between parameters or after opening/before closing parens.
|
|
9632
|
+
* Also checks inside ObjectPattern (destructuring) params for
|
|
9633
|
+
* empty lines after {, before }, or between properties.
|
|
9499
9634
|
*
|
|
9500
9635
|
* ✓ Good:
|
|
9501
9636
|
* function test(
|
|
@@ -9503,12 +9638,23 @@ const noEmptyLinesInFunctionCalls = {
|
|
|
9503
9638
|
* param2,
|
|
9504
9639
|
* ) {}
|
|
9505
9640
|
*
|
|
9641
|
+
* const Button = ({
|
|
9642
|
+
* children,
|
|
9643
|
+
* className,
|
|
9644
|
+
* }) => {};
|
|
9645
|
+
*
|
|
9506
9646
|
* ✗ Bad:
|
|
9507
9647
|
* function test(
|
|
9508
9648
|
* param1,
|
|
9509
9649
|
*
|
|
9510
9650
|
* param2,
|
|
9511
9651
|
* ) {}
|
|
9652
|
+
*
|
|
9653
|
+
* const Button = ({
|
|
9654
|
+
*
|
|
9655
|
+
* children,
|
|
9656
|
+
* className,
|
|
9657
|
+
* }) => {};
|
|
9512
9658
|
*/
|
|
9513
9659
|
const noEmptyLinesInFunctionParams = {
|
|
9514
9660
|
create(context) {
|
|
@@ -9580,24 +9726,64 @@ const noEmptyLinesInFunctionParams = {
|
|
|
9580
9726
|
|
|
9581
9727
|
// Check inside ObjectPattern params for empty lines between destructured props
|
|
9582
9728
|
params.forEach((param) => {
|
|
9583
|
-
if (param.type === "ObjectPattern" && param.properties.length >
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
const next = param.properties[i + 1];
|
|
9729
|
+
if (param.type === "ObjectPattern" && param.properties.length > 0) {
|
|
9730
|
+
const firstProp = param.properties[0];
|
|
9731
|
+
const lastProp = param.properties[param.properties.length - 1];
|
|
9587
9732
|
|
|
9588
|
-
|
|
9589
|
-
|
|
9733
|
+
// Find the opening brace of ObjectPattern
|
|
9734
|
+
const openBrace = sourceCode.getFirstToken(param);
|
|
9590
9735
|
|
|
9736
|
+
if (openBrace && openBrace.value === "{") {
|
|
9737
|
+
// Check for empty line after opening brace
|
|
9738
|
+
if (firstProp.loc.start.line - openBrace.loc.end.line > 1) {
|
|
9591
9739
|
context.report({
|
|
9592
9740
|
fix: (fixer) => fixer.replaceTextRange(
|
|
9593
|
-
[
|
|
9594
|
-
"\n" + " ".repeat(
|
|
9741
|
+
[openBrace.range[1], firstProp.range[0]],
|
|
9742
|
+
"\n" + " ".repeat(firstProp.loc.start.column),
|
|
9595
9743
|
),
|
|
9596
|
-
message: "No empty
|
|
9597
|
-
node:
|
|
9744
|
+
message: "No empty line after opening brace in destructuring",
|
|
9745
|
+
node: firstProp,
|
|
9598
9746
|
});
|
|
9599
9747
|
}
|
|
9600
9748
|
}
|
|
9749
|
+
|
|
9750
|
+
// Find the closing brace of ObjectPattern
|
|
9751
|
+
const closeBrace = sourceCode.getLastToken(param);
|
|
9752
|
+
|
|
9753
|
+
if (closeBrace && closeBrace.value === "}") {
|
|
9754
|
+
// Check for empty line before closing brace
|
|
9755
|
+
if (closeBrace.loc.start.line - lastProp.loc.end.line > 1) {
|
|
9756
|
+
context.report({
|
|
9757
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
9758
|
+
[lastProp.range[1], closeBrace.range[0]],
|
|
9759
|
+
"\n" + " ".repeat(closeBrace.loc.start.column),
|
|
9760
|
+
),
|
|
9761
|
+
message: "No empty line before closing brace in destructuring",
|
|
9762
|
+
node: lastProp,
|
|
9763
|
+
});
|
|
9764
|
+
}
|
|
9765
|
+
}
|
|
9766
|
+
|
|
9767
|
+
// Check for empty lines between properties
|
|
9768
|
+
if (param.properties.length > 1) {
|
|
9769
|
+
for (let i = 0; i < param.properties.length - 1; i += 1) {
|
|
9770
|
+
const current = param.properties[i];
|
|
9771
|
+
const next = param.properties[i + 1];
|
|
9772
|
+
|
|
9773
|
+
if (next.loc.start.line - current.loc.end.line > 1) {
|
|
9774
|
+
const commaToken = sourceCode.getTokenAfter(current, (t) => t.value === ",");
|
|
9775
|
+
|
|
9776
|
+
context.report({
|
|
9777
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
9778
|
+
[commaToken.range[1], next.range[0]],
|
|
9779
|
+
"\n" + " ".repeat(next.loc.start.column),
|
|
9780
|
+
),
|
|
9781
|
+
message: "No empty lines between destructured properties",
|
|
9782
|
+
node: next,
|
|
9783
|
+
});
|
|
9784
|
+
}
|
|
9785
|
+
}
|
|
9786
|
+
}
|
|
9601
9787
|
}
|
|
9602
9788
|
});
|
|
9603
9789
|
};
|
|
@@ -14412,6 +14598,36 @@ const componentPropsInlineType = {
|
|
|
14412
14598
|
}
|
|
14413
14599
|
}
|
|
14414
14600
|
});
|
|
14601
|
+
|
|
14602
|
+
// Check that last member has trailing comma
|
|
14603
|
+
if (members.length > 0) {
|
|
14604
|
+
const lastMember = members[members.length - 1];
|
|
14605
|
+
const lastMemberText = sourceCode.getText(lastMember);
|
|
14606
|
+
|
|
14607
|
+
if (!lastMemberText.trimEnd().endsWith(",")) {
|
|
14608
|
+
context.report({
|
|
14609
|
+
fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
|
|
14610
|
+
message: "Last props type property must have trailing comma",
|
|
14611
|
+
node: lastMember,
|
|
14612
|
+
});
|
|
14613
|
+
}
|
|
14614
|
+
}
|
|
14615
|
+
|
|
14616
|
+
// Check for empty lines before closing brace
|
|
14617
|
+
if (members.length > 0 && closeBraceToken) {
|
|
14618
|
+
const lastMember = members[members.length - 1];
|
|
14619
|
+
|
|
14620
|
+
if (closeBraceToken.loc.start.line - lastMember.loc.end.line > 1) {
|
|
14621
|
+
context.report({
|
|
14622
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
14623
|
+
[lastMember.range[1], closeBraceToken.range[0]],
|
|
14624
|
+
"\n" + baseIndent,
|
|
14625
|
+
),
|
|
14626
|
+
message: "No empty line before closing brace in props type",
|
|
14627
|
+
node: closeBraceToken,
|
|
14628
|
+
});
|
|
14629
|
+
}
|
|
14630
|
+
}
|
|
14415
14631
|
}
|
|
14416
14632
|
|
|
14417
14633
|
return;
|
|
@@ -14619,6 +14835,20 @@ const componentPropsInlineType = {
|
|
|
14619
14835
|
}
|
|
14620
14836
|
}
|
|
14621
14837
|
});
|
|
14838
|
+
|
|
14839
|
+
// Check that last member has trailing comma
|
|
14840
|
+
if (members.length > 0) {
|
|
14841
|
+
const lastMember = members[members.length - 1];
|
|
14842
|
+
const lastMemberText = sourceCode.getText(lastMember);
|
|
14843
|
+
|
|
14844
|
+
if (!lastMemberText.trimEnd().endsWith(",")) {
|
|
14845
|
+
context.report({
|
|
14846
|
+
fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
|
|
14847
|
+
message: "Last props type property must have trailing comma",
|
|
14848
|
+
node: lastMember,
|
|
14849
|
+
});
|
|
14850
|
+
}
|
|
14851
|
+
}
|
|
14622
14852
|
}
|
|
14623
14853
|
};
|
|
14624
14854
|
|
|
@@ -16956,19 +17186,24 @@ const enumFormat = {
|
|
|
16956
17186
|
}
|
|
16957
17187
|
|
|
16958
17188
|
// Check member ends with comma, not semicolon
|
|
16959
|
-
|
|
17189
|
+
// Skip last member when multiple members - handled by combined check below
|
|
17190
|
+
const isLastMember = index === members.length - 1;
|
|
16960
17191
|
|
|
16961
|
-
if (
|
|
16962
|
-
|
|
16963
|
-
fix(fixer) {
|
|
16964
|
-
const lastChar = memberText.lastIndexOf(";");
|
|
16965
|
-
const absolutePos = member.range[0] + lastChar;
|
|
17192
|
+
if (!isLastMember || members.length === 1) {
|
|
17193
|
+
const memberText = sourceCode.getText(member);
|
|
16966
17194
|
|
|
16967
|
-
|
|
16968
|
-
|
|
16969
|
-
|
|
16970
|
-
|
|
16971
|
-
|
|
17195
|
+
if (memberText.trimEnd().endsWith(";")) {
|
|
17196
|
+
context.report({
|
|
17197
|
+
fix(fixer) {
|
|
17198
|
+
const lastChar = memberText.lastIndexOf(";");
|
|
17199
|
+
const absolutePos = member.range[0] + lastChar;
|
|
17200
|
+
|
|
17201
|
+
return fixer.replaceTextRange([absolutePos, absolutePos + 1], ",");
|
|
17202
|
+
},
|
|
17203
|
+
message: "Enum members must end with comma (,) not semicolon (;)",
|
|
17204
|
+
node: member,
|
|
17205
|
+
});
|
|
17206
|
+
}
|
|
16972
17207
|
}
|
|
16973
17208
|
|
|
16974
17209
|
// Check formatting for multiple members
|
|
@@ -17018,6 +17253,67 @@ const enumFormat = {
|
|
|
17018
17253
|
}
|
|
17019
17254
|
}
|
|
17020
17255
|
});
|
|
17256
|
+
|
|
17257
|
+
// Check closing brace position and trailing comma/semicolon (for multiple members)
|
|
17258
|
+
if (members.length > 1) {
|
|
17259
|
+
const lastMemberText = sourceCode.getText(lastMember);
|
|
17260
|
+
const trimmedText = lastMemberText.trimEnd();
|
|
17261
|
+
// Check both: text ends with comma OR there's a comma token after the member
|
|
17262
|
+
const tokenAfterLast = sourceCode.getTokenAfter(lastMember);
|
|
17263
|
+
const hasTrailingComma = trimmedText.endsWith(",") || (tokenAfterLast && tokenAfterLast.value === ",");
|
|
17264
|
+
const hasTrailingSemicolon = trimmedText.endsWith(";");
|
|
17265
|
+
const braceOnSameLine = closeBraceToken && closeBraceToken.loc.start.line === lastMember.loc.end.line;
|
|
17266
|
+
|
|
17267
|
+
// Handle semicolon on last member (needs replacement with comma)
|
|
17268
|
+
if (hasTrailingSemicolon) {
|
|
17269
|
+
const lastSemicolon = lastMemberText.lastIndexOf(";");
|
|
17270
|
+
const absolutePos = lastMember.range[0] + lastSemicolon;
|
|
17271
|
+
|
|
17272
|
+
if (braceOnSameLine) {
|
|
17273
|
+
// Both semicolon and brace issues - fix together
|
|
17274
|
+
context.report({
|
|
17275
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
17276
|
+
[absolutePos, closeBraceToken.range[0]],
|
|
17277
|
+
",\n" + baseIndent,
|
|
17278
|
+
),
|
|
17279
|
+
message: "Last enum member must end with comma and closing brace must be on its own line",
|
|
17280
|
+
node: lastMember,
|
|
17281
|
+
});
|
|
17282
|
+
} else {
|
|
17283
|
+
// Just semicolon issue
|
|
17284
|
+
context.report({
|
|
17285
|
+
fix: (fixer) => fixer.replaceTextRange([absolutePos, absolutePos + 1], ","),
|
|
17286
|
+
message: "Enum members must end with comma (,) not semicolon (;)",
|
|
17287
|
+
node: lastMember,
|
|
17288
|
+
});
|
|
17289
|
+
}
|
|
17290
|
+
} else if (!hasTrailingComma && braceOnSameLine) {
|
|
17291
|
+
// Both missing comma and brace issues - fix together
|
|
17292
|
+
context.report({
|
|
17293
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
17294
|
+
[lastMember.range[1], closeBraceToken.range[0]],
|
|
17295
|
+
",\n" + baseIndent,
|
|
17296
|
+
),
|
|
17297
|
+
message: "Last enum member must have trailing comma and closing brace must be on its own line",
|
|
17298
|
+
node: lastMember,
|
|
17299
|
+
});
|
|
17300
|
+
} else if (!hasTrailingComma) {
|
|
17301
|
+
context.report({
|
|
17302
|
+
fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
|
|
17303
|
+
message: "Last enum member must have trailing comma",
|
|
17304
|
+
node: lastMember,
|
|
17305
|
+
});
|
|
17306
|
+
} else if (braceOnSameLine) {
|
|
17307
|
+
context.report({
|
|
17308
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
17309
|
+
[lastMember.range[1], closeBraceToken.range[0]],
|
|
17310
|
+
"\n" + baseIndent,
|
|
17311
|
+
),
|
|
17312
|
+
message: "Closing brace must be on its own line",
|
|
17313
|
+
node: closeBraceToken,
|
|
17314
|
+
});
|
|
17315
|
+
}
|
|
17316
|
+
}
|
|
17021
17317
|
},
|
|
17022
17318
|
};
|
|
17023
17319
|
},
|
|
@@ -17201,19 +17497,24 @@ const interfaceFormat = {
|
|
|
17201
17497
|
}
|
|
17202
17498
|
|
|
17203
17499
|
// Check property ends with comma, not semicolon
|
|
17204
|
-
|
|
17500
|
+
// Skip last member when multiple members - handled by combined check below
|
|
17501
|
+
const isLastMember = index === members.length - 1;
|
|
17205
17502
|
|
|
17206
|
-
if (
|
|
17207
|
-
|
|
17208
|
-
fix(fixer) {
|
|
17209
|
-
const lastChar = memberText.lastIndexOf(";");
|
|
17210
|
-
const absolutePos = member.range[0] + lastChar;
|
|
17503
|
+
if (!isLastMember || members.length === 1) {
|
|
17504
|
+
const memberText = sourceCode.getText(member);
|
|
17211
17505
|
|
|
17212
|
-
|
|
17213
|
-
|
|
17214
|
-
|
|
17215
|
-
|
|
17216
|
-
|
|
17506
|
+
if (memberText.trimEnd().endsWith(";")) {
|
|
17507
|
+
context.report({
|
|
17508
|
+
fix(fixer) {
|
|
17509
|
+
const lastChar = memberText.lastIndexOf(";");
|
|
17510
|
+
const absolutePos = member.range[0] + lastChar;
|
|
17511
|
+
|
|
17512
|
+
return fixer.replaceTextRange([absolutePos, absolutePos + 1], ",");
|
|
17513
|
+
},
|
|
17514
|
+
message: "Interface properties must end with comma (,) not semicolon (;)",
|
|
17515
|
+
node: member,
|
|
17516
|
+
});
|
|
17517
|
+
}
|
|
17217
17518
|
}
|
|
17218
17519
|
|
|
17219
17520
|
// Check formatting for multiple members
|
|
@@ -17263,6 +17564,67 @@ const interfaceFormat = {
|
|
|
17263
17564
|
}
|
|
17264
17565
|
}
|
|
17265
17566
|
});
|
|
17567
|
+
|
|
17568
|
+
// Check closing brace position and trailing comma/semicolon (for multiple members)
|
|
17569
|
+
if (members.length > 1) {
|
|
17570
|
+
const lastMemberText = sourceCode.getText(lastMember);
|
|
17571
|
+
const trimmedText = lastMemberText.trimEnd();
|
|
17572
|
+
// Check both: text ends with comma OR there's a comma token after the member
|
|
17573
|
+
const tokenAfterLast = sourceCode.getTokenAfter(lastMember);
|
|
17574
|
+
const hasTrailingComma = trimmedText.endsWith(",") || (tokenAfterLast && tokenAfterLast.value === ",");
|
|
17575
|
+
const hasTrailingSemicolon = trimmedText.endsWith(";");
|
|
17576
|
+
const braceOnSameLine = closeBraceToken.loc.start.line === lastMember.loc.end.line;
|
|
17577
|
+
|
|
17578
|
+
// Handle semicolon on last member (needs replacement with comma)
|
|
17579
|
+
if (hasTrailingSemicolon) {
|
|
17580
|
+
const lastSemicolon = lastMemberText.lastIndexOf(";");
|
|
17581
|
+
const absolutePos = lastMember.range[0] + lastSemicolon;
|
|
17582
|
+
|
|
17583
|
+
if (braceOnSameLine) {
|
|
17584
|
+
// Both semicolon and brace issues - fix together
|
|
17585
|
+
context.report({
|
|
17586
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
17587
|
+
[absolutePos, closeBraceToken.range[0]],
|
|
17588
|
+
",\n" + baseIndent,
|
|
17589
|
+
),
|
|
17590
|
+
message: "Last interface property must end with comma and closing brace must be on its own line",
|
|
17591
|
+
node: lastMember,
|
|
17592
|
+
});
|
|
17593
|
+
} else {
|
|
17594
|
+
// Just semicolon issue
|
|
17595
|
+
context.report({
|
|
17596
|
+
fix: (fixer) => fixer.replaceTextRange([absolutePos, absolutePos + 1], ","),
|
|
17597
|
+
message: "Interface properties must end with comma (,) not semicolon (;)",
|
|
17598
|
+
node: lastMember,
|
|
17599
|
+
});
|
|
17600
|
+
}
|
|
17601
|
+
} else if (!hasTrailingComma && braceOnSameLine) {
|
|
17602
|
+
// Both missing comma and brace issues - fix together
|
|
17603
|
+
context.report({
|
|
17604
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
17605
|
+
[lastMember.range[1], closeBraceToken.range[0]],
|
|
17606
|
+
",\n" + baseIndent,
|
|
17607
|
+
),
|
|
17608
|
+
message: "Last interface property must have trailing comma and closing brace must be on its own line",
|
|
17609
|
+
node: lastMember,
|
|
17610
|
+
});
|
|
17611
|
+
} else if (!hasTrailingComma) {
|
|
17612
|
+
context.report({
|
|
17613
|
+
fix: (fixer) => fixer.insertTextAfter(lastMember, ","),
|
|
17614
|
+
message: "Last interface property must have trailing comma",
|
|
17615
|
+
node: lastMember,
|
|
17616
|
+
});
|
|
17617
|
+
} else if (braceOnSameLine) {
|
|
17618
|
+
context.report({
|
|
17619
|
+
fix: (fixer) => fixer.replaceTextRange(
|
|
17620
|
+
[lastMember.range[1], closeBraceToken.range[0]],
|
|
17621
|
+
"\n" + baseIndent,
|
|
17622
|
+
),
|
|
17623
|
+
message: "Closing brace must be on its own line",
|
|
17624
|
+
node: closeBraceToken,
|
|
17625
|
+
});
|
|
17626
|
+
}
|
|
17627
|
+
}
|
|
17266
17628
|
},
|
|
17267
17629
|
};
|
|
17268
17630
|
},
|
package/package.json
CHANGED