Repository: google/yamlfmt
Pull request #54 (2022-10-05 14:42:25)

Support disallowing YAML anchors and aliases
Fixes https://github.com/google/yamlfmt/issues/51

This is a fairly rough first attempt. It adds a "during action" to compliment the before- and after-actions, which operates on the parsed YAML node instead of raw bytes.

Let me know what you think.


Patch:
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000..e565ec3
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,39 @@
+name: Github Actions for Releases
+
+on:
+  push:
+    tags:
+      - "*"
+
+jobs:
+  release:
+    runs-on: ubuntu-latest
+    if: startsWith(github.ref, 'refs/tags/')
+    strategy:
+      matrix:
+        version: ['1.19']
+    env:
+      GORELEASER_CURRENT_TAG: ${{ github.ref_name }}
+    steps:
+      - name: Checkout repo
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - name: Setup Golang
+        uses: actions/setup-go@v3
+        with:
+          go-version: ${{ matrix.version }}
+          check-latest: true
+          cache: true
+      - name: Build and package with GoReleaser
+        uses: goreleaser/goreleaser-action@v3
+        with:
+          distribution: goreleaser
+          version: latest
+          args: --skip-publish
+      - name: Release
+        uses: softprops/action-gh-release@v1
+        with:
+          files: |
+            dist/checksums.txt
+            dist/*.tar.gz
diff --git a/.gitignore b/.gitignore
index 2bf3c64..1aebf3a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,9 @@
+# Sometimes use these for testing
 .yamlfmt
+tmp
+
+# Goreleaser build folder
 dist/
 
+# vscode settings
 .vscode
diff --git a/formatters/basic/README.md b/formatters/basic/README.md
index f911524..73544ab 100644
--- a/formatters/basic/README.md
+++ b/formatters/basic/README.md
@@ -11,3 +11,4 @@ The basic formatter is a barebones formatter that simply takes the data provided
 | `line_ending`            | `lf` or `crlf` | `crlf` on Windows, `lf` otherwise | Parse and write the file with "lf" or "crlf" line endings. This setting will be overwritten by the global `line_ending`. |
 | `emoji_support`          | bool           | false   | Support encoding utf-8 emojis |
 | `retain_line_breaks`     | bool           | false   | Retain line breaks in formatted yaml |
+| `disallow_anchors`       | bool           | false   | If true, reject any YAML anchors or aliases found in the document. |
diff --git a/formatters/basic/config.go b/formatters/basic/config.go
index 6e3775b..108a99a 100644
--- a/formatters/basic/config.go
+++ b/formatters/basic/config.go
@@ -26,6 +26,7 @@ type Config struct {
 	EmojiSupport         bool                   `mapstructure:"emoji_support"`
 	LineEnding           yamlfmt.LineBreakStyle `mapstructure:"line_ending"`
 	RetainLineBreaks     bool                   `mapstructure:"retain_line_breaks"`
+	DisallowAnchors      bool                   `mapstructure:"disallow_anchors"`
 }
 
 func DefaultConfig() *Config {
diff --git a/formatters/basic/factory.go b/formatters/basic/factory.go
index 6655b77..eb536b0 100644
--- a/formatters/basic/factory.go
+++ b/formatters/basic/factory.go
@@ -38,7 +38,8 @@ func (f *BasicFormatterFactory) NewFormatter(configData map[string]interface{})
 
 func newFormatter(config *Config) yamlfmt.Formatter {
 	return &BasicFormatter{
-		Config:   config,
-		Features: ConfigureFeaturesFromConfig(config),
+		Config:       config,
+		Features:     ConfigureFeaturesFromConfig(config),
+		YAMLFeatures: ConfigureYAMLFeaturesFromConfig(config),
 	}
 }
diff --git a/formatters/basic/features.go b/formatters/basic/features.go
index 3edf0d8..a879061 100644
--- a/formatters/basic/features.go
+++ b/formatters/basic/features.go
@@ -16,6 +16,7 @@ package basic
 
 import (
 	"github.com/google/yamlfmt"
+	"github.com/google/yamlfmt/internal/anchors"
 	"github.com/google/yamlfmt/internal/hotfix"
 )
 
@@ -59,3 +60,11 @@ func ConfigureFeaturesFromConfig(config *Config) yamlfmt.FeatureList {
 	}
 	return features
 }
+
+func ConfigureYAMLFeaturesFromConfig(config *Config) YAMLFeatureList {
+	var features YAMLFeatureList
+	if config.DisallowAnchors {
+		features = append(features, anchors.Check)
+	}
+	return features
+}
diff --git a/formatters/basic/formatter.go b/formatters/basic/formatter.go
index c07264a..77600c9 100644
--- a/formatters/basic/formatter.go
+++ b/formatters/basic/formatter.go
@@ -26,8 +26,9 @@ import (
 const BasicFormatterType string = "basic"
 
 type BasicFormatter struct {
-	Config   *Config
-	Features yamlfmt.FeatureList
+	Config       *Config
+	Features     yamlfmt.FeatureList
+	YAMLFeatures YAMLFeatureList
 }
 
 // yamlfmt.Formatter interface
@@ -59,6 +60,13 @@ func (f *BasicFormatter) Format(input []byte) ([]byte, error) {
 		documents = append(documents, docNode)
 	}
 
+	// Run all YAML features.
+	for _, d := range documents {
+		if err := f.YAMLFeatures.ApplyFeatures(d); err != nil {
+			return nil, err
+		}
+	}
+
 	var b bytes.Buffer
 	e := yaml.NewEncoder(&b)
 	e.SetIndent(f.Config.Indent)
@@ -77,3 +85,16 @@ func (f *BasicFormatter) Format(input []byte) ([]byte, error) {
 
 	return resultYaml, nil
 }
+
+type YAMLFeatureList []YAMLFeatureFunc
+
+func (fl YAMLFeatureList) ApplyFeatures(node yaml.Node) error {
+	for _, f := range fl {
+		if err := f(node); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+type YAMLFeatureFunc func(yaml.Node) error
diff --git a/internal/anchors/check.go b/internal/anchors/check.go
new file mode 100644
index 0000000..8c33f6d
--- /dev/null
+++ b/internal/anchors/check.go
@@ -0,0 +1,37 @@
+// Copyright 2022 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 anchors
+
+import (
+	"errors"
+	"fmt"
+
+	"gopkg.in/yaml.v3"
+)
+
+func Check(n yaml.Node) error {
+	if n.Kind == yaml.AliasNode {
+		return errors.New("alias node found")
+	}
+	if n.Anchor != "" {
+		return fmt.Errorf("node references anchor %q", n.Anchor)
+	}
+	for _, c := range n.Content {
+		if err := Check(*c); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/internal/hotfix/unicode.go b/internal/hotfix/unicode.go
index 49acdd2..4374eaf 100644
--- a/internal/hotfix/unicode.go
+++ b/internal/hotfix/unicode.go
@@ -34,7 +34,7 @@ func ParseUnicodePoints(content []byte) ([]byte, error) {
 
 	var err error
 	for err != errEndOfBuffer {
-		if p.peek() == '\\' {
+		if p.peek() == '\\' && p.peekAhead(1) == 'U' {
 			err = p.parseUTF8CodePoint()
 			continue
 		}
@@ -49,7 +49,7 @@ func ParseUnicodePoints(content []byte) ([]byte, error) {
 type unicodeParser struct {
 	buf []byte
 	out []byte
-	pos int
+	pos uint
 }
 
 var (
@@ -61,6 +61,10 @@ func (p *unicodeParser) peek() byte {
 	return p.buf[p.pos]
 }
 
+func (p *unicodeParser) peekAhead(n uint) byte {
+	return p.buf[p.pos+n]
+}
+
 func (p *unicodeParser) write() {
 	p.out = append(p.out, p.peek())
 }
@@ -71,7 +75,7 @@ func (p *unicodeParser) writeArbitrary(b []byte) {
 
 func (p *unicodeParser) next() error {
 	p.pos++
-	if p.pos == len(p.buf) {
+	if p.pos == uint(len(p.buf)) {
 		return errEndOfBuffer
 	}
 	return nil
diff --git a/yamlfmt.go b/yamlfmt.go
index 386e731..34dfe7e 100644
--- a/yamlfmt.go
+++ b/yamlfmt.go
@@ -111,8 +111,11 @@ type FeatureApplyError struct {
 }
 
 func (e *FeatureApplyError) Error() string {
-	action := "Before"
-	if e.mode == FeatureApplyAfter {
+	var action string
+	switch e.mode {
+	case FeatureApplyBefore:
+		action = "Before"
+	case FeatureApplyAfter:
 		action = "After"
 	}
 	return fmt.Sprintf("Feature %s %sAction failed with error: %v", e.featureName, action, e.err)


Test patch:
diff --git a/internal/anchors/check_test.go b/internal/anchors/check_test.go
new file mode 100644
index 0000000..b5ecaa6
--- /dev/null
+++ b/internal/anchors/check_test.go
@@ -0,0 +1,66 @@
+// Copyright 2022 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 anchors_test
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/google/yamlfmt/internal/anchors"
+	"gopkg.in/yaml.v3"
+)
+
+func TestCheck(t *testing.T) {
+	for _, c := range []struct {
+		desc    string
+		in      string
+		wantErr bool
+	}{{
+		desc:    "no anchors",
+		in:      "foo: bar",
+		wantErr: false,
+	}, {
+		desc: "anchor",
+		// From https://support.atlassian.com/bitbucket-cloud/docs/yaml-anchors/
+		in: `
+definitions:
+  steps:
+    - step: &build-test
+        name: Build and test
+        script:
+          - mvn package
+        artifacts:
+          - target/**
+
+pipelines:
+  branches:
+    develop:
+      - step: *build-test
+    main:
+      - step: *build-test
+`,
+		wantErr: true,
+	}} {
+		t.Run(c.desc, func(t *testing.T) {
+			var docNode yaml.Node
+			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 {
+				t.Errorf("Check() error = %v, wantErr %v", err, c.wantErr)
+			}
+		})
+	}
+}
diff --git a/internal/hotfix/unicode_test.go b/internal/hotfix/unicode_test.go
index 1e9814e..35a4979 100644
--- a/internal/hotfix/unicode_test.go
+++ b/internal/hotfix/unicode_test.go
@@ -37,6 +37,11 @@ func TestParseEmoji(t *testing.T) {
 			yamlStr:     "a: 😼 👑\n",
 			expectedStr: "a: \"😼 👑\"\n",
 		},
+		{
+			name:        "retains backslashes",
+			yamlStr:     "a: \\look at my backslash\n",
+			expectedStr: "a: \\look at my backslash\n",
+		},
 	}
 
 	f := &basic.BasicFormatter{Config: basic.DefaultConfig()}