diff options
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/command/format/diagnostic.go')
-rw-r--r-- | vendor/github.com/hashicorp/terraform/command/format/diagnostic.go | 295 |
1 files changed, 295 insertions, 0 deletions
diff --git a/vendor/github.com/hashicorp/terraform/command/format/diagnostic.go b/vendor/github.com/hashicorp/terraform/command/format/diagnostic.go new file mode 100644 index 0000000..3dd9238 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/command/format/diagnostic.go | |||
@@ -0,0 +1,295 @@ | |||
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 | } | ||