]>
Commit | Line | Data |
---|---|---|
15c0b25d AP |
1 | package json |
2 | ||
3 | import ( | |
4 | "encoding/json" | |
5 | "fmt" | |
15c0b25d AP |
6 | |
7 | "github.com/hashicorp/hcl2/hcl" | |
107c1cdb | 8 | "github.com/zclconf/go-cty/cty" |
15c0b25d AP |
9 | ) |
10 | ||
11 | func parseFileContent(buf []byte, filename string) (node, hcl.Diagnostics) { | |
12 | tokens := scan(buf, pos{ | |
13 | Filename: filename, | |
14 | Pos: hcl.Pos{ | |
15 | Byte: 0, | |
16 | Line: 1, | |
17 | Column: 1, | |
18 | }, | |
19 | }) | |
20 | p := newPeeker(tokens) | |
21 | node, diags := parseValue(p) | |
22 | if len(diags) == 0 && p.Peek().Type != tokenEOF { | |
23 | diags = diags.Append(&hcl.Diagnostic{ | |
24 | Severity: hcl.DiagError, | |
25 | Summary: "Extraneous data after value", | |
26 | Detail: "Extra characters appear after the JSON value.", | |
27 | Subject: p.Peek().Range.Ptr(), | |
28 | }) | |
29 | } | |
30 | return node, diags | |
31 | } | |
32 | ||
33 | func parseValue(p *peeker) (node, hcl.Diagnostics) { | |
34 | tok := p.Peek() | |
35 | ||
36 | wrapInvalid := func(n node, diags hcl.Diagnostics) (node, hcl.Diagnostics) { | |
37 | if n != nil { | |
38 | return n, diags | |
39 | } | |
40 | return invalidVal{tok.Range}, diags | |
41 | } | |
42 | ||
43 | switch tok.Type { | |
44 | case tokenBraceO: | |
45 | return wrapInvalid(parseObject(p)) | |
46 | case tokenBrackO: | |
47 | return wrapInvalid(parseArray(p)) | |
48 | case tokenNumber: | |
49 | return wrapInvalid(parseNumber(p)) | |
50 | case tokenString: | |
51 | return wrapInvalid(parseString(p)) | |
52 | case tokenKeyword: | |
53 | return wrapInvalid(parseKeyword(p)) | |
54 | case tokenBraceC: | |
55 | return wrapInvalid(nil, hcl.Diagnostics{ | |
56 | { | |
57 | Severity: hcl.DiagError, | |
107c1cdb | 58 | Summary: "Missing JSON value", |
15c0b25d AP |
59 | Detail: "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.", |
60 | Subject: &tok.Range, | |
61 | }, | |
62 | }) | |
63 | case tokenBrackC: | |
64 | return wrapInvalid(nil, hcl.Diagnostics{ | |
65 | { | |
66 | Severity: hcl.DiagError, | |
67 | Summary: "Missing array element value", | |
68 | Detail: "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.", | |
69 | Subject: &tok.Range, | |
70 | }, | |
71 | }) | |
72 | case tokenEOF: | |
73 | return wrapInvalid(nil, hcl.Diagnostics{ | |
74 | { | |
75 | Severity: hcl.DiagError, | |
76 | Summary: "Missing value", | |
77 | Detail: "The JSON data ends prematurely.", | |
78 | Subject: &tok.Range, | |
79 | }, | |
80 | }) | |
81 | default: | |
82 | return wrapInvalid(nil, hcl.Diagnostics{ | |
83 | { | |
84 | Severity: hcl.DiagError, | |
85 | Summary: "Invalid start of value", | |
86 | Detail: "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.", | |
87 | Subject: &tok.Range, | |
88 | }, | |
89 | }) | |
90 | } | |
91 | } | |
92 | ||
93 | func tokenCanStartValue(tok token) bool { | |
94 | switch tok.Type { | |
95 | case tokenBraceO, tokenBrackO, tokenNumber, tokenString, tokenKeyword: | |
96 | return true | |
97 | default: | |
98 | return false | |
99 | } | |
100 | } | |
101 | ||
102 | func parseObject(p *peeker) (node, hcl.Diagnostics) { | |
103 | var diags hcl.Diagnostics | |
104 | ||
105 | open := p.Read() | |
106 | attrs := []*objectAttr{} | |
107 | ||
108 | // recover is used to shift the peeker to what seems to be the end of | |
109 | // our object, so that when we encounter an error we leave the peeker | |
110 | // at a reasonable point in the token stream to continue parsing. | |
111 | recover := func(tok token) { | |
112 | open := 1 | |
113 | for { | |
114 | switch tok.Type { | |
115 | case tokenBraceO: | |
116 | open++ | |
117 | case tokenBraceC: | |
118 | open-- | |
119 | if open <= 1 { | |
120 | return | |
121 | } | |
122 | case tokenEOF: | |
123 | // Ran out of source before we were able to recover, | |
124 | // so we'll bail here and let the caller deal with it. | |
125 | return | |
126 | } | |
127 | tok = p.Read() | |
128 | } | |
129 | } | |
130 | ||
131 | Token: | |
132 | for { | |
133 | if p.Peek().Type == tokenBraceC { | |
134 | break Token | |
135 | } | |
136 | ||
137 | keyNode, keyDiags := parseValue(p) | |
138 | diags = diags.Extend(keyDiags) | |
139 | if keyNode == nil { | |
140 | return nil, diags | |
141 | } | |
142 | ||
143 | keyStrNode, ok := keyNode.(*stringVal) | |
144 | if !ok { | |
145 | return nil, diags.Append(&hcl.Diagnostic{ | |
146 | Severity: hcl.DiagError, | |
107c1cdb ND |
147 | Summary: "Invalid object property name", |
148 | Detail: "A JSON object property name must be a string", | |
15c0b25d AP |
149 | Subject: keyNode.StartRange().Ptr(), |
150 | }) | |
151 | } | |
152 | ||
153 | key := keyStrNode.Value | |
154 | ||
155 | colon := p.Read() | |
156 | if colon.Type != tokenColon { | |
157 | recover(colon) | |
158 | ||
159 | if colon.Type == tokenBraceC || colon.Type == tokenComma { | |
160 | // Catch common mistake of using braces instead of brackets | |
161 | // for an object. | |
162 | return nil, diags.Append(&hcl.Diagnostic{ | |
163 | Severity: hcl.DiagError, | |
164 | Summary: "Missing object value", | |
165 | Detail: "A JSON object attribute must have a value, introduced by a colon.", | |
166 | Subject: &colon.Range, | |
167 | }) | |
168 | } | |
169 | ||
170 | if colon.Type == tokenEquals { | |
171 | // Possible confusion with native HCL syntax. | |
172 | return nil, diags.Append(&hcl.Diagnostic{ | |
173 | Severity: hcl.DiagError, | |
107c1cdb | 174 | Summary: "Missing property value colon", |
15c0b25d AP |
175 | Detail: "JSON uses a colon as its name/value delimiter, not an equals sign.", |
176 | Subject: &colon.Range, | |
177 | }) | |
178 | } | |
179 | ||
180 | return nil, diags.Append(&hcl.Diagnostic{ | |
181 | Severity: hcl.DiagError, | |
107c1cdb ND |
182 | Summary: "Missing property value colon", |
183 | Detail: "A colon must appear between an object property's name and its value.", | |
15c0b25d AP |
184 | Subject: &colon.Range, |
185 | }) | |
186 | } | |
187 | ||
188 | valNode, valDiags := parseValue(p) | |
189 | diags = diags.Extend(valDiags) | |
190 | if valNode == nil { | |
191 | return nil, diags | |
192 | } | |
193 | ||
194 | attrs = append(attrs, &objectAttr{ | |
195 | Name: key, | |
196 | Value: valNode, | |
197 | NameRange: keyStrNode.SrcRange, | |
198 | }) | |
199 | ||
200 | switch p.Peek().Type { | |
201 | case tokenComma: | |
202 | comma := p.Read() | |
203 | if p.Peek().Type == tokenBraceC { | |
204 | // Special error message for this common mistake | |
205 | return nil, diags.Append(&hcl.Diagnostic{ | |
206 | Severity: hcl.DiagError, | |
207 | Summary: "Trailing comma in object", | |
107c1cdb | 208 | Detail: "JSON does not permit a trailing comma after the final property in an object.", |
15c0b25d AP |
209 | Subject: &comma.Range, |
210 | }) | |
211 | } | |
212 | continue Token | |
213 | case tokenEOF: | |
214 | return nil, diags.Append(&hcl.Diagnostic{ | |
215 | Severity: hcl.DiagError, | |
216 | Summary: "Unclosed object", | |
217 | Detail: "No closing brace was found for this JSON object.", | |
218 | Subject: &open.Range, | |
219 | }) | |
220 | case tokenBrackC: | |
221 | // Consume the bracket anyway, so that we don't return with the peeker | |
222 | // at a strange place. | |
223 | p.Read() | |
224 | return nil, diags.Append(&hcl.Diagnostic{ | |
225 | Severity: hcl.DiagError, | |
226 | Summary: "Mismatched braces", | |
227 | Detail: "A JSON object must be closed with a brace, not a bracket.", | |
228 | Subject: p.Peek().Range.Ptr(), | |
229 | }) | |
230 | case tokenBraceC: | |
231 | break Token | |
232 | default: | |
233 | recover(p.Read()) | |
234 | return nil, diags.Append(&hcl.Diagnostic{ | |
235 | Severity: hcl.DiagError, | |
236 | Summary: "Missing attribute seperator comma", | |
107c1cdb | 237 | Detail: "A comma must appear between each property definition in an object.", |
15c0b25d AP |
238 | Subject: p.Peek().Range.Ptr(), |
239 | }) | |
240 | } | |
241 | ||
242 | } | |
243 | ||
244 | close := p.Read() | |
245 | return &objectVal{ | |
246 | Attrs: attrs, | |
247 | SrcRange: hcl.RangeBetween(open.Range, close.Range), | |
248 | OpenRange: open.Range, | |
249 | CloseRange: close.Range, | |
250 | }, diags | |
251 | } | |
252 | ||
253 | func parseArray(p *peeker) (node, hcl.Diagnostics) { | |
254 | var diags hcl.Diagnostics | |
255 | ||
256 | open := p.Read() | |
257 | vals := []node{} | |
258 | ||
259 | // recover is used to shift the peeker to what seems to be the end of | |
260 | // our array, so that when we encounter an error we leave the peeker | |
261 | // at a reasonable point in the token stream to continue parsing. | |
262 | recover := func(tok token) { | |
263 | open := 1 | |
264 | for { | |
265 | switch tok.Type { | |
266 | case tokenBrackO: | |
267 | open++ | |
268 | case tokenBrackC: | |
269 | open-- | |
270 | if open <= 1 { | |
271 | return | |
272 | } | |
273 | case tokenEOF: | |
274 | // Ran out of source before we were able to recover, | |
275 | // so we'll bail here and let the caller deal with it. | |
276 | return | |
277 | } | |
278 | tok = p.Read() | |
279 | } | |
280 | } | |
281 | ||
282 | Token: | |
283 | for { | |
284 | if p.Peek().Type == tokenBrackC { | |
285 | break Token | |
286 | } | |
287 | ||
288 | valNode, valDiags := parseValue(p) | |
289 | diags = diags.Extend(valDiags) | |
290 | if valNode == nil { | |
291 | return nil, diags | |
292 | } | |
293 | ||
294 | vals = append(vals, valNode) | |
295 | ||
296 | switch p.Peek().Type { | |
297 | case tokenComma: | |
298 | comma := p.Read() | |
299 | if p.Peek().Type == tokenBrackC { | |
300 | // Special error message for this common mistake | |
301 | return nil, diags.Append(&hcl.Diagnostic{ | |
302 | Severity: hcl.DiagError, | |
303 | Summary: "Trailing comma in array", | |
107c1cdb | 304 | Detail: "JSON does not permit a trailing comma after the final value in an array.", |
15c0b25d AP |
305 | Subject: &comma.Range, |
306 | }) | |
307 | } | |
308 | continue Token | |
309 | case tokenColon: | |
310 | recover(p.Read()) | |
311 | return nil, diags.Append(&hcl.Diagnostic{ | |
312 | Severity: hcl.DiagError, | |
313 | Summary: "Invalid array value", | |
314 | Detail: "A colon is not used to introduce values in a JSON array.", | |
315 | Subject: p.Peek().Range.Ptr(), | |
316 | }) | |
317 | case tokenEOF: | |
318 | recover(p.Read()) | |
319 | return nil, diags.Append(&hcl.Diagnostic{ | |
320 | Severity: hcl.DiagError, | |
321 | Summary: "Unclosed object", | |
322 | Detail: "No closing bracket was found for this JSON array.", | |
323 | Subject: &open.Range, | |
324 | }) | |
325 | case tokenBraceC: | |
326 | recover(p.Read()) | |
327 | return nil, diags.Append(&hcl.Diagnostic{ | |
328 | Severity: hcl.DiagError, | |
329 | Summary: "Mismatched brackets", | |
330 | Detail: "A JSON array must be closed with a bracket, not a brace.", | |
331 | Subject: p.Peek().Range.Ptr(), | |
332 | }) | |
333 | case tokenBrackC: | |
334 | break Token | |
335 | default: | |
336 | recover(p.Read()) | |
337 | return nil, diags.Append(&hcl.Diagnostic{ | |
338 | Severity: hcl.DiagError, | |
339 | Summary: "Missing attribute seperator comma", | |
340 | Detail: "A comma must appear between each value in an array.", | |
341 | Subject: p.Peek().Range.Ptr(), | |
342 | }) | |
343 | } | |
344 | ||
345 | } | |
346 | ||
347 | close := p.Read() | |
348 | return &arrayVal{ | |
349 | Values: vals, | |
350 | SrcRange: hcl.RangeBetween(open.Range, close.Range), | |
351 | OpenRange: open.Range, | |
352 | }, diags | |
353 | } | |
354 | ||
355 | func parseNumber(p *peeker) (node, hcl.Diagnostics) { | |
356 | tok := p.Read() | |
357 | ||
358 | // Use encoding/json to validate the number syntax. | |
359 | // TODO: Do this more directly to produce better diagnostics. | |
360 | var num json.Number | |
361 | err := json.Unmarshal(tok.Bytes, &num) | |
362 | if err != nil { | |
363 | return nil, hcl.Diagnostics{ | |
364 | { | |
365 | Severity: hcl.DiagError, | |
366 | Summary: "Invalid JSON number", | |
367 | Detail: fmt.Sprintf("There is a syntax error in the given JSON number."), | |
368 | Subject: &tok.Range, | |
369 | }, | |
370 | } | |
371 | } | |
372 | ||
107c1cdb ND |
373 | // We want to guarantee that we parse numbers the same way as cty (and thus |
374 | // native syntax HCL) would here, so we'll use the cty parser even though | |
375 | // in most other cases we don't actually introduce cty concepts until | |
376 | // decoding time. We'll unwrap the parsed float immediately afterwards, so | |
377 | // the cty value is just a temporary helper. | |
378 | nv, err := cty.ParseNumberVal(string(num)) | |
15c0b25d AP |
379 | if err != nil { |
380 | // Should never happen if above passed, since JSON numbers are a subset | |
107c1cdb | 381 | // of what cty can parse... |
15c0b25d AP |
382 | return nil, hcl.Diagnostics{ |
383 | { | |
384 | Severity: hcl.DiagError, | |
385 | Summary: "Invalid JSON number", | |
386 | Detail: fmt.Sprintf("There is a syntax error in the given JSON number."), | |
387 | Subject: &tok.Range, | |
388 | }, | |
389 | } | |
390 | } | |
391 | ||
392 | return &numberVal{ | |
107c1cdb | 393 | Value: nv.AsBigFloat(), |
15c0b25d AP |
394 | SrcRange: tok.Range, |
395 | }, nil | |
396 | } | |
397 | ||
398 | func parseString(p *peeker) (node, hcl.Diagnostics) { | |
399 | tok := p.Read() | |
400 | var str string | |
401 | err := json.Unmarshal(tok.Bytes, &str) | |
402 | ||
403 | if err != nil { | |
404 | var errRange hcl.Range | |
405 | if serr, ok := err.(*json.SyntaxError); ok { | |
406 | errOfs := serr.Offset | |
407 | errPos := tok.Range.Start | |
408 | errPos.Byte += int(errOfs) | |
409 | ||
410 | // TODO: Use the byte offset to properly count unicode | |
411 | // characters for the column, and mark the whole of the | |
412 | // character that was wrong as part of our range. | |
413 | errPos.Column += int(errOfs) | |
414 | ||
415 | errEndPos := errPos | |
416 | errEndPos.Byte++ | |
417 | errEndPos.Column++ | |
418 | ||
419 | errRange = hcl.Range{ | |
420 | Filename: tok.Range.Filename, | |
421 | Start: errPos, | |
422 | End: errEndPos, | |
423 | } | |
424 | } else { | |
425 | errRange = tok.Range | |
426 | } | |
427 | ||
428 | var contextRange *hcl.Range | |
429 | if errRange != tok.Range { | |
430 | contextRange = &tok.Range | |
431 | } | |
432 | ||
433 | // FIXME: Eventually we should parse strings directly here so | |
434 | // we can produce a more useful error message in the face fo things | |
435 | // such as invalid escapes, etc. | |
436 | return nil, hcl.Diagnostics{ | |
437 | { | |
438 | Severity: hcl.DiagError, | |
439 | Summary: "Invalid JSON string", | |
440 | Detail: fmt.Sprintf("There is a syntax error in the given JSON string."), | |
441 | Subject: &errRange, | |
442 | Context: contextRange, | |
443 | }, | |
444 | } | |
445 | } | |
446 | ||
447 | return &stringVal{ | |
448 | Value: str, | |
449 | SrcRange: tok.Range, | |
450 | }, nil | |
451 | } | |
452 | ||
453 | func parseKeyword(p *peeker) (node, hcl.Diagnostics) { | |
454 | tok := p.Read() | |
455 | s := string(tok.Bytes) | |
456 | ||
457 | switch s { | |
458 | case "true": | |
459 | return &booleanVal{ | |
460 | Value: true, | |
461 | SrcRange: tok.Range, | |
462 | }, nil | |
463 | case "false": | |
464 | return &booleanVal{ | |
465 | Value: false, | |
466 | SrcRange: tok.Range, | |
467 | }, nil | |
468 | case "null": | |
469 | return &nullVal{ | |
470 | SrcRange: tok.Range, | |
471 | }, nil | |
472 | case "undefined", "NaN", "Infinity": | |
473 | return nil, hcl.Diagnostics{ | |
474 | { | |
475 | Severity: hcl.DiagError, | |
476 | Summary: "Invalid JSON keyword", | |
477 | Detail: fmt.Sprintf("The JavaScript identifier %q cannot be used in JSON.", s), | |
478 | Subject: &tok.Range, | |
479 | }, | |
480 | } | |
481 | default: | |
482 | var dym string | |
483 | if suggest := keywordSuggestion(s); suggest != "" { | |
484 | dym = fmt.Sprintf(" Did you mean %q?", suggest) | |
485 | } | |
486 | ||
487 | return nil, hcl.Diagnostics{ | |
488 | { | |
489 | Severity: hcl.DiagError, | |
490 | Summary: "Invalid JSON keyword", | |
491 | Detail: fmt.Sprintf("%q is not a valid JSON keyword.%s", s, dym), | |
492 | Subject: &tok.Range, | |
493 | }, | |
494 | } | |
495 | } | |
496 | } |