Repository: google/yamlfmt
Pull request #247 (2025-05-18 14:33:44)

pkg/yaml: correct alias keys
As outlined in #247, the yaml library did not initially handle this edge case of using an alias node as a key properly. This PR adds functionality to correct that. In case there is some other edge case where this breaks, the `disable_alias_key_correction` formatter config option was added.


Patch:
diff --git a/docs/config-file.md b/docs/config-file.md
index ddb246c..c467f01 100644
--- a/docs/config-file.md
+++ b/docs/config-file.md
@@ -88,6 +88,7 @@ The basic formatter is a barebones formatter that simply takes the data provided
 | `strip_directives`          | bool           | false   | [YAML Directives](https://yaml.org/spec/1.2.2/#3234-directives) are not supported by this formatter. This feature will attempt to strip the directives before formatting and put them back. [Use this feature at your own risk.](#strip_directives) |
 | `array_indent`              | int            | = indent | Set a different indentation level for block sequences specifically. |
 | `indent_root_array`         | int            | false   | Tells the formatter to indent an array that is at the lowest indentation level of the document. |
+| `disable_alias_key_correction` | bool        | false   | Disables functionality to fix alias nodes being used as keys. See #247 for details. |
 
 ## Additional Notes
 
diff --git a/formatters/basic/config.go b/formatters/basic/config.go
index 82e67eb..54e7223 100644
--- a/formatters/basic/config.go
+++ b/formatters/basic/config.go
@@ -21,22 +21,23 @@ import (
 )
 
 type Config struct {
-	Indent                 int                    `mapstructure:"indent"`
-	IncludeDocumentStart   bool                   `mapstructure:"include_document_start"`
-	LineEnding             yamlfmt.LineBreakStyle `mapstructure:"line_ending"`
-	LineLength             int                    `mapstructure:"max_line_length"`
-	RetainLineBreaks       bool                   `mapstructure:"retain_line_breaks"`
-	RetainLineBreaksSingle bool                   `mapstructure:"retain_line_breaks_single"`
-	DisallowAnchors        bool                   `mapstructure:"disallow_anchors"`
-	ScanFoldedAsLiteral    bool                   `mapstructure:"scan_folded_as_literal"`
-	IndentlessArrays       bool                   `mapstructure:"indentless_arrays"`
-	DropMergeTag           bool                   `mapstructure:"drop_merge_tag"`
-	PadLineComments        int                    `mapstructure:"pad_line_comments"`
-	TrimTrailingWhitespace bool                   `mapstructure:"trim_trailing_whitespace"`
-	EOFNewline             bool                   `mapstructure:"eof_newline"`
-	StripDirectives        bool                   `mapstructure:"strip_directives"`
-	ArrayIndent            int                    `mapstructure:"array_indent"`
-	IndentRootArray        bool                   `mapstructure:"indent_root_array"`
+	Indent                    int                    `mapstructure:"indent"`
+	IncludeDocumentStart      bool                   `mapstructure:"include_document_start"`
+	LineEnding                yamlfmt.LineBreakStyle `mapstructure:"line_ending"`
+	LineLength                int                    `mapstructure:"max_line_length"`
+	RetainLineBreaks          bool                   `mapstructure:"retain_line_breaks"`
+	RetainLineBreaksSingle    bool                   `mapstructure:"retain_line_breaks_single"`
+	DisallowAnchors           bool                   `mapstructure:"disallow_anchors"`
+	ScanFoldedAsLiteral       bool                   `mapstructure:"scan_folded_as_literal"`
+	IndentlessArrays          bool                   `mapstructure:"indentless_arrays"`
+	DropMergeTag              bool                   `mapstructure:"drop_merge_tag"`
+	PadLineComments           int                    `mapstructure:"pad_line_comments"`
+	TrimTrailingWhitespace    bool                   `mapstructure:"trim_trailing_whitespace"`
+	EOFNewline                bool                   `mapstructure:"eof_newline"`
+	StripDirectives           bool                   `mapstructure:"strip_directives"`
+	ArrayIndent               int                    `mapstructure:"array_indent"`
+	IndentRootArray           bool                   `mapstructure:"indent_root_array"`
+	DisableAliasKeyCorrection bool                   `mapstructure:"disable_alias_key_correction"`
 }
 
 func DefaultConfig() *Config {
diff --git a/formatters/basic/formatter.go b/formatters/basic/formatter.go
index 14e05d1..e338b9e 100644
--- a/formatters/basic/formatter.go
+++ b/formatters/basic/formatter.go
@@ -20,8 +20,8 @@ import (
 	"errors"
 	"io"
 
-	"github.com/google/yamlfmt/pkg/yaml"
 	"github.com/google/yamlfmt"
+	"github.com/google/yamlfmt/pkg/yaml"
 	"github.com/mitchellh/mapstructure"
 )
 
@@ -119,6 +119,13 @@ func (f *BasicFormatter) getNewEncoder(buf *bytes.Buffer) *yaml.Encoder {
 	}
 	e.SetIndentRootArray(f.Config.IndentRootArray)
 
+	// Yes I know I could SetCorrectAliasKeys(!f.Config.DisableAliasKeyCorrection)
+	// but I know myself and I know I'll get confused and have to go look up
+	// the source again next time I look and forget.
+	if !f.Config.DisableAliasKeyCorrection {
+		e.SetCorrectAliasKeys(true)
+	}
+
 	return e
 }
 
diff --git a/pkg/yaml/apic.go b/pkg/yaml/apic.go
index 76cc0e4..e58b481 100644
--- a/pkg/yaml/apic.go
+++ b/pkg/yaml/apic.go
@@ -214,6 +214,11 @@ func yaml_emitter_set_pad_line_comments(emitter *yaml_emitter_t, pad_line_commen
 	emitter.pad_line_comments = pad_line_comments
 }
 
+// Set correct alias keys.
+func yaml_emitter_set_correct_alias_keys(emitter *yaml_emitter_t, correct_alias_keys bool) {
+	emitter.correct_alias_keys = correct_alias_keys
+}
+
 ///*
 // * Destroy a token object.
 // */
diff --git a/pkg/yaml/emitterc.go b/pkg/yaml/emitterc.go
index 6a93554..5840b06 100644
--- a/pkg/yaml/emitterc.go
+++ b/pkg/yaml/emitterc.go
@@ -906,6 +906,11 @@ func yaml_emitter_emit_alias(emitter *yaml_emitter_t, event *yaml_event_t) bool
 	if !yaml_emitter_process_anchor(emitter) {
 		return false
 	}
+	if emitter.correct_alias_keys && len(emitter.states) > 1 && emitter.states[len(emitter.states)-2] == yaml_EMIT_BLOCK_MAPPING_KEY_STATE {
+		if !put(emitter, ' ') {
+			return false
+		}
+	}
 	emitter.state = emitter.states[len(emitter.states)-1]
 	emitter.states = emitter.states[:len(emitter.states)-1]
 	return true
diff --git a/pkg/yaml/yaml.go b/pkg/yaml/yaml.go
index b336163..5fcdc1a 100644
--- a/pkg/yaml/yaml.go
+++ b/pkg/yaml/yaml.go
@@ -343,6 +343,11 @@ func (e *Encoder) SetPadLineComments(padLineComments int) {
 	yaml_emitter_set_pad_line_comments(&e.encoder.emitter, padLineComments)
 }
 
+// SetCorrectAliasKeys enables alias key syntax correction.
+func (e *Encoder) SetCorrectAliasKeys(correctAliasKeys bool) {
+	yaml_emitter_set_correct_alias_keys(&e.encoder.emitter, correctAliasKeys)
+}
+
 // Close closes the encoder by writing any remaining data.
 // It does not write a stream terminating string "...".
 func (e *Encoder) Close() (err error) {
diff --git a/pkg/yaml/yamlh.go b/pkg/yaml/yamlh.go
index 92ab866..a8ccef7 100644
--- a/pkg/yaml/yamlh.go
+++ b/pkg/yaml/yamlh.go
@@ -741,6 +741,7 @@ type yaml_emitter_t struct {
 	explicit_document_start   bool         // Force an explicit document start
 	assume_folded_as_literal  bool         // Assume blocks were scanned as literals
 	pad_line_comments         int          // The number of spaces to insert before line comments.
+	correct_alias_keys        bool         // Whether to correct alias nodes used as map keys.
 
 	state  yaml_emitter_state_t   // The current emitter state.
 	states []yaml_emitter_state_t // The stack of states.


Test patch:
diff --git a/integrationtest/command/command_test.go b/integrationtest/command/command_test.go
index 1fff369..09a76f2 100644
--- a/integrationtest/command/command_test.go
+++ b/integrationtest/command/command_test.go
@@ -163,3 +163,12 @@ func TestPatternFile(t *testing.T) {
 		Update:  *updateFlag,
 	}.Run(t)
 }
+
+func TestAliasKeyCorrection(t *testing.T) {
+	TestCase{
+		Dir: "alias_key_correction",
+		// TODO: Change arguments to match your test case.
+		Command: yamlfmtWithArgs("."),
+		Update:  *updateFlag,
+	}.Run(t)
+}
diff --git a/integrationtest/command/testdata/alias_key_correction/after/x.yaml b/integrationtest/command/testdata/alias_key_correction/after/x.yaml
new file mode 100755
index 0000000..f53e71d
--- /dev/null
+++ b/integrationtest/command/testdata/alias_key_correction/after/x.yaml
@@ -0,0 +1,18 @@
+# example taken from https://github.com/google/yamlfmt/issues/242
+x-config-values:
+  x-domain-ext: &domain-ext www.example.com
+  x-domain-int: &domain-int example.local
+ingresses:
+  *domain-ext :
+    name: example
+    hosts:
+      - domainName: *domain-ext
+        paths:
+          - path: "/"
+            pathType: Prefix
+  *domain-int :
+    name: example-local
+    hosts:
+      - paths:
+          - path: "/api"
+            pathType: Prefix
diff --git a/integrationtest/command/testdata/alias_key_correction/before/x.yaml b/integrationtest/command/testdata/alias_key_correction/before/x.yaml
new file mode 100644
index 0000000..f53e71d
--- /dev/null
+++ b/integrationtest/command/testdata/alias_key_correction/before/x.yaml
@@ -0,0 +1,18 @@
+# example taken from https://github.com/google/yamlfmt/issues/242
+x-config-values:
+  x-domain-ext: &domain-ext www.example.com
+  x-domain-int: &domain-int example.local
+ingresses:
+  *domain-ext :
+    name: example
+    hosts:
+      - domainName: *domain-ext
+        paths:
+          - path: "/"
+            pathType: Prefix
+  *domain-int :
+    name: example-local
+    hosts:
+      - paths:
+          - path: "/api"
+            pathType: Prefix
diff --git a/integrationtest/command/testdata/alias_key_correction/stdout/stderr.txt b/integrationtest/command/testdata/alias_key_correction/stdout/stderr.txt
new file mode 100644
index 0000000..e69de29
diff --git a/integrationtest/command/testdata/alias_key_correction/stdout/stdout.txt b/integrationtest/command/testdata/alias_key_correction/stdout/stdout.txt
new file mode 100644
index 0000000..e69de29
diff --git a/integrationtest/command/testdata/print_conf_file/stdout/stdout.txt b/integrationtest/command/testdata/print_conf_file/stdout/stdout.txt
index c63453f..4a077bb 100644
--- a/integrationtest/command/testdata/print_conf_file/stdout/stdout.txt
+++ b/integrationtest/command/testdata/print_conf_file/stdout/stdout.txt
@@ -14,6 +14,7 @@ output_format: default
 regex_exclude: []
 formatter:
     array_indent: 0
+    disable_alias_key_correction: false
     disallow_anchors: false
     drop_merge_tag: false
     eof_newline: false
diff --git a/integrationtest/command/testdata/print_conf_flags/stdout/stdout.txt b/integrationtest/command/testdata/print_conf_flags/stdout/stdout.txt
index a17cd94..2299a2e 100644
--- a/integrationtest/command/testdata/print_conf_flags/stdout/stdout.txt
+++ b/integrationtest/command/testdata/print_conf_flags/stdout/stdout.txt
@@ -13,6 +13,7 @@ output_format: default
 regex_exclude: []
 formatter:
     array_indent: 0
+    disable_alias_key_correction: false
     disallow_anchors: false
     drop_merge_tag: false
     eof_newline: false
diff --git a/integrationtest/command/testdata/print_conf_flags_and_file/stdout/stdout.txt b/integrationtest/command/testdata/print_conf_flags_and_file/stdout/stdout.txt
index 0f4e277..75d75fc 100644
--- a/integrationtest/command/testdata/print_conf_flags_and_file/stdout/stdout.txt
+++ b/integrationtest/command/testdata/print_conf_flags_and_file/stdout/stdout.txt
@@ -14,6 +14,7 @@ output_format: default
 regex_exclude: []
 formatter:
     array_indent: 0
+    disable_alias_key_correction: false
     disallow_anchors: false
     drop_merge_tag: false
     eof_newline: false
diff --git a/pkg/yaml/formattest/encode_test.go b/pkg/yaml/formattest/encode_test.go
index ae67f74..2af2195 100644
--- a/pkg/yaml/formattest/encode_test.go
+++ b/pkg/yaml/formattest/encode_test.go
@@ -134,3 +134,14 @@ func TestImplicitDocumentStartComments(t *testing.T) {
 		},
 	}.Run(t)
 }
