Repository: goccy/go-yaml (go)

Issue:
Incorrect string representation of MappingNode in flow style
**Describe the bug**

The `String` representation of `MappingNode` in flow style is incorrect.

**To Reproduce**

1. Parse a yaml in map type into a `MappingNode`.
1. Set `SetIsFlowStyle` to true.
1. Call the `String` method of the node.

https://go.dev/play/p/yViy2dYoVUk

```yaml
volumes:
  name: config-volume
  configMap:
    name: app-config
```

```go
package main

import (
	"fmt"

	"github.com/goccy/go-yaml/ast"
	"github.com/goccy/go-yaml/parser"
)

func main() {
	a, err := parser.ParseFile("data.yaml", 0)
	if err != nil {
		panic(err)
	}
	d := a.Docs[0]
	n := d.Body.(*ast.MappingNode)
	n.SetIsFlowStyle(true)
	fmt.Println(n.String())
}
```

The actual output is:

```yaml
{volumes:
{name: config-volume, configMap:
{name: app-config}}}
```

**Expected behavior**

The output should be:

```yaml
{volumes: {name: config-volume, configMap: {name: app-config}}}
```

**Version Variables**
 - Go version: `go1.23.4 darwin/arm64`
 - go-yaml's Version: `1.15.15` (This is not a regression; I have tested with older versions as well)


Pull request:
Fix string representation of MappingNode in flow style
fixes #622

- [x] Describe the purpose for which you created this PR.  
- [x] Create test code that corresponds to the modification

Patch:
diff --git a/ast/ast.go b/ast/ast.go
index 6855a69..3882850 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -1362,6 +1362,7 @@ type MappingValueNode struct {
 	Key          MapKeyNode
 	Value        Node
 	FootComment  *CommentGroupNode
+	IsFlowStyle  bool
 }
 
 // Replace replace value node.
