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