197 lines
6.4 KiB
Go
197 lines
6.4 KiB
Go
|
package typeexpr
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
|
||
|
"github.com/hashicorp/hcl/v2"
|
||
|
"github.com/zclconf/go-cty/cty"
|
||
|
)
|
||
|
|
||
|
const invalidTypeSummary = "Invalid type specification"
|
||
|
|
||
|
// getType is the internal implementation of both Type and TypeConstraint,
|
||
|
// using the passed flag to distinguish. When constraint is false, the "any"
|
||
|
// keyword will produce an error.
|
||
|
func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||
|
// First we'll try for one of our keywords
|
||
|
kw := hcl.ExprAsKeyword(expr)
|
||
|
switch kw {
|
||
|
case "bool":
|
||
|
return cty.Bool, nil
|
||
|
case "string":
|
||
|
return cty.String, nil
|
||
|
case "number":
|
||
|
return cty.Number, nil
|
||
|
case "any":
|
||
|
if constraint {
|
||
|
return cty.DynamicPseudoType, nil
|
||
|
}
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw),
|
||
|
Subject: expr.Range().Ptr(),
|
||
|
}}
|
||
|
case "list", "map", "set":
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw),
|
||
|
Subject: expr.Range().Ptr(),
|
||
|
}}
|
||
|
case "object":
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.",
|
||
|
Subject: expr.Range().Ptr(),
|
||
|
}}
|
||
|
case "tuple":
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: "The tuple type constructor requires one argument specifying the element types as a list.",
|
||
|
Subject: expr.Range().Ptr(),
|
||
|
}}
|
||
|
case "":
|
||
|
// okay! we'll fall through and try processing as a call, then.
|
||
|
default:
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: fmt.Sprintf("The keyword %q is not a valid type specification.", kw),
|
||
|
Subject: expr.Range().Ptr(),
|
||
|
}}
|
||
|
}
|
||
|
|
||
|
// If we get down here then our expression isn't just a keyword, so we'll
|
||
|
// try to process it as a call instead.
|
||
|
call, diags := hcl.ExprCall(expr)
|
||
|
if diags.HasErrors() {
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).",
|
||
|
Subject: expr.Range().Ptr(),
|
||
|
}}
|
||
|
}
|
||
|
|
||
|
switch call.Name {
|
||
|
case "bool", "string", "number", "any":
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name),
|
||
|
Subject: &call.ArgsRange,
|
||
|
}}
|
||
|
}
|
||
|
|
||
|
if len(call.Arguments) != 1 {
|
||
|
contextRange := call.ArgsRange
|
||
|
subjectRange := call.ArgsRange
|
||
|
if len(call.Arguments) > 1 {
|
||
|
// If we have too many arguments (as opposed to too _few_) then
|
||
|
// we'll highlight the extraneous arguments as the diagnostic
|
||
|
// subject.
|
||
|
subjectRange = hcl.RangeBetween(call.Arguments[1].Range(), call.Arguments[len(call.Arguments)-1].Range())
|
||
|
}
|
||
|
|
||
|
switch call.Name {
|
||
|
case "list", "set", "map":
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name),
|
||
|
Subject: &subjectRange,
|
||
|
Context: &contextRange,
|
||
|
}}
|
||
|
case "object":
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.",
|
||
|
Subject: &subjectRange,
|
||
|
Context: &contextRange,
|
||
|
}}
|
||
|
case "tuple":
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: "The tuple type constructor requires one argument specifying the element types as a list.",
|
||
|
Subject: &subjectRange,
|
||
|
Context: &contextRange,
|
||
|
}}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch call.Name {
|
||
|
|
||
|
case "list":
|
||
|
ety, diags := getType(call.Arguments[0], constraint)
|
||
|
return cty.List(ety), diags
|
||
|
case "set":
|
||
|
ety, diags := getType(call.Arguments[0], constraint)
|
||
|
return cty.Set(ety), diags
|
||
|
case "map":
|
||
|
ety, diags := getType(call.Arguments[0], constraint)
|
||
|
return cty.Map(ety), diags
|
||
|
case "object":
|
||
|
attrDefs, diags := hcl.ExprMap(call.Arguments[0])
|
||
|
if diags.HasErrors() {
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.",
|
||
|
Subject: call.Arguments[0].Range().Ptr(),
|
||
|
Context: expr.Range().Ptr(),
|
||
|
}}
|
||
|
}
|
||
|
|
||
|
atys := make(map[string]cty.Type)
|
||
|
for _, attrDef := range attrDefs {
|
||
|
attrName := hcl.ExprAsKeyword(attrDef.Key)
|
||
|
if attrName == "" {
|
||
|
diags = append(diags, &hcl.Diagnostic{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: "Object constructor map keys must be attribute names.",
|
||
|
Subject: attrDef.Key.Range().Ptr(),
|
||
|
Context: expr.Range().Ptr(),
|
||
|
})
|
||
|
continue
|
||
|
}
|
||
|
aty, attrDiags := getType(attrDef.Value, constraint)
|
||
|
diags = append(diags, attrDiags...)
|
||
|
atys[attrName] = aty
|
||
|
}
|
||
|
return cty.Object(atys), diags
|
||
|
case "tuple":
|
||
|
elemDefs, diags := hcl.ExprList(call.Arguments[0])
|
||
|
if diags.HasErrors() {
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: "Tuple type constructor requires a list of element types.",
|
||
|
Subject: call.Arguments[0].Range().Ptr(),
|
||
|
Context: expr.Range().Ptr(),
|
||
|
}}
|
||
|
}
|
||
|
etys := make([]cty.Type, len(elemDefs))
|
||
|
for i, defExpr := range elemDefs {
|
||
|
ety, elemDiags := getType(defExpr, constraint)
|
||
|
diags = append(diags, elemDiags...)
|
||
|
etys[i] = ety
|
||
|
}
|
||
|
return cty.Tuple(etys), diags
|
||
|
default:
|
||
|
// Can't access call.Arguments in this path because we've not validated
|
||
|
// that it contains exactly one expression here.
|
||
|
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||
|
Severity: hcl.DiagError,
|
||
|
Summary: invalidTypeSummary,
|
||
|
Detail: fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name),
|
||
|
Subject: expr.Range().Ptr(),
|
||
|
}}
|
||
|
}
|
||
|
}
|