// Copyright 2014 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package proptools import ( "fmt" "reflect" "sort" "strconv" "strings" "text/scanner" "github.com/google/blueprint/parser" ) const maxUnpackErrors = 10 type UnpackError struct { Err error Pos scanner.Position } func (e *UnpackError) Error() string { return fmt.Sprintf("%s: %s", e.Pos, e.Err) } // packedProperty helps to track properties usage (`used` will be true) type packedProperty struct { property *parser.Property used bool } // unpackContext keeps compound names and their values in a map. It is initialized from // parsed properties. type unpackContext struct { propertyMap map[string]*packedProperty errs []error } // UnpackProperties populates the list of runtime values ("property structs") from the parsed properties. // If a property a.b.c has a value, a field with the matching name in each runtime value is initialized // from it. See PropertyNameForField for field and property name matching. // For instance, if the input contains // // { foo: "abc", bar: {x: 1},} // // and a runtime value being has been declared as // // var v struct { Foo string; Bar int } // // then v.Foo will be set to "abc" and v.Bar will be set to 1 // (cf. unpack_test.go for further examples) // // The type of a receiving field has to match the property type, i.e., a bool/int/string field // can be set from a property with bool/int/string value, a struct can be set from a map (only the // matching fields are set), and an slice can be set from a list. // If a field of a runtime value has been already set prior to the UnpackProperties, the new value // is appended to it (see somewhat inappropriately named ExtendBasicType). // The same property can initialize fields in multiple runtime values. It is an error if any property // value was not used to initialize at least one field. func UnpackProperties(properties []*parser.Property, objects ...interface{}) (map[string]*parser.Property, []error) { var unpackContext unpackContext unpackContext.propertyMap = make(map[string]*packedProperty) if !unpackContext.buildPropertyMap("", properties) { return nil, unpackContext.errs } for _, obj := range objects { valueObject := reflect.ValueOf(obj) if !isStructPtr(valueObject.Type()) { panic(fmt.Errorf("properties must be *struct, got %s", valueObject.Type())) } unpackContext.unpackToStruct("", valueObject.Elem()) if len(unpackContext.errs) >= maxUnpackErrors { return nil, unpackContext.errs } } // Gather property map, and collect any unused properties. // Avoid reporting subproperties of unused properties. result := make(map[string]*parser.Property) var unusedNames []string for name, v := range unpackContext.propertyMap { if v.used { result[name] = v.property } else { unusedNames = append(unusedNames, name) } } if len(unusedNames) == 0 && len(unpackContext.errs) == 0 { return result, nil } return nil, unpackContext.reportUnusedNames(unusedNames) } func (ctx *unpackContext) reportUnusedNames(unusedNames []string) []error { sort.Strings(unusedNames) var lastReported string for _, name := range unusedNames { // if 'foo' has been reported, ignore 'foo\..*' and 'foo\[.*' if lastReported != "" { trimmed := strings.TrimPrefix(name, lastReported) if trimmed != name && (trimmed[0] == '.' || trimmed[0] == '[') { continue } } ctx.errs = append(ctx.errs, &UnpackError{ fmt.Errorf("unrecognized property %q", name), ctx.propertyMap[name].property.ColonPos}) lastReported = name } return ctx.errs } func (ctx *unpackContext) buildPropertyMap(prefix string, properties []*parser.Property) bool { nOldErrors := len(ctx.errs) for _, property := range properties { name := fieldPath(prefix, property.Name) if first, present := ctx.propertyMap[name]; present { ctx.addError( &UnpackError{fmt.Errorf("property %q already defined", name), property.ColonPos}) if ctx.addError( &UnpackError{fmt.Errorf("<-- previous definition here"), first.property.ColonPos}) { return false } continue } ctx.propertyMap[name] = &packedProperty{property, false} switch propValue := property.Value.Eval().(type) { case *parser.Map: ctx.buildPropertyMap(name, propValue.Properties) case *parser.List: // If it is a list, unroll it unless its elements are of primitive type // (no further mapping will be needed in that case, so we avoid cluttering // the map). if len(propValue.Values) == 0 { continue } if t := propValue.Values[0].Type(); t == parser.StringType || t == parser.Int64Type || t == parser.BoolType { continue } itemProperties := make([]*parser.Property, len(propValue.Values), len(propValue.Values)) for i, expr := range propValue.Values { itemProperties[i] = &parser.Property{ Name: property.Name + "[" + strconv.Itoa(i) + "]", NamePos: property.NamePos, ColonPos: property.ColonPos, Value: expr, } } if !ctx.buildPropertyMap(prefix, itemProperties) { return false } } } return len(ctx.errs) == nOldErrors } func fieldPath(prefix, fieldName string) string { if prefix == "" { return fieldName } return prefix + "." + fieldName } func (ctx *unpackContext) addError(e error) bool { ctx.errs = append(ctx.errs, e) return len(ctx.errs) < maxUnpackErrors } func (ctx *unpackContext) unpackToStruct(namePrefix string, structValue reflect.Value) { structType := structValue.Type() for i := 0; i < structValue.NumField(); i++ { fieldValue := structValue.Field(i) field := structType.Field(i) // In Go 1.7, runtime-created structs are unexported, so it's not // possible to create an exported anonymous field with a generated // type. So workaround this by special-casing "BlueprintEmbed" to // behave like an anonymous field for structure unpacking. if field.Name == "BlueprintEmbed" { field.Name = "" field.Anonymous = true } if field.PkgPath != "" { // This is an unexported field, so just skip it. continue } propertyName := fieldPath(namePrefix, PropertyNameForField(field.Name)) if !fieldValue.CanSet() { panic(fmt.Errorf("field %s is not settable", propertyName)) } // Get the property value if it was specified. packedProperty, propertyIsSet := ctx.propertyMap[propertyName] origFieldValue := fieldValue // To make testing easier we validate the struct field's type regardless // of whether or not the property was specified in the parsed string. // TODO(ccross): we don't validate types inside nil struct pointers // Move type validation to a function that runs on each factory once switch kind := fieldValue.Kind(); kind { case reflect.Bool, reflect.String, reflect.Struct, reflect.Slice: // Do nothing case reflect.Interface: if fieldValue.IsNil() { panic(fmt.Errorf("field %s contains a nil interface", propertyName)) } fieldValue = fieldValue.Elem() elemType := fieldValue.Type() if elemType.Kind() != reflect.Ptr { panic(fmt.Errorf("field %s contains a non-pointer interface", propertyName)) } fallthrough case reflect.Ptr: switch ptrKind := fieldValue.Type().Elem().Kind(); ptrKind { case reflect.Struct: if fieldValue.IsNil() && (propertyIsSet || field.Anonymous) { // Instantiate nil struct pointers // Set into origFieldValue in case it was an interface, in which case // fieldValue points to the unsettable pointer inside the interface fieldValue = reflect.New(fieldValue.Type().Elem()) origFieldValue.Set(fieldValue) } fieldValue = fieldValue.Elem() case reflect.Bool, reflect.Int64, reflect.String: // Nothing default: panic(fmt.Errorf("field %s contains a pointer to %s", propertyName, ptrKind)) } case reflect.Int, reflect.Uint: if !HasTag(field, "blueprint", "mutated") { panic(fmt.Errorf(`int field %s must be tagged blueprint:"mutated"`, propertyName)) } default: panic(fmt.Errorf("unsupported kind for field %s: %s", propertyName, kind)) } if field.Anonymous && isStruct(fieldValue.Type()) { ctx.unpackToStruct(namePrefix, fieldValue) continue } if !propertyIsSet { // This property wasn't specified. continue } packedProperty.used = true property := packedProperty.property if HasTag(field, "blueprint", "mutated") { if !ctx.addError( &UnpackError{ fmt.Errorf("mutated field %s cannot be set in a Blueprint file", propertyName), property.ColonPos, }) { return } continue } if isStruct(fieldValue.Type()) { if property.Value.Eval().Type() != parser.MapType { ctx.addError(&UnpackError{ fmt.Errorf("can't assign %s value to map property %q", property.Value.Type(), property.Name), property.Value.Pos(), }) continue } ctx.unpackToStruct(propertyName, fieldValue) if len(ctx.errs) >= maxUnpackErrors { return } } else if isSlice(fieldValue.Type()) { if unpackedValue, ok := ctx.unpackToSlice(propertyName, property, fieldValue.Type()); ok { ExtendBasicType(fieldValue, unpackedValue, Append) } if len(ctx.errs) >= maxUnpackErrors { return } } else { unpackedValue, err := propertyToValue(fieldValue.Type(), property) if err != nil && !ctx.addError(err) { return } ExtendBasicType(fieldValue, unpackedValue, Append) } } } // unpackSlice creates a value of a given slice type from the property which should be a list func (ctx *unpackContext) unpackToSlice( sliceName string, property *parser.Property, sliceType reflect.Type) (reflect.Value, bool) { propValueAsList, ok := property.Value.Eval().(*parser.List) if !ok { ctx.addError(&UnpackError{ fmt.Errorf("can't assign %s value to list property %q", property.Value.Type(), property.Name), property.Value.Pos(), }) return reflect.MakeSlice(sliceType, 0, 0), false } exprs := propValueAsList.Values value := reflect.MakeSlice(sliceType, 0, len(exprs)) if len(exprs) == 0 { return value, true } // The function to construct an item value depends on the type of list elements. var getItemFunc func(*parser.Property, reflect.Type) (reflect.Value, bool) switch exprs[0].Type() { case parser.BoolType, parser.StringType, parser.Int64Type: getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) { value, err := propertyToValue(t, property) if err != nil { ctx.addError(err) return value, false } return value, true } case parser.ListType: getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) { return ctx.unpackToSlice(property.Name, property, t) } case parser.MapType: getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) { itemValue := reflect.New(t).Elem() ctx.unpackToStruct(property.Name, itemValue) return itemValue, true } case parser.NotEvaluatedType: getItemFunc = func(property *parser.Property, t reflect.Type) (reflect.Value, bool) { return reflect.New(t), false } default: panic(fmt.Errorf("bizarre property expression type: %v", exprs[0].Type())) } itemProperty := &parser.Property{NamePos: property.NamePos, ColonPos: property.ColonPos} elemType := sliceType.Elem() isPtr := elemType.Kind() == reflect.Ptr for i, expr := range exprs { itemProperty.Name = sliceName + "[" + strconv.Itoa(i) + "]" itemProperty.Value = expr if packedProperty, ok := ctx.propertyMap[itemProperty.Name]; ok { packedProperty.used = true } if isPtr { if itemValue, ok := getItemFunc(itemProperty, elemType.Elem()); ok { ptrValue := reflect.New(itemValue.Type()) ptrValue.Elem().Set(itemValue) value = reflect.Append(value, ptrValue) } } else { if itemValue, ok := getItemFunc(itemProperty, elemType); ok { value = reflect.Append(value, itemValue) } } } return value, true } // propertyToValue creates a value of a given value type from the property. func propertyToValue(typ reflect.Type, property *parser.Property) (reflect.Value, error) { var value reflect.Value var baseType reflect.Type isPtr := typ.Kind() == reflect.Ptr if isPtr { baseType = typ.Elem() } else { baseType = typ } switch kind := baseType.Kind(); kind { case reflect.Bool: b, ok := property.Value.Eval().(*parser.Bool) if !ok { return value, &UnpackError{ fmt.Errorf("can't assign %s value to bool property %q", property.Value.Type(), property.Name), property.Value.Pos(), } } value = reflect.ValueOf(b.Value) case reflect.Int64: b, ok := property.Value.Eval().(*parser.Int64) if !ok { return value, &UnpackError{ fmt.Errorf("can't assign %s value to int64 property %q", property.Value.Type(), property.Name), property.Value.Pos(), } } value = reflect.ValueOf(b.Value) case reflect.String: s, ok := property.Value.Eval().(*parser.String) if !ok { return value, &UnpackError{ fmt.Errorf("can't assign %s value to string property %q", property.Value.Type(), property.Name), property.Value.Pos(), } } value = reflect.ValueOf(s.Value) default: return value, &UnpackError{ fmt.Errorf("cannot assign %s value %s to %s property %s", property.Value.Type(), property.Value, kind, typ), property.NamePos} } if isPtr { ptrValue := reflect.New(value.Type()) ptrValue.Elem().Set(value) return ptrValue, nil } return value, nil }