• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// 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
15package proptools
16
17import (
18	"fmt"
19	"reflect"
20	"sort"
21	"strconv"
22	"strings"
23	"text/scanner"
24
25	"github.com/google/blueprint/parser"
26)
27
28const 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
39// packedProperty helps to track properties usage (`used` will be true)
40type packedProperty struct {
41	property *parser.Property
42	used     bool
43}
44
45// 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}
51
52// 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//
57//	{ foo: "abc", bar: {x: 1},}
58//
59// and a runtime value being has been declared as
60//
61//	var v struct { Foo string; Bar int }
62//
63// then v.Foo will be set to "abc" and v.Bar will be set to 1
64// (cf. unpack_test.go for further examples)
65//
66// The type of a receiving field has to match the property type, i.e., a bool/int/string field
67// can be set from a property with bool/int/string value, a struct can be set from a map (only the
68// matching fields are set), and an slice can be set from a list.
69// If a field of a runtime value has been already set prior to the UnpackProperties, the new value
70// is appended to it (see somewhat inappropriately named ExtendBasicType).
71// The same property can initialize fields in multiple runtime values. It is an error if any property
72// value was not used to initialize at least one field.
73func UnpackProperties(properties []*parser.Property, objects ...interface{}) (map[string]*parser.Property, []error) {
74	var unpackContext unpackContext
75	unpackContext.propertyMap = make(map[string]*packedProperty)
76	if !unpackContext.buildPropertyMap("", properties) {
77		return nil, unpackContext.errs
78	}
79
80	for _, obj := range objects {
81		valueObject := reflect.ValueOf(obj)
82		if !isStructPtr(valueObject.Type()) {
83			panic(fmt.Errorf("properties must be *struct, got %s",
84				valueObject.Type()))
85		}
86		unpackContext.unpackToStruct("", valueObject.Elem())
87		if len(unpackContext.errs) >= maxUnpackErrors {
88			return nil, unpackContext.errs
89		}
90	}
91
92	// Gather property map, and collect any unused properties.
93	// Avoid reporting subproperties of unused properties.
94	result := make(map[string]*parser.Property)
95	var unusedNames []string
96	for name, v := range unpackContext.propertyMap {
97		if v.used {
98			result[name] = v.property
99		} else {
100			unusedNames = append(unusedNames, name)
101		}
102	}
103	if len(unusedNames) == 0 && len(unpackContext.errs) == 0 {
104		return result, nil
105	}
106	return nil, unpackContext.reportUnusedNames(unusedNames)
107}
108
109func (ctx *unpackContext) reportUnusedNames(unusedNames []string) []error {
110	sort.Strings(unusedNames)
111	var lastReported string
112	for _, name := range unusedNames {
113		// if 'foo' has been reported, ignore 'foo\..*' and 'foo\[.*'
114		if lastReported != "" {
115			trimmed := strings.TrimPrefix(name, lastReported)
116			if trimmed != name && (trimmed[0] == '.' || trimmed[0] == '[') {
117				continue
118			}
119		}
120		ctx.errs = append(ctx.errs, &UnpackError{
121			fmt.Errorf("unrecognized property %q", name),
122			ctx.propertyMap[name].property.ColonPos})
123		lastReported = name
124	}
125	return ctx.errs
126}
127
128func (ctx *unpackContext) buildPropertyMap(prefix string, properties []*parser.Property) bool {
129	nOldErrors := len(ctx.errs)
130	for _, property := range properties {
131		name := fieldPath(prefix, property.Name)
132		if first, present := ctx.propertyMap[name]; present {
133			ctx.addError(
134				&UnpackError{fmt.Errorf("property %q already defined", name), property.ColonPos})
135			if ctx.addError(
136				&UnpackError{fmt.Errorf("<-- previous definition here"), first.property.ColonPos}) {
137				return false
138			}
139			continue
140		}
141
142		ctx.propertyMap[name] = &packedProperty{property, false}
143		switch propValue := property.Value.Eval().(type) {
144		case *parser.Map:
145			ctx.buildPropertyMap(name, propValue.Properties)
146		case *parser.List:
147			// If it is a list, unroll it unless its elements are of primitive type
148			// (no further mapping will be needed in that case, so we avoid cluttering
149			// the map).
150			if len(propValue.Values) == 0 {
151				continue
152			}
153			if t := propValue.Values[0].Type(); t == parser.StringType || t == parser.Int64Type || t == parser.BoolType {
154				continue
155			}
156
157			itemProperties := make([]*parser.Property, len(propValue.Values), len(propValue.Values))
158			for i, expr := range propValue.Values {
159				itemProperties[i] = &parser.Property{
160					Name:     property.Name + "[" + strconv.Itoa(i) + "]",
161					NamePos:  property.NamePos,
162					ColonPos: property.ColonPos,
163					Value:    expr,
164				}
165			}
166			if !ctx.buildPropertyMap(prefix, itemProperties) {
167				return false
168			}
169		}
170	}
171
172	return len(ctx.errs) == nOldErrors
173}
174
175func fieldPath(prefix, fieldName string) string {
176	if prefix == "" {
177		return fieldName
178	}
179	return prefix + "." + fieldName
180}
181
182func (ctx *unpackContext) addError(e error) bool {
183	ctx.errs = append(ctx.errs, e)
184	return len(ctx.errs) < maxUnpackErrors
185}
186
187func (ctx *unpackContext) unpackToStruct(namePrefix string, structValue reflect.Value) {
188	structType := structValue.Type()
189
190	for i := 0; i < structValue.NumField(); i++ {
191		fieldValue := structValue.Field(i)
192		field := structType.Field(i)
193
194		// In Go 1.7, runtime-created structs are unexported, so it's not
195		// possible to create an exported anonymous field with a generated
196		// type. So workaround this by special-casing "BlueprintEmbed" to
197		// behave like an anonymous field for structure unpacking.
198		if field.Name == "BlueprintEmbed" {
199			field.Name = ""
200			field.Anonymous = true
201		}
202
203		if field.PkgPath != "" {
204			// This is an unexported field, so just skip it.
205			continue
206		}
207
208		propertyName := fieldPath(namePrefix, PropertyNameForField(field.Name))
209
210		if !fieldValue.CanSet() {
211			panic(fmt.Errorf("field %s is not settable", propertyName))
212		}
213
214		// Get the property value if it was specified.
215		packedProperty, propertyIsSet := ctx.propertyMap[propertyName]
216
217		origFieldValue := fieldValue
218
219		// To make testing easier we validate the struct field's type regardless
220		// of whether or not the property was specified in the parsed string.
221		// TODO(ccross): we don't validate types inside nil struct pointers
222		// Move type validation to a function that runs on each factory once
223		switch kind := fieldValue.Kind(); kind {
224		case reflect.Bool, reflect.String, reflect.Struct, reflect.Slice:
225			// Do nothing
226		case reflect.Interface:
227			if fieldValue.IsNil() {
228				panic(fmt.Errorf("field %s contains a nil interface", propertyName))
229			}
230			fieldValue = fieldValue.Elem()
231			elemType := fieldValue.Type()
232			if elemType.Kind() != reflect.Ptr {
233				panic(fmt.Errorf("field %s contains a non-pointer interface", propertyName))
234			}
235			fallthrough
236		case reflect.Ptr:
237			switch ptrKind := fieldValue.Type().Elem().Kind(); ptrKind {
238			case reflect.Struct:
239				if fieldValue.IsNil() && (propertyIsSet || field.Anonymous) {
240					// Instantiate nil struct pointers
241					// Set into origFieldValue in case it was an interface, in which case
242					// fieldValue points to the unsettable pointer inside the interface
243					fieldValue = reflect.New(fieldValue.Type().Elem())
244					origFieldValue.Set(fieldValue)
245				}
246				fieldValue = fieldValue.Elem()
247			case reflect.Bool, reflect.Int64, reflect.String:
248				// Nothing
249			default:
250				panic(fmt.Errorf("field %s contains a pointer to %s", propertyName, ptrKind))
251			}
252
253		case reflect.Int, reflect.Uint:
254			if !HasTag(field, "blueprint", "mutated") {
255				panic(fmt.Errorf(`int field %s must be tagged blueprint:"mutated"`, propertyName))
256			}
257
258		default:
259			panic(fmt.Errorf("unsupported kind for field %s: %s", propertyName, kind))
260		}
261
262		if field.Anonymous && isStruct(fieldValue.Type()) {
263			ctx.unpackToStruct(namePrefix, fieldValue)
264			continue
265		}
266
267		if !propertyIsSet {
268			// This property wasn't specified.
269			continue
270		}
271
272		packedProperty.used = true
273		property := packedProperty.property
274
275		if HasTag(field, "blueprint", "mutated") {
276			if !ctx.addError(
277				&UnpackError{
278					fmt.Errorf("mutated field %s cannot be set in a Blueprint file", propertyName),
279					property.ColonPos,
280				}) {
281				return
282			}
283			continue
284		}
285
286		if isStruct(fieldValue.Type()) {
287			if property.Value.Eval().Type() != parser.MapType {
288				ctx.addError(&UnpackError{
289					fmt.Errorf("can't assign %s value to map property %q",
290						property.Value.Type(), property.Name),
291					property.Value.Pos(),
292				})
293				continue
294			}
295			ctx.unpackToStruct(propertyName, fieldValue)
296			if len(ctx.errs) >= maxUnpackErrors {
297				return
298			}
299		} else if isSlice(fieldValue.Type()) {
300			if unpackedValue, ok := ctx.unpackToSlice(propertyName, property, fieldValue.Type()); ok {
301				ExtendBasicType(fieldValue, unpackedValue, Append)
302			}
303			if len(ctx.errs) >= maxUnpackErrors {
304				return
305			}
306
307		} else {
308			unpackedValue, err := propertyToValue(fieldValue.Type(), property)
309			if err != nil && !ctx.addError(err) {
310				return
311			}
312			ExtendBasicType(fieldValue, unpackedValue, Append)
313		}
314	}
315}
316
317// unpackSlice creates a value of a given slice type from the property which should be a list
318func (ctx *unpackContext) unpackToSlice(
319	sliceName string, property *parser.Property, sliceType reflect.Type) (reflect.Value, bool) {
320	propValueAsList, ok := property.Value.Eval().(*parser.List)
321	if !ok {
322		ctx.addError(&UnpackError{
323			fmt.Errorf("can't assign %s value to list property %q",
324				property.Value.Type(), property.Name),
325			property.Value.Pos(),
326		})
327		return reflect.MakeSlice(sliceType, 0, 0), false
328	}
329	exprs := propValueAsList.Values
330	value := reflect.MakeSlice(sliceType, 0, len(exprs))
331	if len(exprs) == 0 {
332		return value, true
333	}
334
335	// The function to construct an item value depends on the type of list elements.
336	var getItemFunc func(*parser.Property, reflect.Type) (reflect.Value, bool)
337	switch exprs[0].Type() {
338	case parser.BoolType, parser.StringType, parser.Int64Type:
339		getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
340			value, err := propertyToValue(t, property)
341			if err != nil {
342				ctx.addError(err)
343				return value, false
344			}
345			return value, true
346		}
347	case parser.ListType:
348		getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
349			return ctx.unpackToSlice(property.Name, property, t)
350		}
351	case parser.MapType:
352		getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
353			itemValue := reflect.New(t).Elem()
354			ctx.unpackToStruct(property.Name, itemValue)
355			return itemValue, true
356		}
357	case parser.NotEvaluatedType:
358		getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
359			return reflect.New(t), false
360		}
361	default:
362		panic(fmt.Errorf("bizarre property expression type: %v", exprs[0].Type()))
363	}
364
365	itemProperty := &parser.Property{NamePos: property.NamePos, ColonPos: property.ColonPos}
366	elemType := sliceType.Elem()
367	isPtr := elemType.Kind() == reflect.Ptr
368
369	for i, expr := range exprs {
370		itemProperty.Name = sliceName + "[" + strconv.Itoa(i) + "]"
371		itemProperty.Value = expr
372		if packedProperty, ok := ctx.propertyMap[itemProperty.Name]; ok {
373			packedProperty.used = true
374		}
375		if isPtr {
376			if itemValue, ok := getItemFunc(itemProperty, elemType.Elem()); ok {
377				ptrValue := reflect.New(itemValue.Type())
378				ptrValue.Elem().Set(itemValue)
379				value = reflect.Append(value, ptrValue)
380			}
381		} else {
382			if itemValue, ok := getItemFunc(itemProperty, elemType); ok {
383				value = reflect.Append(value, itemValue)
384			}
385		}
386	}
387	return value, true
388}
389
390// propertyToValue creates a value of a given value type from the property.
391func propertyToValue(typ reflect.Type, property *parser.Property) (reflect.Value, error) {
392	var value reflect.Value
393	var baseType reflect.Type
394	isPtr := typ.Kind() == reflect.Ptr
395	if isPtr {
396		baseType = typ.Elem()
397	} else {
398		baseType = typ
399	}
400
401	switch kind := baseType.Kind(); kind {
402	case reflect.Bool:
403		b, ok := property.Value.Eval().(*parser.Bool)
404		if !ok {
405			return value, &UnpackError{
406				fmt.Errorf("can't assign %s value to bool property %q",
407					property.Value.Type(), property.Name),
408				property.Value.Pos(),
409			}
410		}
411		value = reflect.ValueOf(b.Value)
412
413	case reflect.Int64:
414		b, ok := property.Value.Eval().(*parser.Int64)
415		if !ok {
416			return value, &UnpackError{
417				fmt.Errorf("can't assign %s value to int64 property %q",
418					property.Value.Type(), property.Name),
419				property.Value.Pos(),
420			}
421		}
422		value = reflect.ValueOf(b.Value)
423
424	case reflect.String:
425		s, ok := property.Value.Eval().(*parser.String)
426		if !ok {
427			return value, &UnpackError{
428				fmt.Errorf("can't assign %s value to string property %q",
429					property.Value.Type(), property.Name),
430				property.Value.Pos(),
431			}
432		}
433		value = reflect.ValueOf(s.Value)
434
435	default:
436		return value, &UnpackError{
437			fmt.Errorf("cannot assign %s value %s to %s property %s", property.Value.Type(), property.Value, kind, typ),
438			property.NamePos}
439	}
440
441	if isPtr {
442		ptrValue := reflect.New(value.Type())
443		ptrValue.Elem().Set(value)
444		return ptrValue, nil
445	}
446	return value, nil
447}
448