| 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) |
| var optAttrs []string |
| 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 |
| } |
| atyExpr := attrDef.Value |
| |
| // the attribute type expression might be wrapped in the special |
| // modifier optional(...) to indicate an optional attribute. If |
| // so, we'll unwrap that first and make a note about it being |
| // optional for when we construct the type below. |
| if call, callDiags := hcl.ExprCall(atyExpr); !callDiags.HasErrors() { |
| if call.Name == "optional" { |
| if len(call.Arguments) < 1 { |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: invalidTypeSummary, |
| Detail: "Optional attribute modifier requires the attribute type as its argument.", |
| Subject: call.ArgsRange.Ptr(), |
| Context: atyExpr.Range().Ptr(), |
| }) |
| continue |
| } |
| if constraint { |
| if len(call.Arguments) > 1 { |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: invalidTypeSummary, |
| Detail: "Optional attribute modifier expects only one argument: the attribute type.", |
| Subject: call.ArgsRange.Ptr(), |
| Context: atyExpr.Range().Ptr(), |
| }) |
| } |
| optAttrs = append(optAttrs, attrName) |
| } else { |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: invalidTypeSummary, |
| Detail: "Optional attribute modifier is only for type constraints, not for exact types.", |
| Subject: call.NameRange.Ptr(), |
| Context: atyExpr.Range().Ptr(), |
| }) |
| } |
| atyExpr = call.Arguments[0] |
| } |
| } |
| |
| aty, attrDiags := getType(atyExpr, constraint) |
| diags = append(diags, attrDiags...) |
| atys[attrName] = aty |
| } |
| // NOTE: ObjectWithOptionalAttrs is experimental in cty at the |
| // time of writing, so this interface might change even in future |
| // minor versions of cty. We're accepting that because Terraform |
| // itself is considering optional attributes as experimental right now. |
| return cty.ObjectWithOptionalAttrs(atys, optAttrs), 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 |
| case "optional": |
| return cty.DynamicPseudoType, hcl.Diagnostics{{ |
| Severity: hcl.DiagError, |
| Summary: invalidTypeSummary, |
| Detail: fmt.Sprintf("Keyword %q is valid only as a modifier for object type attributes.", call.Name), |
| Subject: call.NameRange.Ptr(), |
| }} |
| 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(), |
| }} |
| } |
| } |