@tuomashatakka/eslint-config 3.1.0 → 3.1.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.
@@ -2,7 +2,8 @@ name: Publish to npm
2
2
 
3
3
  on:
4
4
  push:
5
- tags: ['v*']
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
- ref: main
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
- - name: Extract version from tag
25
- id: version
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
- - run: npm ci
32
- - run: npm run test
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: Commit version bump back to main
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 add package.json package-lock.json
44
- git commit -m "${{ steps.version.outputs.VERSION }}" || echo "No changes to commit"
45
- git push origin main
58
+ git tag "v${{ steps.version.outputs.CURRENT }}"
59
+ git push origin "v${{ steps.version.outputs.CURRENT }}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuomashatakka/eslint-config",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "Default eslint configuration",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
@@ -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
- const innerText = sourceCode.getText(singleStatement)
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
  })
@@ -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
  }
@@ -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
+ }