]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/terraform/command/format/diagnostic.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / command / format / diagnostic.go
1 package format
2
3 import (
4 "bufio"
5 "bytes"
6 "fmt"
7 "sort"
8 "strings"
9
10 "github.com/hashicorp/hcl2/hcl"
11 "github.com/hashicorp/hcl2/hcled"
12 "github.com/hashicorp/hcl2/hclparse"
13 "github.com/hashicorp/terraform/tfdiags"
14 "github.com/mitchellh/colorstring"
15 wordwrap "github.com/mitchellh/go-wordwrap"
16 "github.com/zclconf/go-cty/cty"
17 )
18
19 // Diagnostic formats a single diagnostic message.
20 //
21 // The width argument specifies at what column the diagnostic messages will
22 // be wrapped. If set to zero, messages will not be wrapped by this function
23 // at all. Although the long-form text parts of the message are wrapped,
24 // not all aspects of the message are guaranteed to fit within the specified
25 // terminal width.
26 func Diagnostic(diag tfdiags.Diagnostic, sources map[string][]byte, color *colorstring.Colorize, width int) string {
27 if diag == nil {
28 // No good reason to pass a nil diagnostic in here...
29 return ""
30 }
31
32 var buf bytes.Buffer
33
34 switch diag.Severity() {
35 case tfdiags.Error:
36 buf.WriteString(color.Color("\n[bold][red]Error: [reset]"))
37 case tfdiags.Warning:
38 buf.WriteString(color.Color("\n[bold][yellow]Warning: [reset]"))
39 default:
40 // Clear out any coloring that might be applied by Terraform's UI helper,
41 // so our result is not context-sensitive.
42 buf.WriteString(color.Color("\n[reset]"))
43 }
44
45 desc := diag.Description()
46 sourceRefs := diag.Source()
47
48 // We don't wrap the summary, since we expect it to be terse, and since
49 // this is where we put the text of a native Go error it may not always
50 // be pure text that lends itself well to word-wrapping.
51 fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), desc.Summary)
52
53 if sourceRefs.Subject != nil {
54 // We'll borrow HCL's range implementation here, because it has some
55 // handy features to help us produce a nice source code snippet.
56 highlightRange := sourceRefs.Subject.ToHCL()
57 snippetRange := highlightRange
58 if sourceRefs.Context != nil {
59 snippetRange = sourceRefs.Context.ToHCL()
60 }
61
62 // Make sure the snippet includes the highlight. This should be true
63 // for any reasonable diagnostic, but we'll make sure.
64 snippetRange = hcl.RangeOver(snippetRange, highlightRange)
65 if snippetRange.Empty() {
66 snippetRange.End.Byte++
67 snippetRange.End.Column++
68 }
69 if highlightRange.Empty() {
70 highlightRange.End.Byte++
71 highlightRange.End.Column++
72 }
73
74 var src []byte
75 if sources != nil {
76 src = sources[snippetRange.Filename]
77 }
78 if src == nil {
79 // This should generally not happen, as long as sources are always
80 // loaded through the main loader. We may load things in other
81 // ways in weird cases, so we'll tolerate it at the expense of
82 // a not-so-helpful error message.
83 fmt.Fprintf(&buf, " on %s line %d:\n (source code not available)\n", highlightRange.Filename, highlightRange.Start.Line)
84 } else {
85 file, offset := parseRange(src, highlightRange)
86
87 headerRange := highlightRange
88
89 contextStr := hcled.ContextString(file, offset-1)
90 if contextStr != "" {
91 contextStr = ", in " + contextStr
92 }
93
94 fmt.Fprintf(&buf, " on %s line %d%s:\n", headerRange.Filename, headerRange.Start.Line, contextStr)
95
96 // Config snippet rendering
97 sc := hcl.NewRangeScanner(src, highlightRange.Filename, bufio.ScanLines)
98 for sc.Scan() {
99 lineRange := sc.Range()
100 if !lineRange.Overlaps(snippetRange) {
101 continue
102 }
103 beforeRange, highlightedRange, afterRange := lineRange.PartitionAround(highlightRange)
104 before := beforeRange.SliceBytes(src)
105 highlighted := highlightedRange.SliceBytes(src)
106 after := afterRange.SliceBytes(src)
107 fmt.Fprintf(
108 &buf, color.Color("%4d: %s[underline]%s[reset]%s\n"),
109 lineRange.Start.Line,
110 before, highlighted, after,
111 )
112 }
113
114 }
115
116 if fromExpr := diag.FromExpr(); fromExpr != nil {
117 // We may also be able to generate information about the dynamic
118 // values of relevant variables at the point of evaluation, then.
119 // This is particularly useful for expressions that get evaluated
120 // multiple times with different values, such as blocks using
121 // "count" and "for_each", or within "for" expressions.
122 expr := fromExpr.Expression
123 ctx := fromExpr.EvalContext
124 vars := expr.Variables()
125 stmts := make([]string, 0, len(vars))
126 seen := make(map[string]struct{}, len(vars))
127 Traversals:
128 for _, traversal := range vars {
129 for len(traversal) > 1 {
130 val, diags := traversal.TraverseAbs(ctx)
131 if diags.HasErrors() {
132 // Skip anything that generates errors, since we probably
133 // already have the same error in our diagnostics set
134 // already.
135 traversal = traversal[:len(traversal)-1]
136 continue
137 }
138
139 traversalStr := traversalStr(traversal)
140 if _, exists := seen[traversalStr]; exists {
141 continue Traversals // don't show duplicates when the same variable is referenced multiple times
142 }
143 switch {
144 case !val.IsKnown():
145 // Can't say anything about this yet, then.
146 continue Traversals
147 case val.IsNull():
148 stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is null"), traversalStr))
149 default:
150 stmts = append(stmts, fmt.Sprintf(color.Color("[bold]%s[reset] is %s"), traversalStr, compactValueStr(val)))
151 }
152 seen[traversalStr] = struct{}{}
153 }
154 }
155
156 sort.Strings(stmts) // FIXME: Should maybe use a traversal-aware sort that can sort numeric indexes properly?
157
158 if len(stmts) > 0 {
159 fmt.Fprint(&buf, color.Color(" [dark_gray]|----------------[reset]\n"))
160 }
161 for _, stmt := range stmts {
162 fmt.Fprintf(&buf, color.Color(" [dark_gray]|[reset] %s\n"), stmt)
163 }
164 }
165
166 buf.WriteByte('\n')
167 }
168
169 if desc.Detail != "" {
170 detail := desc.Detail
171 if width != 0 {
172 detail = wordwrap.WrapString(detail, uint(width))
173 }
174 fmt.Fprintf(&buf, "%s\n", detail)
175 }
176
177 return buf.String()
178 }
179
180 func parseRange(src []byte, rng hcl.Range) (*hcl.File, int) {
181 filename := rng.Filename
182 offset := rng.Start.Byte
183
184 // We need to re-parse here to get a *hcl.File we can interrogate. This
185 // is not awesome since we presumably already parsed the file earlier too,
186 // but this re-parsing is architecturally simpler than retaining all of
187 // the hcl.File objects and we only do this in the case of an error anyway
188 // so the overhead here is not a big problem.
189 parser := hclparse.NewParser()
190 var file *hcl.File
191 var diags hcl.Diagnostics
192 if strings.HasSuffix(filename, ".json") {
193 file, diags = parser.ParseJSON(src, filename)
194 } else {
195 file, diags = parser.ParseHCL(src, filename)
196 }
197 if diags.HasErrors() {
198 return file, offset
199 }
200
201 return file, offset
202 }
203
204 // traversalStr produces a representation of an HCL traversal that is compact,
205 // resembles HCL native syntax, and is suitable for display in the UI.
206 func traversalStr(traversal hcl.Traversal) string {
207 // This is a specialized subset of traversal rendering tailored to
208 // producing helpful contextual messages in diagnostics. It is not
209 // comprehensive nor intended to be used for other purposes.
210
211 var buf bytes.Buffer
212 for _, step := range traversal {
213 switch tStep := step.(type) {
214 case hcl.TraverseRoot:
215 buf.WriteString(tStep.Name)
216 case hcl.TraverseAttr:
217 buf.WriteByte('.')
218 buf.WriteString(tStep.Name)
219 case hcl.TraverseIndex:
220 buf.WriteByte('[')
221 if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {
222 buf.WriteString(compactValueStr(tStep.Key))
223 } else {
224 // We'll just use a placeholder for more complex values,
225 // since otherwise our result could grow ridiculously long.
226 buf.WriteString("...")
227 }
228 buf.WriteByte(']')
229 }
230 }
231 return buf.String()
232 }
233
234 // compactValueStr produces a compact, single-line summary of a given value
235 // that is suitable for display in the UI.
236 //
237 // For primitives it returns a full representation, while for more complex
238 // types it instead summarizes the type, size, etc to produce something
239 // that is hopefully still somewhat useful but not as verbose as a rendering
240 // of the entire data structure.
241 func compactValueStr(val cty.Value) string {
242 // This is a specialized subset of value rendering tailored to producing
243 // helpful but concise messages in diagnostics. It is not comprehensive
244 // nor intended to be used for other purposes.
245
246 ty := val.Type()
247 switch {
248 case val.IsNull():
249 return "null"
250 case !val.IsKnown():
251 // Should never happen here because we should filter before we get
252 // in here, but we'll do something reasonable rather than panic.
253 return "(not yet known)"
254 case ty == cty.Bool:
255 if val.True() {
256 return "true"
257 }
258 return "false"
259 case ty == cty.Number:
260 bf := val.AsBigFloat()
261 return bf.Text('g', 10)
262 case ty == cty.String:
263 // Go string syntax is not exactly the same as HCL native string syntax,
264 // but we'll accept the minor edge-cases where this is different here
265 // for now, just to get something reasonable here.
266 return fmt.Sprintf("%q", val.AsString())
267 case ty.IsCollectionType() || ty.IsTupleType():
268 l := val.LengthInt()
269 switch l {
270 case 0:
271 return "empty " + ty.FriendlyName()
272 case 1:
273 return ty.FriendlyName() + " with 1 element"
274 default:
275 return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l)
276 }
277 case ty.IsObjectType():
278 atys := ty.AttributeTypes()
279 l := len(atys)
280 switch l {
281 case 0:
282 return "object with no attributes"
283 case 1:
284 var name string
285 for k := range atys {
286 name = k
287 }
288 return fmt.Sprintf("object with 1 attribute %q", name)
289 default:
290 return fmt.Sprintf("object with %d attributes", l)
291 }
292 default:
293 return ty.FriendlyName()
294 }
295 }