package json import ( "encoding/json" "fmt" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" ) func parseFileContent(buf []byte, filename string, start hcl.Pos) (node, hcl.Diagnostics) { tokens := scan(buf, pos{Filename: filename, Pos: start}) p := newPeeker(tokens) node, diags := parseValue(p) if len(diags) == 0 && p.Peek().Type != tokenEOF { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Extraneous data after value", Detail: "Extra characters appear after the JSON value.", Subject: p.Peek().Range.Ptr(), }) } return node, diags } func parseExpression(buf []byte, filename string, start hcl.Pos) (node, hcl.Diagnostics) { tokens := scan(buf, pos{Filename: filename, Pos: start}) p := newPeeker(tokens) node, diags := parseValue(p) if len(diags) == 0 && p.Peek().Type != tokenEOF { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Extraneous data after value", Detail: "Extra characters appear after the JSON value.", Subject: p.Peek().Range.Ptr(), }) } return node, diags } func parseValue(p *peeker) (node, hcl.Diagnostics) { tok := p.Peek() wrapInvalid := func(n node, diags hcl.Diagnostics) (node, hcl.Diagnostics) { if n != nil { return n, diags } return invalidVal{tok.Range}, diags } switch tok.Type { case tokenBraceO: return wrapInvalid(parseObject(p)) case tokenBrackO: return wrapInvalid(parseArray(p)) case tokenNumber: return wrapInvalid(parseNumber(p)) case tokenString: return wrapInvalid(parseString(p)) case tokenKeyword: return wrapInvalid(parseKeyword(p)) case tokenBraceC: return wrapInvalid(nil, hcl.Diagnostics{ { Severity: hcl.DiagError, Summary: "Missing JSON value", Detail: "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.", Subject: &tok.Range, }, }) case tokenBrackC: return wrapInvalid(nil, hcl.Diagnostics{ { Severity: hcl.DiagError, Summary: "Missing array element value", Detail: "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.", Subject: &tok.Range, }, }) case tokenEOF: return wrapInvalid(nil, hcl.Diagnostics{ { Severity: hcl.DiagError, Summary: "Missing value", Detail: "The JSON data ends prematurely.", Subject: &tok.Range, }, }) default: return wrapInvalid(nil, hcl.Diagnostics{ { Severity: hcl.DiagError, Summary: "Invalid start of value", Detail: "A JSON value must start with a brace, a bracket, a number, a string, or a keyword.", Subject: &tok.Range, }, }) } } func tokenCanStartValue(tok token) bool { switch tok.Type { case tokenBraceO, tokenBrackO, tokenNumber, tokenString, tokenKeyword: return true default: return false } } func parseObject(p *peeker) (node, hcl.Diagnostics) { var diags hcl.Diagnostics open := p.Read() attrs := []*objectAttr{} // recover is used to shift the peeker to what seems to be the end of // our object, so that when we encounter an error we leave the peeker // at a reasonable point in the token stream to continue parsing. recover := func(tok token) { open := 1 for { switch tok.Type { case tokenBraceO: open++ case tokenBraceC: open-- if open <= 1 { return } case tokenEOF: // Ran out of source before we were able to recover, // so we'll bail here and let the caller deal with it. return } tok = p.Read() } } Token: for { if p.Peek().Type == tokenBraceC { break Token } keyNode, keyDiags := parseValue(p) diags = diags.Extend(keyDiags) if keyNode == nil { return nil, diags } keyStrNode, ok := keyNode.(*stringVal) if !ok { return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid object property name", Detail: "A JSON object property name must be a string", Subject: keyNode.StartRange().Ptr(), }) } key := keyStrNode.Value colon := p.Read() if colon.Type != tokenColon { recover(colon) if colon.Type == tokenBraceC || colon.Type == tokenComma { // Catch common mistake of using braces instead of brackets // for an object. return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing object value", Detail: "A JSON object attribute must have a value, introduced by a colon.", Subject: &colon.Range, }) } if colon.Type == tokenEquals { // Possible confusion with native HCL syntax. return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing property value colon", Detail: "JSON uses a colon as its name/value delimiter, not an equals sign.", Subject: &colon.Range, }) } return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing property value colon", Detail: "A colon must appear between an object property's name and its value.", Subject: &colon.Range, }) } valNode, valDiags := parseValue(p) diags = diags.Extend(valDiags) if valNode == nil { return nil, diags } attrs = append(attrs, &objectAttr{ Name: key, Value: valNode, NameRange: keyStrNode.SrcRange, }) switch p.Peek().Type { case tokenComma: comma := p.Read() if p.Peek().Type == tokenBraceC { // Special error message for this common mistake return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Trailing comma in object", Detail: "JSON does not permit a trailing comma after the final property in an object.", Subject: &comma.Range, }) } continue Token case tokenEOF: return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Unclosed object", Detail: "No closing brace was found for this JSON object.", Subject: &open.Range, }) case tokenBrackC: // Consume the bracket anyway, so that we don't return with the peeker // at a strange place. p.Read() return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Mismatched braces", Detail: "A JSON object must be closed with a brace, not a bracket.", Subject: p.Peek().Range.Ptr(), }) case tokenBraceC: break Token default: recover(p.Read()) return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing attribute seperator comma", Detail: "A comma must appear between each property definition in an object.", Subject: p.Peek().Range.Ptr(), }) } } close := p.Read() return &objectVal{ Attrs: attrs, SrcRange: hcl.RangeBetween(open.Range, close.Range), OpenRange: open.Range, CloseRange: close.Range, }, diags } func parseArray(p *peeker) (node, hcl.Diagnostics) { var diags hcl.Diagnostics open := p.Read() vals := []node{} // recover is used to shift the peeker to what seems to be the end of // our array, so that when we encounter an error we leave the peeker // at a reasonable point in the token stream to continue parsing. recover := func(tok token) { open := 1 for { switch tok.Type { case tokenBrackO: open++ case tokenBrackC: open-- if open <= 1 { return } case tokenEOF: // Ran out of source before we were able to recover, // so we'll bail here and let the caller deal with it. return } tok = p.Read() } } Token: for { if p.Peek().Type == tokenBrackC { break Token } valNode, valDiags := parseValue(p) diags = diags.Extend(valDiags) if valNode == nil { return nil, diags } vals = append(vals, valNode) switch p.Peek().Type { case tokenComma: comma := p.Read() if p.Peek().Type == tokenBrackC { // Special error message for this common mistake return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Trailing comma in array", Detail: "JSON does not permit a trailing comma after the final value in an array.", Subject: &comma.Range, }) } continue Token case tokenColon: recover(p.Read()) return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid array value", Detail: "A colon is not used to introduce values in a JSON array.", Subject: p.Peek().Range.Ptr(), }) case tokenEOF: recover(p.Read()) return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Unclosed object", Detail: "No closing bracket was found for this JSON array.", Subject: &open.Range, }) case tokenBraceC: recover(p.Read()) return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Mismatched brackets", Detail: "A JSON array must be closed with a bracket, not a brace.", Subject: p.Peek().Range.Ptr(), }) case tokenBrackC: break Token default: recover(p.Read()) return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing attribute seperator comma", Detail: "A comma must appear between each value in an array.", Subject: p.Peek().Range.Ptr(), }) } } close := p.Read() return &arrayVal{ Values: vals, SrcRange: hcl.RangeBetween(open.Range, close.Range), OpenRange: open.Range, }, diags } func parseNumber(p *peeker) (node, hcl.Diagnostics) { tok := p.Read() // Use encoding/json to validate the number syntax. // TODO: Do this more directly to produce better diagnostics. var num json.Number err := json.Unmarshal(tok.Bytes, &num) if err != nil { return nil, hcl.Diagnostics{ { Severity: hcl.DiagError, Summary: "Invalid JSON number", Detail: fmt.Sprintf("There is a syntax error in the given JSON number."), Subject: &tok.Range, }, } } // We want to guarantee that we parse numbers the same way as cty (and thus // native syntax HCL) would here, so we'll use the cty parser even though // in most other cases we don't actually introduce cty concepts until // decoding time. We'll unwrap the parsed float immediately afterwards, so // the cty value is just a temporary helper. nv, err := cty.ParseNumberVal(string(num)) if err != nil { // Should never happen if above passed, since JSON numbers are a subset // of what cty can parse... return nil, hcl.Diagnostics{ { Severity: hcl.DiagError, Summary: "Invalid JSON number", Detail: fmt.Sprintf("There is a syntax error in the given JSON number."), Subject: &tok.Range, }, } } return &numberVal{ Value: nv.AsBigFloat(), SrcRange: tok.Range, }, nil } func parseString(p *peeker) (node, hcl.Diagnostics) { tok := p.Read() var str string err := json.Unmarshal(tok.Bytes, &str) if err != nil { var errRange hcl.Range if serr, ok := err.(*json.SyntaxError); ok { errOfs := serr.Offset errPos := tok.Range.Start errPos.Byte += int(errOfs) // TODO: Use the byte offset to properly count unicode // characters for the column, and mark the whole of the // character that was wrong as part of our range. errPos.Column += int(errOfs) errEndPos := errPos errEndPos.Byte++ errEndPos.Column++ errRange = hcl.Range{ Filename: tok.Range.Filename, Start: errPos, End: errEndPos, } } else { errRange = tok.Range } var contextRange *hcl.Range if errRange != tok.Range { contextRange = &tok.Range } // FIXME: Eventually we should parse strings directly here so // we can produce a more useful error message in the face fo things // such as invalid escapes, etc. return nil, hcl.Diagnostics{ { Severity: hcl.DiagError, Summary: "Invalid JSON string", Detail: fmt.Sprintf("There is a syntax error in the given JSON string."), Subject: &errRange, Context: contextRange, }, } } return &stringVal{ Value: str, SrcRange: tok.Range, }, nil } func parseKeyword(p *peeker) (node, hcl.Diagnostics) { tok := p.Read() s := string(tok.Bytes) switch s { case "true": return &booleanVal{ Value: true, SrcRange: tok.Range, }, nil case "false": return &booleanVal{ Value: false, SrcRange: tok.Range, }, nil case "null": return &nullVal{ SrcRange: tok.Range, }, nil case "undefined", "NaN", "Infinity": return nil, hcl.Diagnostics{ { Severity: hcl.DiagError, Summary: "Invalid JSON keyword", Detail: fmt.Sprintf("The JavaScript identifier %q cannot be used in JSON.", s), Subject: &tok.Range, }, } default: var dym string if suggest := keywordSuggestion(s); suggest != "" { dym = fmt.Sprintf(" Did you mean %q?", suggest) } return nil, hcl.Diagnostics{ { Severity: hcl.DiagError, Summary: "Invalid JSON keyword", Detail: fmt.Sprintf("%q is not a valid JSON keyword.%s", s, dym), Subject: &tok.Range, }, } } }