+
+func TestAliasKey(t *testing.T) {
+	formatTestCase{
+		name:             "alias_key",
+		folder:           "alias_key",
+		configureDecoder: noopDecoder,
+		configureEncoder: func(enc *yaml.Encoder) {
+			enc.SetCorrectAliasKeys(true)
+		},
+	}.Run(t)
+}
diff --git a/pkg/yaml/formattest/testcase.go b/pkg/yaml/formattest/testcase.go
index 932da51..812da50 100644
--- a/pkg/yaml/formattest/testcase.go
+++ b/pkg/yaml/formattest/testcase.go
@@ -28,10 +28,12 @@ import (
 
 type decoderConfigureFunc func(*yaml.Decoder)
 
-func noopDecoder(dec *yaml.Decoder) {}
+func noopDecoder(_ *yaml.Decoder) {}
 
 type encoderConfigureFunc func(*yaml.Encoder)
 
+// func noopEncoder(_ *yaml.Encoder) {}
+
 type formatTestCase struct {
 	name             string
 	folder           string
diff --git a/pkg/yaml/formattest/testdata/alias_key/expected.yaml b/pkg/yaml/formattest/testdata/alias_key/expected.yaml
new file mode 100644
index 0000000..6f10834
--- /dev/null
+++ b/pkg/yaml/formattest/testdata/alias_key/expected.yaml
@@ -0,0 +1,4 @@
+map_key: &anchor hello
+map:
+    *anchor :
+        key: value
diff --git a/pkg/yaml/formattest/testdata/alias_key/input.yaml b/pkg/yaml/formattest/testdata/alias_key/input.yaml
new file mode 100644
index 0000000..6f10834
--- /dev/null
+++ b/pkg/yaml/formattest/testdata/alias_key/input.yaml
@@ -0,0 +1,4 @@
+map_key: &anchor hello
+map:
+    *anchor :
+        key: value