blob: 4a0858c4ae9ec6ac59d772bdc3ac9c5c9bd04a70 [file] [log] [blame]
Colin Cross8e0c5112015-01-23 14:15:10 -08001// Copyright 2014 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
Colin Crossf27c5e42020-01-02 09:37:49 -080015package proptools
Jamie Gennis1bc967e2014-05-27 16:34:41 -070016
17import (
Jamie Gennis1bc967e2014-05-27 16:34:41 -070018 "fmt"
Colin Cross4adc8192015-06-22 13:38:45 -070019 "reflect"
Sasha Smundak29fdcad2020-02-11 22:39:47 -080020 "sort"
21 "strconv"
22 "strings"
Colin Crossf27c5e42020-01-02 09:37:49 -080023 "text/scanner"
Colin Cross4adc8192015-06-22 13:38:45 -070024
Jamie Gennis6cafc2c2015-03-20 22:39:29 -040025 "github.com/google/blueprint/parser"
Jamie Gennis1bc967e2014-05-27 16:34:41 -070026)
27
Colin Crossf27c5e42020-01-02 09:37:49 -080028const maxUnpackErrors = 10
29
30type UnpackError struct {
31 Err error
32 Pos scanner.Position
33}
34
35func (e *UnpackError) Error() string {
36 return fmt.Sprintf("%s: %s", e.Pos, e.Err)
37}
38
Sasha Smundak29fdcad2020-02-11 22:39:47 -080039// packedProperty helps to track properties usage (`used` will be true)
Jamie Gennis1bc967e2014-05-27 16:34:41 -070040type packedProperty struct {
41 property *parser.Property
Sasha Smundak29fdcad2020-02-11 22:39:47 -080042 used bool
Jamie Gennis1bc967e2014-05-27 16:34:41 -070043}
44
Sasha Smundak29fdcad2020-02-11 22:39:47 -080045// unpackContext keeps compound names and their values in a map. It is initialized from
46// parsed properties.
47type unpackContext struct {
48 propertyMap map[string]*packedProperty
49 errs []error
50}
Jamie Gennis1bc967e2014-05-27 16:34:41 -070051
Sasha Smundak29fdcad2020-02-11 22:39:47 -080052// UnpackProperties populates the list of runtime values ("property structs") from the parsed properties.
53// If a property a.b.c has a value, a field with the matching name in each runtime value is initialized
54// from it. See PropertyNameForField for field and property name matching.
55// For instance, if the input contains
56// { foo: "abc", bar: {x: 1},}
57// and a runtime value being has been declared as
58// var v struct { Foo string; Bar int }
59// then v.Foo will be set to "abc" and v.Bar will be set to 1
60// (cf. unpack_test.go for further examples)
61//
62// The type of a receiving field has to match the property type, i.e., a bool/int/string field
63// can be set from a property with bool/int/string value, a struct can be set from a map (only the
64// matching fields are set), and an slice can be set from a list.
65// If a field of a runtime value has been already set prior to the UnpackProperties, the new value
66// is appended to it (see somewhat inappropriately named ExtendBasicType).
67// The same property can initialize fields in multiple runtime values. It is an error if any property
68// value was not used to initialize at least one field.
69func UnpackProperties(properties []*parser.Property, objects ...interface{}) (map[string]*parser.Property, []error) {
70 var unpackContext unpackContext
71 unpackContext.propertyMap = make(map[string]*packedProperty)
72 if !unpackContext.buildPropertyMap("", properties) {
73 return nil, unpackContext.errs
Jamie Gennis87622922014-09-30 11:38:25 -070074 }
75
Sasha Smundak29fdcad2020-02-11 22:39:47 -080076 for _, obj := range objects {
77 valueObject := reflect.ValueOf(obj)
78 if !isStructPtr(valueObject.Type()) {
Colin Cross6898d262020-01-27 16:48:30 -080079 panic(fmt.Errorf("properties must be *struct, got %s",
Sasha Smundak29fdcad2020-02-11 22:39:47 -080080 valueObject.Type()))
Jamie Gennis87622922014-09-30 11:38:25 -070081 }
Sasha Smundak29fdcad2020-02-11 22:39:47 -080082 unpackContext.unpackToStruct("", valueObject.Elem())
83 if len(unpackContext.errs) >= maxUnpackErrors {
84 return nil, unpackContext.errs
Jamie Gennis87622922014-09-30 11:38:25 -070085 }
86 }
87
Sasha Smundak29fdcad2020-02-11 22:39:47 -080088 // Gather property map, and collect any unused properties.
89 // Avoid reporting subproperties of unused properties.
Jamie Gennis87622922014-09-30 11:38:25 -070090 result := make(map[string]*parser.Property)
Sasha Smundak29fdcad2020-02-11 22:39:47 -080091 var unusedNames []string
92 for name, v := range unpackContext.propertyMap {
93 if v.used {
94 result[name] = v.property
95 } else {
96 unusedNames = append(unusedNames, name)
Jamie Gennis87622922014-09-30 11:38:25 -070097 }
98 }
Sasha Smundak29fdcad2020-02-11 22:39:47 -080099 if len(unusedNames) == 0 && len(unpackContext.errs) == 0 {
100 return result, nil
Jamie Gennis87622922014-09-30 11:38:25 -0700101 }
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800102 return nil, unpackContext.reportUnusedNames(unusedNames)
Jamie Gennis87622922014-09-30 11:38:25 -0700103}
104
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800105func (ctx *unpackContext) reportUnusedNames(unusedNames []string) []error {
106 sort.Strings(unusedNames)
107 var lastReported string
108 for _, name := range unusedNames {
109 // if 'foo' has been reported, ignore 'foo\..*' and 'foo\[.*'
110 if lastReported != "" {
111 trimmed := strings.TrimPrefix(name, lastReported)
112 if trimmed != name && (trimmed[0] == '.' || trimmed[0] == '[') {
Jamie Gennis87622922014-09-30 11:38:25 -0700113 continue
114 }
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800115 }
116 ctx.errs = append(ctx.errs, &UnpackError{
117 fmt.Errorf("unrecognized property %q", name),
118 ctx.propertyMap[name].property.ColonPos})
119 lastReported = name
120 }
121 return ctx.errs
122}
123
124func (ctx *unpackContext) buildPropertyMap(prefix string, properties []*parser.Property) bool {
125 nOldErrors := len(ctx.errs)
126 for _, property := range properties {
127 name := fieldPath(prefix, property.Name)
128 if first, present := ctx.propertyMap[name]; present {
129 ctx.addError(
130 &UnpackError{fmt.Errorf("property %q already defined", name), property.ColonPos})
131 if ctx.addError(
132 &UnpackError{fmt.Errorf("<-- previous definition here"), first.property.ColonPos}) {
133 return false
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700134 }
135 continue
136 }
137
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800138 ctx.propertyMap[name] = &packedProperty{property, false}
Sasha Smundakde4d9f92020-03-03 17:36:00 -0800139 switch propValue := property.Value.Eval().(type) {
140 case *parser.Map:
141 ctx.buildPropertyMap(name, propValue.Properties)
142 case *parser.List:
143 // If it is a list, unroll it unless its elements are of primitive type
144 // (no further mapping will be needed in that case, so we avoid cluttering
145 // the map).
146 if len(propValue.Values) == 0 {
147 continue
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800148 }
Sasha Smundakde4d9f92020-03-03 17:36:00 -0800149 if t := propValue.Values[0].Type(); t == parser.StringType || t == parser.Int64Type || t == parser.BoolType {
150 continue
151 }
152
153 itemProperties := make([]*parser.Property, len(propValue.Values), len(propValue.Values))
154 for i, expr := range propValue.Values {
155 itemProperties[i] = &parser.Property{
156 Name: property.Name + "[" + strconv.Itoa(i) + "]",
157 NamePos: property.NamePos,
158 ColonPos: property.ColonPos,
159 Value: expr,
160 }
161 }
162 if !ctx.buildPropertyMap(prefix, itemProperties) {
163 return false
164 }
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800165 }
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700166 }
167
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800168 return len(ctx.errs) == nOldErrors
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700169}
170
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800171func fieldPath(prefix, fieldName string) string {
172 if prefix == "" {
173 return fieldName
174 }
175 return prefix + "." + fieldName
176}
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700177
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800178func (ctx *unpackContext) addError(e error) bool {
179 ctx.errs = append(ctx.errs, e)
180 return len(ctx.errs) < maxUnpackErrors
181}
182
183func (ctx *unpackContext) unpackToStruct(namePrefix string, structValue reflect.Value) {
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700184 structType := structValue.Type()
185
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700186 for i := 0; i < structValue.NumField(); i++ {
187 fieldValue := structValue.Field(i)
188 field := structType.Field(i)
189
Dan Willemsenec5ba982016-09-06 17:20:57 -0700190 // In Go 1.7, runtime-created structs are unexported, so it's not
191 // possible to create an exported anonymous field with a generated
192 // type. So workaround this by special-casing "BlueprintEmbed" to
193 // behave like an anonymous field for structure unpacking.
194 if field.Name == "BlueprintEmbed" {
195 field.Name = ""
196 field.Anonymous = true
197 }
198
Jamie Gennisd4c53d82014-06-22 17:02:55 -0700199 if field.PkgPath != "" {
200 // This is an unexported field, so just skip it.
201 continue
202 }
203
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800204 propertyName := fieldPath(namePrefix, PropertyNameForField(field.Name))
Colin Cross9d1469d2015-11-20 17:03:25 -0800205
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700206 if !fieldValue.CanSet() {
Colin Cross9d1469d2015-11-20 17:03:25 -0800207 panic(fmt.Errorf("field %s is not settable", propertyName))
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700208 }
209
Colin Crossa4371352016-08-08 17:24:03 -0700210 // Get the property value if it was specified.
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800211 packedProperty, propertyIsSet := ctx.propertyMap[propertyName]
Colin Crossa4371352016-08-08 17:24:03 -0700212
Colin Crossc3d73122016-08-05 17:19:36 -0700213 origFieldValue := fieldValue
214
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700215 // To make testing easier we validate the struct field's type regardless
216 // of whether or not the property was specified in the parsed string.
Colin Crossa4371352016-08-08 17:24:03 -0700217 // TODO(ccross): we don't validate types inside nil struct pointers
218 // Move type validation to a function that runs on each factory once
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700219 switch kind := fieldValue.Kind(); kind {
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800220 case reflect.Bool, reflect.String, reflect.Struct, reflect.Slice:
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700221 // Do nothing
Jamie Gennis87622922014-09-30 11:38:25 -0700222 case reflect.Interface:
223 if fieldValue.IsNil() {
Colin Cross9d1469d2015-11-20 17:03:25 -0800224 panic(fmt.Errorf("field %s contains a nil interface", propertyName))
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700225 }
Jamie Gennis87622922014-09-30 11:38:25 -0700226 fieldValue = fieldValue.Elem()
227 elemType := fieldValue.Type()
228 if elemType.Kind() != reflect.Ptr {
Colin Cross9d1469d2015-11-20 17:03:25 -0800229 panic(fmt.Errorf("field %s contains a non-pointer interface", propertyName))
Jamie Gennis87622922014-09-30 11:38:25 -0700230 }
231 fallthrough
232 case reflect.Ptr:
Colin Cross80117682015-10-30 15:53:55 -0700233 switch ptrKind := fieldValue.Type().Elem().Kind(); ptrKind {
234 case reflect.Struct:
Colin Crossa4371352016-08-08 17:24:03 -0700235 if fieldValue.IsNil() && (propertyIsSet || field.Anonymous) {
236 // Instantiate nil struct pointers
237 // Set into origFieldValue in case it was an interface, in which case
238 // fieldValue points to the unsettable pointer inside the interface
Colin Crossc3d73122016-08-05 17:19:36 -0700239 fieldValue = reflect.New(fieldValue.Type().Elem())
240 origFieldValue.Set(fieldValue)
Colin Cross80117682015-10-30 15:53:55 -0700241 }
242 fieldValue = fieldValue.Elem()
Nan Zhangf5865442017-11-01 14:03:28 -0700243 case reflect.Bool, reflect.Int64, reflect.String:
Colin Cross80117682015-10-30 15:53:55 -0700244 // Nothing
245 default:
Colin Cross9d1469d2015-11-20 17:03:25 -0800246 panic(fmt.Errorf("field %s contains a pointer to %s", propertyName, ptrKind))
Jamie Gennis87622922014-09-30 11:38:25 -0700247 }
Colin Cross4adc8192015-06-22 13:38:45 -0700248
Colin Cross11a114f2014-12-17 16:46:09 -0800249 case reflect.Int, reflect.Uint:
Colin Crossf27c5e42020-01-02 09:37:49 -0800250 if !HasTag(field, "blueprint", "mutated") {
Colin Cross9d1469d2015-11-20 17:03:25 -0800251 panic(fmt.Errorf(`int field %s must be tagged blueprint:"mutated"`, propertyName))
Colin Cross11a114f2014-12-17 16:46:09 -0800252 }
253
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700254 default:
Colin Cross9d1469d2015-11-20 17:03:25 -0800255 panic(fmt.Errorf("unsupported kind for field %s: %s", propertyName, kind))
256 }
257
Colin Cross6898d262020-01-27 16:48:30 -0800258 if field.Anonymous && isStruct(fieldValue.Type()) {
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800259 ctx.unpackToStruct(namePrefix, fieldValue)
Colin Cross9d1469d2015-11-20 17:03:25 -0800260 continue
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700261 }
262
Colin Crossa4371352016-08-08 17:24:03 -0700263 if !propertyIsSet {
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700264 // This property wasn't specified.
265 continue
266 }
267
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800268 packedProperty.used = true
269 property := packedProperty.property
Colin Cross11a114f2014-12-17 16:46:09 -0800270
Colin Crossf27c5e42020-01-02 09:37:49 -0800271 if HasTag(field, "blueprint", "mutated") {
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800272 if !ctx.addError(
Colin Crossf27c5e42020-01-02 09:37:49 -0800273 &UnpackError{
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800274 fmt.Errorf("mutated field %s cannot be set in a Blueprint file", propertyName),
275 property.ColonPos,
276 }) {
277 return
Colin Cross11a114f2014-12-17 16:46:09 -0800278 }
279 continue
280 }
281
Colin Cross6898d262020-01-27 16:48:30 -0800282 if isStruct(fieldValue.Type()) {
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800283 ctx.unpackToStruct(propertyName, fieldValue)
284 if len(ctx.errs) >= maxUnpackErrors {
285 return
286 }
287 } else if isSlice(fieldValue.Type()) {
288 if unpackedValue, ok := ctx.unpackToSlice(propertyName, property, fieldValue.Type()); ok {
289 ExtendBasicType(fieldValue, unpackedValue, Append)
290 }
291 if len(ctx.errs) >= maxUnpackErrors {
292 return
Colin Cross05b36072017-07-28 17:51:37 -0700293 }
294
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800295 } else {
296 unpackedValue, err := propertyToValue(fieldValue.Type(), property)
297 if err != nil && !ctx.addError(err) {
298 return
Colin Cross05b36072017-07-28 17:51:37 -0700299 }
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800300 ExtendBasicType(fieldValue, unpackedValue, Append)
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700301 }
302 }
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700303}
304
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800305// unpackSlice creates a value of a given slice type from the property which should be a list
306func (ctx *unpackContext) unpackToSlice(
307 sliceName string, property *parser.Property, sliceType reflect.Type) (reflect.Value, bool) {
308 propValueAsList, ok := property.Value.Eval().(*parser.List)
309 if !ok {
310 ctx.addError(fmt.Errorf("%s: can't assign %s value to list property %q",
311 property.Value.Pos(), property.Value.Type(), property.Name))
312 return reflect.MakeSlice(sliceType, 0, 0), false
313 }
314 exprs := propValueAsList.Values
315 value := reflect.MakeSlice(sliceType, 0, len(exprs))
316 if len(exprs) == 0 {
317 return value, true
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700318 }
319
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800320 // The function to construct an item value depends on the type of list elements.
321 var getItemFunc func(*parser.Property, reflect.Type) (reflect.Value, bool)
322 switch exprs[0].Type() {
323 case parser.BoolType, parser.StringType, parser.Int64Type:
324 getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
325 value, err := propertyToValue(t, property)
326 if err != nil {
327 ctx.addError(err)
328 return value, false
329 }
330 return value, true
331 }
332 case parser.ListType:
333 getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
334 return ctx.unpackToSlice(property.Name, property, t)
335 }
336 case parser.MapType:
337 getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
338 itemValue := reflect.New(t).Elem()
339 ctx.unpackToStruct(property.Name, itemValue)
340 return itemValue, true
341 }
342 case parser.NotEvaluatedType:
343 getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
344 return reflect.New(t), false
345 }
346 default:
347 panic(fmt.Errorf("bizarre property expression type: %v", exprs[0].Type()))
348 }
349
350 itemProperty := &parser.Property{NamePos: property.NamePos, ColonPos: property.ColonPos}
351 elemType := sliceType.Elem()
352 isPtr := elemType.Kind() == reflect.Ptr
353
354 for i, expr := range exprs {
355 itemProperty.Name = sliceName + "[" + strconv.Itoa(i) + "]"
356 itemProperty.Value = expr
357 if packedProperty, ok := ctx.propertyMap[itemProperty.Name]; ok {
358 packedProperty.used = true
359 }
360 if isPtr {
361 if itemValue, ok := getItemFunc(itemProperty, elemType.Elem()); ok {
362 ptrValue := reflect.New(itemValue.Type())
363 ptrValue.Elem().Set(itemValue)
364 value = reflect.Append(value, ptrValue)
365 }
366 } else {
367 if itemValue, ok := getItemFunc(itemProperty, elemType); ok {
368 value = reflect.Append(value, itemValue)
369 }
370 }
371 }
372 return value, true
373}
374
375// propertyToValue creates a value of a given value type from the property.
376func propertyToValue(typ reflect.Type, property *parser.Property) (reflect.Value, error) {
377 var value reflect.Value
378 var baseType reflect.Type
379 isPtr := typ.Kind() == reflect.Ptr
380 if isPtr {
381 baseType = typ.Elem()
382 } else {
383 baseType = typ
384 }
385
386 switch kind := baseType.Kind(); kind {
Colin Cross05b36072017-07-28 17:51:37 -0700387 case reflect.Bool:
388 b, ok := property.Value.Eval().(*parser.Bool)
Colin Crosse32cc802016-06-07 12:28:16 -0700389 if !ok {
Colin Cross05b36072017-07-28 17:51:37 -0700390 return value, fmt.Errorf("%s: can't assign %s value to bool property %q",
391 property.Value.Pos(), property.Value.Type(), property.Name)
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700392 }
Colin Cross05b36072017-07-28 17:51:37 -0700393 value = reflect.ValueOf(b.Value)
394
Nan Zhangf5865442017-11-01 14:03:28 -0700395 case reflect.Int64:
396 b, ok := property.Value.Eval().(*parser.Int64)
397 if !ok {
398 return value, fmt.Errorf("%s: can't assign %s value to int64 property %q",
399 property.Value.Pos(), property.Value.Type(), property.Name)
400 }
401 value = reflect.ValueOf(b.Value)
402
Colin Cross05b36072017-07-28 17:51:37 -0700403 case reflect.String:
404 s, ok := property.Value.Eval().(*parser.String)
405 if !ok {
406 return value, fmt.Errorf("%s: can't assign %s value to string property %q",
407 property.Value.Pos(), property.Value.Type(), property.Name)
408 }
409 value = reflect.ValueOf(s.Value)
410
Colin Cross05b36072017-07-28 17:51:37 -0700411 default:
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800412 return value, &UnpackError{
413 fmt.Errorf("cannot assign %s value %s to %s property %s", property.Value.Type(), property.Value, kind, typ),
414 property.NamePos}
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700415 }
416
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800417 if isPtr {
Colin Cross05b36072017-07-28 17:51:37 -0700418 ptrValue := reflect.New(value.Type())
419 ptrValue.Elem().Set(value)
Sasha Smundak29fdcad2020-02-11 22:39:47 -0800420 return ptrValue, nil
Colin Cross05b36072017-07-28 17:51:37 -0700421 }
Colin Cross05b36072017-07-28 17:51:37 -0700422 return value, nil
Jamie Gennis1bc967e2014-05-27 16:34:41 -0700423}