@tuomashatakka/eslint-config 3.1.0 → 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/.github/workflows/publish.yml +28 -14
- package/README.md +60 -114
- package/package.json +1 -1
- package/plugins/omit/rules/omit-unnecessary-parens-brackets.mjs +34 -2
- package/plugins/omit/utils.mjs +29 -0
- package/plugins/whitespaced/rules/aligned-assignments.mjs +185 -248
- package/rules.mjs +1 -1
- package/test/fixtures/omit-braces-asi.invalid.ts +6 -0
- package/test/fixtures/omit-braces-hazards.valid.ts +18 -0
- 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
|
@@ -2,7 +2,8 @@ name: Publish to npm
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths: ['package.json']
|
|
6
7
|
|
|
7
8
|
jobs:
|
|
8
9
|
publish:
|
|
@@ -12,34 +13,47 @@ jobs:
|
|
|
12
13
|
steps:
|
|
13
14
|
- uses: actions/checkout@v4
|
|
14
15
|
with:
|
|
15
|
-
|
|
16
|
+
fetch-depth: 2
|
|
16
17
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
17
18
|
|
|
19
|
+
- name: Detect version change
|
|
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
|
+
|
|
18
34
|
- uses: actions/setup-node@v4
|
|
35
|
+
if: steps.version.outputs.CHANGED == 'true'
|
|
19
36
|
with:
|
|
20
37
|
node-version: 22
|
|
21
38
|
registry-url: https://registry.npmjs.org
|
|
22
39
|
cache: npm
|
|
23
40
|
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
|
|
27
|
-
|
|
28
|
-
- name: Sync package.json version with tag
|
|
29
|
-
run: npm version ${{ steps.version.outputs.VERSION }} --no-git-tag-version
|
|
41
|
+
- if: steps.version.outputs.CHANGED == 'true'
|
|
42
|
+
run: npm ci
|
|
30
43
|
|
|
31
|
-
-
|
|
32
|
-
|
|
44
|
+
- if: steps.version.outputs.CHANGED == 'true'
|
|
45
|
+
run: npm run test
|
|
33
46
|
|
|
34
47
|
- name: Publish to npm
|
|
48
|
+
if: steps.version.outputs.CHANGED == 'true'
|
|
35
49
|
run: npm publish --access public
|
|
36
50
|
env:
|
|
37
51
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
38
52
|
|
|
39
|
-
- name:
|
|
53
|
+
- name: Tag release
|
|
54
|
+
if: steps.version.outputs.CHANGED == 'true'
|
|
40
55
|
run: |
|
|
41
56
|
git config user.name "github-actions[bot]"
|
|
42
57
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
43
|
-
git
|
|
44
|
-
git
|
|
45
|
-
git push origin main
|
|
58
|
+
git tag "v${{ steps.version.outputs.CURRENT }}"
|
|
59
|
+
git push origin "v${{ steps.version.outputs.CURRENT }}"
|
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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @fileoverview Rule to omit unnecessary parentheses, brackets, and braces
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { isValidDotNotationIdentifier, isDeclaration, isParenthesized } from '../utils.mjs'
|
|
5
|
+
import { isValidDotNotationIdentifier, isDeclaration, isParenthesized, statementCanAbsorbElse } from '../utils.mjs'
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
export default {
|
|
@@ -160,13 +160,39 @@ function checkUnnecessaryBraces (blockStatement, controllingNode) {
|
|
|
160
160
|
if (singleStatement.type === 'BlockStatement')
|
|
161
161
|
return
|
|
162
162
|
|
|
163
|
+
// Dangling-else hazard: stripping the braces around an if/else if's
|
|
164
|
+
// consequent could let a trailing `else` rebind to a dangling inner
|
|
165
|
+
// `if`. Skip the fix entirely in that case.
|
|
166
|
+
const isConsequentOfIfWithElse =
|
|
167
|
+
controllingNode &&
|
|
168
|
+
controllingNode.type === 'IfStatement' &&
|
|
169
|
+
controllingNode.consequent === blockStatement &&
|
|
170
|
+
Boolean(controllingNode.alternate)
|
|
171
|
+
|
|
172
|
+
if (isConsequentOfIfWithElse && statementCanAbsorbElse(singleStatement))
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
// ASI hazard: when the next syntactic token after `}` is a keyword
|
|
176
|
+
// (`else` / `while` of a do-while), stripping the braces leaves the
|
|
177
|
+
// inner statement directly followed by that keyword on the same line.
|
|
178
|
+
// For statements that don't end with `;` or `}` in the source (e.g.
|
|
179
|
+
// `return x` inside a block where ASI fired at the closing brace),
|
|
180
|
+
// the result no longer parses. Detect this and either re-inject a `;`
|
|
181
|
+
// or skip the fix.
|
|
182
|
+
const tokenAfterBlock = sourceCode.getTokenAfter(blockStatement)
|
|
183
|
+
const needsAsiGuard =
|
|
184
|
+
tokenAfterBlock &&
|
|
185
|
+
tokenAfterBlock.type === 'Keyword' &&
|
|
186
|
+
(tokenAfterBlock.value === 'else' || tokenAfterBlock.value === 'while') &&
|
|
187
|
+
tokenAfterBlock.loc.start.line === blockStatement.loc.end.line
|
|
188
|
+
|
|
163
189
|
context.report({
|
|
164
190
|
node: blockStatement,
|
|
165
191
|
messageId: 'unnecessaryBraces',
|
|
166
192
|
fix (fixer) {
|
|
167
193
|
const firstToken = sourceCode.getFirstToken(blockStatement)
|
|
168
194
|
const lastToken = sourceCode.getLastToken(blockStatement)
|
|
169
|
-
|
|
195
|
+
let innerText = sourceCode.getText(singleStatement)
|
|
170
196
|
|
|
171
197
|
if (!firstToken || firstToken.value !== '{' || !lastToken || lastToken.value !== '}')
|
|
172
198
|
return null
|
|
@@ -190,6 +216,12 @@ function checkUnnecessaryBraces (blockStatement, controllingNode) {
|
|
|
190
216
|
return null
|
|
191
217
|
}
|
|
192
218
|
|
|
219
|
+
if (needsAsiGuard) {
|
|
220
|
+
const trimmed = innerText.replace(/\s+$/, '')
|
|
221
|
+
if (!trimmed.endsWith(';') && !trimmed.endsWith('}'))
|
|
222
|
+
innerText = `${trimmed};`
|
|
223
|
+
}
|
|
224
|
+
|
|
193
225
|
return fixer.replaceTextRange(blockStatement.range, innerText)
|
|
194
226
|
},
|
|
195
227
|
})
|
package/plugins/omit/utils.mjs
CHANGED
|
@@ -84,8 +84,37 @@ function isParenthesized (node, sourceCode) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
// Returns true when `stmt`, if placed where an `else` may follow, would
|
|
88
|
+
// cause the trailing `else` to bind to an inner `if` instead of the outer
|
|
89
|
+
// controlling node (the classic dangling-else hazard).
|
|
90
|
+
export
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
function statementCanAbsorbElse (stmt) {
|
|
94
|
+
if (!stmt)
|
|
95
|
+
return false
|
|
96
|
+
|
|
97
|
+
switch (stmt.type) {
|
|
98
|
+
case 'IfStatement':
|
|
99
|
+
if (!stmt.alternate)
|
|
100
|
+
return true
|
|
101
|
+
return statementCanAbsorbElse(stmt.alternate)
|
|
102
|
+
case 'ForStatement':
|
|
103
|
+
case 'ForInStatement':
|
|
104
|
+
case 'ForOfStatement':
|
|
105
|
+
case 'WhileStatement':
|
|
106
|
+
case 'WithStatement':
|
|
107
|
+
case 'LabeledStatement':
|
|
108
|
+
return statementCanAbsorbElse(stmt.body)
|
|
109
|
+
default:
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
87
115
|
export default {
|
|
88
116
|
isParenthesized,
|
|
89
117
|
isDeclaration,
|
|
90
118
|
isValidDotNotationIdentifier,
|
|
119
|
+
statementCanAbsorbElse,
|
|
91
120
|
}
|
|
@@ -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,6 @@
|
|
|
1
|
+
// expect-warning: omit/omit-unnecessary-parens-brackets
|
|
2
|
+
// Stripping these braces would produce `return 'x' else if (...) return 'red' else return null`
|
|
3
|
+
// which is an ASI parse error. The autofix must inject `;` so the result still parses.
|
|
4
|
+
export function pickColor (x: unknown): string | null {
|
|
5
|
+
if (x === true) { return 'x' } else if (x === 'blue') { return 'red' } else { return null }
|
|
6
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface Material { uniforms: { time: { value: number }}}
|
|
2
|
+
declare function isAnimatedShader (m: Material): boolean
|
|
3
|
+
|
|
4
|
+
type MeshType = { material: Material | Material[] | null | undefined }
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// Dangling-else hazard: the outer braces are load-bearing — without
|
|
8
|
+
// them, the trailing `else` would re-bind to `if (isAnimatedShader(m))`,
|
|
9
|
+
// silently changing semantics. The omit rule must NOT flag these braces.
|
|
10
|
+
export function update (mesh: MeshType, t: number) {
|
|
11
|
+
if (Array.isArray(mesh.material)) {
|
|
12
|
+
for (const m of mesh.material)
|
|
13
|
+
if (isAnimatedShader(m))
|
|
14
|
+
m.uniforms.time.value = t
|
|
15
|
+
}
|
|
16
|
+
else if (isAnimatedShader(mesh.material!))
|
|
17
|
+
mesh.material!.uniforms.time.value = t
|
|
18
|
+
}
|
|
@@ -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
|
+
}
|