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//   { 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
74	}
75
76	for _, obj := range objects {
77		valueObject := reflect.ValueOf(obj)
78		if !isStructPtr(valueObject.Type()) {
79			panic(fmt.Errorf("properties must be *struct, got %s",
80				valueObject.Type()))
81		}
82		unpackContext.unpackToStruct("", valueObject.Elem())
83		if len(unpackContext.errs) >= maxUnpackErrors {
84			return nil, unpackContext.errs
85		}
86	}
87
88	// Gather property map, and collect any unused properties.
89	// Avoid reporting subproperties of unused properties.
90	result := make(map[string]*parser.Property)
91	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)
97		}
98	}
99	if len(unusedNames) == 0 && len(unpackContext.errs) == 0 {
100		return result, nil
101	}
102	return nil, unpackContext.reportUnusedNames(unusedNames)
103}
104
105func (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] == '[') {
113				continue
114			}
115		}
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
134			}
135			continue
136		}
137
138		ctx.propertyMap[name] = &packedProperty{property, false}
139		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
148			}
149			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			}
165		}
166	}
167
168	return len(ctx.errs) == nOldErrors
169}
170
171func fieldPath(prefix, fieldName string) string {
172	if prefix == "" {
173		return fieldName
174	}
175	return prefix + "." + fieldName
176}
177
178func (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) {
184	structType := structValue.Type()
185
186	for i := 0; i < structValue.NumField(); i++ {
187		fieldValue := structValue.Field(i)
188		field := structType.Field(i)
189
190		// 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
199		if field.PkgPath != "" {
200			// This is an unexported field, so just skip it.
201			continue
202		}
203
204		propertyName := fieldPath(namePrefix, PropertyNameForField(field.Name))
205
206		if !fieldValue.CanSet() {
207			panic(fmt.Errorf("field %s is not settable", propertyName))
208		}
209
210		// Get the property value if it was specified.
211		packedProperty, propertyIsSet := ctx.propertyMap[propertyName]
212
213		origFieldValue := fieldValue
214
215		// 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.
217		// TODO(ccross): we don't validate types inside nil struct pointers
218		// Move type validation to a function that runs on each factory once
219		switch kind := fieldValue.Kind(); kind {
220		case reflect.Bool, reflect.String, reflect.Struct, reflect.Slice:
221			// Do nothing
222		case reflect.Interface:
223			if fieldValue.IsNil() {
224				panic(fmt.Errorf("field %s contains a nil interface", propertyName))
225			}
226			fieldValue = fieldValue.Elem()
227			elemType := fieldValue.Type()
228			if elemType.Kind() != reflect.Ptr {
229				panic(fmt.Errorf("field %s contains a non-pointer interface", propertyName))
230			}
231			fallthrough
232		case reflect.Ptr:
233			switch ptrKind := fieldValue.Type().Elem().Kind(); ptrKind {
234			case reflect.Struct:
235				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
239					fieldValue = reflect.New(fieldValue.Type().Elem())
240					origFieldValue.Set(fieldValue)
241				}
242				fieldValue = fieldValue.Elem()
243			case reflect.Bool, reflect.Int64, reflect.String:
244				// Nothing
245			default:
246				panic(fmt.Errorf("field %s contains a pointer to %s", propertyName, ptrKind))
247			}
248
249		case reflect.Int, reflect.Uint:
250			if !HasTag(field, "blueprint", "mutated") {
251				panic(fmt.Errorf(`int field %s must be tagged blueprint:"mutated"`, propertyName))
252			}
253
254		default:
255			panic(fmt.Errorf("unsupported kind for field %s: %s", propertyName, kind))
256		}
257
258		if field.Anonymous && isStruct(fieldValue.Type()) {
259			ctx.unpackToStruct(namePrefix, fieldValue)
260			continue
261		}
262
263		if !propertyIsSet {
264			// This property wasn't specified.
265			continue
266		}
267
268		packedProperty.used = true
269		property := packedProperty.property
270
271		if HasTag(field, "blueprint", "mutated") {
272			if !ctx.addError(
273				&UnpackError{
274					fmt.Errorf("mutated field %s cannot be set in a Blueprint file", propertyName),
275					property.ColonPos,
276				}) {
277				return
278			}
279			continue
280		}
281
282		if isStruct(fieldValue.Type()) {
283			if property.Value.Eval().Type() != parser.MapType {
284				ctx.addError(&UnpackError{
285					fmt.Errorf("can't assign %s value to map property %q",
286						property.Value.Type(), property.Name),
287					property.Value.Pos(),
288				})
289				continue
290			}
291			ctx.unpackToStruct(propertyName, fieldValue)
292			if len(ctx.errs) >= maxUnpackErrors {
293				return
294			}
295		} else if isSlice(fieldValue.Type()) {
296			if unpackedValue, ok := ctx.unpackToSlice(propertyName, property, fieldValue.Type()); ok {
297				ExtendBasicType(fieldValue, unpackedValue, Append)
298			}
299			if len(ctx.errs) >= maxUnpackErrors {
300				return
301			}
302
303		} else {
304			unpackedValue, err := propertyToValue(fieldValue.Type(), property)
305			if err != nil && !ctx.addError(err) {
306				return
307			}
308			ExtendBasicType(fieldValue, unpackedValue, Append)
309		}
310	}
311}
312
313// unpackSlice creates a value of a given slice type from the property which should be a list
314func (ctx *unpackContext) unpackToSlice(
315	sliceName string, property *parser.Property, sliceType reflect.Type) (reflect.Value, bool) {
316	propValueAsList, ok := property.Value.Eval().(*parser.List)
317	if !ok {
318		ctx.addError(&UnpackError{
319			fmt.Errorf("can't assign %s value to list property %q",
320				property.Value.Type(), property.Name),
321			property.Value.Pos(),
322		})
323		return reflect.MakeSlice(sliceType, 0, 0), false
324	}
325	exprs := propValueAsList.Values
326	value := reflect.MakeSlice(sliceType, 0, len(exprs))
327	if len(exprs) == 0 {
328		return value, true
329	}
330
331	// The function to construct an item value depends on the type of list elements.
332	var getItemFunc func(*parser.Property, reflect.Type) (reflect.Value, bool)
333	switch exprs[0].Type() {
334	case parser.BoolType, parser.StringType, parser.Int64Type:
335		getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
336			value, err := propertyToValue(t, property)
337			if err != nil {
338				ctx.addError(err)
339				return value, false
340			}
341			return value, true
342		}
343	case parser.ListType:
344		getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
345			return ctx.unpackToSlice(property.Name, property, t)
346		}
347	case parser.MapType:
348		getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
349			itemValue := reflect.New(t).Elem()
350			ctx.unpackToStruct(property.Name, itemValue)
351			return itemValue, true
352		}
353	case parser.NotEvaluatedType:
354		getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) {
355			return reflect.New(t), false
356		}
357	default:
358		panic(fmt.Errorf("bizarre property expression type: %v", exprs[0].Type()))
359	}
360
361	itemProperty := &parser.Property{NamePos: property.NamePos, ColonPos: property.ColonPos}
362	elemType := sliceType.Elem()
363	isPtr := elemType.Kind() == reflect.Ptr
364
365	for i, expr := range exprs {
366		itemProperty.Name = sliceName + "[" + strconv.Itoa(i) + "]"
367		itemProperty.Value = expr
368		if packedProperty, ok := ctx.propertyMap[itemProperty.Name]; ok {
369			packedProperty.used = true
370		}
371		if isPtr {
372			if itemValue, ok := getItemFunc(itemProperty, elemType.Elem()); ok {
373				ptrValue := reflect.New(itemValue.Type())
374				ptrValue.Elem().Set(itemValue)
375				value = reflect.Append(value, ptrValue)
376			}
377		} else {
378			if itemValue, ok := getItemFunc(itemProperty, elemType); ok {
379				value = reflect.Append(value, itemValue)
380			}
381		}
382	}
383	return value, true
384}
385
386// propertyToValue creates a value of a given value type from the property.
387func propertyToValue(typ reflect.Type, property *parser.Property) (reflect.Value, error) {
388	var value reflect.Value
389	var baseType reflect.Type
390	isPtr := typ.Kind() == reflect.Ptr
391	if isPtr {
392		baseType = typ.Elem()
393	} else {
394		baseType = typ
395	}
396
397	switch kind := baseType.Kind(); kind {
398	case reflect.Bool:
399		b, ok := property.Value.Eval().(*parser.Bool)
400		if !ok {
401			return value, &UnpackError{
402				fmt.Errorf("can't assign %s value to bool property %q",
403					property.Value.Type(), property.Name),
404				property.Value.Pos(),
405			}
406		}
407		value = reflect.ValueOf(b.Value)
408
409	case reflect.Int64:
410		b, ok := property.Value.Eval().(*parser.Int64)
411		if !ok {
412			return value, &UnpackError{
413				fmt.Errorf("can't assign %s value to int64 property %q",
414					property.Value.Type(), property.Name),
415				property.Value.Pos(),
416			}
417		}
418		value = reflect.ValueOf(b.Value)
419
420	case reflect.String:
421		s, ok := property.Value.Eval().(*parser.String)
422		if !ok {
423			return value, &UnpackError{
424				fmt.Errorf("can't assign %s value to string property %q",
425					property.Value.Type(), property.Name),
426				property.Value.Pos(),
427			}
428		}
429		value = reflect.ValueOf(s.Value)
430
431	default:
432		return value, &UnpackError{
433			fmt.Errorf("cannot assign %s value %s to %s property %s", property.Value.Type(), property.Value, kind, typ),
434			property.NamePos}
435	}
436
437	if isPtr {
438		ptrValue := reflect.New(value.Type())
439		ptrValue.Elem().Set(value)
440		return ptrValue, nil
441	}
442	return value, nil
443}
444