Repository: goccy/go-yaml
Pull request #648 (2025-02-11 09:52:54)

Fix format with alias
resolve https://github.com/goccy/go-yaml/issues/638

codecov-commenter: :warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov.

## [Codecov](https://app.codecov.io/gh/goccy/go-yaml/pull/648?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 76.97%. Comparing base [(`c331468`)](https://app.codecov.io/gh/goccy/go-yaml/commit/c331468e5bb5e04670ba70a724084a567c01b748?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima) to head [(`855b26d`)](https://app.codecov.io/gh/goccy/go-yaml/commit/855b26d9e5b34ad0b965ea03dcfea75fa3b82952?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Masaaki+Goshima).

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #648      +/-   ##
==========================================
- Coverage   77.25%   76.97%   -0.28%     
==========================================
  Files          22       22              
  Lines        7914     7909       -5     
==========================================
- Hits         6114     6088      -26     
- Misses       1378     1406      +28     
+ Partials      422      415       -7     
```

</details>

Patch:
diff --git a/decode.go b/decode.go
index c93868f..ec57ff4 100644
--- a/decode.go
+++ b/decode.go
@@ -758,12 +758,7 @@ 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
 }
 
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"`