]>
Commit | Line | Data |
---|---|---|
15c0b25d AP |
1 | package hcl |
2 | ||
3 | import ( | |
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 | ||
15 | type 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. | |
34 | func 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 | ||
43 | func (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 | ||
208 | func (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 |
218 | func (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 | ||
246 | func (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 |
302 | func 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 | } |