Repository: google/yamlfmt (go)

Issue:
sequence style
Is there a way to enforce block style sequences?

```yaml
foo:
  - a
  - b
  - c
```

as opposed to 

```yaml
foo: ["a", "b", "c"]
```


Pull request:
formatters/basic: add force_array_style
This PR adds the `force_array_style` feature to the basic formatter, which allows the user to force either `block` or `flow` sequence style in the formatted document.

Fixes #243 

Patch:
diff --git a/docs/config-file.md b/docs/config-file.md
index c467f01..41dd782 100644
--- a/docs/config-file.md
+++ b/docs/config-file.md
@@ -89,6 +89,7 @@ The basic formatter is a barebones formatter that simply takes the data provided
 | `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. |
+| `force_array_style`         | `flow`, `block`, or empty | empty   | If set, forces arrays to be output in a particular style, either `flow` (`[]`) or `block` (`- x`). If unset, the style from the original document is used. |
 
 ## Additional Notes
 
diff --git a/formatters/basic/config.go b/formatters/basic/config.go
index 54e7223..2a32d05 100644
--- a/formatters/basic/config.go
+++ b/formatters/basic/config.go
@@ -18,26 +18,28 @@ import (
 	"runtime"
 
 	"github.com/google/yamlfmt"
+	yamlFeatures "github.com/google/yamlfmt/formatters/basic/features"
 )
 
 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"`
