| 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, |
| }, |
| } |
| } |
| } |