Repository: goccy/go-yaml
Pull request #60 (2019-12-13 06:16:31)

Fix disallow unknown field error
Related #56 
I noticed error when use inline field. I'm sorry...

Ex)

```go
type Child struct {
	B int
}

var v struct {
	Child `yaml:",inline"`
	A     string
}
yml := `---
a: string
b: 0
`

d := yaml.NewDecoder(strings.NewReader(yml), yaml.DisallowUnknownField())
err := d.Decode(&v)
fmt.Println(err)
// OUT:
// [3:1] unknown field "b"
//        1 | ---
//        2 | a: string
//     >  3 | b: 0
//           ^

```


Patch:
diff --git a/decode.go b/decode.go
index dd739d1..7ced1c4 100644
--- a/decode.go
+++ b/decode.go
@@ -269,6 +269,45 @@ func errTypeMismatch(dstType, srcType reflect.Type) *typeError {
 	return &typeError{dstType: dstType, srcType: srcType}
 }
 
+type unknownFieldError struct {
+	err error
+}
+
+func (e *unknownFieldError) Error() string {
+	return e.err.Error()
+}
+
+func errUnknownField(msg string, tk *token.Token) *unknownFieldError {
+	return &unknownFieldError{err: errors.ErrSyntax(msg, tk)}
+}
+
+func (d *Decoder) deleteStructKeys(structValue reflect.Value, unknownFields map[string]ast.Node) error {
+	strType := structValue.Type()
+	structFieldMap, err := structFieldMap(strType)
+	if err != nil {
+		return errors.Wrapf(err, "failed to create struct field map")
+	}
+
+	for j := 0; j < strType.NumField(); j++ {
+		field := structValue.Type().Field(j)
+		if isIgnoredStructField(field) {
+			continue
+		}
+
+		structField, ok := structFieldMap[field.Name]
+		if !ok {
+			continue
+		}
+
+		if structField.IsInline {
+			d.deleteStructKeys(structValue.FieldByName(field.Name), unknownFields)
+		} else {
+			delete(unknownFields, structField.RenderName)
+		}
+	}
+	return nil
+}
+
 func (d *Decoder) decodeValue(dst reflect.Value, src ast.Node) error {
 	valueType := dst.Type()
 	if unmarshaler, ok := dst.Addr().Interface().(BytesUnmarshaler); ok {
@@ -578,10 +617,22 @@ func (d *Decoder) decodeStruct(dst reflect.Value, src ast.Node) error {
 						te.structFieldName = &fieldName
 					}
 					foundErr = te
+					continue
+				}
+
+				if d.disallowUnknownField {
+					var ufe *unknownFieldError
+					if !xerrors.As(err, &ufe) {
+						foundErr = err
+						continue
+					}
+
+					if err = d.deleteStructKeys(fieldValue, unknownFields); err != nil {
+						return errors.Wrapf(err, "cannot delete struct keys")
+					}
 				} else {
-					foundErr = err
+					continue
 				}
-				continue
 			}
 			d.setDefaultValueIfConflicted(newFieldValue, structFieldMap)
 			fieldValue.Set(d.castToAssignableValue(newFieldValue, fieldValue.Type()))
@@ -637,7 +688,7 @@ func (d *Decoder) decodeStruct(dst reflect.Value, src ast.Node) error {
 	}
 	if len(unknownFields) != 0 && d.disallowUnknownField {
 		for key, node := range unknownFields {
-			return errors.ErrSyntax(fmt.Sprintf(`unknown field "%s"`, key), node.GetToken())
+			return errUnknownField(fmt.Sprintf(`unknown field "%s"`, key), node.GetToken())
 		}
 	}
 	if foundErr != nil {


Test patch:
diff --git a/decode_test.go b/decode_test.go
index e42ada5..e7e4360 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -1194,6 +1194,47 @@ b_yaml: b_yaml_value
 	}
 }
 
+func TestDecoder_DisallowUnknownField(t *testing.T) {
+	t.Run("different level keys with same name", func(t *testing.T) {
+		var v struct {
+			C Child `yaml:"c"`
+		}
+		yml := `---
+b: 1
+c:
+  b: 1
+`
+
+		err := yaml.NewDecoder(strings.NewReader(yml), yaml.DisallowUnknownField()).Decode(&v)
+		if err == nil {
+			t.Fatalf("error expected")
+		}
+	})
+	t.Run("inline", func(t *testing.T) {
+		var v struct {
+			Child `yaml:",inline"`
+			A     string `yaml:"a"`
+		}
+		yml := `---
+a: a
+b: 1
+`
+
+		if err := yaml.NewDecoder(strings.NewReader(yml), yaml.DisallowUnknownField()).Decode(&v); err != nil {
+			t.Fatalf(`parsing should succeed: %s`, err)
+		}
+		if v.A != "a" {
+			t.Fatalf("v.A should be `a`, got `%s`", v.A)
+		}
+		if v.B != 1 {
+			t.Fatalf("v.B should be 1, got %d", v.B)
+		}
+		if v.C != 0 {
+			t.Fatalf("v.C should be 0, got %d", v.C)
+		}
+	})
+}
+
 func TestDecoder_DefaultValues(t *testing.T) {
 	v := struct {
 		A string `yaml:"a"`