@tuomashatakka/eslint-config 3.1.1 → 3.2.1
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/.github/workflows/publish.yml +16 -49
- package/README.md +60 -114
- package/package.json +1 -1
- package/plugins/whitespaced/rules/aligned-assignments.mjs +206 -249
- package/rules.mjs +2 -2
- 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
|
@@ -1,59 +1,26 @@
|
|
|
1
|
-
name: Publish
|
|
1
|
+
name: Publish Package
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write # Required for OIDC
|
|
10
|
+
contents: read
|
|
7
11
|
|
|
8
12
|
jobs:
|
|
9
13
|
publish:
|
|
10
14
|
runs-on: ubuntu-latest
|
|
11
|
-
permissions:
|
|
12
|
-
contents: write
|
|
13
15
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
15
|
-
with:
|
|
16
|
-
fetch-depth: 2
|
|
17
|
-
token: ${{ secrets.GITHUB_TOKEN }}
|
|
16
|
+
- uses: actions/checkout@v6
|
|
18
17
|
|
|
19
|
-
-
|
|
20
|
-
id: version
|
|
21
|
-
run: |
|
|
22
|
-
CURRENT=$(jq -r .version package.json)
|
|
23
|
-
PREVIOUS=$(git show HEAD~1:package.json 2>/dev/null | jq -r .version 2>/dev/null || echo "")
|
|
24
|
-
echo "CURRENT=$CURRENT" >> "$GITHUB_OUTPUT"
|
|
25
|
-
echo "PREVIOUS=$PREVIOUS" >> "$GITHUB_OUTPUT"
|
|
26
|
-
if [ "$CURRENT" != "$PREVIOUS" ] && [ -n "$CURRENT" ]; then
|
|
27
|
-
echo "CHANGED=true" >> "$GITHUB_OUTPUT"
|
|
28
|
-
echo "Version bumped: $PREVIOUS -> $CURRENT"
|
|
29
|
-
else
|
|
30
|
-
echo "CHANGED=false" >> "$GITHUB_OUTPUT"
|
|
31
|
-
echo "No version change ($CURRENT); skipping publish."
|
|
32
|
-
fi
|
|
33
|
-
|
|
34
|
-
- uses: actions/setup-node@v4
|
|
35
|
-
if: steps.version.outputs.CHANGED == 'true'
|
|
18
|
+
- uses: actions/setup-node@v6
|
|
36
19
|
with:
|
|
37
|
-
node-version:
|
|
38
|
-
registry-url: https://registry.npmjs.org
|
|
39
|
-
cache:
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- if: steps.version.outputs.CHANGED == 'true'
|
|
45
|
-
run: npm run test
|
|
46
|
-
|
|
47
|
-
- name: Publish to npm
|
|
48
|
-
if: steps.version.outputs.CHANGED == 'true'
|
|
49
|
-
run: npm publish --access public
|
|
50
|
-
env:
|
|
51
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
52
|
-
|
|
53
|
-
- name: Tag release
|
|
54
|
-
if: steps.version.outputs.CHANGED == 'true'
|
|
55
|
-
run: |
|
|
56
|
-
git config user.name "github-actions[bot]"
|
|
57
|
-
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
58
|
-
git tag "v${{ steps.version.outputs.CURRENT }}"
|
|
59
|
-
git push origin "v${{ steps.version.outputs.CURRENT }}"
|
|
20
|
+
node-version: '24'
|
|
21
|
+
registry-url: 'https://registry.npmjs.org'
|
|
22
|
+
package-manager-cache: false # never use caching in release builds
|
|
23
|
+
- run: npm ci
|
|
24
|
+
- run: npm run build --if-present
|
|
25
|
+
- run: npm test
|
|
26
|
+
- run: npm publish # Or: npm stage publish
|
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,298 @@ 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] ]
|
|
195
|
+
const targetEqualsCol = Math.max(...subBlock.map(r => r.lhsEndCol)) + 1
|
|
196
|
+
const allAligned = subBlock.every(r => r.equalsToken.loc.start.column === targetEqualsCol)
|
|
214
197
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const currentDecl = declarationsWithInits[i]
|
|
198
|
+
if (allAligned)
|
|
199
|
+
return
|
|
218
200
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
201
|
+
// Report the entire block as a single issue
|
|
202
|
+
const firstRow = subBlock[0]
|
|
203
|
+
const lastRow = subBlock[subBlock.length - 1]
|
|
204
|
+
|
|
205
|
+
context.report({
|
|
206
|
+
loc: {
|
|
207
|
+
start: firstRow.reportNode.loc.start,
|
|
208
|
+
end: lastRow.reportNode.loc.end,
|
|
209
|
+
},
|
|
210
|
+
messageId: 'misalignedAssignment',
|
|
211
|
+
fix (fixer) {
|
|
212
|
+
const fixes = []
|
|
213
|
+
for (const row of subBlock) {
|
|
214
|
+
const currentCol = row.equalsToken.loc.start.column
|
|
215
|
+
if (currentCol === targetEqualsCol)
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
const desiredPad = targetEqualsCol - row.lhsEndCol
|
|
219
|
+
if (desiredPad >= 1) {
|
|
220
|
+
fixes.push(fixer.replaceTextRange(
|
|
221
|
+
[ row.lhsEndIdx, row.equalsToken.range[0] ],
|
|
222
|
+
' '.repeat(desiredPad)
|
|
223
|
+
))
|
|
224
|
+
}
|
|
224
225
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
adjacentGroups.push(currentGroup)
|
|
229
|
-
|
|
230
|
-
adjacentGroups.forEach(checkAlignment)
|
|
231
|
-
}
|
|
232
|
-
else
|
|
233
|
-
checkAlignment(declarationsWithInits)
|
|
226
|
+
return fixes.length > 0 ? fixes : null
|
|
227
|
+
},
|
|
228
|
+
})
|
|
234
229
|
}
|
|
235
230
|
|
|
236
231
|
|
|
237
|
-
// ──
|
|
232
|
+
// ── Type colon alignment (orthogonal to equals alignment) ─────────────
|
|
238
233
|
|
|
239
|
-
function
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return equalsToken ? equalsToken.loc.start.column : null
|
|
234
|
+
function getTypeColonColumn (declarator) {
|
|
235
|
+
if (declarator.id && declarator.id.typeAnnotation) {
|
|
236
|
+
const colonToken = sourceCode.getFirstToken(declarator.id.typeAnnotation)
|
|
237
|
+
return colonToken ? colonToken.loc.start.column : null
|
|
238
|
+
}
|
|
239
|
+
return null
|
|
246
240
|
}
|
|
247
241
|
|
|
248
242
|
|
|
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
|
|
258
|
-
|
|
259
|
-
return leftText + ' '.repeat(padding) + '= ' + rightText
|
|
260
|
-
}
|
|
243
|
+
function checkTypeAlignment (declarators) {
|
|
244
|
+
if (!alignTypes || declarators.length < blockSize)
|
|
245
|
+
return
|
|
261
246
|
|
|
247
|
+
const annotated = declarators.filter(d => d.id?.typeAnnotation)
|
|
262
248
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return sourceCode.getText(left).trimEnd().length
|
|
266
|
-
}
|
|
267
|
-
|
|
249
|
+
if (annotated.length < blockSize)
|
|
250
|
+
return
|
|
268
251
|
|
|
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
|
-
}
|
|
252
|
+
if (ignoreTypesMismatch && annotated.length !== declarators.length)
|
|
253
|
+
return
|
|
277
254
|
|
|
255
|
+
const colonColumns = annotated.map(getTypeColonColumn).filter(c => c !== null)
|
|
278
256
|
|
|
279
|
-
|
|
280
|
-
if (stmts.length < blockSize)
|
|
257
|
+
if (colonColumns.length < blockSize)
|
|
281
258
|
return
|
|
282
259
|
|
|
283
|
-
const
|
|
284
|
-
const
|
|
260
|
+
const maxColonCol = Math.max(...colonColumns)
|
|
261
|
+
const allAligned = colonColumns.every(c => c === maxColonCol)
|
|
285
262
|
|
|
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
|
-
}
|
|
263
|
+
if (allAligned)
|
|
264
|
+
return
|
|
294
265
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
266
|
+
// Report the entire block as a single issue
|
|
267
|
+
const firstDecl = annotated[0]
|
|
268
|
+
const lastDecl = annotated[annotated.length - 1]
|
|
269
|
+
|
|
270
|
+
context.report({
|
|
271
|
+
loc: {
|
|
272
|
+
start: firstDecl.loc.start,
|
|
273
|
+
end: lastDecl.loc.end,
|
|
274
|
+
},
|
|
275
|
+
messageId: 'misalignedTypes',
|
|
276
|
+
fix (fixer) {
|
|
277
|
+
const fixes = []
|
|
278
|
+
for (const declarator of annotated) {
|
|
279
|
+
const colonCol = getTypeColonColumn(declarator)
|
|
280
|
+
|
|
281
|
+
if (colonCol === null || colonCol === maxColonCol)
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
const colonToken = sourceCode.getFirstToken(declarator.id.typeAnnotation)
|
|
285
|
+
const idEndIdx = declarator.id.range[1]
|
|
286
|
+
const desiredPad = maxColonCol - colonToken.loc.start.column
|
|
287
|
+
|
|
288
|
+
if (desiredPad > 0) {
|
|
289
|
+
fixes.push(fixer.replaceTextRange(
|
|
290
|
+
[ colonToken.range[0], colonToken.range[0] ],
|
|
291
|
+
' '.repeat(desiredPad)
|
|
292
|
+
))
|
|
293
|
+
} else if (desiredPad < 0) {
|
|
294
|
+
// Colon is too far to the right, need to move it left
|
|
295
|
+
// This is more complex, so we'll skip it for now
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return fixes.length > 0 ? fixes : null
|
|
300
|
+
},
|
|
305
301
|
})
|
|
306
302
|
}
|
|
307
303
|
|
|
308
304
|
|
|
309
|
-
|
|
310
|
-
if (!alignMemberAssignments)
|
|
311
|
-
return
|
|
305
|
+
// ── Block processor ───────────────────────────────────────────────────
|
|
312
306
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
stmt.expression &&
|
|
316
|
-
stmt.expression.type === 'AssignmentExpression' &&
|
|
317
|
-
stmt.expression.operator === '=' &&
|
|
318
|
-
stmt.expression.left.type === 'MemberExpression'
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
if (memberStmts.length < blockSize)
|
|
307
|
+
function processStatements (statements) {
|
|
308
|
+
if (!statements || statements.length === 0)
|
|
322
309
|
return
|
|
323
310
|
|
|
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
|
-
|
|
311
|
+
const rows = collectRows(statements)
|
|
344
312
|
|
|
345
|
-
|
|
313
|
+
for (const group of groupByAdjacency(rows))
|
|
314
|
+
for (const subBlock of splitByKind(group))
|
|
315
|
+
checkAndFix(subBlock)
|
|
346
316
|
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
317
|
+
const declarators = []
|
|
318
|
+
for (const stmt of statements)
|
|
319
|
+
if (stmt.type === 'VariableDeclaration')
|
|
320
|
+
for (const declarator of stmt.declarations)
|
|
321
|
+
if (declarator.init)
|
|
322
|
+
declarators.push(declarator)
|
|
350
323
|
|
|
351
|
-
|
|
352
|
-
if (statement.type === 'VariableDeclaration')
|
|
353
|
-
declarations.push(...statement.declarations)
|
|
354
|
-
|
|
355
|
-
processDeclarationGroup(declarations)
|
|
356
|
-
processMemberAssignments(scopeBody)
|
|
324
|
+
checkTypeAlignment(declarators)
|
|
357
325
|
}
|
|
358
326
|
|
|
359
327
|
|
|
360
328
|
return {
|
|
361
329
|
Program (node) {
|
|
362
|
-
|
|
330
|
+
processStatements(node.body)
|
|
363
331
|
},
|
|
364
332
|
|
|
365
333
|
BlockStatement (node) {
|
|
366
|
-
|
|
334
|
+
processStatements(node.body)
|
|
367
335
|
},
|
|
368
336
|
|
|
369
337
|
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)
|
|
338
|
+
processStatements(node.consequent)
|
|
382
339
|
},
|
|
383
340
|
}
|
|
384
341
|
},
|
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: false }],
|
|
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 }],
|
|
@@ -90,7 +90,7 @@ export const rules = {
|
|
|
90
90
|
'@stylistic/quotes': [ 'warn', 'single', { avoidEscape: true, allowTemplateLiterals: 'always' }],
|
|
91
91
|
'@stylistic/jsx-newline': [ 'warn', { prevent: true, allowMultilines: true }],
|
|
92
92
|
'@stylistic/jsx-equals-spacing': [ 'warn', 'never' ],
|
|
93
|
-
'@stylistic/jsx-max-props-per-line': [ 'warn', { maximum:
|
|
93
|
+
'@stylistic/jsx-max-props-per-line': [ 'warn', { maximum: 3, when: 'multiline' }],
|
|
94
94
|
'@stylistic/jsx-self-closing-comp': [ 'warn', { component: true, html: true }],
|
|
95
95
|
'@stylistic/jsx-one-expression-per-line': [ 'warn', { allow: 'non-jsx' }],
|
|
96
96
|
'@stylistic/jsx-tag-spacing': [ 'warn', { closingSlash: 'never', beforeSelfClosing: 'always', beforeClosing: 'never', afterOpening: 'never' }],
|
|
@@ -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
|
+
}
|