@herb-tools/language-server 0.8.10 → 0.9.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.
- package/dist/action_view_helpers.js +19 -0
- package/dist/action_view_helpers.js.map +1 -0
- package/dist/autofix_service.js +1 -1
- package/dist/autofix_service.js.map +1 -1
- package/dist/code_action_service.js +3 -6
- package/dist/code_action_service.js.map +1 -1
- package/dist/comment_ast_utils.js +206 -0
- package/dist/comment_ast_utils.js.map +1 -0
- package/dist/comment_service.js +175 -0
- package/dist/comment_service.js.map +1 -0
- package/dist/diagnostics.js +0 -233
- package/dist/diagnostics.js.map +1 -1
- package/dist/document_highlight_service.js +196 -0
- package/dist/document_highlight_service.js.map +1 -0
- package/dist/document_save_service.js +16 -6
- package/dist/document_save_service.js.map +1 -1
- package/dist/folding_range_service.js +209 -0
- package/dist/folding_range_service.js.map +1 -0
- package/dist/formatting_service.js +4 -4
- package/dist/formatting_service.js.map +1 -1
- package/dist/herb-language-server.js +152936 -41156
- package/dist/herb-language-server.js.map +1 -1
- package/dist/hover_service.js +70 -0
- package/dist/hover_service.js.map +1 -0
- package/dist/index.cjs +1299 -333
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/line_context_collector.js +73 -0
- package/dist/line_context_collector.js.map +1 -0
- package/dist/linter_service.js +27 -6
- package/dist/linter_service.js.map +1 -1
- package/dist/parser_service.js +6 -5
- package/dist/parser_service.js.map +1 -1
- package/dist/range_utils.js +65 -0
- package/dist/range_utils.js.map +1 -0
- package/dist/rewrite_code_action_service.js +135 -0
- package/dist/rewrite_code_action_service.js.map +1 -0
- package/dist/server.js +39 -2
- package/dist/server.js.map +1 -1
- package/dist/service.js +10 -0
- package/dist/service.js.map +1 -1
- package/dist/types/action_view_helpers.d.ts +5 -0
- package/dist/types/comment_ast_utils.d.ts +20 -0
- package/dist/types/comment_service.d.ts +14 -0
- package/dist/types/diagnostics.d.ts +1 -35
- package/dist/types/document_highlight_service.d.ts +28 -0
- package/dist/types/document_save_service.d.ts +8 -0
- package/dist/types/folding_range_service.d.ts +35 -0
- package/dist/types/formatting_service.d.ts +1 -1
- package/dist/types/hover_service.d.ts +8 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/line_context_collector.d.ts +19 -0
- package/dist/types/linter_service.d.ts +1 -0
- package/dist/types/parser_service.d.ts +2 -1
- package/dist/types/range_utils.d.ts +16 -0
- package/dist/types/rewrite_code_action_service.d.ts +11 -0
- package/dist/types/service.d.ts +10 -0
- package/dist/types/utils.d.ts +4 -8
- package/dist/utils.js +10 -15
- package/dist/utils.js.map +1 -1
- package/package.json +10 -5
- package/src/action_view_helpers.ts +23 -0
- package/src/autofix_service.ts +1 -1
- package/src/code_action_service.ts +3 -6
- package/src/comment_ast_utils.ts +282 -0
- package/src/comment_service.ts +228 -0
- package/src/diagnostics.ts +1 -305
- package/src/document_highlight_service.ts +267 -0
- package/src/document_save_service.ts +19 -7
- package/src/folding_range_service.ts +287 -0
- package/src/formatting_service.ts +4 -4
- package/src/hover_service.ts +90 -0
- package/src/index.ts +4 -0
- package/src/line_context_collector.ts +97 -0
- package/src/linter_service.ts +35 -9
- package/src/parser_service.ts +9 -10
- package/src/range_utils.ts +90 -0
- package/src/rewrite_code_action_service.ts +165 -0
- package/src/server.ts +54 -2
- package/src/service.ts +15 -0
- package/src/utils.ts +12 -21
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { TextEdit, Range, Position } from "vscode-languageserver/node"
|
|
2
|
+
import { TextDocument } from "vscode-languageserver-textdocument"
|
|
3
|
+
|
|
4
|
+
import { ParserService } from "./parser_service"
|
|
5
|
+
import { LineContextCollector } from "./line_context_collector"
|
|
6
|
+
|
|
7
|
+
import { lspLine } from "./range_utils"
|
|
8
|
+
import { determineStrategy, commentLineContent, uncommentLineContent } from "./comment_ast_utils"
|
|
9
|
+
|
|
10
|
+
import type { LineInfo } from "./line_context_collector"
|
|
11
|
+
import type { ERBContentNode, HTMLCommentNode } from "@herb-tools/core"
|
|
12
|
+
|
|
13
|
+
export class CommentService {
|
|
14
|
+
private parserService: ParserService
|
|
15
|
+
|
|
16
|
+
constructor(parserService: ParserService) {
|
|
17
|
+
this.parserService = parserService
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
toggleLineComment(document: TextDocument, range: Range): TextEdit[] {
|
|
21
|
+
const parseResult = this.parserService.parseDocument(document)
|
|
22
|
+
const collector = new LineContextCollector()
|
|
23
|
+
|
|
24
|
+
collector.visit(parseResult.document)
|
|
25
|
+
|
|
26
|
+
const startLine = range.start.line
|
|
27
|
+
const endLine = range.end.line
|
|
28
|
+
const lineInfos: LineInfo[] = []
|
|
29
|
+
|
|
30
|
+
for (let line = startLine; line <= endLine; line++) {
|
|
31
|
+
const lineText = document.getText(Range.create(line, 0, line + 1, 0)).replace(/\n$/, "")
|
|
32
|
+
|
|
33
|
+
if (lineText.trim() === "") {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (this.lineIsIfFalseWrapped(lineText) !== null) {
|
|
38
|
+
lineInfos.push({ line, context: "erb-comment", node: null })
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const htmlCommentNode = collector.htmlCommentNodesPerLine.get(line)
|
|
43
|
+
const info = collector.lineMap.get(line)
|
|
44
|
+
|
|
45
|
+
if (htmlCommentNode && this.htmlCommentSpansLine(htmlCommentNode, lineText)) {
|
|
46
|
+
lineInfos.push({ line, context: "html-comment", node: htmlCommentNode })
|
|
47
|
+
} else if (info) {
|
|
48
|
+
if (info.context === "html-comment") {
|
|
49
|
+
lineInfos.push({ line, context: "html-content", node: null })
|
|
50
|
+
} else {
|
|
51
|
+
lineInfos.push(info)
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
lineInfos.push({ line, context: "html-content", node: null })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (lineInfos.length === 0) return []
|
|
59
|
+
|
|
60
|
+
const allCommented = lineInfos.every(
|
|
61
|
+
info => info.context === "erb-comment" || info.context === "html-comment"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const edits: TextEdit[] = []
|
|
65
|
+
|
|
66
|
+
if (allCommented) {
|
|
67
|
+
for (const info of lineInfos) {
|
|
68
|
+
const lineText = document.getText(Range.create(info.line, 0, info.line + 1, 0)).replace(/\n$/, "")
|
|
69
|
+
const edit = this.uncommentLine(info, lineText, collector)
|
|
70
|
+
|
|
71
|
+
if (edit) edits.push(edit)
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
for (const info of lineInfos) {
|
|
75
|
+
if (info.context === "erb-comment" || info.context === "html-comment") continue
|
|
76
|
+
|
|
77
|
+
const lineText = document.getText(Range.create(info.line, 0, info.line + 1, 0)).replace(/\n$/, "")
|
|
78
|
+
const erbNodes = collector.erbNodesPerLine.get(info.line) || []
|
|
79
|
+
const edit = this.commentLine(info, lineText, erbNodes, collector)
|
|
80
|
+
|
|
81
|
+
if (edit) edits.push(edit)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return edits
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
toggleBlockComment(document: TextDocument, range: Range): TextEdit[] {
|
|
89
|
+
const startLine = range.start.line
|
|
90
|
+
const endLine = range.end.line
|
|
91
|
+
|
|
92
|
+
const firstLineText = document.getText(Range.create(startLine, 0, startLine + 1, 0)).replace(/\n$/, "")
|
|
93
|
+
const lastLineText = document.getText(Range.create(endLine, 0, endLine + 1, 0)).replace(/\n$/, "")
|
|
94
|
+
const isWrapped = firstLineText.trim() === "<% if false %>" && lastLineText.trim() === "<% end %>"
|
|
95
|
+
|
|
96
|
+
if (isWrapped) {
|
|
97
|
+
return [
|
|
98
|
+
TextEdit.del(Range.create(endLine, 0, endLine + 1, 0)),
|
|
99
|
+
TextEdit.del(Range.create(startLine, 0, startLine + 1, 0)),
|
|
100
|
+
]
|
|
101
|
+
} else {
|
|
102
|
+
const firstLineIndent = this.getIndentation(firstLineText)
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
TextEdit.insert(Position.create(endLine + 1, 0), `${firstLineIndent}<% end %>\n`),
|
|
106
|
+
TextEdit.insert(Position.create(startLine, 0), `${firstLineIndent}<% if false %>\n`),
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private commentLine(info: LineInfo, lineText: string, erbNodes: ERBContentNode[], collector: LineContextCollector): TextEdit | null {
|
|
112
|
+
const lineRange = Range.create(info.line, 0, info.line, lineText.length)
|
|
113
|
+
const indent = this.getIndentation(lineText)
|
|
114
|
+
const content = lineText.trimStart()
|
|
115
|
+
const htmlCommentNode = collector.htmlCommentNodesPerLine.get(info.line)
|
|
116
|
+
|
|
117
|
+
if (htmlCommentNode) {
|
|
118
|
+
return TextEdit.replace(lineRange, `${indent}<% if false %>${content}<% end %>`)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const strategy = determineStrategy(erbNodes, lineText)
|
|
122
|
+
|
|
123
|
+
if (strategy === "single-erb") {
|
|
124
|
+
const node = erbNodes[0]
|
|
125
|
+
const insertColumn = node.tag_opening!.location.start.column + 2
|
|
126
|
+
|
|
127
|
+
return TextEdit.insert(Position.create(info.line, insertColumn), "#")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = commentLineContent(content, erbNodes, strategy, this.parserService)
|
|
131
|
+
|
|
132
|
+
return TextEdit.replace(lineRange, indent + result)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private lineIsIfFalseWrapped(lineText: string): string | null {
|
|
136
|
+
const trimmed = lineText.trimStart()
|
|
137
|
+
const indent = this.getIndentation(lineText)
|
|
138
|
+
|
|
139
|
+
if (trimmed.startsWith("<% if false %>") && trimmed.endsWith("<% end %>")) {
|
|
140
|
+
const inner = trimmed.slice("<% if false %>".length, -"<% end %>".length)
|
|
141
|
+
|
|
142
|
+
return indent + inner
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private uncommentLine(info: LineInfo, lineText: string, collector: LineContextCollector): TextEdit | null {
|
|
149
|
+
const lineRange = Range.create(info.line, 0, info.line, lineText.length)
|
|
150
|
+
const indent = this.getIndentation(lineText)
|
|
151
|
+
const ifFalseContent = this.lineIsIfFalseWrapped(lineText)
|
|
152
|
+
|
|
153
|
+
if (ifFalseContent !== null) {
|
|
154
|
+
return TextEdit.replace(lineRange, ifFalseContent)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (info.context === "erb-comment") {
|
|
158
|
+
const node = info.node as ERBContentNode
|
|
159
|
+
if (!node?.tag_opening || !node?.tag_closing) return null
|
|
160
|
+
|
|
161
|
+
const contentValue = (node as any).content?.value as string | null
|
|
162
|
+
const trimmedContent = contentValue?.trim() || ""
|
|
163
|
+
|
|
164
|
+
if (trimmedContent.startsWith("<") && !trimmedContent.startsWith("<%")) {
|
|
165
|
+
return TextEdit.replace(lineRange, `${indent}${trimmedContent}`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (lspLine(node.tag_opening.location.start) !== info.line) return null
|
|
169
|
+
|
|
170
|
+
const erbNodes = collector.erbNodesPerLine.get(info.line) || []
|
|
171
|
+
|
|
172
|
+
if (erbNodes.length > 1) {
|
|
173
|
+
const content = lineText.trimStart()
|
|
174
|
+
const result = uncommentLineContent(content, this.parserService)
|
|
175
|
+
|
|
176
|
+
return TextEdit.replace(lineRange, indent + result)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const hashColumn = node.tag_opening.location.start.column + 2
|
|
180
|
+
|
|
181
|
+
if (
|
|
182
|
+
contentValue?.startsWith(" graphql ") ||
|
|
183
|
+
contentValue?.startsWith(" %= ") ||
|
|
184
|
+
contentValue?.startsWith(" == ") ||
|
|
185
|
+
contentValue?.startsWith(" % ") ||
|
|
186
|
+
contentValue?.startsWith(" = ") ||
|
|
187
|
+
contentValue?.startsWith(" - ")
|
|
188
|
+
) {
|
|
189
|
+
return TextEdit.del(Range.create(info.line, hashColumn, info.line, hashColumn + 2))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return TextEdit.del(Range.create(info.line, hashColumn, info.line, hashColumn + 1))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (info.context === "html-comment") {
|
|
196
|
+
const commentNode = info.node as HTMLCommentNode | null
|
|
197
|
+
|
|
198
|
+
if (commentNode?.comment_start && commentNode?.comment_end) {
|
|
199
|
+
const contentStart = commentNode.comment_start.location.end.column
|
|
200
|
+
const contentEnd = commentNode.comment_end.location.start.column
|
|
201
|
+
const innerContent = lineText.substring(contentStart, contentEnd).trim()
|
|
202
|
+
|
|
203
|
+
const result = uncommentLineContent(innerContent, this.parserService)
|
|
204
|
+
|
|
205
|
+
return TextEdit.replace(lineRange, `${indent}${result}`)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private htmlCommentSpansLine(node: HTMLCommentNode, lineText: string): boolean {
|
|
213
|
+
if (!node.comment_start || !node.comment_end) return false
|
|
214
|
+
|
|
215
|
+
const commentStart = node.comment_start.location.start.column
|
|
216
|
+
const commentEnd = node.comment_end.location.end.column
|
|
217
|
+
const contentBefore = lineText.substring(0, commentStart).trim()
|
|
218
|
+
const contentAfter = lineText.substring(commentEnd).trim()
|
|
219
|
+
|
|
220
|
+
return contentBefore === "" && contentAfter === ""
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private getIndentation(lineText: string): string {
|
|
224
|
+
const match = lineText.match(/^(\s*)/)
|
|
225
|
+
|
|
226
|
+
return match ? match[1] : ""
|
|
227
|
+
}
|
|
228
|
+
}
|
package/src/diagnostics.ts
CHANGED
|
@@ -1,26 +1,5 @@
|
|
|
1
1
|
import { TextDocument } from "vscode-languageserver-textdocument"
|
|
2
|
-
import { Connection, Diagnostic
|
|
3
|
-
import { Visitor } from "@herb-tools/core"
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
Node,
|
|
7
|
-
ERBCaseNode,
|
|
8
|
-
ERBCaseMatchNode,
|
|
9
|
-
ERBIfNode,
|
|
10
|
-
ERBElseNode,
|
|
11
|
-
ERBUnlessNode,
|
|
12
|
-
ERBForNode,
|
|
13
|
-
ERBWhileNode,
|
|
14
|
-
ERBUntilNode,
|
|
15
|
-
ERBWhenNode,
|
|
16
|
-
ERBBeginNode,
|
|
17
|
-
ERBRescueNode,
|
|
18
|
-
ERBEnsureNode,
|
|
19
|
-
ERBBlockNode,
|
|
20
|
-
ERBInNode,
|
|
21
|
-
} from "@herb-tools/core"
|
|
22
|
-
|
|
23
|
-
import { isHTMLTextNode } from "@herb-tools/core"
|
|
2
|
+
import { Connection, Diagnostic } from "vscode-languageserver/node"
|
|
24
3
|
|
|
25
4
|
import { ParserService } from "./parser_service"
|
|
26
5
|
import { LinterService } from "./linter_service"
|
|
@@ -57,12 +36,10 @@ export class Diagnostics {
|
|
|
57
36
|
} else {
|
|
58
37
|
const parseResult = this.parserService.parseDocument(textDocument)
|
|
59
38
|
const lintResult = await this.linterService.lintDocument(textDocument)
|
|
60
|
-
const unreachableCodeDiagnostics = this.getUnreachableCodeDiagnostics(parseResult.document)
|
|
61
39
|
|
|
62
40
|
allDiagnostics = [
|
|
63
41
|
...parseResult.diagnostics,
|
|
64
42
|
...lintResult.diagnostics,
|
|
65
|
-
...unreachableCodeDiagnostics,
|
|
66
43
|
]
|
|
67
44
|
}
|
|
68
45
|
|
|
@@ -70,12 +47,6 @@ export class Diagnostics {
|
|
|
70
47
|
this.sendDiagnosticsFor(textDocument)
|
|
71
48
|
}
|
|
72
49
|
|
|
73
|
-
private getUnreachableCodeDiagnostics(document: Node): Diagnostic[] {
|
|
74
|
-
const collector = new UnreachableCodeCollector()
|
|
75
|
-
collector.visit(document)
|
|
76
|
-
return collector.diagnostics
|
|
77
|
-
}
|
|
78
|
-
|
|
79
50
|
async refreshDocument(document: TextDocument) {
|
|
80
51
|
await this.validate(document)
|
|
81
52
|
}
|
|
@@ -96,278 +67,3 @@ export class Diagnostics {
|
|
|
96
67
|
this.diagnostics.delete(textDocument)
|
|
97
68
|
}
|
|
98
69
|
}
|
|
99
|
-
|
|
100
|
-
export class UnreachableCodeCollector extends Visitor {
|
|
101
|
-
diagnostics: Diagnostic[] = []
|
|
102
|
-
private processedIfNodes: Set<ERBIfNode> = new Set()
|
|
103
|
-
private processedElseNodes: Set<ERBElseNode> = new Set()
|
|
104
|
-
|
|
105
|
-
visitERBCaseNode(node: ERBCaseNode): void {
|
|
106
|
-
this.checkUnreachableChildren(node.children)
|
|
107
|
-
this.checkAndMarkElseClause(node.else_clause)
|
|
108
|
-
this.visitChildNodes(node)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
visitERBCaseMatchNode(node: ERBCaseMatchNode): void {
|
|
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
|
-
if (!node.then_keyword) {
|
|
189
|
-
this.checkEmptyStatements(node, node.statements, "when")
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
this.visitChildNodes(node)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
visitERBBeginNode(node: ERBBeginNode): void {
|
|
196
|
-
this.checkEmptyStatements(node, node.statements, "begin")
|
|
197
|
-
this.visitChildNodes(node)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
visitERBRescueNode(node: ERBRescueNode): void {
|
|
201
|
-
this.checkEmptyStatements(node, node.statements, "rescue")
|
|
202
|
-
this.visitChildNodes(node)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
visitERBEnsureNode(node: ERBEnsureNode): void {
|
|
206
|
-
this.checkEmptyStatements(node, node.statements, "ensure")
|
|
207
|
-
this.visitChildNodes(node)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
visitERBBlockNode(node: ERBBlockNode): void {
|
|
211
|
-
this.checkEmptyStatements(node, node.body, "block")
|
|
212
|
-
this.visitChildNodes(node)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
visitERBInNode(node: ERBInNode): void {
|
|
216
|
-
if (!node.then_keyword) {
|
|
217
|
-
this.checkEmptyStatements(node, node.statements, "in")
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
this.visitChildNodes(node)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private checkUnreachableChildren(children: Node[]): void {
|
|
224
|
-
for (const child of children) {
|
|
225
|
-
if (isHTMLTextNode(child) && child.content.trim() === "") {
|
|
226
|
-
continue
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
this.addDiagnostic(
|
|
230
|
-
child.location,
|
|
231
|
-
"Unreachable code: content between case and when/in is never executed"
|
|
232
|
-
)
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private checkEmptyStatements(node: Node, statements: Node[], blockType: string): void {
|
|
237
|
-
this.checkEmptyStatementsWithEndLocation(node, statements, blockType, null)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
private checkEmptyStatementsWithEndLocation(node: Node, statements: Node[], blockType: string, subsequentNode: Node | null): void {
|
|
241
|
-
if (this.statementsHaveContent(statements)) {
|
|
242
|
-
return
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const startLocation = node.location.start
|
|
246
|
-
const endLocation = subsequentNode
|
|
247
|
-
? subsequentNode.location.start
|
|
248
|
-
: node.location.end
|
|
249
|
-
|
|
250
|
-
this.addDiagnostic(
|
|
251
|
-
{ start: startLocation, end: endLocation },
|
|
252
|
-
`Empty ${blockType} block: this control flow statement has no content`
|
|
253
|
-
)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
private addDiagnostic(location: { start: { line: number; column: number }, end: { line: number; column: number } }, message: string): void {
|
|
257
|
-
const diagnostic: Diagnostic = {
|
|
258
|
-
range: {
|
|
259
|
-
start: {
|
|
260
|
-
line: this.toZeroBased(location.start.line),
|
|
261
|
-
character: location.start.column
|
|
262
|
-
},
|
|
263
|
-
end: {
|
|
264
|
-
line: this.toZeroBased(location.end.line),
|
|
265
|
-
character: location.end.column
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
message,
|
|
269
|
-
severity: DiagnosticSeverity.Hint,
|
|
270
|
-
tags: [DiagnosticTag.Unnecessary],
|
|
271
|
-
source: "Herb Language Server"
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
this.diagnostics.push(diagnostic)
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
private statementsHaveContent(statements: Node[]): boolean {
|
|
278
|
-
return statements.some(statement => {
|
|
279
|
-
if (isHTMLTextNode(statement)) {
|
|
280
|
-
return statement.content.trim() !== ""
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return true
|
|
284
|
-
})
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private checkAndMarkElseClause(elseClause: ERBElseNode | null): void {
|
|
288
|
-
if (!elseClause) {
|
|
289
|
-
return
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
this.processedElseNodes.add(elseClause)
|
|
293
|
-
if (!this.statementsHaveContent(elseClause.statements)) {
|
|
294
|
-
this.checkEmptyStatements(elseClause, elseClause.statements, "else")
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
private markIfChainAsProcessed(node: ERBIfNode): void {
|
|
299
|
-
this.processedIfNodes.add(node)
|
|
300
|
-
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
301
|
-
if (current.type === 'AST_ERB_IF_NODE') {
|
|
302
|
-
this.processedIfNodes.add(current as ERBIfNode)
|
|
303
|
-
}
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
private markElseNodesInIfChain(node: ERBIfNode): void {
|
|
308
|
-
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
309
|
-
if (current.type === 'AST_ERB_ELSE_NODE') {
|
|
310
|
-
this.processedElseNodes.add(current as ERBElseNode)
|
|
311
|
-
}
|
|
312
|
-
})
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private traverseSubsequentNodes(startNode: Node | null, callback: (node: Node) => void): void {
|
|
316
|
-
let current = startNode
|
|
317
|
-
while (current) {
|
|
318
|
-
callback(current)
|
|
319
|
-
|
|
320
|
-
if ('subsequent' in current) {
|
|
321
|
-
current = (current as any).subsequent
|
|
322
|
-
} else {
|
|
323
|
-
break
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
private checkIfChainParts(node: ERBIfNode): void {
|
|
329
|
-
if (!this.statementsHaveContent(node.statements)) {
|
|
330
|
-
this.checkEmptyStatementsWithEndLocation(node, node.statements, "if", node.subsequent)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
334
|
-
if (!('statements' in current) || !Array.isArray((current as any).statements)) {
|
|
335
|
-
return
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (this.statementsHaveContent((current as any).statements)) {
|
|
339
|
-
return
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const blockType = current.type === 'AST_ERB_IF_NODE' ? 'elsif' : 'else'
|
|
343
|
-
const nextSubsequent = 'subsequent' in current ? (current as any).subsequent : null
|
|
344
|
-
|
|
345
|
-
if (nextSubsequent) {
|
|
346
|
-
this.checkEmptyStatementsWithEndLocation(current, (current as any).statements, blockType, nextSubsequent)
|
|
347
|
-
} else {
|
|
348
|
-
this.checkEmptyStatements(current, (current as any).statements, blockType)
|
|
349
|
-
}
|
|
350
|
-
})
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
private isEntireIfChainEmpty(node: ERBIfNode): boolean {
|
|
354
|
-
if (this.statementsHaveContent(node.statements)) {
|
|
355
|
-
return false
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
let hasContent = false
|
|
359
|
-
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
360
|
-
if ('statements' in current && Array.isArray((current as any).statements)) {
|
|
361
|
-
if (this.statementsHaveContent((current as any).statements)) {
|
|
362
|
-
hasContent = true
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
return !hasContent
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
private toZeroBased(line: number): number {
|
|
371
|
-
return line - 1
|
|
372
|
-
}
|
|
373
|
-
}
|