eslint-plugin-code-style 1.7.2 → 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 +23 -0
- package/README.md +236 -0
- package/index.js +184 -36
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ 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
|
+
|
|
10
33
|
## [1.7.2] - 2026-02-02
|
|
11
34
|
|
|
12
35
|
### Fixed
|
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") {
|
|
@@ -4097,7 +4118,7 @@ const ternaryConditionMultiline = {
|
|
|
4097
4118
|
const testEndLine = test.loc.end.line;
|
|
4098
4119
|
const isMultiLine = testStartLine !== testEndLine;
|
|
4099
4120
|
|
|
4100
|
-
// ≤maxOperands operands: keep condition on single line
|
|
4121
|
+
// ≤maxOperands operands: keep condition on single line, and try to collapse whole ternary
|
|
4101
4122
|
if (operands.length <= maxOperands) {
|
|
4102
4123
|
const firstOperandStartLine = operands[0].loc.start.line;
|
|
4103
4124
|
const allOperandsStartOnSameLine = operands.every(
|
|
@@ -4108,29 +4129,82 @@ const ternaryConditionMultiline = {
|
|
|
4108
4129
|
(op) => isBinaryExpressionSplitHandler(op),
|
|
4109
4130
|
);
|
|
4110
4131
|
|
|
4111
|
-
if
|
|
4112
|
-
|
|
4113
|
-
fix: (fixer) => {
|
|
4114
|
-
const buildSameLineHandler = (n) => {
|
|
4115
|
-
if (n.type === "LogicalExpression" && !isParenthesizedHandler(n)) {
|
|
4116
|
-
const leftText = buildSameLineHandler(n.left);
|
|
4117
|
-
const rightText = buildSameLineHandler(n.right);
|
|
4132
|
+
// Check if ? or : is on its own line without its value
|
|
4133
|
+
const hasOperatorOnOwnLine = isOperatorOnOwnLineHandler(node);
|
|
4118
4134
|
|
|
4119
|
-
|
|
4120
|
-
|
|
4135
|
+
// Check if ternary is multiline (could be collapsed)
|
|
4136
|
+
const isTernaryMultiline = node.loc.start.line !== node.loc.end.line;
|
|
4121
4137
|
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
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);
|
|
4125
4143
|
|
|
4126
|
-
|
|
4127
|
-
|
|
4144
|
+
return `${leftText} ${n.operator} ${rightText}`;
|
|
4145
|
+
}
|
|
4128
4146
|
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
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
|
+
}
|
|
4134
4208
|
}
|
|
4135
4209
|
|
|
4136
4210
|
return;
|
|
@@ -4178,6 +4252,11 @@ const ternaryConditionMultiline = {
|
|
|
4178
4252
|
isCorrectionNeeded = true;
|
|
4179
4253
|
}
|
|
4180
4254
|
}
|
|
4255
|
+
|
|
4256
|
+
// Check if ? or : is on its own line without its value
|
|
4257
|
+
if (!isCorrectionNeeded && isOperatorOnOwnLineHandler(node)) {
|
|
4258
|
+
isCorrectionNeeded = true;
|
|
4259
|
+
}
|
|
4181
4260
|
}
|
|
4182
4261
|
|
|
4183
4262
|
if (isCorrectionNeeded) {
|
|
@@ -4267,6 +4346,8 @@ const ternaryConditionMultiline = {
|
|
|
4267
4346
|
* statement (if, try, for, while, etc.) and the next statement,
|
|
4268
4347
|
* unless the next statement is part of the same construct (else, catch, finally).
|
|
4269
4348
|
*
|
|
4349
|
+
* Note: Consecutive if statements are handled by if-else-spacing rule.
|
|
4350
|
+
*
|
|
4270
4351
|
* ✓ Good:
|
|
4271
4352
|
* if (condition) {
|
|
4272
4353
|
* doSomething();
|
|
@@ -4347,6 +4428,11 @@ const emptyLineAfterBlock = {
|
|
|
4347
4428
|
// Get the next statement
|
|
4348
4429
|
const nextStmt = grandparent.body[stmtIndex + 1];
|
|
4349
4430
|
|
|
4431
|
+
// Skip consecutive if statements - handled by if-else-spacing rule
|
|
4432
|
+
if (parent.type === "IfStatement" && nextStmt.type === "IfStatement") {
|
|
4433
|
+
return;
|
|
4434
|
+
}
|
|
4435
|
+
|
|
4350
4436
|
// Get the actual end of the current statement
|
|
4351
4437
|
const currentEndLine = getStatementEndLineHandler(parent);
|
|
4352
4438
|
const nextStartLine = nextStmt.loc.start.line;
|
|
@@ -7512,12 +7598,16 @@ const classNameNoExtraSpaces = {
|
|
|
7512
7598
|
*
|
|
7513
7599
|
* Description:
|
|
7514
7600
|
* Enforce Tailwind CSS class ordering in class string variables,
|
|
7515
|
-
* object properties, and return statements.
|
|
7516
|
-
* 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.
|
|
7517
7603
|
* Uses smart detection: checks if values look like Tailwind classes.
|
|
7518
7604
|
*
|
|
7519
|
-
*
|
|
7520
|
-
*
|
|
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.
|
|
7521
7611
|
*
|
|
7522
7612
|
* ✓ Good:
|
|
7523
7613
|
* const variants = { primary: "bg-blue-500 hover:bg-blue-600" };
|
|
@@ -7846,6 +7936,11 @@ const classNameMultiline = {
|
|
|
7846
7936
|
return getLineIndent(current);
|
|
7847
7937
|
}
|
|
7848
7938
|
|
|
7939
|
+
// For return statements, use the return keyword's indentation
|
|
7940
|
+
if (current.type === "ReturnStatement") {
|
|
7941
|
+
return getLineIndent(current);
|
|
7942
|
+
}
|
|
7943
|
+
|
|
7849
7944
|
current = current.parent;
|
|
7850
7945
|
}
|
|
7851
7946
|
|
|
@@ -9534,6 +9629,8 @@ const noEmptyLinesInFunctionCalls = {
|
|
|
9534
9629
|
* Description:
|
|
9535
9630
|
* Function parameter lists should not contain empty lines
|
|
9536
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.
|
|
9537
9634
|
*
|
|
9538
9635
|
* ✓ Good:
|
|
9539
9636
|
* function test(
|
|
@@ -9541,12 +9638,23 @@ const noEmptyLinesInFunctionCalls = {
|
|
|
9541
9638
|
* param2,
|
|
9542
9639
|
* ) {}
|
|
9543
9640
|
*
|
|
9641
|
+
* const Button = ({
|
|
9642
|
+
* children,
|
|
9643
|
+
* className,
|
|
9644
|
+
* }) => {};
|
|
9645
|
+
*
|
|
9544
9646
|
* ✗ Bad:
|
|
9545
9647
|
* function test(
|
|
9546
9648
|
* param1,
|
|
9547
9649
|
*
|
|
9548
9650
|
* param2,
|
|
9549
9651
|
* ) {}
|
|
9652
|
+
*
|
|
9653
|
+
* const Button = ({
|
|
9654
|
+
*
|
|
9655
|
+
* children,
|
|
9656
|
+
* className,
|
|
9657
|
+
* }) => {};
|
|
9550
9658
|
*/
|
|
9551
9659
|
const noEmptyLinesInFunctionParams = {
|
|
9552
9660
|
create(context) {
|
|
@@ -9618,24 +9726,64 @@ const noEmptyLinesInFunctionParams = {
|
|
|
9618
9726
|
|
|
9619
9727
|
// Check inside ObjectPattern params for empty lines between destructured props
|
|
9620
9728
|
params.forEach((param) => {
|
|
9621
|
-
if (param.type === "ObjectPattern" && param.properties.length >
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
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];
|
|
9625
9732
|
|
|
9626
|
-
|
|
9627
|
-
|
|
9733
|
+
// Find the opening brace of ObjectPattern
|
|
9734
|
+
const openBrace = sourceCode.getFirstToken(param);
|
|
9628
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) {
|
|
9629
9739
|
context.report({
|
|
9630
9740
|
fix: (fixer) => fixer.replaceTextRange(
|
|
9631
|
-
[
|
|
9632
|
-
"\n" + " ".repeat(
|
|
9741
|
+
[openBrace.range[1], firstProp.range[0]],
|
|
9742
|
+
"\n" + " ".repeat(firstProp.loc.start.column),
|
|
9633
9743
|
),
|
|
9634
|
-
message: "No empty
|
|
9635
|
-
node:
|
|
9744
|
+
message: "No empty line after opening brace in destructuring",
|
|
9745
|
+
node: firstProp,
|
|
9746
|
+
});
|
|
9747
|
+
}
|
|
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,
|
|
9636
9763
|
});
|
|
9637
9764
|
}
|
|
9638
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
|
+
}
|
|
9639
9787
|
}
|
|
9640
9788
|
});
|
|
9641
9789
|
};
|
package/package.json
CHANGED