Skip to content

Commit 6e41686

Browse files
authored
Reflect 0.15 changes in diagnostics output (#29)
* Reflect 0.15 changes in diagnostics output * Reflect format versioning * validate: Add test
1 parent 10a0ad8 commit 6e41686

File tree

2 files changed

+223
-4
lines changed

2 files changed

+223
-4
lines changed

validate.go

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package tfjson
22

3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
)
8+
39
// Pos represents a position in a config file
410
type Pos struct {
511
Line int `json:"line"`
@@ -14,20 +20,115 @@ type Range struct {
1420
End Pos `json:"end"`
1521
}
1622

23+
type DiagnosticSeverity string
24+
25+
// These severities map to the tfdiags.Severity values, plus an explicit
26+
// unknown in case that enum grows without us noticing here.
27+
const (
28+
DiagnosticSeverityUnknown DiagnosticSeverity = "unknown"
29+
DiagnosticSeverityError DiagnosticSeverity = "error"
30+
DiagnosticSeverityWarning DiagnosticSeverity = "warning"
31+
)
32+
1733
// Diagnostic represents information to be presented to a user about an
1834
// error or anomaly in parsing or evaluating configuration
1935
type Diagnostic struct {
20-
Severity string `json:"severity,omitempty"`
21-
Summary string `json:"summary,omitempty"`
22-
Detail string `json:"detail,omitempty"`
23-
Range *Range `json:"range,omitempty"`
36+
Severity DiagnosticSeverity `json:"severity,omitempty"`
37+
38+
Summary string `json:"summary,omitempty"`
39+
Detail string `json:"detail,omitempty"`
40+
Range *Range `json:"range,omitempty"`
41+
42+
Snippet *DiagnosticSnippet `json:"snippet,omitempty"`
43+
}
44+
45+
// DiagnosticSnippet represents source code information about the diagnostic.
46+
// It is possible for a diagnostic to have a source (and therefore a range) but
47+
// no source code can be found. In this case, the range field will be present and
48+
// the snippet field will not.
49+
type DiagnosticSnippet struct {
50+
// Context is derived from HCL's hcled.ContextString output. This gives a
51+
// high-level summary of the root context of the diagnostic: for example,
52+
// the resource block in which an expression causes an error.
53+
Context *string `json:"context"`
54+
55+
// Code is a possibly-multi-line string of Terraform configuration, which
56+
// includes both the diagnostic source and any relevant context as defined
57+
// by the diagnostic.
58+
Code string `json:"code"`
59+
60+
// StartLine is the line number in the source file for the first line of
61+
// the snippet code block. This is not necessarily the same as the value of
62+
// Range.Start.Line, as it is possible to have zero or more lines of
63+
// context source code before the diagnostic range starts.
64+
StartLine int `json:"start_line"`
65+
66+
// HighlightStartOffset is the character offset into Code at which the
67+
// diagnostic source range starts, which ought to be highlighted as such by
68+
// the consumer of this data.
69+
HighlightStartOffset int `json:"highlight_start_offset"`
70+
71+
// HighlightEndOffset is the character offset into Code at which the
72+
// diagnostic source range ends.
73+
HighlightEndOffset int `json:"highlight_end_offset"`
74+
75+
// Values is a sorted slice of expression values which may be useful in
76+
// understanding the source of an error in a complex expression.
77+
Values []DiagnosticExpressionValue `json:"values"`
78+
}
79+
80+
// DiagnosticExpressionValue represents an HCL traversal string (e.g.
81+
// "var.foo") and a statement about its value while the expression was
82+
// evaluated (e.g. "is a string", "will be known only after apply"). These are
83+
// intended to help the consumer diagnose why an expression caused a diagnostic
84+
// to be emitted.
85+
type DiagnosticExpressionValue struct {
86+
Traversal string `json:"traversal"`
87+
Statement string `json:"statement"`
2488
}
2589

2690
// ValidateOutput represents JSON output from terraform validate
2791
// (available from 0.12 onwards)
2892
type ValidateOutput struct {
93+
FormatVersion string `json:"format_version"`
94+
2995
Valid bool `json:"valid"`
3096
ErrorCount int `json:"error_count"`
3197
WarningCount int `json:"warning_count"`
3298
Diagnostics []Diagnostic `json:"diagnostics"`
3399
}
100+
101+
// Validate checks to ensure that data is present, and the
102+
// version matches the version supported by this library.
103+
func (vo *ValidateOutput) Validate() error {
104+
if vo == nil {
105+
return errors.New("validation output is nil")
106+
}
107+
108+
if vo.FormatVersion == "" {
109+
// The format was not versioned in the past
110+
return nil
111+
}
112+
113+
supportedVersion := "0.1"
114+
if vo.FormatVersion != supportedVersion {
115+
return fmt.Errorf("unsupported validation output format version: expected %q, got %q",
116+
supportedVersion, vo.FormatVersion)
117+
}
118+
119+
return nil
120+
}
121+
122+
func (vo *ValidateOutput) UnmarshalJSON(b []byte) error {
123+
type rawOutput ValidateOutput
124+
var schemas rawOutput
125+
126+
err := json.Unmarshal(b, &schemas)
127+
if err != nil {
128+
return err
129+
}
130+
131+
*vo = *(*ValidateOutput)(&schemas)
132+
133+
return vo.Validate()
134+
}

validate_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,121 @@ func TestValidateOutput_basic(t *testing.T) {
107107
t.Fatalf("output mismatch: %s", diff)
108108
}
109109
}
110+
111+
func TestValidateOutput_versioned(t *testing.T) {
112+
errOutput := `{
113+
"format_version": "0.1",
114+
"valid": false,
115+
"error_count": 1,
116+
"warning_count": 1,
117+
"diagnostics": [
118+
{
119+
"severity": "warning",
120+
"summary": "Deprecated Attribute",
121+
"detail": "Deprecated in favor of project_id",
122+
"range": {
123+
"filename": "main.tf",
124+
"start": {
125+
"line": 21,
126+
"column": 25,
127+
"byte": 408
128+
},
129+
"end": {
130+
"line": 21,
131+
"column": 42,
132+
"byte": 425
133+
}
134+
},
135+
"snippet": {
136+
"context": "resource \"google_project_access_approval_settings\" \"project_access_approval\"",
137+
"code": " project = \"my-project-name\"",
138+
"start_line": 21,
139+
"highlight_start_offset": 24,
140+
"highlight_end_offset": 41,
141+
"values": []
142+
}
143+
},
144+
{
145+
"severity": "error",
146+
"summary": "Missing required argument",
147+
"detail": "The argument \"enrolled_services\" is required, but no definition was found.",
148+
"range": {
149+
"filename": "main.tf",
150+
"start": {
151+
"line": 19,
152+
"column": 78,
153+
"byte": 340
154+
},
155+
"end": {
156+
"line": 19,
157+
"column": 79,
158+
"byte": 341
159+
}
160+
},
161+
"snippet": {
162+
"context": "resource \"google_project_access_approval_settings\" \"project_access_approval\"",
163+
"code": "resource \"google_project_access_approval_settings\" \"project_access_approval\" {",
164+
"start_line": 19,
165+
"highlight_start_offset": 77,
166+
"highlight_end_offset": 78,
167+
"values": []
168+
}
169+
}
170+
]
171+
}`
172+
var parsed ValidateOutput
173+
if err := json.Unmarshal([]byte(errOutput), &parsed); err != nil {
174+
t.Fatal(err)
175+
}
176+
177+
expected := &ValidateOutput{
178+
FormatVersion: "0.1",
179+
ErrorCount: 1,
180+
WarningCount: 1,
181+
Diagnostics: []Diagnostic{
182+
{
183+
Severity: "warning",
184+
Summary: "Deprecated Attribute",
185+
Detail: "Deprecated in favor of project_id",
186+
Range: &Range{
187+
Filename: "main.tf",
188+
Start: Pos{Line: 21, Column: 25, Byte: 408},
189+
End: Pos{Line: 21, Column: 42, Byte: 425},
190+
},
191+
Snippet: &DiagnosticSnippet{
192+
Context: ptrToString(`resource "google_project_access_approval_settings" "project_access_approval"`),
193+
Code: ` project = "my-project-name"`,
194+
StartLine: 21,
195+
HighlightStartOffset: 24,
196+
HighlightEndOffset: 41,
197+
Values: []DiagnosticExpressionValue{},
198+
},
199+
},
200+
{
201+
Severity: "error",
202+
Summary: "Missing required argument",
203+
Detail: `The argument "enrolled_services" is required, but no definition was found.`,
204+
Range: &Range{
205+
Filename: "main.tf",
206+
Start: Pos{Line: 19, Column: 78, Byte: 340},
207+
End: Pos{Line: 19, Column: 79, Byte: 341},
208+
},
209+
Snippet: &DiagnosticSnippet{
210+
Context: ptrToString(`resource "google_project_access_approval_settings" "project_access_approval"`),
211+
Code: `resource "google_project_access_approval_settings" "project_access_approval" {`,
212+
StartLine: 19,
213+
HighlightStartOffset: 77,
214+
HighlightEndOffset: 78,
215+
Values: []DiagnosticExpressionValue{},
216+
},
217+
},
218+
},
219+
}
220+
if diff := cmp.Diff(expected, &parsed); diff != "" {
221+
t.Fatalf("output mismatch: %s", diff)
222+
}
223+
}
224+
225+
func ptrToString(val string) *string {
226+
return &val
227+
}

0 commit comments

Comments
 (0)