@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 CHANGED
@@ -1,164 +1,110 @@
1
1
  # @tuomashatakka/eslint-config
2
2
 
3
- > Opinionated yet functional ESLint configuration with comprehensive TypeScript, React, and JSX support
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
- For Next.js, React and TypeScript projects using modern flat config format.
5
+ Requires ESLint 9.13+.
6
6
 
7
- ## ๐Ÿš€ Recent Updates (v2.5.0)
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
- ## ๐Ÿ”ง Usage
13
+ ## Usage
28
14
 
29
- ### Using the full config
15
+ Create `eslint.config.mjs` in your project root.
30
16
 
31
- Create an `eslint.config.mjs` file in your project root:
17
+ ### Use the full config
32
18
 
33
- ```javascript
19
+ ```js
34
20
  import config from '@tuomashatakka/eslint-config'
35
21
 
36
22
  export default config
37
23
  ```
38
24
 
39
- ### Using just the rules
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
- ```javascript
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
- ## ๐ŸŽฏ Features
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
- ### Plugin Integration
85
- - **@stylistic/eslint-plugin** - Modern code formatting
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
- # Lint the configuration itself
102
- npm run lint
46
+ export default [{ rules }]
103
47
  ```
104
48
 
105
- ### Test Coverage
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 package uses ESLint's flat config format and has a clean structure:
51
+ The config bundles four local plugins. Override any rule the same way you would override a standard ESLint rule.
114
52
 
115
- - `index.mjs` - Exports the full config, baseConfig, and rules
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
- ## ๐Ÿ” Migration Guide
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
- ### From v2.4.0 to v2.5.0
63
+ Options for `whitespaced/aligned-assignments`:
122
64
 
123
- 1. **Remove tailwindcss dependency** (if manually installed)
124
- 2. **Update package.json** - Latest versions are automatically handled
125
- 3. **No breaking changes** - All existing code continues to work
126
- 4. **Enhanced JSX support** - Better formatting for React components
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
- ### Deprecated Configurations
129
- - `allowTemplateLiterals: true` โ†’ `allowTemplateLiterals: 'always'`
130
- - Tailwind CSS rules removed (use dedicated Tailwind tools instead)
74
+ ### `omit/omit-unnecessary-parens-brackets`
131
75
 
132
- ## ๐Ÿ› Troubleshooting
76
+ Removes unnecessary parentheses, brackets, and braces. No options.
133
77
 
134
- ### Common Issues
78
+ ### `no-inline-types/no-inline-multiline-types`
135
79
 
136
- **Template literal warnings:**
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
- **JSX formatting issues:**
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
- **TypeScript parsing errors:**
144
- - Verify your tsconfig.json is valid
145
- - Ensure proper file extensions (.ts, .tsx)
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
- ## ๐Ÿ“ˆ Performance
93
+ ## Package structure
148
94
 
149
- - **Zero runtime dependencies** in production
150
- - **Fast linting** with optimized rule selection
151
- - **Minimal plugin surface area** for better performance
152
- - **Comprehensive but efficient** rule set
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
- ## ๐Ÿค Contributing
100
+ ## Scripts
155
101
 
156
- 1. Fork the repository
157
- 2. Create a feature branch
158
- 3. Add tests for new rules
159
- 4. Run the test suite: `npm run test`
160
- 5. Submit a pull request
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuomashatakka/eslint-config",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Default eslint configuration",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
@@ -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
- function getEqualsColumn (declarator) {
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 getTypeColonColumn (declarator) {
59
- if (declarator.id && declarator.id.typeAnnotation) {
60
- const colonToken = sourceCode.getFirstToken(declarator.id.typeAnnotation)
61
- return colonToken ? colonToken.loc.start.column : null
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 areNodesAdjacent (node1, node2) {
68
- return node2.loc.start.line === node1.loc.end.line + 1
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 haveSameKind (declarations) {
73
- if (!declarations.length)
74
- return true
63
+ function declaratorRow (declarator) {
64
+ if (!declarator.init)
65
+ return null
75
66
 
76
- const firstKind = declarations[0].parent.kind
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
- function allHaveTypes (declarations) {
82
- return declarations.every(decl =>
83
- decl.id && decl.id.typeAnnotation
84
- )
85
- }
86
-
72
+ const { col, idx } = declaratorLhsEnd(declarator)
87
73
 
88
- function anyHaveTypes (declarations) {
89
- return declarations.some(decl =>
90
- decl.id && decl.id.typeAnnotation
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 getMaxEqualsColumn (declarations) {
96
- return Math.max(...declarations.map(getEqualsColumn))
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
- function getMaxTypeColonColumn (declarations) {
101
- const columns = declarations
102
- .map(getTypeColonColumn)
103
- .filter(column => column !== null)
91
+ const equalsToken = findEqualsToken(expr.right)
104
92
 
105
- return columns.length ? Math.max(...columns) : null
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 getFixedDeclaration (declarator, targetMaxEqualsColumn) {
110
- const originalText = sourceCode.getText(declarator)
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
- if (!initText)
115
- return originalText
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
- // Get the equals token
118
- const equalsToken = sourceCode.getTokenBefore(
119
- declarator.init,
120
- token => token.value === '='
121
- )
124
+ return rows
125
+ }
122
126
 
123
- if (!equalsToken)
124
- return originalText
125
127
 
126
- const currentEqualsColumn = equalsToken.loc.start.column
127
- const padding = targetMaxEqualsColumn - currentEqualsColumn
128
+ // โ”€โ”€ Adjacency grouping โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
128
129
 
129
- if (padding <= 0)
130
- return originalText
130
+ function groupByAdjacency (rows) {
131
+ if (rows.length === 0)
132
+ return []
131
133
 
132
- // Build the fixed text: id + padding + '= ' + init
133
- const result = idText + ' '.repeat(padding) + '= ' + initText
134
+ const sorted = [ ...rows ].sort((a, b) => a.line - b.line)
135
+ const groups = []
136
+ let current = [ sorted[0] ]
134
137
 
135
- return result
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
- function getIdLengthWithType (declarator) {
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
- function checkAlignment (declarations) {
150
- if (declarations.length < blockSize)
151
- return
154
+ return ignoreAdjacent ? groups : (rows.length >= blockSize ? [ sorted ] : [])
155
+ }
152
156
 
153
- if (ignoreIfAssignmentsNotInBlock && !haveSameKind(declarations))
154
- return
155
157
 
156
- // Get actual equals column positions
157
- const equalsColumns = declarations.map(d => getEqualsColumn(d)).filter(c => c !== null)
158
+ // โ”€โ”€ Kind-transition sub-block split โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
158
159
 
159
- // Check if already aligned by comparing equals columns directly
160
- if (equalsColumns.length >= 2) {
161
- const maxEqualsCol = Math.max(...equalsColumns)
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 maxIdLength = Math.max(...declarations.map(getIdLengthWithType))
164
+ const subBlocks = []
165
+ let current = []
166
+ let lockedKind = null
168
167
 
169
- let maxTypeLength = null
170
- if (alignTypes && anyHaveTypes(declarations)) {
171
- if (ignoreTypesMismatch && !allHaveTypes(declarations)) {
172
- // Skip only type alignment but still do equals alignment
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
- const typeLengths = declarations
176
- .map(d => d.id?.typeAnnotation ? sourceCode.getText(d.id.typeAnnotation).length : 0)
177
- .filter(l => l > 0)
178
- maxTypeLength = typeLengths.length ? Math.max(...typeLengths) : null
176
+ if (current.length >= blockSize)
177
+ subBlocks.push(current)
178
+ current = [ row ]
179
+ lockedKind = row.kind
179
180
  }
180
- }
181
181
 
182
- const maxEqualsColumn = Math.max(...equalsColumns)
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
- const declarationsWithInits = declarations.filter(decl => decl.init)
189
+ // โ”€โ”€ Alignment check + fix โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
207
190
 
208
- if (declarationsWithInits.length < blockSize)
191
+ function checkAndFix (subBlock) {
192
+ if (subBlock.length < blockSize)
209
193
  return
210
194
 
211
- if (ignoreAdjacent) {
212
- const adjacentGroups = []
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
- if (currentGroup.length >= blockSize)
228
- adjacentGroups.push(currentGroup)
198
+ if (allAligned)
199
+ return
229
200
 
230
- adjacentGroups.forEach(checkAlignment)
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
- // โ”€โ”€ Member assignment alignment (this.prop = value, obj.prop = value) โ”€โ”€โ”€โ”€
224
+ // โ”€โ”€ Type colon alignment (orthogonal to equals alignment) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
238
225
 
239
- function getMemberAssignEqualsColumn (exprStmt) {
240
- const assignExpr = exprStmt.expression
241
- const equalsToken = sourceCode.getTokenBefore(
242
- assignExpr.right,
243
- token => token.value === '=' && token.type === 'Punctuator'
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 getFixedMemberAssignment (exprStmt, targetMaxLeftLength) {
250
- const assignExpr = exprStmt.expression
251
- const left = assignExpr.left
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
- return leftText + ' '.repeat(padding) + '= ' + rightText
260
- }
239
+ const annotated = declarators.filter(d => d.id?.typeAnnotation)
261
240
 
241
+ if (annotated.length < blockSize)
242
+ return
262
243
 
263
- function getMemberLeftLength (exprStmt) {
264
- const left = exprStmt.expression.left
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
- function getMemberEqualsColumn (exprStmt) {
270
- const assignExpr = exprStmt.expression
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
- function checkMemberAlignment (stmts) {
280
- if (stmts.length < blockSize)
255
+ if (allAligned)
281
256
  return
282
257
 
283
- const lengths = stmts.map(getMemberLeftLength)
284
- const maxLength = Math.max(...lengths)
258
+ for (const declarator of annotated) {
259
+ const colonCol = getTypeColonColumn(declarator)
285
260
 
286
- // Check if already aligned by comparing equals columns
287
- const equalsColumns = stmts.map(s => getMemberEqualsColumn(s)).filter(c => c !== null)
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
- stmts.forEach(stmt => {
296
- const length = getMemberLeftLength(stmt)
297
- if (length !== maxLength)
298
- context.report({
299
- node: stmt.expression,
300
- messageId: 'misalignedAssignment',
301
- fix (fixer) {
302
- return fixer.replaceText(stmt.expression, getFixedMemberAssignment(stmt, maxLength))
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
- function processMemberAssignments (blockBody) {
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
- if (memberStmts.length < blockSize)
287
+ function processStatements (statements) {
288
+ if (!statements || statements.length === 0)
322
289
  return
323
290
 
324
- if (ignoreAdjacent) {
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
- // โ”€โ”€ Shared block processor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
293
+ for (const group of groupByAdjacency(rows))
294
+ for (const subBlock of splitByKind(group))
295
+ checkAndFix(subBlock)
346
296
 
347
- function processBlockVariables (node) {
348
- const scopeBody = node.type === 'Program' ? node.body : node.body ? node.body : []
349
- const declarations = []
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
- for (const statement of scopeBody)
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
- processBlockVariables(node)
310
+ processStatements(node.body)
363
311
  },
364
312
 
365
313
  BlockStatement (node) {
366
- processBlockVariables(node)
314
+ processStatements(node.body)
367
315
  },
368
316
 
369
317
  SwitchCase (node) {
370
- if (!node.consequent)
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', { alignMemberAssignments: false, alignTypes: true }],
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 }],
@@ -8,6 +8,6 @@ class TestRunner {
8
8
 
9
9
  constructor () {
10
10
  this.fixturesDir = path.join('/test', 'fixtures')
11
- this.configPath = path.join('/test', '../index.mjs')
11
+ this.configPath = path.join('/test', '../index.mjs')
12
12
  }
13
13
  }
@@ -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
+ }