Skip to content

Commit 70bc432

Browse files
command/views/json: Never generate invalid diagnostic snippet offsets
Because our snippet generator is trying to select whole lines to include in the snippet, it has some edge cases for odd situations where the relevant source range starts or ends directly at a newline, which were previously causing this logic to return out-of-bounds offsets into the code snippet string. Although arguably it'd be better for the original diagnostics to report more reasonable source ranges, it's better for us to report a slightly-inaccurate snippet than to crash altogether, and so we'll extend our existing range checks to check both bounds of the string and thus avoid downstreams having to deal with out-of-bounds indices. For completeness here I also added some similar logic to the human-oriented diagnostic formatter, which consumes the result of the JSON diagnostic builder. That's not really needed with the additional checks in the JSON diagnostic builder, but it's nice to reinforce that this code can't panic (in this way, at least) even if its input isn't valid.
1 parent c687ebe commit 70bc432

File tree

4 files changed

+101
-3
lines changed

4 files changed

+101
-3
lines changed

internal/command/format/diagnostic.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,21 @@ func appendSourceSnippets(buf *bytes.Buffer, diag *viewsjson.Diagnostic, color *
250250
}
251251
}
252252

253+
// If either start or end is out of range for the code buffer then
254+
// we'll cap them at the bounds just to avoid a panic, although
255+
// this would happen only if there's a bug in the code generating
256+
// the snippet objects.
257+
if start < 0 {
258+
start = 0
259+
} else if start > len(code) {
260+
start = len(code)
261+
}
262+
if end < 0 {
263+
end = 0
264+
} else if end > len(code) {
265+
end = len(code)
266+
}
267+
253268
before, highlight, after := code[0:start], code[start:end], code[end:]
254269
code = fmt.Sprintf(color.Color("%s[underline]%s[reset]%s"), before, highlight, after)
255270

internal/command/views/json/diagnostic.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,23 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost
221221
// to the code snippet string.
222222
start := highlightRange.Start.Byte - codeStartByte
223223
end := start + (highlightRange.End.Byte - highlightRange.Start.Byte)
224-
if start > len(codeStr) {
224+
225+
// We can end up with some quirky results here in edge cases like
226+
// when a source range starts or ends at a newline character,
227+
// so we'll cap the results at the bounds of the highlight range
228+
// so that consumers of this data don't need to contend with
229+
// out-of-bounds errors themselves.
230+
if start < 0 {
231+
start = 0
232+
} else if start > len(codeStr) {
225233
start = len(codeStr)
226234
}
227-
if end > len(codeStr) {
235+
if end < 0 {
236+
end = 0
237+
} else if end > len(codeStr) {
228238
end = len(codeStr)
229239
}
240+
230241
diagnostic.Snippet.HighlightStartOffset = start
231242
diagnostic.Snippet.HighlightEndOffset = end
232243

internal/command/views/json/diagnostic_test.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ func TestNewDiagnostic(t *testing.T) {
2929
}
3030
}
3131
`),
32-
"short.tf": []byte("bad source code"),
32+
"short.tf": []byte("bad source code"),
33+
"odd-comment.tf": []byte("foo\n\n#\n"),
3334
"values.tf": []byte(`[
3435
var.a,
3536
var.b,
@@ -285,6 +286,51 @@ func TestNewDiagnostic(t *testing.T) {
285286
},
286287
},
287288
},
289+
"error whose range starts at a newline": {
290+
&hcl.Diagnostic{
291+
Severity: hcl.DiagError,
292+
Summary: "Invalid newline",
293+
Detail: "How awkward!",
294+
Subject: &hcl.Range{
295+
Filename: "odd-comment.tf",
296+
Start: hcl.Pos{Line: 2, Column: 5, Byte: 4},
297+
End: hcl.Pos{Line: 3, Column: 1, Byte: 6},
298+
},
299+
},
300+
&Diagnostic{
301+
Severity: "error",
302+
Summary: "Invalid newline",
303+
Detail: "How awkward!",
304+
Range: &DiagnosticRange{
305+
Filename: "odd-comment.tf",
306+
Start: Pos{
307+
Line: 2,
308+
Column: 5,
309+
Byte: 4,
310+
},
311+
End: Pos{
312+
Line: 3,
313+
Column: 1,
314+
Byte: 6,
315+
},
316+
},
317+
Snippet: &DiagnosticSnippet{
318+
Code: `#`,
319+
StartLine: 2,
320+
Values: []DiagnosticExpressionValue{},
321+
322+
// Due to the range starting at a newline on a blank
323+
// line, we end up stripping off the initial newline
324+
// to produce only a one-line snippet. That would
325+
// therefore cause the start offset to naturally be
326+
// -1, just before the Code we returned, but then we
327+
// force it to zero so that the result will still be
328+
// in range for a byte-oriented slice of Code.
329+
HighlightStartOffset: 0,
330+
HighlightEndOffset: 1,
331+
},
332+
},
333+
},
288334
"error with source code subject and known expression": {
289335
&hcl.Diagnostic{
290336
Severity: hcl.DiagError,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"severity": "error",
3+
"summary": "Invalid newline",
4+
"detail": "How awkward!",
5+
"range": {
6+
"filename": "odd-comment.tf",
7+
"start": {
8+
"line": 2,
9+
"column": 5,
10+
"byte": 4
11+
},
12+
"end": {
13+
"line": 3,
14+
"column": 1,
15+
"byte": 6
16+
}
17+
},
18+
"snippet": {
19+
"context": null,
20+
"code": "#",
21+
"start_line": 2,
22+
"highlight_start_offset": 0,
23+
"highlight_end_offset": 1,
24+
"values": []
25+
}
26+
}

0 commit comments

Comments
 (0)