@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.
- package/dist/diagnostics.js +194 -16
- package/dist/diagnostics.js.map +1 -1
- package/dist/herb-language-server.js +394 -50
- package/dist/herb-language-server.js.map +1 -1
- package/dist/index.cjs +195 -17
- package/dist/index.cjs.map +1 -1
- package/dist/types/diagnostics.d.ts +25 -1
- package/package.json +5 -5
- package/src/diagnostics.ts +251 -16
- package/CHANGELOG.md +0 -7
package/src/diagnostics.ts
CHANGED
|
@@ -6,7 +6,18 @@ import type {
|
|
|
6
6
|
Node,
|
|
7
7
|
ERBCaseNode,
|
|
8
8
|
ERBCaseMatchNode,
|
|
9
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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.
|