]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blame - vendor/github.com/hashicorp/hcl2/hcl/diagnostic_text.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / hcl2 / hcl / diagnostic_text.go
CommitLineData
15c0b25d
AP
1package hcl
2
3import (
4 "bufio"
107c1cdb 5 "bytes"
15c0b25d
AP
6 "errors"
7 "fmt"
8 "io"
107c1cdb 9 "sort"
15c0b25d
AP
10
11 wordwrap "github.com/mitchellh/go-wordwrap"
107c1cdb 12 "github.com/zclconf/go-cty/cty"
15c0b25d
AP
13)
14
15type diagnosticTextWriter struct {
16 files map[string]*File
17 wr io.Writer
18 width uint
19 color bool
20}
21
22// NewDiagnosticTextWriter creates a DiagnosticWriter that writes diagnostics
23// to the given writer as formatted text.
24//
25// It is designed to produce text appropriate to print in a monospaced font
26// in a terminal of a particular width, or optionally with no width limit.
27//
28// The given width may be zero to disable word-wrapping of the detail text
29// and truncation of source code snippets.
30//
31// If color is set to true, the output will include VT100 escape sequences to
32// color-code the severity indicators. It is suggested to turn this off if
33// the target writer is not a terminal.
34func NewDiagnosticTextWriter(wr io.Writer, files map[string]*File, width uint, color bool) DiagnosticWriter {
35 return &diagnosticTextWriter{
36 files: files,
37 wr: wr,
38 width: width,
39 color: color,
40 }
41}
42
43func (w *diagnosticTextWriter) WriteDiagnostic(diag *Diagnostic) error {
44 if diag == nil {
45 return errors.New("nil diagnostic")
46 }
47
48 var colorCode, highlightCode, resetCode string
49 if w.color {
50 switch diag.Severity {
51 case DiagError:
52 colorCode = "\x1b[31m"
53 case DiagWarning:
54 colorCode = "\x1b[33m"
55 }
56 resetCode = "\x1b[0m"
57 highlightCode = "\x1b[1;4m"
58 }
59
60 var severityStr string
61 switch diag.Severity {
62 case DiagError:
63 severityStr = "Error"
64 case DiagWarning:
65 severityStr = "Warning"
66 default:
67 // should never happen
68 severityStr = "???????"
69 }
70
71 fmt.Fprintf(w.wr, "%s%s%s: %s\n\n", colorCode, severityStr, resetCode, diag.Summary)
72
73 if diag.Subject != nil {
74 snipRange := *diag.Subject
75 highlightRange := snipRange
76 if diag.Context != nil {
77 // Show enough of the source code to include both the subject
78 // and context ranges, which overlap in all reasonable
79 // situations.
80 snipRange = RangeOver(snipRange, *diag.Context)
81 }
82 // We can't illustrate an empty range, so we'll turn such ranges into
83 // single-character ranges, which might not be totally valid (may point
84 // off the end of a line, or off the end of the file) but are good
85 // enough for the bounds checks we do below.
86 if snipRange.Empty() {
87 snipRange.End.Byte++
88 snipRange.End.Column++
89 }
90 if highlightRange.Empty() {
91 highlightRange.End.Byte++
92 highlightRange.End.Column++
93 }
94
95 file := w.files[diag.Subject.Filename]
96 if file == nil || file.Bytes == nil {
97 fmt.Fprintf(w.wr, " on %s line %d:\n (source code not available)\n\n", diag.Subject.Filename, diag.Subject.Start.Line)
98 } else {
99
100 var contextLine string
101 if diag.Subject != nil {
102 contextLine = contextString(file, diag.Subject.Start.Byte)
103 if contextLine != "" {
104 contextLine = ", in " + contextLine
105 }
106 }
107
108 fmt.Fprintf(w.wr, " on %s line %d%s:\n", diag.Subject.Filename, diag.Subject.Start.Line, contextLine)
109
110 src := file.Bytes
111 sc := NewRangeScanner(src, diag.Subject.Filename, bufio.ScanLines)
112
113 for sc.Scan() {
114 lineRange := sc.Range()
115 if !lineRange.Overlaps(snipRange) {
116 continue
117 }
118
119 beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange)
120 if highlightedRange.Empty() {
121 fmt.Fprintf(w.wr, "%4d: %s\n", lineRange.Start.Line, sc.Bytes())
122 } else {
123 before := beforeRange.SliceBytes(src)
124 highlighted := highlightedRange.SliceBytes(src)
125 after := afterRange.SliceBytes(src)
126 fmt.Fprintf(
127 w.wr, "%4d: %s%s%s%s%s\n",
128 lineRange.Start.Line,
129 before,
130 highlightCode, highlighted, resetCode,
131 after,
132 )
133 }
134
135 }
136
137 w.wr.Write([]byte{'\n'})
138 }
107c1cdb
ND
139
140 if diag.Expression != nil && diag.EvalContext != nil {
141 // We will attempt to render the values for any variables
142 // referenced in the given expression as additional context, for
143 // situations where the same expression is evaluated multiple
144 // times in different scopes.
145 expr := diag.Expression
146 ctx := diag.EvalContext
147
148 vars := expr.Variables()
149 stmts := make([]string, 0, len(vars))
150 seen := make(map[string]struct{}, len(vars))
151 for _, traversal := range vars {
152 val, diags := traversal.TraverseAbs(ctx)
153 if diags.HasErrors() {
154 // Skip anything that generates errors, since we probably
155 // already have the same error in our diagnostics set
156 // already.
157 continue
158 }
159
160 traversalStr := w.traversalStr(traversal)
161 if _, exists := seen[traversalStr]; exists {
162 continue // don't show duplicates when the same variable is referenced multiple times
163 }
164 switch {
165 case !val.IsKnown():
166 // Can't say anything about this yet, then.
167 continue
168 case val.IsNull():
169 stmts = append(stmts, fmt.Sprintf("%s set to null", traversalStr))
170 default:
171 stmts = append(stmts, fmt.Sprintf("%s as %s", traversalStr, w.valueStr(val)))
172 }
173 seen[traversalStr] = struct{}{}
174 }
175
176 sort.Strings(stmts) // FIXME: Should maybe use a traversal-aware sort that can sort numeric indexes properly?
177 last := len(stmts) - 1
178
179 for i, stmt := range stmts {
180 switch i {
181 case 0:
182 w.wr.Write([]byte{'w', 'i', 't', 'h', ' '})
183 default:
184 w.wr.Write([]byte{' ', ' ', ' ', ' ', ' '})
185 }
186 w.wr.Write([]byte(stmt))
187 switch i {
188 case last:
189 w.wr.Write([]byte{'.', '\n', '\n'})
190 default:
191 w.wr.Write([]byte{',', '\n'})
192 }
193 }
194 }
15c0b25d
AP
195 }
196
197 if diag.Detail != "" {
198 detail := diag.Detail
199 if w.width != 0 {
200 detail = wordwrap.WrapString(detail, w.width)
201 }
202 fmt.Fprintf(w.wr, "%s\n\n", detail)
203 }
204
205 return nil
206}
207
208func (w *diagnosticTextWriter) WriteDiagnostics(diags Diagnostics) error {
209 for _, diag := range diags {
210 err := w.WriteDiagnostic(diag)
211 if err != nil {
212 return err
213 }
214 }
215 return nil
216}
217
107c1cdb
ND
218func (w *diagnosticTextWriter) traversalStr(traversal Traversal) string {
219 // This is a specialized subset of traversal rendering tailored to
220 // producing helpful contextual messages in diagnostics. It is not
221 // comprehensive nor intended to be used for other purposes.
222
223 var buf bytes.Buffer
224 for _, step := range traversal {
225 switch tStep := step.(type) {
226 case TraverseRoot:
227 buf.WriteString(tStep.Name)
228 case TraverseAttr:
229 buf.WriteByte('.')
230 buf.WriteString(tStep.Name)
231 case TraverseIndex:
232 buf.WriteByte('[')
233 if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {
234 buf.WriteString(w.valueStr(tStep.Key))
235 } else {
236 // We'll just use a placeholder for more complex values,
237 // since otherwise our result could grow ridiculously long.
238 buf.WriteString("...")
239 }
240 buf.WriteByte(']')
241 }
242 }
243 return buf.String()
244}
245
246func (w *diagnosticTextWriter) valueStr(val cty.Value) string {
247 // This is a specialized subset of value rendering tailored to producing
248 // helpful but concise messages in diagnostics. It is not comprehensive
249 // nor intended to be used for other purposes.
250
251 ty := val.Type()
252 switch {
253 case val.IsNull():
254 return "null"
255 case !val.IsKnown():
256 // Should never happen here because we should filter before we get
257 // in here, but we'll do something reasonable rather than panic.
258 return "(not yet known)"
259 case ty == cty.Bool:
260 if val.True() {
261 return "true"
262 }
263 return "false"
264 case ty == cty.Number:
265 bf := val.AsBigFloat()
266 return bf.Text('g', 10)
267 case ty == cty.String:
268 // Go string syntax is not exactly the same as HCL native string syntax,
269 // but we'll accept the minor edge-cases where this is different here
270 // for now, just to get something reasonable here.
271 return fmt.Sprintf("%q", val.AsString())
272 case ty.IsCollectionType() || ty.IsTupleType():
273 l := val.LengthInt()
274 switch l {
275 case 0:
276 return "empty " + ty.FriendlyName()
277 case 1:
278 return ty.FriendlyName() + " with 1 element"
279 default:
280 return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l)
281 }
282 case ty.IsObjectType():
283 atys := ty.AttributeTypes()
284 l := len(atys)
285 switch l {
286 case 0:
287 return "object with no attributes"
288 case 1:
289 var name string
290 for k := range atys {
291 name = k
292 }
293 return fmt.Sprintf("object with 1 attribute %q", name)
294 default:
295 return fmt.Sprintf("object with %d attributes", l)
296 }
297 default:
298 return ty.FriendlyName()
299 }
300}
301
15c0b25d
AP
302func contextString(file *File, offset int) string {
303 type contextStringer interface {
304 ContextString(offset int) string
305 }
306
307 if cser, ok := file.Nav.(contextStringer); ok {
308 return cser.ContextString(offset)
309 }
310 return ""
311}