@@ -1398,6 +1399,7 @@ func (n *MappingValueNode) AddColumn(col int) {
 
 // SetIsFlowStyle set value to IsFlowStyle field recursively.
 func (n *MappingValueNode) SetIsFlowStyle(isFlow bool) {
+	n.IsFlowStyle = isFlow
 	switch value := n.Value.(type) {
 	case *MappingNode:
 		value.SetIsFlowStyle(isFlow)
@@ -1436,7 +1438,7 @@ func (n *MappingValueNode) toString() string {
 	keyComment := n.Key.GetComment()
 	if _, ok := n.Value.(ScalarNode); ok {
 		return fmt.Sprintf("%s%s: %s", space, n.Key.String(), n.Value.String())
-	} else if keyIndentLevel < valueIndentLevel {
+	} else if keyIndentLevel < valueIndentLevel && !n.IsFlowStyle {
 		if keyComment != nil {
 			return fmt.Sprintf(
 				"%s%s: %s\n%s",
diff --git a/decode.go b/decode.go
index c93868f..dadb3e7 100644
--- a/decode.go
+++ b/decode.go
@@ -545,75 +545,6 @@ func (d *Decoder) nodeToValue(node ast.Node) (any, error) {
 	return nil, nil
 }
 
-func (d *Decoder) resolveAlias(node ast.Node) (ast.Node, error) {
-	d.stepIn()
-	defer d.stepOut()
-	if d.isExceededMaxDepth() {
-		return nil, ErrExceededMaxDepth
-	}
-
-	switch n := node.(type) {
-	case *ast.MappingNode:
-		for idx, v := range n.Values {
-			value, err := d.resolveAlias(v)
-			if err != nil {
-				return nil, err
-			}
-			n.Values[idx], _ = value.(*ast.MappingValueNode)
-		}
-	case *ast.TagNode:
-		value, err := d.resolveAlias(n.Value)
-		if err != nil {
-			return nil, err
-		}
-		n.Value = value
-	case *ast.MappingKeyNode:
-		value, err := d.resolveAlias(n.Value)
-		if err != nil {
-			return nil, err
-		}
-		n.Value = value
-	case *ast.MappingValueNode:
-		if n.Key.IsMergeKey() && n.Value.Type() == ast.AliasType {
-			value, err := d.resolveAlias(n.Value)
-			if err != nil {
-				return nil, err
-			}
-			keyColumn := n.Key.GetToken().Position.Column
-			requiredColumn := keyColumn + 2
-			value.AddColumn(requiredColumn)
-			n.Value = value
-		} else {
-			key, err := d.resolveAlias(n.Key)
-			if err != nil {
-				return nil, err
-			}
-			n.Key, _ = key.(ast.MapKeyNode)
-			value, err := d.resolveAlias(n.Value)
-			if err != nil {
-				return nil, err
-			}
-			n.Value = value
-		}
-	case *ast.SequenceNode:
-		for idx, v := range n.Values {
-			value, err := d.resolveAlias(v)
-			if err != nil {
-				return nil, err
-			}
-			n.Values[idx] = value
-		}
-	case *ast.AliasNode:
-		aliasName := n.Value.GetToken().Value
-		node := d.anchorNodeMap[aliasName]
-		if node == nil {
-			return nil, fmt.Errorf("cannot find anchor by alias name %s", aliasName)
-		}
-		return d.resolveAlias(node)
-	}
-	return node, nil
-}
-
 func (d *Decoder) getMapNode(node ast.Node, isMerge bool) (ast.MapNode, error) {
 	d.stepIn()
 	defer d.stepOut()
@@ -758,36 +689,17 @@ func (d *Decoder) deleteStructKeys(structType reflect.Type, unknownFields map[st
 }
 
 func (d *Decoder) unmarshalableDocument(node ast.Node) ([]byte, error) {
-	var err error
-	node, err = d.resolveAlias(node)
-	if err != nil {
-		return nil, err
-	}
-	doc := format.FormatNode(node)
+	doc := format.FormatNodeWithResolvedAlias(node, d.anchorNodeMap)
 	return []byte(doc), nil
 }
 
-func (d *Decoder) unmarshalableText(node ast.Node) ([]byte, bool, error) {
-	var err error
-	node, err = d.resolveAlias(node)
-	if err != nil {
-		return nil, false, err
-	}
-	if node.Type() == ast.AnchorType {
-		node = node.(*ast.AnchorNode).Value
+func (d *Decoder) unmarshalableText(node ast.Node) ([]byte, bool) {
+	doc := format.FormatNodeWithResolvedAlias(node, d.anchorNodeMap)
+	var v string
+	if err := Unmarshal([]byte(doc), &v); err != nil {
+		return nil, false
 	}
-	switch n := node.(type) {
-	case *ast.StringNode:
-		return []byte(n.Value), true, nil
-	case *ast.LiteralNode:
-		return []byte(n.Value.GetToken().Value), true, nil
-	default:
-		scalar, ok := n.(ast.ScalarNode)
-		if ok {
-			return []byte(fmt.Sprint(scalar.GetValue())), true, nil
-		}
-	}
-	return nil, false, nil
+	return []byte(v), true
 }
 
 type jsonUnmarshaler interface {
@@ -924,10 +836,7 @@ func (d *Decoder) decodeByUnmarshaler(ctx context.Context, dst reflect.Value, sr
 	}
 
 	if unmarshaler, isText := iface.(encoding.TextUnmarshaler); isText {
-		b, ok, err := d.unmarshalableText(src)
-		if err != nil {
-			return err
-		}
+		b, ok := d.unmarshalableText(src)
 		if ok {
 			if err := unmarshaler.UnmarshalText(b); err != nil {
 				return err
diff --git a/internal/format/format.go b/internal/format/format.go
index 5391f1c..5a6e63e 100644
--- a/internal/format/format.go
+++ b/internal/format/format.go
@@ -7,6 +7,16 @@ import (
 	"github.com/goccy/go-yaml/token"
 )
 
+func FormatNodeWithResolvedAlias(n ast.Node, anchorNodeMap map[string]ast.Node) string {
+	tk := n.GetToken()
+	if tk == nil {
+		return ""
+	}
+	formatter := newFormatter(tk, hasComment(n))
+	formatter.anchorNodeMap = anchorNodeMap
+	return formatter.format(n)
+}
+
 func FormatNode(n ast.Node) string {
 	tk := n.GetToken()
 	if tk == nil {
@@ -124,6 +134,7 @@ func hasComment(n ast.Node) bool {
 type Formatter struct {
 	existsComment    bool
 	tokenToOriginMap map[*token.Token]string
+	anchorNodeMap    map[string]ast.Node
 }
 
 func newFormatter(tk *token.Token, existsComment bool) *Formatter {
@@ -294,6 +305,19 @@ func (f *Formatter) formatAnchor(n *ast.AnchorNode) string {
 }
 
 func (f *Formatter) formatAlias(n *ast.AliasNode) string {
+	if f.anchorNodeMap != nil {
+		node := f.anchorNodeMap[n.Value.GetToken().Value]
+		if node != nil {
+			formatted := f.formatNode(node)
+			// If formatted text contains newline characters, indentation needs to be considered.
+			if strings.Contains(formatted, "\n") {
+				// If the first character is not a newline, the first line should be output without indentation.
+				isIgnoredFirstLine := !strings.HasPrefix(formatted, "\n")
+				formatted = f.addIndentSpace(n.GetToken().Position.IndentNum, formatted, isIgnoredFirstLine)
+			}
+			return formatted
+		}
+	}
 	return f.origin(n.Start) + f.formatNode(n.Value)
 }
 
@@ -385,7 +409,7 @@ func (f *Formatter) trimIndentSpace(trimIndentNum int, v string) string {
 	}
 	lines := strings.Split(normalizeNewLineChars(v), "\n")
 	out := make([]string, 0, len(lines))
-	for _, line := range strings.Split(v, "\n") {
+	for _, line := range lines {
 		var cnt int
 		out = append(out, strings.TrimLeftFunc(line, func(r rune) bool {
 			cnt++
@@ -395,6 +419,23 @@ func (f *Formatter) trimIndentSpace(trimIndentNum int, v string) string {
 	return strings.Join(out, "\n")
 }
 
+func (f *Formatter) addIndentSpace(indentNum int, v string, isIgnoredFirstLine bool) string {
+	if indentNum == 0 {
+		return v
+	}
+	indent := strings.Repeat(" ", indentNum)
+	lines := strings.Split(normalizeNewLineChars(v), "\n")
+	out := make([]string, 0, len(lines))
+	for idx, line := range lines {
+		if line == "" || (isIgnoredFirstLine && idx == 0) {
+			out = append(out, line)
+			continue
+		}
+		out = append(out, indent+line)
+	}
+	return strings.Join(out, "\n")
+}
+
 // normalizeNewLineChars normalize CRLF and CR to LF.
 func normalizeNewLineChars(v string) string {
 	return strings.ReplaceAll(strings.ReplaceAll(v, "\r\n", "\n"), "\r", "\n")


Test patch:
diff --git a/decode_test.go b/decode_test.go
index 6790395..f26af65 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -3151,6 +3151,41 @@ func TestMapKeyCustomUnmarshaler(t *testing.T) {
 	}
 }
 
+type bytesUnmershalerWithMapAlias struct{}
+
+func (*bytesUnmershalerWithMapAlias) UnmarshalYAML(b []byte) error {
+	expected := strings.TrimPrefix(`
+stuff:
+  bar:
+    - one
+    - two
+
+`, "\n")
+	if string(b) != expected {
+		return fmt.Errorf("failed to decode: expected:\n[%s]\nbut got:\n[%s]\n", expected, string(b))
+	}
+	return nil
+}
+
+func TestBytesUnmarshalerWithMapAlias(t *testing.T) {
+	yml := `
+x-foo: &data
+  bar:
+    - one
+    - two
+
+foo:
+  stuff: *data
+`
+	type T struct {
+		Foo bytesUnmershalerWithMapAlias `yaml:"foo"`
+	}
+	var v T
+	if err := yaml.Unmarshal([]byte(yml), &v); err != nil {
+		t.Fatal(err)
+	}
+}
+
 func TestDecoderPreservesDefaultValues(t *testing.T) {
 	type nested struct {
 		Val string `yaml:"val"`
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 487bfe1..cae4bff 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -1715,6 +1715,70 @@ multiple:
 	}
 }
 
+func TestInFlowStyle(t *testing.T) {
+	type inFlowStyle interface {
+		SetIsFlowStyle(bool)
+	}
+
+	tests := []struct {
+		source string
+		expect string
+	}{
+		{
+			`
+  - foo
+  - bar
+  - baz
+`,
+			`[foo, bar, baz]
+`,
+		},
+		{
+			`
+foo: bar
+baz: fizz
+`,
+			`{foo: bar, baz: fizz}
+`,
+		},
+		{
+			`
+foo:
+  - bar
+  - baz
+  - fizz: buzz
+`,
+			`{foo: [bar, baz, {fizz: buzz}]}
+`,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.source, func(t *testing.T) {
+			f, err := parser.ParseBytes([]byte(test.source), parser.ParseComments)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if len(f.Docs) != 1 {
+				t.Fatal("failed to parse content")
+			}
+
+			ifs, ok := f.Docs[0].Body.(inFlowStyle)
+			if !ok {
+				t.Fatalf("failed to get inFlowStyle. got: %T", f.Docs[0].Body)
+			}
+			ifs.SetIsFlowStyle(true)
+
+			got := f.String()
+
+			if got != test.expect {
+				t.Fatalf("failed to parse comment:\nexpected:\n%s\ngot:\n%s", test.expect, got)
+			}
+		})
+	}
+}
+
 func TestNodePath(t *testing.T) {
 	yml := `
 a: # commentA