-	DisableAliasKeyCorrection bool                   `mapstructure:"disable_alias_key_correction"`
+	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"`
+	ForceArrayStyle           yamlFeatures.SequenceStyle `mapstructure:"force_array_style"`
 }
 
 func DefaultConfig() *Config {
diff --git a/formatters/basic/features.go b/formatters/basic/features.go
index 88356de..fc933f4 100644
--- a/formatters/basic/features.go
+++ b/formatters/basic/features.go
@@ -15,9 +15,8 @@
 package basic
 
 import (
-	"github.com/google/yamlfmt/pkg/yaml"
 	"github.com/google/yamlfmt"
-	"github.com/google/yamlfmt/formatters/basic/anchors"
+	yamlFeatures "github.com/google/yamlfmt/formatters/basic/features"
 	"github.com/google/yamlfmt/internal/features"
 	"github.com/google/yamlfmt/internal/hotfix"
 )
@@ -55,24 +54,19 @@ func ConfigureFeaturesFromConfig(config *Config) yamlfmt.FeatureList {
 	return configuredFeatures
 }
 
-// These features will directly use the `yaml.Node` type and
-// as such are specific to this formatter.
-type YAMLFeatureFunc func(yaml.Node) error
-type YAMLFeatureList []YAMLFeatureFunc
+func ConfigureYAMLFeaturesFromConfig(config *Config) yamlFeatures.YAMLFeatureList {
+	var featureList yamlFeatures.YAMLFeatureList
 
-func (y YAMLFeatureList) ApplyFeatures(node yaml.Node) error {
-	for _, f := range y {
-		if err := f(node); err != nil {
-			return err
-		}
+	if config.DisallowAnchors {
+		featureList = append(featureList, yamlFeatures.Check)
 	}
-	return nil
-}
 
-func ConfigureYAMLFeaturesFromConfig(config *Config) YAMLFeatureList {
-	var features YAMLFeatureList
-	if config.DisallowAnchors {
-		features = append(features, anchors.Check)
+	if config.ForceArrayStyle != "" {
+		featureList = append(
+			featureList,
+			yamlFeatures.FeatureForceSequenceStyle(config.ForceArrayStyle),
+		)
 	}
-	return features
+
+	return featureList
 }
diff --git a/formatters/basic/anchors/check.go b/formatters/basic/features/check.go
similarity index 98%
rename from formatters/basic/anchors/check.go
rename to formatters/basic/features/check.go
index 6c4d63a..368de5c 100644
--- a/formatters/basic/anchors/check.go
+++ b/formatters/basic/features/check.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package anchors
+package features
 
 import (
 	"errors"
diff --git a/formatters/basic/features/force_sequence.go b/formatters/basic/features/force_sequence.go
new file mode 100644
index 0000000..f3f956c
--- /dev/null
+++ b/formatters/basic/features/force_sequence.go
@@ -0,0 +1,43 @@
+// Copyright 2025 Google LLC
+//
+// 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 features
+
+import "github.com/google/yamlfmt/pkg/yaml"
+
+type SequenceStyle string
+
+const (
+	SequenceStyleBlock SequenceStyle = "block"
+	SequenceStyleFlow  SequenceStyle = "flow"
+)
+
+func FeatureForceSequenceStyle(style SequenceStyle) YAMLFeatureFunc {
+	var styleVal yaml.Style
+	if style == SequenceStyleFlow {
+		styleVal = yaml.FlowStyle
+	}
+	var forceStyle YAMLFeatureFunc
+	forceStyle = func(n yaml.Node) error {
+		var err error
+		for _, c := range n.Content {
+			if c.Kind == yaml.SequenceNode {
+				c.Style = styleVal
+			}
+			err = forceStyle(*c)
+		}
+		return err
+	}
+	return forceStyle
+}
diff --git a/formatters/basic/features/yaml_feature.go b/formatters/basic/features/yaml_feature.go
new file mode 100644
index 0000000..2657a53
--- /dev/null
+++ b/formatters/basic/features/yaml_feature.go
@@ -0,0 +1,31 @@
+// Copyright 2025 Google LLC
+//
+// 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 features
+
+import "github.com/google/yamlfmt/pkg/yaml"
+
+// These features will directly use the `yaml.Node` type and
+// as such are specific to this formatter.
+type YAMLFeatureFunc func(yaml.Node) error
+type YAMLFeatureList []YAMLFeatureFunc
+
+func (y YAMLFeatureList) ApplyFeatures(node yaml.Node) error {
+	for _, f := range y {
+		if err := f(node); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/formatters/basic/formatter.go b/formatters/basic/formatter.go
index e338b9e..f8c9b7b 100644
--- a/formatters/basic/formatter.go
+++ b/formatters/basic/formatter.go
@@ -21,6 +21,7 @@ import (
 	"io"
 
 	"github.com/google/yamlfmt"
+	yamlFeature "github.com/google/yamlfmt/formatters/basic/features"
 	"github.com/google/yamlfmt/pkg/yaml"
 	"github.com/mitchellh/mapstructure"
 )
@@ -30,7 +31,7 @@ const BasicFormatterType string = "basic"
 type BasicFormatter struct {
 	Config       *Config
 	Features     yamlfmt.FeatureList
-	YAMLFeatures YAMLFeatureList
+	YAMLFeatures yamlFeature.YAMLFeatureList
 }
 
 // yamlfmt.Formatter interface


Test patch:
diff --git a/formatters/basic/anchors/check_test.go b/formatters/basic/features/check_test.go
similarity index 91%
rename from formatters/basic/anchors/check_test.go
rename to formatters/basic/features/check_test.go
index c600763..5161748 100644
--- a/formatters/basic/anchors/check_test.go
+++ b/formatters/basic/features/check_test.go
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package anchors_test
+package features_test
 
 import (
 	"strings"
 	"testing"
 
+	"github.com/google/yamlfmt/formatters/basic/features"
 	"github.com/google/yamlfmt/pkg/yaml"
-	"github.com/google/yamlfmt/formatters/basic/anchors"
 )
 
 func TestCheck(t *testing.T) {
@@ -58,7 +58,7 @@ pipelines:
 			if err := yaml.NewDecoder(strings.NewReader(c.in)).Decode(&docNode); err != nil {
 				t.Fatalf("parse error: %v", err)
 			}
-			if err := anchors.Check(docNode); (err != nil) != c.wantErr {
+			if err := features.Check(docNode); (err != nil) != c.wantErr {
 				t.Errorf("Check() error = %v, wantErr %v", err, c.wantErr)
 			}
 		})
diff --git a/formatters/basic/formatter_test.go b/formatters/basic/formatter_test.go
index 17b4077..587b21d 100644
--- a/formatters/basic/formatter_test.go
+++ b/formatters/basic/formatter_test.go
@@ -20,13 +20,15 @@ import (
 
 	"github.com/google/yamlfmt"
 	"github.com/google/yamlfmt/formatters/basic"
+	"github.com/google/yamlfmt/formatters/basic/features"
 	"github.com/google/yamlfmt/internal/assert"
 )
 
 func newFormatter(config *basic.Config) *basic.BasicFormatter {
 	return &basic.BasicFormatter{
-		Config:   config,
-		Features: basic.ConfigureFeaturesFromConfig(config),
+		Config:       config,
+		Features:     basic.ConfigureFeaturesFromConfig(config),
+		YAMLFeatures: basic.ConfigureYAMLFeaturesFromConfig(config),
 	}
 }
 
@@ -401,3 +403,55 @@ func TestIndentRootArray(t *testing.T) {
 		t.Fatalf("expected: '%s', got: '%s'", expectedYml, result)
 	}
 }
+
+func TestForceFlowSequence(t *testing.T) {
+	config := basic.DefaultConfig()
+	config.ForceArrayStyle = features.SequenceStyleFlow
+	f := newFormatter(config)
+
+	yml := `a:
+  - 1
+  - 2
+  - 3
+b: [1, 2, 3]
+`
+	expectedYml := `a: [1, 2, 3]
+b: [1, 2, 3]
+`
+
+	result, err := f.Format([]byte(yml))
+	assert.NilErr(t, err)
+	resultStr := string(result)
+	if resultStr != expectedYml {
+		t.Fatalf("expected: '%s', got: '%s'", expectedYml, result)
+	}
+}
+
+func TestForceBlockSequence(t *testing.T) {
+	config := basic.DefaultConfig()
+	config.ForceArrayStyle = features.SequenceStyleBlock
+	f := newFormatter(config)
+
+	yml := `a:
+  - 1
+  - 2
+  - 3
+b: [1, 2, 3]
+`
+	expectedYml := `a:
+  - 1
+  - 2
+  - 3
+b:
+  - 1
+  - 2
+  - 3
+`
+
+	result, err := f.Format([]byte(yml))
+	assert.NilErr(t, err)
+	resultStr := string(result)
+	if resultStr != expectedYml {
+		t.Fatalf("expected: '%s', got: '%s'", expectedYml, result)
+	}
+}
diff --git a/integrationtest/command/command_test.go b/integrationtest/command/command_test.go
index 09a76f2..5cc34af 100644
--- a/integrationtest/command/command_test.go
+++ b/integrationtest/command/command_test.go
@@ -166,9 +166,24 @@ func TestPatternFile(t *testing.T) {
 
 func TestAliasKeyCorrection(t *testing.T) {
 	TestCase{
-		Dir: "alias_key_correction",
-		// TODO: Change arguments to match your test case.
+		Dir:     "alias_key_correction",
 		Command: yamlfmtWithArgs("."),
 		Update:  *updateFlag,
 	}.Run(t)
 }
+
+func TestForceBlockStyle(t *testing.T) {
+	TestCase{
+		Dir:     "force_block_style",
+		Command: yamlfmtWithArgs("-formatter force_array_style=block ."),
+		Update:  *updateFlag,
+	}.Run(t)
+}
+
+func TestForceFlowStyle(t *testing.T) {
+	TestCase{
+		Dir:     "force_flow_style",
+		Command: yamlfmtWithArgs("-formatter force_array_style=flow ."),
+		Update:  *updateFlag,
+	}.Run(t)
+}
diff --git a/integrationtest/command/testdata/force_block_style/after/x.yaml b/integrationtest/command/testdata/force_block_style/after/x.yaml
new file mode 100755
index 0000000..54bd6ae
--- /dev/null
+++ b/integrationtest/command/testdata/force_block_style/after/x.yaml
@@ -0,0 +1,8 @@
+a:
+  - 1
+  - 2
+  - 3
+b:
+  - 1
+  - 2
+  - 3
diff --git a/integrationtest/command/testdata/force_block_style/before/x.yaml b/integrationtest/command/testdata/force_block_style/before/x.yaml
new file mode 100644
index 0000000..2bd58f1
--- /dev/null
+++ b/integrationtest/command/testdata/force_block_style/before/x.yaml
@@ -0,0 +1,5 @@
+a:
+  - 1
+  - 2
+  - 3
+b: [1, 2, 3]
diff --git a/integrationtest/command/testdata/force_block_style/stdout/stderr.txt b/integrationtest/command/testdata/force_block_style/stdout/stderr.txt
new file mode 100644
index 0000000..e69de29
diff --git a/integrationtest/command/testdata/force_block_style/stdout/stdout.txt b/integrationtest/command/testdata/force_block_style/stdout/stdout.txt
new file mode 100644
index 0000000..e69de29
diff --git a/integrationtest/command/testdata/force_flow_style/after/x.yaml b/integrationtest/command/testdata/force_flow_style/after/x.yaml
new file mode 100755
index 0000000..a7c8449
--- /dev/null
+++ b/integrationtest/command/testdata/force_flow_style/after/x.yaml
@@ -0,0 +1,2 @@
+a: [1, 2, 3]
+b: [1, 2, 3]
diff --git a/integrationtest/command/testdata/force_flow_style/before/x.yaml b/integrationtest/command/testdata/force_flow_style/before/x.yaml
new file mode 100644
index 0000000..2bd58f1
--- /dev/null
+++ b/integrationtest/command/testdata/force_flow_style/before/x.yaml
@@ -0,0 +1,5 @@
+a:
+  - 1
+  - 2
+  - 3
+b: [1, 2, 3]
diff --git a/integrationtest/command/testdata/force_flow_style/stdout/stderr.txt b/integrationtest/command/testdata/force_flow_style/stdout/stderr.txt
new file mode 100644
index 0000000..e69de29
diff --git a/integrationtest/command/testdata/force_flow_style/stdout/stdout.txt b/integrationtest/command/testdata/force_flow_style/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 4a077bb..d6dadc6 100644
--- a/integrationtest/command/testdata/print_conf_file/stdout/stdout.txt
+++ b/integrationtest/command/testdata/print_conf_file/stdout/stdout.txt
@@ -18,6 +18,7 @@ formatter:
     disallow_anchors: false
     drop_merge_tag: false
     eof_newline: false
+    force_array_style: ""
     include_document_start: true
     indent: 2
     indent_root_array: false
diff --git a/integrationtest/command/testdata/print_conf_flags/stdout/stdout.txt b/integrationtest/command/testdata/print_conf_flags/stdout/stdout.txt
index 2299a2e..7dda069 100644
--- a/integrationtest/command/testdata/print_conf_flags/stdout/stdout.txt
+++ b/integrationtest/command/testdata/print_conf_flags/stdout/stdout.txt
@@ -17,6 +17,7 @@ formatter:
     disallow_anchors: false
     drop_merge_tag: false
     eof_newline: false
+    force_array_style: ""
     include_document_start: false
     indent: 2
     indent_root_array: 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 75d75fc..3aff161 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
@@ -18,6 +18,7 @@ formatter:
     disallow_anchors: false
     drop_merge_tag: false
     eof_newline: false
+    force_array_style: ""
     include_document_start: true
     indent: 2
     indent_root_array: false