@tuomashatakka/eslint-config 3.1.1 โ 3.2.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/README.md +60 -114
- package/package.json +1 -1
- package/plugins/whitespaced/rules/aligned-assignments.mjs +185 -248
- package/rules.mjs +1 -1
- package/test/fixtures/whitespaced-members.valid.ts +1 -1
- package/test/fixtures/whitespaced-mixed.invalid.ts +68 -0
- package/test/fixtures/whitespaced-mixed.valid.ts +66 -0
package/README.md
CHANGED
|
@@ -1,164 +1,110 @@
|
|
|
1
1
|
# @tuomashatakka/eslint-config
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Opinionated ESLint flat config for TypeScript, React, and JSX projects. Bundles four in-house plugins (`whitespaced`, `omit`, `no-inline-types`, `react-strict`) on top of `@stylistic`, `typescript-eslint`, and `eslint-plugin-react`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Requires ESLint 9.13+.
|
|
6
6
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
### โจ Major Improvements
|
|
10
|
-
- **Removed Tailwind ESLint Plugin** - Streamlined configuration by removing tailwindcss plugin dependency
|
|
11
|
-
- **Migrated React Rules to @stylistic/jsx** - Moved all React styling rules to the dedicated stylistic JSX plugin for better separation of concerns
|
|
12
|
-
- **Updated All Plugins to Latest Versions** - Comprehensive dependency modernization with compatibility fixes
|
|
13
|
-
- **Added Comprehensive Test Suite** - Complete test coverage for formatting scenarios and edge cases
|
|
14
|
-
|
|
15
|
-
### ๐ง Technical Modernization
|
|
16
|
-
- Updated `@stylistic/eslint-plugin` to v5.2.0
|
|
17
|
-
- Added `@stylistic/eslint-plugin-jsx` for dedicated JSX formatting
|
|
18
|
-
- Fixed deprecated `allowTemplateLiterals` configuration (migrated from boolean to 'always'/'never')
|
|
19
|
-
- Consolidated plugin architecture for better maintainability
|
|
20
|
-
|
|
21
|
-
## ๐ฆ Installation
|
|
7
|
+
## Installation
|
|
22
8
|
|
|
23
9
|
```bash
|
|
24
10
|
npm install --save-dev @tuomashatakka/eslint-config
|
|
25
11
|
```
|
|
26
12
|
|
|
27
|
-
##
|
|
13
|
+
## Usage
|
|
28
14
|
|
|
29
|
-
|
|
15
|
+
Create `eslint.config.mjs` in your project root.
|
|
30
16
|
|
|
31
|
-
|
|
17
|
+
### Use the full config
|
|
32
18
|
|
|
33
|
-
```
|
|
19
|
+
```js
|
|
34
20
|
import config from '@tuomashatakka/eslint-config'
|
|
35
21
|
|
|
36
22
|
export default config
|
|
37
23
|
```
|
|
38
24
|
|
|
39
|
-
###
|
|
40
|
-
|
|
41
|
-
If you want to use only the rules in your own config:
|
|
42
|
-
|
|
43
|
-
```javascript
|
|
44
|
-
import { rules } from '@tuomashatakka/eslint-config'
|
|
45
|
-
|
|
46
|
-
export default [
|
|
47
|
-
// Your custom config here
|
|
48
|
-
{
|
|
49
|
-
// ...
|
|
50
|
-
rules
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Custom Configuration
|
|
25
|
+
### Extend or override
|
|
56
26
|
|
|
57
|
-
```
|
|
27
|
+
```js
|
|
58
28
|
import { baseConfig, rules } from '@tuomashatakka/eslint-config'
|
|
59
29
|
|
|
60
30
|
export default [
|
|
61
31
|
...baseConfig,
|
|
62
32
|
{
|
|
63
|
-
// Your custom overrides
|
|
64
33
|
rules: {
|
|
65
34
|
...rules,
|
|
66
|
-
'no-console': 'off'
|
|
67
|
-
}
|
|
68
|
-
}
|
|
35
|
+
'no-console': 'off',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
69
38
|
]
|
|
70
39
|
```
|
|
71
40
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
### Code Quality Rules
|
|
75
|
-
- **Complexity Control** - Max complexity: 14, max statements: 40
|
|
76
|
-
- **Functional Patterns** - Encourages functional programming practices
|
|
77
|
-
- **Type Safety** - Comprehensive TypeScript integration
|
|
78
|
-
|
|
79
|
-
### Stylistic Formatting
|
|
80
|
-
- **Consistent Spacing** - Aligned object properties, consistent indentation
|
|
81
|
-
- **Modern Syntax** - Arrow functions, template literals, destructuring
|
|
82
|
-
- **JSX Excellence** - Dedicated JSX formatting with proper component patterns
|
|
41
|
+
### Use only the rules
|
|
83
42
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- **@stylistic/eslint-plugin-jsx** - JSX-specific formatting rules
|
|
87
|
-
- **typescript-eslint** - TypeScript language support
|
|
88
|
-
- **eslint-plugin-react** - React component best practices
|
|
89
|
-
- **eslint-plugin-import** - Import/export management
|
|
90
|
-
- **Custom Plugins** - Specialized rules for code quality
|
|
91
|
-
|
|
92
|
-
## ๐งช Testing
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
# Run all tests
|
|
96
|
-
npm run test
|
|
97
|
-
|
|
98
|
-
# Test formatting capabilities
|
|
99
|
-
npm run test:format
|
|
43
|
+
```js
|
|
44
|
+
import { rules } from '@tuomashatakka/eslint-config'
|
|
100
45
|
|
|
101
|
-
|
|
102
|
-
npm run lint
|
|
46
|
+
export default [{ rules }]
|
|
103
47
|
```
|
|
104
48
|
|
|
105
|
-
|
|
106
|
-
- โ
Basic JavaScript patterns
|
|
107
|
-
- โ
Complex TypeScript scenarios
|
|
108
|
-
- โ
React/JSX components
|
|
109
|
-
- โ
Edge cases and formatting challenges
|
|
110
|
-
|
|
111
|
-
## ๐ Structure
|
|
49
|
+
## Rules configuration
|
|
112
50
|
|
|
113
|
-
The
|
|
51
|
+
The config bundles four local plugins. Override any rule the same way you would override a standard ESLint rule.
|
|
114
52
|
|
|
115
|
-
|
|
116
|
-
- `rules.mjs` - Contains all the ESLint rules organized by category
|
|
117
|
-
- `test/` - Comprehensive test suite with fixtures and runners
|
|
53
|
+
### `whitespaced/*`
|
|
118
54
|
|
|
119
|
-
|
|
55
|
+
| Rule | Description |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `whitespaced/aligned-assignments` | Vertically aligns `=` in adjacent declaration and member-assignment blocks. |
|
|
58
|
+
| `whitespaced/block-padding` | Enforces blank-line padding inside blocks, with docstring exceptions. |
|
|
59
|
+
| `whitespaced/class-property-grouping` | Groups class properties by visibility and kind. |
|
|
60
|
+
| `whitespaced/consistent-line-spacing` | Enforces consistent blank lines before and after statements. |
|
|
61
|
+
| `whitespaced/multiline-format` | Enforces consistent formatting for multiline objects and arrays. |
|
|
120
62
|
|
|
121
|
-
|
|
63
|
+
Options for `whitespaced/aligned-assignments`:
|
|
122
64
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
65
|
+
| Option | Type | Default | Effect |
|
|
66
|
+
| --- | --- | --- | --- |
|
|
67
|
+
| `blockSize` | integer | `2` | Minimum number of adjacent assignments before alignment applies. |
|
|
68
|
+
| `ignoreAdjacent` | boolean | `true` | Only align rows on consecutive lines. |
|
|
69
|
+
| `ignoreIfAssignmentsNotInBlock` | boolean | `true` | Split a group at every declaration-kind transition (`const` โ `let`). Member assignments are wildcards and join any sub-block. |
|
|
70
|
+
| `alignTypes` | boolean | `false` | Also align type-annotation colons. |
|
|
71
|
+
| `ignoreTypesMismatch` | boolean | `true` | Skip colon alignment when only some rows have type annotations. |
|
|
72
|
+
| `alignMemberAssignments` | boolean | `true` | Include `obj.prop = โฆ` lines in alignment blocks alongside `const`/`let`/`var`. |
|
|
127
73
|
|
|
128
|
-
###
|
|
129
|
-
- `allowTemplateLiterals: true` โ `allowTemplateLiterals: 'always'`
|
|
130
|
-
- Tailwind CSS rules removed (use dedicated Tailwind tools instead)
|
|
74
|
+
### `omit/omit-unnecessary-parens-brackets`
|
|
131
75
|
|
|
132
|
-
|
|
76
|
+
Removes unnecessary parentheses, brackets, and braces. No options.
|
|
133
77
|
|
|
134
|
-
###
|
|
78
|
+
### `no-inline-types/no-inline-multiline-types`
|
|
135
79
|
|
|
136
|
-
|
|
137
|
-
- Update to v2.5.0+ for the fixed configuration
|
|
80
|
+
Disallows inline multi-line `TSTypeLiteral` annotations; requires extraction to a named `type` or `interface`. No options.
|
|
138
81
|
|
|
139
|
-
|
|
140
|
-
- Ensure you're using files with proper extensions (.jsx, .tsx)
|
|
141
|
-
- Check that React is properly detected in settings
|
|
82
|
+
### `react-strict/*`
|
|
142
83
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
-
|
|
84
|
+
| Rule | Description |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `react-strict/jsx-prop-layout` | Enforces JSX prop ordering: `key`/`ref`, then `className`/`style`, then `data-`/`aria-`, then regular props, callbacks last. |
|
|
87
|
+
| `react-strict/no-complex-jsx-map` | Disallows complex `.map()` callbacks with inline logic inside JSX. |
|
|
88
|
+
| `react-strict/no-jsx-value-calculations` | Disallows value calculations and assignments inside JSX return blocks. |
|
|
89
|
+
| `react-strict/no-nested-divs` | Disallows nested `<div>` elements in favor of semantic HTML5 tags. |
|
|
90
|
+
| `react-strict/no-style-prop` | Disallows the inline `style` prop except in drag/drop interactions. |
|
|
91
|
+
| `react-strict/prefer-no-use-effect` | Discourages `useEffect` in favor of context, custom hooks, or event-driven patterns. |
|
|
146
92
|
|
|
147
|
-
##
|
|
93
|
+
## Package structure
|
|
148
94
|
|
|
149
|
-
-
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
95
|
+
- `index.mjs` โ exports `config` (default), `baseConfig`, and `rules`.
|
|
96
|
+
- `rules.mjs` โ the rule map.
|
|
97
|
+
- `plugins/` โ local plugin sources.
|
|
98
|
+
- `test/` โ fixtures and runner.
|
|
153
99
|
|
|
154
|
-
##
|
|
100
|
+
## Scripts
|
|
155
101
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
102
|
+
```bash
|
|
103
|
+
npm run lint # lint the config itself
|
|
104
|
+
npm run test # run all fixture tests
|
|
105
|
+
npm run test:format # run formatting-only fixtures
|
|
106
|
+
```
|
|
161
107
|
|
|
162
108
|
## License
|
|
163
109
|
|
|
164
|
-
ISC
|
|
110
|
+
ISC
|
package/package.json
CHANGED
|
@@ -31,12 +31,11 @@ export default {
|
|
|
31
31
|
misalignedTypes: 'Type declarations should be vertically aligned within blocks.',
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
|
+
|
|
34
35
|
create (context) {
|
|
35
36
|
const sourceCode = context.sourceCode || context.getSourceCode()
|
|
36
37
|
const options = context.options[0] || {}
|
|
37
38
|
|
|
38
|
-
const alignComments = options.alignComments !== undefined ? options.alignComments : false
|
|
39
|
-
const alignLiterals = options.alignLiterals !== undefined ? options.alignLiterals : false
|
|
40
39
|
const blockSize = options.blockSize !== undefined ? options.blockSize : 2
|
|
41
40
|
const ignoreAdjacent = options.ignoreAdjacent !== undefined ? options.ignoreAdjacent : true
|
|
42
41
|
const ignoreIfAssignmentsNotInBlock = options.ignoreIfAssignmentsNotInBlock !== undefined ? options.ignoreIfAssignmentsNotInBlock : true
|
|
@@ -45,340 +44,278 @@ export default {
|
|
|
45
44
|
const alignMemberAssignments = options.alignMemberAssignments !== undefined ? options.alignMemberAssignments : true
|
|
46
45
|
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
const equalsToken = sourceCode.getTokenBefore(
|
|
50
|
-
declarator.init,
|
|
51
|
-
token => token.value === '='
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
return equalsToken ? equalsToken.loc.start.column : null
|
|
55
|
-
}
|
|
56
|
-
|
|
47
|
+
// โโ Row collection โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
57
48
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return null
|
|
49
|
+
function findEqualsToken (rightNode) {
|
|
50
|
+
return sourceCode.getTokenBefore(
|
|
51
|
+
rightNode,
|
|
52
|
+
token => token.type === 'Punctuator' && token.value === '='
|
|
53
|
+
)
|
|
64
54
|
}
|
|
65
55
|
|
|
66
56
|
|
|
67
|
-
function
|
|
68
|
-
|
|
57
|
+
function declaratorLhsEnd (declarator) {
|
|
58
|
+
const target = declarator.id?.typeAnnotation ?? declarator.id
|
|
59
|
+
return { col: target.loc.end.column, idx: target.range[1] }
|
|
69
60
|
}
|
|
70
61
|
|
|
71
62
|
|
|
72
|
-
function
|
|
73
|
-
if (!
|
|
74
|
-
return
|
|
63
|
+
function declaratorRow (declarator) {
|
|
64
|
+
if (!declarator.init)
|
|
65
|
+
return null
|
|
75
66
|
|
|
76
|
-
const
|
|
77
|
-
return declarations.every(decl => decl.parent.kind === firstKind)
|
|
78
|
-
}
|
|
67
|
+
const equalsToken = findEqualsToken(declarator.init)
|
|
79
68
|
|
|
69
|
+
if (!equalsToken)
|
|
70
|
+
return null
|
|
80
71
|
|
|
81
|
-
|
|
82
|
-
return declarations.every(decl =>
|
|
83
|
-
decl.id && decl.id.typeAnnotation
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
72
|
+
const { col, idx } = declaratorLhsEnd(declarator)
|
|
87
73
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
74
|
+
return {
|
|
75
|
+
reportNode: declarator,
|
|
76
|
+
lhsEndCol: col,
|
|
77
|
+
lhsEndIdx: idx,
|
|
78
|
+
equalsToken,
|
|
79
|
+
line: equalsToken.loc.start.line,
|
|
80
|
+
kind: declarator.parent.kind,
|
|
81
|
+
}
|
|
92
82
|
}
|
|
93
83
|
|
|
94
84
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
}
|
|
85
|
+
function memberAssignmentRow (stmt) {
|
|
86
|
+
const expr = stmt.expression
|
|
98
87
|
|
|
88
|
+
if (!expr || expr.type !== 'AssignmentExpression' || expr.operator !== '=' || expr.left.type !== 'MemberExpression')
|
|
89
|
+
return null
|
|
99
90
|
|
|
100
|
-
|
|
101
|
-
const columns = declarations
|
|
102
|
-
.map(getTypeColonColumn)
|
|
103
|
-
.filter(column => column !== null)
|
|
91
|
+
const equalsToken = findEqualsToken(expr.right)
|
|
104
92
|
|
|
105
|
-
|
|
93
|
+
if (!equalsToken)
|
|
94
|
+
return null
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
reportNode: expr,
|
|
98
|
+
lhsEndCol: expr.left.loc.end.column,
|
|
99
|
+
lhsEndIdx: expr.left.range[1],
|
|
100
|
+
equalsToken,
|
|
101
|
+
line: equalsToken.loc.start.line,
|
|
102
|
+
kind: 'member',
|
|
103
|
+
}
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
|
|
109
|
-
function
|
|
110
|
-
const
|
|
111
|
-
const idText = sourceCode.getText(declarator.id)
|
|
112
|
-
const initText = declarator.init ? sourceCode.getText(declarator.init) : ''
|
|
107
|
+
function collectRows (statements) {
|
|
108
|
+
const rows = []
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
for (const stmt of statements)
|
|
111
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
112
|
+
for (const declarator of stmt.declarations) {
|
|
113
|
+
const row = declaratorRow(declarator)
|
|
114
|
+
if (row)
|
|
115
|
+
rows.push(row)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (alignMemberAssignments && stmt.type === 'ExpressionStatement') {
|
|
119
|
+
const row = memberAssignmentRow(stmt)
|
|
120
|
+
if (row)
|
|
121
|
+
rows.push(row)
|
|
122
|
+
}
|
|
116
123
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
declarator.init,
|
|
120
|
-
token => token.value === '='
|
|
121
|
-
)
|
|
124
|
+
return rows
|
|
125
|
+
}
|
|
122
126
|
|
|
123
|
-
if (!equalsToken)
|
|
124
|
-
return originalText
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
const padding = targetMaxEqualsColumn - currentEqualsColumn
|
|
128
|
+
// โโ Adjacency grouping โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
function groupByAdjacency (rows) {
|
|
131
|
+
if (rows.length === 0)
|
|
132
|
+
return []
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
+
const sorted = [ ...rows ].sort((a, b) => a.line - b.line)
|
|
135
|
+
const groups = []
|
|
136
|
+
let current = [ sorted[0] ]
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
139
|
+
const prev = sorted[i - 1]
|
|
140
|
+
const row = sorted[i]
|
|
137
141
|
|
|
142
|
+
if (ignoreAdjacent && row.line !== prev.line + 1) {
|
|
143
|
+
if (current.length >= blockSize)
|
|
144
|
+
groups.push(current)
|
|
145
|
+
current = []
|
|
146
|
+
}
|
|
138
147
|
|
|
139
|
-
|
|
140
|
-
const idText = sourceCode.getText(declarator.id)
|
|
141
|
-
if (declarator.id && declarator.id.typeAnnotation) {
|
|
142
|
-
const typeText = sourceCode.getText(declarator.id.typeAnnotation)
|
|
143
|
-
return idText.length + 1 + typeText.length
|
|
148
|
+
current.push(row)
|
|
144
149
|
}
|
|
145
|
-
return idText.length
|
|
146
|
-
}
|
|
147
150
|
|
|
151
|
+
if (current.length >= blockSize)
|
|
152
|
+
groups.push(current)
|
|
148
153
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return
|
|
154
|
+
return ignoreAdjacent ? groups : (rows.length >= blockSize ? [ sorted ] : [])
|
|
155
|
+
}
|
|
152
156
|
|
|
153
|
-
if (ignoreIfAssignmentsNotInBlock && !haveSameKind(declarations))
|
|
154
|
-
return
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
const equalsColumns = declarations.map(d => getEqualsColumn(d)).filter(c => c !== null)
|
|
158
|
+
// โโ Kind-transition sub-block split โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
const minEqualsCol = Math.min(...equalsColumns)
|
|
163
|
-
if (maxEqualsCol === minEqualsCol)
|
|
164
|
-
return
|
|
165
|
-
}
|
|
160
|
+
function splitByKind (group) {
|
|
161
|
+
if (!ignoreIfAssignmentsNotInBlock)
|
|
162
|
+
return [ group ]
|
|
166
163
|
|
|
167
|
-
const
|
|
164
|
+
const subBlocks = []
|
|
165
|
+
let current = []
|
|
166
|
+
let lockedKind = null
|
|
168
167
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
for (const row of group)
|
|
169
|
+
if (row.kind === 'member')
|
|
170
|
+
current.push(row)
|
|
171
|
+
else if (lockedKind === null || lockedKind === row.kind) {
|
|
172
|
+
current.push(row)
|
|
173
|
+
lockedKind = row.kind
|
|
173
174
|
}
|
|
174
175
|
else {
|
|
175
|
-
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
if (current.length >= blockSize)
|
|
177
|
+
subBlocks.push(current)
|
|
178
|
+
current = [ row ]
|
|
179
|
+
lockedKind = row.kind
|
|
179
180
|
}
|
|
180
|
-
}
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
declarations.forEach(declarator => {
|
|
185
|
-
const equalsCol = getEqualsColumn(declarator)
|
|
186
|
-
|
|
187
|
-
if (equalsCol !== null && equalsCol !== maxEqualsColumn)
|
|
188
|
-
context.report({
|
|
189
|
-
node: declarator,
|
|
190
|
-
messageId: 'misalignedAssignment',
|
|
191
|
-
fix (fixer) {
|
|
192
|
-
return fixer.replaceText(
|
|
193
|
-
declarator,
|
|
194
|
-
getFixedDeclaration(declarator, maxEqualsColumn)
|
|
195
|
-
)
|
|
196
|
-
},
|
|
197
|
-
})
|
|
198
|
-
})
|
|
199
|
-
}
|
|
182
|
+
if (current.length >= blockSize)
|
|
183
|
+
subBlocks.push(current)
|
|
200
184
|
|
|
185
|
+
return subBlocks
|
|
186
|
+
}
|
|
201
187
|
|
|
202
|
-
function processDeclarationGroup (declarations) {
|
|
203
|
-
if (!declarations.length)
|
|
204
|
-
return
|
|
205
188
|
|
|
206
|
-
|
|
189
|
+
// โโ Alignment check + fix โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
207
190
|
|
|
208
|
-
|
|
191
|
+
function checkAndFix (subBlock) {
|
|
192
|
+
if (subBlock.length < blockSize)
|
|
209
193
|
return
|
|
210
194
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
let currentGroup = [ declarationsWithInits[0] ]
|
|
214
|
-
|
|
215
|
-
for (let i = 1; i < declarationsWithInits.length; i++) {
|
|
216
|
-
const prevDecl = declarationsWithInits[i - 1]
|
|
217
|
-
const currentDecl = declarationsWithInits[i]
|
|
218
|
-
|
|
219
|
-
if (areNodesAdjacent(prevDecl, currentDecl))
|
|
220
|
-
currentGroup.push(currentDecl); else {
|
|
221
|
-
if (currentGroup.length >= blockSize)
|
|
222
|
-
adjacentGroups.push(currentGroup)
|
|
223
|
-
currentGroup = [ currentDecl ]
|
|
224
|
-
}
|
|
225
|
-
}
|
|
195
|
+
const targetEqualsCol = Math.max(...subBlock.map(r => r.lhsEndCol)) + 1
|
|
196
|
+
const allAligned = subBlock.every(r => r.equalsToken.loc.start.column === targetEqualsCol)
|
|
226
197
|
|
|
227
|
-
|
|
228
|
-
|
|
198
|
+
if (allAligned)
|
|
199
|
+
return
|
|
229
200
|
|
|
230
|
-
|
|
201
|
+
for (const row of subBlock) {
|
|
202
|
+
const currentCol = row.equalsToken.loc.start.column
|
|
203
|
+
|
|
204
|
+
if (currentCol === targetEqualsCol)
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
context.report({
|
|
208
|
+
node: row.reportNode,
|
|
209
|
+
messageId: 'misalignedAssignment',
|
|
210
|
+
fix (fixer) {
|
|
211
|
+
const desiredPad = targetEqualsCol - row.lhsEndCol
|
|
212
|
+
if (desiredPad < 1)
|
|
213
|
+
return null
|
|
214
|
+
return fixer.replaceTextRange(
|
|
215
|
+
[ row.lhsEndIdx, row.equalsToken.range[0] ],
|
|
216
|
+
' '.repeat(desiredPad)
|
|
217
|
+
)
|
|
218
|
+
},
|
|
219
|
+
})
|
|
231
220
|
}
|
|
232
|
-
else
|
|
233
|
-
checkAlignment(declarationsWithInits)
|
|
234
221
|
}
|
|
235
222
|
|
|
236
223
|
|
|
237
|
-
// โโ
|
|
224
|
+
// โโ Type colon alignment (orthogonal to equals alignment) โโโโโโโโโโโโโ
|
|
238
225
|
|
|
239
|
-
function
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return equalsToken ? equalsToken.loc.start.column : null
|
|
226
|
+
function getTypeColonColumn (declarator) {
|
|
227
|
+
if (declarator.id && declarator.id.typeAnnotation) {
|
|
228
|
+
const colonToken = sourceCode.getFirstToken(declarator.id.typeAnnotation)
|
|
229
|
+
return colonToken ? colonToken.loc.start.column : null
|
|
230
|
+
}
|
|
231
|
+
return null
|
|
246
232
|
}
|
|
247
233
|
|
|
248
234
|
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const right = assignExpr.right
|
|
253
|
-
const leftText = sourceCode.getText(left)
|
|
254
|
-
const rightText = sourceCode.getText(right)
|
|
255
|
-
|
|
256
|
-
const leftLength = leftText.length
|
|
257
|
-
const padding = targetMaxLeftLength - leftLength
|
|
235
|
+
function checkTypeAlignment (declarators) {
|
|
236
|
+
if (!alignTypes || declarators.length < blockSize)
|
|
237
|
+
return
|
|
258
238
|
|
|
259
|
-
|
|
260
|
-
}
|
|
239
|
+
const annotated = declarators.filter(d => d.id?.typeAnnotation)
|
|
261
240
|
|
|
241
|
+
if (annotated.length < blockSize)
|
|
242
|
+
return
|
|
262
243
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return sourceCode.getText(left).trimEnd().length
|
|
266
|
-
}
|
|
244
|
+
if (ignoreTypesMismatch && annotated.length !== declarators.length)
|
|
245
|
+
return
|
|
267
246
|
|
|
247
|
+
const colonColumns = annotated.map(getTypeColonColumn).filter(c => c !== null)
|
|
268
248
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const equalsToken = sourceCode.getTokenBefore(
|
|
272
|
-
assignExpr.right,
|
|
273
|
-
token => token.value === '=' && token.type === 'Punctuator'
|
|
274
|
-
)
|
|
275
|
-
return equalsToken ? equalsToken.loc.start.column : null
|
|
276
|
-
}
|
|
249
|
+
if (colonColumns.length < blockSize)
|
|
250
|
+
return
|
|
277
251
|
|
|
252
|
+
const maxColonCol = Math.max(...colonColumns)
|
|
253
|
+
const allAligned = colonColumns.every(c => c === maxColonCol)
|
|
278
254
|
|
|
279
|
-
|
|
280
|
-
if (stmts.length < blockSize)
|
|
255
|
+
if (allAligned)
|
|
281
256
|
return
|
|
282
257
|
|
|
283
|
-
const
|
|
284
|
-
|
|
258
|
+
for (const declarator of annotated) {
|
|
259
|
+
const colonCol = getTypeColonColumn(declarator)
|
|
285
260
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (equalsColumns.length >= 2) {
|
|
289
|
-
const maxCol = Math.max(...equalsColumns)
|
|
290
|
-
const allAligned = equalsColumns.every(c => c === maxCol)
|
|
291
|
-
if (allAligned)
|
|
292
|
-
return
|
|
293
|
-
}
|
|
261
|
+
if (colonCol === null || colonCol === maxColonCol)
|
|
262
|
+
continue
|
|
294
263
|
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
264
|
+
const colonToken = sourceCode.getFirstToken(declarator.id.typeAnnotation)
|
|
265
|
+
const idEndIdx = declarator.id.range[1]
|
|
266
|
+
const desiredPad = maxColonCol - declarator.id.loc.end.column
|
|
267
|
+
|
|
268
|
+
if (desiredPad < 0)
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
context.report({
|
|
272
|
+
node: declarator,
|
|
273
|
+
messageId: 'misalignedTypes',
|
|
274
|
+
fix (fixer) {
|
|
275
|
+
return fixer.replaceTextRange(
|
|
276
|
+
[ idEndIdx, colonToken.range[0] ],
|
|
277
|
+
' '.repeat(desiredPad)
|
|
278
|
+
)
|
|
279
|
+
},
|
|
280
|
+
})
|
|
281
|
+
}
|
|
306
282
|
}
|
|
307
283
|
|
|
308
284
|
|
|
309
|
-
|
|
310
|
-
if (!alignMemberAssignments)
|
|
311
|
-
return
|
|
312
|
-
|
|
313
|
-
const memberStmts = blockBody.filter(stmt =>
|
|
314
|
-
stmt.type === 'ExpressionStatement' &&
|
|
315
|
-
stmt.expression &&
|
|
316
|
-
stmt.expression.type === 'AssignmentExpression' &&
|
|
317
|
-
stmt.expression.operator === '=' &&
|
|
318
|
-
stmt.expression.left.type === 'MemberExpression'
|
|
319
|
-
)
|
|
285
|
+
// โโ Block processor โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
320
286
|
|
|
321
|
-
|
|
287
|
+
function processStatements (statements) {
|
|
288
|
+
if (!statements || statements.length === 0)
|
|
322
289
|
return
|
|
323
290
|
|
|
324
|
-
|
|
325
|
-
const groups = []
|
|
326
|
-
let group = [ memberStmts[0] ]
|
|
327
|
-
|
|
328
|
-
for (let i = 1; i < memberStmts.length; i++)
|
|
329
|
-
if (areNodesAdjacent(memberStmts[i - 1], memberStmts[i]))
|
|
330
|
-
group.push(memberStmts[i])
|
|
331
|
-
else {
|
|
332
|
-
if (group.length >= blockSize)
|
|
333
|
-
groups.push(group)
|
|
334
|
-
group = [ memberStmts[i] ]
|
|
335
|
-
}
|
|
336
|
-
if (group.length >= blockSize)
|
|
337
|
-
groups.push(group)
|
|
338
|
-
groups.forEach(checkMemberAlignment)
|
|
339
|
-
}
|
|
340
|
-
else
|
|
341
|
-
checkMemberAlignment(memberStmts)
|
|
342
|
-
}
|
|
343
|
-
|
|
291
|
+
const rows = collectRows(statements)
|
|
344
292
|
|
|
345
|
-
|
|
293
|
+
for (const group of groupByAdjacency(rows))
|
|
294
|
+
for (const subBlock of splitByKind(group))
|
|
295
|
+
checkAndFix(subBlock)
|
|
346
296
|
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
297
|
+
const declarators = []
|
|
298
|
+
for (const stmt of statements)
|
|
299
|
+
if (stmt.type === 'VariableDeclaration')
|
|
300
|
+
for (const declarator of stmt.declarations)
|
|
301
|
+
if (declarator.init)
|
|
302
|
+
declarators.push(declarator)
|
|
350
303
|
|
|
351
|
-
|
|
352
|
-
if (statement.type === 'VariableDeclaration')
|
|
353
|
-
declarations.push(...statement.declarations)
|
|
354
|
-
|
|
355
|
-
processDeclarationGroup(declarations)
|
|
356
|
-
processMemberAssignments(scopeBody)
|
|
304
|
+
checkTypeAlignment(declarators)
|
|
357
305
|
}
|
|
358
306
|
|
|
359
307
|
|
|
360
308
|
return {
|
|
361
309
|
Program (node) {
|
|
362
|
-
|
|
310
|
+
processStatements(node.body)
|
|
363
311
|
},
|
|
364
312
|
|
|
365
313
|
BlockStatement (node) {
|
|
366
|
-
|
|
314
|
+
processStatements(node.body)
|
|
367
315
|
},
|
|
368
316
|
|
|
369
317
|
SwitchCase (node) {
|
|
370
|
-
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
const declarations = []
|
|
374
|
-
const memberStmts = []
|
|
375
|
-
|
|
376
|
-
for (const statement of node.consequent)
|
|
377
|
-
if (statement.type === 'VariableDeclaration')
|
|
378
|
-
declarations.push(...statement.declarations)
|
|
379
|
-
|
|
380
|
-
processDeclarationGroup(declarations)
|
|
381
|
-
processMemberAssignments(node.consequent)
|
|
318
|
+
processStatements(node.consequent)
|
|
382
319
|
},
|
|
383
320
|
}
|
|
384
321
|
},
|
package/rules.mjs
CHANGED
|
@@ -41,7 +41,7 @@ export const rules = {
|
|
|
41
41
|
'no-array-constructor': [ 'error' ],
|
|
42
42
|
'omit/omit-unnecessary-parens-brackets': [ 'warn' ],
|
|
43
43
|
'no-inline-types/no-inline-multiline-types': [ 'warn' ],
|
|
44
|
-
'whitespaced/aligned-assignments': [ 'warn', {
|
|
44
|
+
'whitespaced/aligned-assignments': [ 'warn', { alignTypes: true }],
|
|
45
45
|
'@stylistic/function-call-spacing': [ 'warn', 'never' ],
|
|
46
46
|
'@stylistic/computed-property-spacing': [ 'warn', 'never' ],
|
|
47
47
|
'@stylistic/brace-style': [ 'warn', 'stroustrup', { allowSingleLine: false }],
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// expect-warning: whitespaced/aligned-assignments
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
type Bag = { texture: { colorSpace: string }, name: number, x: number, b: number, d: number, value: number }
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// Case 1: const + member assignment merged into one block.
|
|
8
|
+
export function caseMergeBlock (bag: Bag) {
|
|
9
|
+
const texture = bag.texture
|
|
10
|
+
texture.colorSpace = 'srgb'
|
|
11
|
+
return texture
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// Case 2: off-by-1 (the rebuild bug).
|
|
16
|
+
export function caseOffByOne () {
|
|
17
|
+
const a = 1
|
|
18
|
+
const bb = 2
|
|
19
|
+
return a + bb
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// Case 3: over-padded shorter row must shrink to match the longer row.
|
|
24
|
+
export function caseShrink () {
|
|
25
|
+
const a = 1
|
|
26
|
+
const longname = 2
|
|
27
|
+
return a + longname
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
// Case 4: kind transition splits the group into (const+member) and (let+member) sub-blocks.
|
|
32
|
+
export function caseKindSplit (bag: Bag) {
|
|
33
|
+
const a = 1
|
|
34
|
+
bag.b = 2
|
|
35
|
+
let c = 3
|
|
36
|
+
bag.d = 4
|
|
37
|
+
c++
|
|
38
|
+
return a + c
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
// Case 5: TS type annotation must contribute to LHS-end, not just the identifier.
|
|
43
|
+
export function caseTypeAnnotation () {
|
|
44
|
+
const aa: string = 'x'
|
|
45
|
+
const bb: number = 2
|
|
46
|
+
return aa + bb
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
// Case 6: computed and static member access mix.
|
|
51
|
+
export function caseComputedMix (bag: Bag, key: 'name' | 'x') {
|
|
52
|
+
bag[key] = 1
|
|
53
|
+
bag.x = 2
|
|
54
|
+
return bag
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// Case 7: inside SwitchCase.
|
|
59
|
+
export function caseSwitch (n: number, bag: Bag) {
|
|
60
|
+
switch (n) {
|
|
61
|
+
case 1: {
|
|
62
|
+
const a = 1
|
|
63
|
+
bag.b = 2
|
|
64
|
+
return a
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return 0
|
|
68
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
type Bag = { texture: { colorSpace: string }, name: number, x: number, b: number, d: number, value: number }
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// Case 1: const + member assignment, aligned.
|
|
5
|
+
export function caseMergeBlock (bag: Bag) {
|
|
6
|
+
const texture = bag.texture
|
|
7
|
+
texture.colorSpace = 'srgb'
|
|
8
|
+
return texture
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// Case 2: off-by-1 cleanly aligned.
|
|
13
|
+
export function caseOffByOne () {
|
|
14
|
+
const a = 1
|
|
15
|
+
const bb = 2
|
|
16
|
+
return a + bb
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
// Case 3: aligned (the long row dictates the target).
|
|
21
|
+
export function caseShrink () {
|
|
22
|
+
const a = 1
|
|
23
|
+
const longname = 2
|
|
24
|
+
return a + longname
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
// Case 4: kind-split groups, each sub-block aligned within itself.
|
|
29
|
+
export function caseKindSplit (bag: Bag) {
|
|
30
|
+
const a = 1
|
|
31
|
+
bag.b = 2
|
|
32
|
+
|
|
33
|
+
let c = 3
|
|
34
|
+
bag.d = 4
|
|
35
|
+
c++
|
|
36
|
+
return a + c
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// Case 5: TS type annotation contributes to LHS-end (here naturally aligned).
|
|
41
|
+
export function caseTypeAnnotation () {
|
|
42
|
+
const aa: string = 'x'
|
|
43
|
+
const bb: number = 2
|
|
44
|
+
return aa + bb
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
// Case 6: computed and static member access aligned.
|
|
49
|
+
export function caseComputedMix (bag: Bag, key: 'name' | 'x') {
|
|
50
|
+
bag[key] = 1
|
|
51
|
+
bag.x = 2
|
|
52
|
+
return bag
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// Case 7: aligned inside SwitchCase.
|
|
57
|
+
export function caseSwitch (n: number, bag: Bag) {
|
|
58
|
+
switch (n) {
|
|
59
|
+
case 1: {
|
|
60
|
+
const a = 1
|
|
61
|
+
bag.b = 2
|
|
62
|
+
return a
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return 0
|
|
66
|
+
}
|