@herb-tools/language-server 0.8.4 → 0.8.5

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.
@@ -6,7 +6,18 @@ import type {
6
6
  Node,
7
7
  ERBCaseNode,
8
8
  ERBCaseMatchNode,
9
- HTMLTextNode,
9
+ ERBIfNode,
10
+ ERBElseNode,
11
+ ERBUnlessNode,
12
+ ERBForNode,
13
+ ERBWhileNode,
14
+ ERBUntilNode,
15
+ ERBWhenNode,
16
+ ERBBeginNode,
17
+ ERBRescueNode,
18
+ ERBEnsureNode,
19
+ ERBBlockNode,
20
+ ERBInNode,
10
21
  } from "@herb-tools/core"
11
22
 
12
23
  import { isHTMLTextNode } from "@herb-tools/core"
@@ -88,14 +99,118 @@ export class Diagnostics {
88
99
 
89
100
  export class UnreachableCodeCollector extends Visitor {
90
101
  diagnostics: Diagnostic[] = []
102
+ private processedIfNodes: Set<ERBIfNode> = new Set()
103
+ private processedElseNodes: Set<ERBElseNode> = new Set()
91
104
 
92
105
  visitERBCaseNode(node: ERBCaseNode): void {
93
106
  this.checkUnreachableChildren(node.children)
107
+ this.checkAndMarkElseClause(node.else_clause)
94
108
  this.visitChildNodes(node)
95
109
  }
96
110
 
97
111
  visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
98
112
  this.checkUnreachableChildren(node.children)
113
+ this.checkAndMarkElseClause(node.else_clause)
114
+ this.visitChildNodes(node)
115
+ }
116
+
117
+ visitERBIfNode(node: ERBIfNode): void {
118
+ if (this.processedIfNodes.has(node)) {
119
+ return
120
+ }
121
+
122
+ this.markIfChainAsProcessed(node)
123
+
124
+ this.markElseNodesInIfChain(node)
125
+
126
+ const entireChainEmpty = this.isEntireIfChainEmpty(node)
127
+
128
+ if (entireChainEmpty) {
129
+ this.checkEmptyStatements(node, node.statements, "if")
130
+ } else {
131
+ this.checkIfChainParts(node)
132
+ }
133
+
134
+ this.visitChildNodes(node)
135
+ }
136
+
137
+ visitERBElseNode(node: ERBElseNode): void {
138
+ if (this.processedElseNodes.has(node)) {
139
+ this.visitChildNodes(node)
140
+ return
141
+ }
142
+
143
+ this.checkEmptyStatements(node, node.statements, "else")
144
+ this.visitChildNodes(node)
145
+ }
146
+
147
+ visitERBUnlessNode(node: ERBUnlessNode): void {
148
+ const unlessHasContent = this.statementsHaveContent(node.statements)
149
+ const elseHasContent = node.else_clause && this.statementsHaveContent(node.else_clause.statements)
150
+
151
+ if (node.else_clause) {
152
+ this.processedElseNodes.add(node.else_clause)
153
+ }
154
+
155
+ const entireBlockEmpty = !unlessHasContent && !elseHasContent
156
+
157
+ if (entireBlockEmpty) {
158
+ this.checkEmptyStatements(node, node.statements, "unless")
159
+ } else {
160
+ if (!unlessHasContent) {
161
+ this.checkEmptyStatementsWithEndLocation(node, node.statements, "unless", node.else_clause)
162
+ }
163
+
164
+ if (node.else_clause && !elseHasContent) {
165
+ this.checkEmptyStatements(node.else_clause, node.else_clause.statements, "else")
166
+ }
167
+ }
168
+
169
+ this.visitChildNodes(node)
170
+ }
171
+
172
+ visitERBForNode(node: ERBForNode): void {
173
+ this.checkEmptyStatements(node, node.statements, "for")
174
+ this.visitChildNodes(node)
175
+ }
176
+
177
+ visitERBWhileNode(node: ERBWhileNode): void {
178
+ this.checkEmptyStatements(node, node.statements, "while")
179
+ this.visitChildNodes(node)
180
+ }
181
+
182
+ visitERBUntilNode(node: ERBUntilNode): void {
183
+ this.checkEmptyStatements(node, node.statements, "until")
184
+ this.visitChildNodes(node)
185
+ }
186
+
187
+ visitERBWhenNode(node: ERBWhenNode): void {
188
+ this.checkEmptyStatements(node, node.statements, "when")
189
+ this.visitChildNodes(node)
190
+ }
191
+
192
+ visitERBBeginNode(node: ERBBeginNode): void {
193
+ this.checkEmptyStatements(node, node.statements, "begin")
194
+ this.visitChildNodes(node)
195
+ }
196
+
197
+ visitERBRescueNode(node: ERBRescueNode): void {
198
+ this.checkEmptyStatements(node, node.statements, "rescue")
199
+ this.visitChildNodes(node)
200
+ }
201
+
202
+ visitERBEnsureNode(node: ERBEnsureNode): void {
203
+ this.checkEmptyStatements(node, node.statements, "ensure")
204
+ this.visitChildNodes(node)
205
+ }
206
+
207
+ visitERBBlockNode(node: ERBBlockNode): void {
208
+ this.checkEmptyStatements(node, node.body, "block")
209
+ this.visitChildNodes(node)
210
+ }
211
+
212
+ visitERBInNode(node: ERBInNode): void {
213
+ this.checkEmptyStatements(node, node.statements, "in")
99
214
  this.visitChildNodes(node)
100
215
  }
101
216
 
@@ -105,25 +220,145 @@ export class UnreachableCodeCollector extends Visitor {
105
220
  continue
106
221
  }
107
222
 
108
- const diagnostic: Diagnostic = {
109
- range: {
110
- start: {
111
- line: this.toZeroBased(child.location.start.line),
112
- character: child.location.start.column
113
- },
114
- end: {
115
- line: this.toZeroBased(child.location.end.line),
116
- character: child.location.end.column
117
- }
223
+ this.addDiagnostic(
224
+ child.location,
225
+ "Unreachable code: content between case and when/in is never executed"
226
+ )
227
+ }
228
+ }
229
+
230
+ private checkEmptyStatements(node: Node, statements: Node[], blockType: string): void {
231
+ this.checkEmptyStatementsWithEndLocation(node, statements, blockType, null)
232
+ }
233
+
234
+ private checkEmptyStatementsWithEndLocation(node: Node, statements: Node[], blockType: string, subsequentNode: Node | null): void {
235
+ if (this.statementsHaveContent(statements)) {
236
+ return
237
+ }
238
+
239
+ const startLocation = node.location.start
240
+ const endLocation = subsequentNode
241
+ ? subsequentNode.location.start
242
+ : node.location.end
243
+
244
+ this.addDiagnostic(
245
+ { start: startLocation, end: endLocation },
246
+ `Empty ${blockType} block: this control flow statement has no content`
247
+ )
248
+ }
249
+
250
+ private addDiagnostic(location: { start: { line: number; column: number }, end: { line: number; column: number } }, message: string): void {
251
+ const diagnostic: Diagnostic = {
252
+ range: {
253
+ start: {
254
+ line: this.toZeroBased(location.start.line),
255
+ character: location.start.column
118
256
  },
119
- message: "Unreachable code: content between case and when/in is never executed",
120
- severity: DiagnosticSeverity.Hint,
121
- tags: [DiagnosticTag.Unnecessary],
122
- source: "Herb Language Server"
257
+ end: {
258
+ line: this.toZeroBased(location.end.line),
259
+ character: location.end.column
260
+ }
261
+ },
262
+ message,
263
+ severity: DiagnosticSeverity.Hint,
264
+ tags: [DiagnosticTag.Unnecessary],
265
+ source: "Herb Language Server"
266
+ }
267
+
268
+ this.diagnostics.push(diagnostic)
269
+ }
270
+
271
+ private statementsHaveContent(statements: Node[]): boolean {
272
+ return statements.some(statement => {
273
+ if (isHTMLTextNode(statement)) {
274
+ return statement.content.trim() !== ""
123
275
  }
124
276
 
125
- this.diagnostics.push(diagnostic)
277
+ return true
278
+ })
279
+ }
280
+
281
+ private checkAndMarkElseClause(elseClause: ERBElseNode | null): void {
282
+ if (!elseClause) {
283
+ return
126
284
  }
285
+
286
+ this.processedElseNodes.add(elseClause)
287
+ if (!this.statementsHaveContent(elseClause.statements)) {
288
+ this.checkEmptyStatements(elseClause, elseClause.statements, "else")
289
+ }
290
+ }
291
+
292
+ private markIfChainAsProcessed(node: ERBIfNode): void {
293
+ this.processedIfNodes.add(node)
294
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
295
+ if (current.type === 'AST_ERB_IF_NODE') {
296
+ this.processedIfNodes.add(current as ERBIfNode)
297
+ }
298
+ })
299
+ }
300
+
301
+ private markElseNodesInIfChain(node: ERBIfNode): void {
302
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
303
+ if (current.type === 'AST_ERB_ELSE_NODE') {
304
+ this.processedElseNodes.add(current as ERBElseNode)
305
+ }
306
+ })
307
+ }
308
+
309
+ private traverseSubsequentNodes(startNode: Node | null, callback: (node: Node) => void): void {
310
+ let current = startNode
311
+ while (current) {
312
+ callback(current)
313
+
314
+ if ('subsequent' in current) {
315
+ current = (current as any).subsequent
316
+ } else {
317
+ break
318
+ }
319
+ }
320
+ }
321
+
322
+ private checkIfChainParts(node: ERBIfNode): void {
323
+ if (!this.statementsHaveContent(node.statements)) {
324
+ this.checkEmptyStatementsWithEndLocation(node, node.statements, "if", node.subsequent)
325
+ }
326
+
327
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
328
+ if (!('statements' in current) || !Array.isArray((current as any).statements)) {
329
+ return
330
+ }
331
+
332
+ if (this.statementsHaveContent((current as any).statements)) {
333
+ return
334
+ }
335
+
336
+ const blockType = current.type === 'AST_ERB_IF_NODE' ? 'elsif' : 'else'
337
+ const nextSubsequent = 'subsequent' in current ? (current as any).subsequent : null
338
+
339
+ if (nextSubsequent) {
340
+ this.checkEmptyStatementsWithEndLocation(current, (current as any).statements, blockType, nextSubsequent)
341
+ } else {
342
+ this.checkEmptyStatements(current, (current as any).statements, blockType)
343
+ }
344
+ })
345
+ }
346
+
347
+ private isEntireIfChainEmpty(node: ERBIfNode): boolean {
348
+ if (this.statementsHaveContent(node.statements)) {
349
+ return false
350
+ }
351
+
352
+ let hasContent = false
353
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
354
+ if ('statements' in current && Array.isArray((current as any).statements)) {
355
+ if (this.statementsHaveContent((current as any).statements)) {
356
+ hasContent = true
357
+ }
358
+ }
359
+ })
360
+
361
+ return !hasContent
127
362
  }
128
363
 
129
364
  private toZeroBased(line: number): number {
package/CHANGELOG.md DELETED
@@ -1,7 +0,0 @@
1
- ## 0.3.1 (2025-06-23)
2
-
3
- This was a version bump only for @herb-tools/language-server to align it with other projects, there were no code changes.
4
-
5
- ## 0.3.0 (2025-06-21)
6
-
7
- This was a version bump only for @herb-tools/language-server to align it with other projects, there were no code changes.