github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/typeexpr/get_type.go (about) 1 package typeexpr 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/zclconf/go-cty/cty" 8 ) 9 10 const invalidTypeSummary = "Invalid type specification" 11 12 // getType is the internal implementation of both Type and TypeConstraint, 13 // using the passed flag to distinguish. When constraint is false, the "any" 14 // keyword will produce an error. 15 func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) { 16 // First we'll try for one of our keywords 17 kw := hcl.ExprAsKeyword(expr) 18 switch kw { 19 case "bool": 20 return cty.Bool, nil 21 case "string": 22 return cty.String, nil 23 case "number": 24 return cty.Number, nil 25 case "any": 26 if constraint { 27 return cty.DynamicPseudoType, nil 28 } 29 return cty.DynamicPseudoType, hcl.Diagnostics{{ 30 Severity: hcl.DiagError, 31 Summary: invalidTypeSummary, 32 Detail: fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw), 33 Subject: expr.Range().Ptr(), 34 }} 35 case "list", "map", "set": 36 return cty.DynamicPseudoType, hcl.Diagnostics{{ 37 Severity: hcl.DiagError, 38 Summary: invalidTypeSummary, 39 Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw), 40 Subject: expr.Range().Ptr(), 41 }} 42 case "object": 43 return cty.DynamicPseudoType, hcl.Diagnostics{{ 44 Severity: hcl.DiagError, 45 Summary: invalidTypeSummary, 46 Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.", 47 Subject: expr.Range().Ptr(), 48 }} 49 case "tuple": 50 return cty.DynamicPseudoType, hcl.Diagnostics{{ 51 Severity: hcl.DiagError, 52 Summary: invalidTypeSummary, 53 Detail: "The tuple type constructor requires one argument specifying the element types as a list.", 54 Subject: expr.Range().Ptr(), 55 }} 56 case "": 57 // okay! we'll fall through and try processing as a call, then. 58 default: 59 return cty.DynamicPseudoType, hcl.Diagnostics{{ 60 Severity: hcl.DiagError, 61 Summary: invalidTypeSummary, 62 Detail: fmt.Sprintf("The keyword %q is not a valid type specification.", kw), 63 Subject: expr.Range().Ptr(), 64 }} 65 } 66 67 // If we get down here then our expression isn't just a keyword, so we'll 68 // try to process it as a call instead. 69 call, diags := hcl.ExprCall(expr) 70 if diags.HasErrors() { 71 return cty.DynamicPseudoType, hcl.Diagnostics{{ 72 Severity: hcl.DiagError, 73 Summary: invalidTypeSummary, 74 Detail: "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).", 75 Subject: expr.Range().Ptr(), 76 }} 77 } 78 79 switch call.Name { 80 case "bool", "string", "number", "any": 81 return cty.DynamicPseudoType, hcl.Diagnostics{{ 82 Severity: hcl.DiagError, 83 Summary: invalidTypeSummary, 84 Detail: fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name), 85 Subject: &call.ArgsRange, 86 }} 87 } 88 89 if len(call.Arguments) != 1 { 90 contextRange := call.ArgsRange 91 subjectRange := call.ArgsRange 92 if len(call.Arguments) > 1 { 93 // If we have too many arguments (as opposed to too _few_) then 94 // we'll highlight the extraneous arguments as the diagnostic 95 // subject. 96 subjectRange = hcl.RangeBetween(call.Arguments[1].Range(), call.Arguments[len(call.Arguments)-1].Range()) 97 } 98 99 switch call.Name { 100 case "list", "set", "map": 101 return cty.DynamicPseudoType, hcl.Diagnostics{{ 102 Severity: hcl.DiagError, 103 Summary: invalidTypeSummary, 104 Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name), 105 Subject: &subjectRange, 106 Context: &contextRange, 107 }} 108 case "object": 109 return cty.DynamicPseudoType, hcl.Diagnostics{{ 110 Severity: hcl.DiagError, 111 Summary: invalidTypeSummary, 112 Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.", 113 Subject: &subjectRange, 114 Context: &contextRange, 115 }} 116 case "tuple": 117 return cty.DynamicPseudoType, hcl.Diagnostics{{ 118 Severity: hcl.DiagError, 119 Summary: invalidTypeSummary, 120 Detail: "The tuple type constructor requires one argument specifying the element types as a list.", 121 Subject: &subjectRange, 122 Context: &contextRange, 123 }} 124 } 125 } 126 127 switch call.Name { 128 129 case "list": 130 ety, diags := getType(call.Arguments[0], constraint) 131 return cty.List(ety), diags 132 case "set": 133 ety, diags := getType(call.Arguments[0], constraint) 134 return cty.Set(ety), diags 135 case "map": 136 ety, diags := getType(call.Arguments[0], constraint) 137 return cty.Map(ety), diags 138 case "object": 139 attrDefs, diags := hcl.ExprMap(call.Arguments[0]) 140 if diags.HasErrors() { 141 return cty.DynamicPseudoType, hcl.Diagnostics{{ 142 Severity: hcl.DiagError, 143 Summary: invalidTypeSummary, 144 Detail: "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.", 145 Subject: call.Arguments[0].Range().Ptr(), 146 Context: expr.Range().Ptr(), 147 }} 148 } 149 150 atys := make(map[string]cty.Type) 151 var optAttrs []string 152 for _, attrDef := range attrDefs { 153 attrName := hcl.ExprAsKeyword(attrDef.Key) 154 if attrName == "" { 155 diags = append(diags, &hcl.Diagnostic{ 156 Severity: hcl.DiagError, 157 Summary: invalidTypeSummary, 158 Detail: "Object constructor map keys must be attribute names.", 159 Subject: attrDef.Key.Range().Ptr(), 160 Context: expr.Range().Ptr(), 161 }) 162 continue 163 } 164 atyExpr := attrDef.Value 165 166 // the attribute type expression might be wrapped in the special 167 // modifier optional(...) to indicate an optional attribute. If 168 // so, we'll unwrap that first and make a note about it being 169 // optional for when we construct the type below. 170 if call, callDiags := hcl.ExprCall(atyExpr); !callDiags.HasErrors() { 171 if call.Name == "optional" { 172 if len(call.Arguments) < 1 { 173 diags = append(diags, &hcl.Diagnostic{ 174 Severity: hcl.DiagError, 175 Summary: invalidTypeSummary, 176 Detail: "Optional attribute modifier requires the attribute type as its argument.", 177 Subject: call.ArgsRange.Ptr(), 178 Context: atyExpr.Range().Ptr(), 179 }) 180 continue 181 } 182 if constraint { 183 if len(call.Arguments) > 1 { 184 diags = append(diags, &hcl.Diagnostic{ 185 Severity: hcl.DiagError, 186 Summary: invalidTypeSummary, 187 Detail: "Optional attribute modifier expects only one argument: the attribute type.", 188 Subject: call.ArgsRange.Ptr(), 189 Context: atyExpr.Range().Ptr(), 190 }) 191 } 192 optAttrs = append(optAttrs, attrName) 193 } else { 194 diags = append(diags, &hcl.Diagnostic{ 195 Severity: hcl.DiagError, 196 Summary: invalidTypeSummary, 197 Detail: "Optional attribute modifier is only for type constraints, not for exact types.", 198 Subject: call.NameRange.Ptr(), 199 Context: atyExpr.Range().Ptr(), 200 }) 201 } 202 atyExpr = call.Arguments[0] 203 } 204 } 205 206 aty, attrDiags := getType(atyExpr, constraint) 207 diags = append(diags, attrDiags...) 208 atys[attrName] = aty 209 } 210 // NOTE: ObjectWithOptionalAttrs is experimental in cty at the 211 // time of writing, so this interface might change even in future 212 // minor versions of cty. We're accepting that because Terraform 213 // itself is considering optional attributes as experimental right now. 214 return cty.ObjectWithOptionalAttrs(atys, optAttrs), diags 215 case "tuple": 216 elemDefs, diags := hcl.ExprList(call.Arguments[0]) 217 if diags.HasErrors() { 218 return cty.DynamicPseudoType, hcl.Diagnostics{{ 219 Severity: hcl.DiagError, 220 Summary: invalidTypeSummary, 221 Detail: "Tuple type constructor requires a list of element types.", 222 Subject: call.Arguments[0].Range().Ptr(), 223 Context: expr.Range().Ptr(), 224 }} 225 } 226 etys := make([]cty.Type, len(elemDefs)) 227 for i, defExpr := range elemDefs { 228 ety, elemDiags := getType(defExpr, constraint) 229 diags = append(diags, elemDiags...) 230 etys[i] = ety 231 } 232 return cty.Tuple(etys), diags 233 case "optional": 234 return cty.DynamicPseudoType, hcl.Diagnostics{{ 235 Severity: hcl.DiagError, 236 Summary: invalidTypeSummary, 237 Detail: fmt.Sprintf("Keyword %q is valid only as a modifier for object type attributes.", call.Name), 238 Subject: call.NameRange.Ptr(), 239 }} 240 default: 241 // Can't access call.Arguments in this path because we've not validated 242 // that it contains exactly one expression here. 243 return cty.DynamicPseudoType, hcl.Diagnostics{{ 244 Severity: hcl.DiagError, 245 Summary: invalidTypeSummary, 246 Detail: fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name), 247 Subject: expr.Range().Ptr(), 248 }} 249 } 250 }