Repository: google/yamlfmt
Pull request #43 (2022-09-10 15:58:09)

yamlfmt: added and implemented functional features
While adding functionality for small edge cases or formatting differences, I found that the Formatter code was becoming harder to reason about. It would grow linearly with the number of little formatting things I had to do. I have adjusted the pattern to instead be a collection of functional features that can be added at-will.

Created a new `Feature` type that has a Before and After action. Also added the FeatureList type, with code to apply the before or after actions in order on byte slice input.

I converted all existing candidates in the formatter code to use this functional feature pattern instead.

google-cla[bot]: Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this [failed invocation](https://github.com/google/yamlfmt/pull/43/checks?check_run_id=8285816938) of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.
braydonk: This is me under my personal account (I made the fixes on my personal computer while I don't have access to my work one).

Patch:
diff --git a/formatters/basic/README.md b/formatters/basic/README.md
index ec07961..647cb22 100644
--- a/formatters/basic/README.md
+++ b/formatters/basic/README.md
@@ -10,3 +10,4 @@ The basic formatter is a barebones formatter that simply takes the data provided
 | `include_document_start` | bool           | false   | Include `---` at document start |
 | `line_ending`            | `lf` or `crlf` | `crlf` on Windows, `lf` otherwise | Parse and write the file with "lf" or "crlf" line endings |
 | `emoji_support`          | bool           | false   | Support encoding utf-8 emojis |
+| `retain_line_breaks`     | bool           | false   | Retain line breaks in formatted yaml |
diff --git a/formatters/basic/config.go b/formatters/basic/config.go
index 906eae6..3bcc602 100644
--- a/formatters/basic/config.go
+++ b/formatters/basic/config.go
@@ -25,6 +25,7 @@ type Config struct {
 	IncludeDocumentStart bool   `mapstructure:"include_document_start"`
 	EmojiSupport         bool   `mapstructure:"emoji_support"`
 	LineEnding           string `mapstructure:"line_ending"`
+	RetainLineBreaks     bool   `mapstructure:"retain_line_breaks"`
 }
 
 func DefaultConfig() *Config {
diff --git a/formatters/basic/factory.go b/formatters/basic/factory.go
index c3557a7..c43248a 100644
--- a/formatters/basic/factory.go
+++ b/formatters/basic/factory.go
@@ -26,7 +26,7 @@ func (f *BasicFormatterFactory) Type() string {
 }
 
 func (f *BasicFormatterFactory) NewDefault() yamlfmt.Formatter {
-	return &BasicFormatter{Config: DefaultConfig()}
+	return newFormatter(DefaultConfig())
 }
 
 func (f *BasicFormatterFactory) NewWithConfig(configData map[string]interface{}) (yamlfmt.Formatter, error) {
@@ -35,5 +35,12 @@ func (f *BasicFormatterFactory) NewWithConfig(configData map[string]interface{})
 	if err != nil {
 		return nil, err
 	}
-	return &BasicFormatter{Config: config}, nil
+	return newFormatter(config), nil
+}
+
+func newFormatter(config *Config) yamlfmt.Formatter {
+	return &BasicFormatter{
+		Config:   config,
+		Features: ConfigureFeaturesFromConfig(config),
+	}
 }
diff --git a/formatters/basic/features.go b/formatters/basic/features.go
new file mode 100644
index 0000000..780b895
--- /dev/null
+++ b/formatters/basic/features.go
@@ -0,0 +1,61 @@
+// 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 basic
+
+import (
+	"github.com/google/yamlfmt"
+	"github.com/google/yamlfmt/internal/hotfix"
+)
+
+var (
+	featIncludeDocumentStart = yamlfmt.Feature{
+		Name: "Include Document Start",
+		AfterAction: func(content []byte) ([]byte, error) {
+			documentStart := "---\n"
+			return append([]byte(documentStart), content...), nil
+		},
+	}
+	featEmojiSupport = yamlfmt.Feature{
+		Name:        "Emoji Support",
+		AfterAction: hotfix.ParseUnicodePoints,
+	}
+	featCRLFSupport = yamlfmt.Feature{
+		Name:         "CRLF Support",
+		BeforeAction: hotfix.StripCRBytes,
+		AfterAction:  hotfix.WriteCRLFBytes,
+	}
+)
+
+func ConfigureFeaturesFromConfig(config *Config) yamlfmt.FeatureList {
+	features := []yamlfmt.Feature{}
+	if config.EmojiSupport {
+		features = append(features, featEmojiSupport)
+	}
+	if config.IncludeDocumentStart {
+		features = append(features, featIncludeDocumentStart)
+	}
+	if config.LineEnding == yamlfmt.LineBreakStyleCRLF {
+		features = append(features, featCRLFSupport)
+	}
+	if config.RetainLineBreaks {
+		linebreakStr := "\n"
+		if config.LineEnding == yamlfmt.LineBreakStyleCRLF {
+			linebreakStr = "\r\n"
+		}
+		featLineBreak := hotfix.MakeFeatureRetainLineBreak(linebreakStr, config.Indent)
+		features = append(features, featLineBreak)
+	}
+	return features
+}
diff --git a/formatters/basic/formatter.go b/formatters/basic/formatter.go
index 144dd47..c07264a 100644
--- a/formatters/basic/formatter.go
+++ b/formatters/basic/formatter.go
@@ -20,48 +20,32 @@ import (
 	"io"
 
 	"github.com/google/yamlfmt"
-	"github.com/google/yamlfmt/internal/hotfix"
 	"gopkg.in/yaml.v3"
 )
 
 const BasicFormatterType string = "basic"
 
 type BasicFormatter struct {
-	Config *Config
+	Config   *Config
+	Features yamlfmt.FeatureList
 }
 
+// yamlfmt.Formatter interface
+
 func (f *BasicFormatter) Type() string {
 	return BasicFormatterType
 }
 
-func (f *BasicFormatter) Format(yamlContent []byte) ([]byte, error) {
-	var reader *bytes.Reader
-	if f.Config.LineEnding == yamlfmt.LineBreakStyleCRLF {
-		crStrippedContent := hotfix.StripCRBytes(yamlContent)
-		reader = bytes.NewReader(crStrippedContent)
-	} else {
-		reader = bytes.NewReader(yamlContent)
-	}
-
-	encodedContent, err := retainLineBreaks(reader, f.format)
+func (f *BasicFormatter) Format(input []byte) ([]byte, error) {
+	// Run all featurres with BeforeActions
+	yamlContent, err := f.Features.ApplyFeatures(input, yamlfmt.FeatureApplyBefore)
 	if err != nil {
 		return nil, err
 	}
 
-	if f.Config.IncludeDocumentStart {
-		encodedContent = withDocumentStart(encodedContent)
-	}
-	if f.Config.EmojiSupport {
-		encodedContent = hotfix.ParseUnicodePoints(encodedContent)
-	}
-	if f.Config.LineEnding == yamlfmt.LineBreakStyleCRLF {
-		encodedContent = hotfix.WriteCRLFBytes(encodedContent)
-	}
-	return encodedContent, nil
-}
-
-func (f *BasicFormatter) format(in io.Reader) (io.Reader, error) {
-	decoder := yaml.NewDecoder(in)
+	// Format the yaml content
+	reader := bytes.NewReader(yamlContent)
+	decoder := yaml.NewDecoder(reader)
 	documents := []yaml.Node{}
 	for {
 		var docNode yaml.Node
@@ -84,10 +68,12 @@ func (f *BasicFormatter) format(in io.Reader) (io.Reader, error) {
 			return nil, err
 		}
 	}
-	return &b, nil
-}
 
-func withDocumentStart(document []byte) []byte {
-	documentStart := "---\n"
-	return append([]byte(documentStart), document...)
+	// Run all features with AfterActions
+	resultYaml, err := f.Features.ApplyFeatures(b.Bytes(), yamlfmt.FeatureApplyAfter)
+	if err != nil {
+		return nil, err
+	}
+
+	return resultYaml, nil
 }
diff --git a/formatters/basic/line_break.go b/formatters/basic/line_break.go
deleted file mode 100644
index dcac368..0000000
--- a/formatters/basic/line_break.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package basic
-
-import (
-	"bufio"
-	"bytes"
-	"io"
-	"strings"
-)
-
-const lineBreakPlaceholder = "#magic___^_^___line"
-
-// retainLineBreaks keeps the line breaks.
-//
-// The basic idea is to insert/remove placeholder comments in the yaml document before and after the format process.
-func retainLineBreaks(in io.Reader, formatter func(io.Reader) (io.Reader, error)) ([]byte, error) {
-	var buf bytes.Buffer
-	scanner := bufio.NewScanner(in)
-	var padding paddinger
-	for scanner.Scan() {
-		txt := scanner.Text()
-		padding.adjust(indentLen(txt))
-		if strings.TrimSpace(txt) == "" { // line break or empty space line.
-			buf.WriteString(padding.String()) // prepend some padding incase literal multiline strings.
-			buf.WriteString(lineBreakPlaceholder)
-			buf.WriteByte('\n')
-			continue
-		}
-		buf.WriteString(txt)
-		buf.WriteByte('\n')
-	}
-	if err := scanner.Err(); err != nil {
-		return nil, err
-	}
-
-	formatted, err := formatter(&buf)
-	if err != nil {
-		return nil, err
-	}
-
-	buf.Reset() // reuse
-
-	scanner = bufio.NewScanner(formatted)
-	for scanner.Scan() {
-		txt := scanner.Text()
-		if strings.TrimSpace(txt) == "" {
-			// The basic yaml lib inserts newline when there is a comment(either placeholder or by user)
-			// followed by optional line breaks and a `---` multi-documents.
-			// To fix it, the empty line could only be inserted by us.
-			continue
-		}
-		if strings.HasPrefix(strings.TrimLeft(txt, " "), lineBreakPlaceholder) {
-			buf.WriteByte('\n')
-			continue
-		}
-		buf.WriteString(txt)
-		buf.WriteByte('\n')
-	}
-
-	return buf.Bytes(), scanner.Err()
-}
-
-func indentLen(txt string) int {
-	var cnt int
-	for i := 0; i < len(txt); i++ {
-		if txt[i] != ' ' { // Yaml only allows space to indent.
-			break
-		}
-		cnt++
-	}
-	return cnt
-}
-
-type paddinger struct {
-	strings.Builder
-}
-
-func (p *paddinger) adjust(size int) {
-	// Grows if the given size is larger than us and always return the max padding.
-	for diff := size - p.Len(); diff > 0; diff-- {
-		p.WriteByte(' ')
-	}
-}
diff --git a/internal/hotfix/crlf.go b/internal/hotfix/crlf.go
index f8cba6e..6f8d943 100644
--- a/internal/hotfix/crlf.go
+++ b/internal/hotfix/crlf.go
@@ -1,16 +1,32 @@
+// 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 hotfix
 
-func StripCRBytes(crlfContent []byte) []byte {
+// yamlfmt.FeatureFunc
+func StripCRBytes(crlfContent []byte) ([]byte, error) {
 	onlyLf := []byte{}
 	for _, b := range crlfContent {
 		if b != '\r' {
 			onlyLf = append(onlyLf, b)
 		}
 	}
-	return onlyLf
+	return onlyLf, nil
 }
 
-func WriteCRLFBytes(lfContent []byte) []byte {
+// yamlfmt.FeatureFunc
+func WriteCRLFBytes(lfContent []byte) ([]byte, error) {
 	crlfContent := []byte{}
 	for _, b := range lfContent {
 		if b == '\n' {
@@ -18,5 +34,5 @@ func WriteCRLFBytes(lfContent []byte) []byte {
 		}
 		crlfContent = append(crlfContent, b)
 	}
-	return crlfContent
+	return crlfContent, nil
 }
diff --git a/internal/hotfix/retain_line_break.go b/internal/hotfix/retain_line_break.go
new file mode 100644
index 0000000..49fd9f7
--- /dev/null
+++ b/internal/hotfix/retain_line_break.go
@@ -0,0 +1,93 @@
+// 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.
+
+// The features in this file are to retain line breaks.
+// The basic idea is to insert/remove placeholder comments in the yaml document before and after the format process.
+
+package hotfix
+
+import (
+	"bufio"
+	"bytes"
+	"strings"
+
+	"github.com/google/yamlfmt"
+)
+
+const lineBreakPlaceholder = "#magic___^_^___line"
+
+type paddinger struct {
+	strings.Builder
+}
+
+func (p *paddinger) adjust(size int) {
+	// Grows if the given size is larger than us and always return the max padding.
+	for diff := size - p.Len(); diff > 0; diff-- {
+		p.WriteByte(' ')
+	}
+}
+
+func MakeFeatureRetainLineBreak(linebreakStr string, indent int) yamlfmt.Feature {
+	return yamlfmt.Feature{
+		Name:         "Retain Line Breaks",
+		BeforeAction: replaceLineBreakFeature(linebreakStr, indent),
+		AfterAction:  restoreLineBreakFeature(linebreakStr),
+	}
+}
+
+func replaceLineBreakFeature(newlineStr string, indentSize int) yamlfmt.FeatureFunc {
+	return func(content []byte) ([]byte, error) {
+		var buf bytes.Buffer
+		reader := bytes.NewReader(content)
+		scanner := bufio.NewScanner(reader)
+		var padding paddinger
+		for scanner.Scan() {
+			txt := scanner.Text()
+			padding.adjust(indentSize)
+			if strings.TrimSpace(txt) == "" { // line break or empty space line.
+				buf.WriteString(padding.String()) // prepend some padding incase literal multiline strings.
+				buf.WriteString(lineBreakPlaceholder)
+				buf.WriteString(newlineStr)
+				continue
+			}
+			buf.WriteString(txt)
+			buf.WriteByte('\n')
+		}
+		return buf.Bytes(), scanner.Err()
+	}
+}
+
+func restoreLineBreakFeature(newlineStr string) yamlfmt.FeatureFunc {
+	return func(content []byte) ([]byte, error) {
+		var buf bytes.Buffer
+		reader := bytes.NewReader(content)
+		scanner := bufio.NewScanner(reader)
+		for scanner.Scan() {
+			txt := scanner.Text()
+			if strings.TrimSpace(txt) == "" {
+				// The basic yaml lib inserts newline when there is a comment(either placeholder or by user)
+				// followed by optional line breaks and a `---` multi-documents.
+				// To fix it, the empty line could only be inserted by us.
+				continue
+			}
+			if strings.HasPrefix(strings.TrimLeft(txt, " "), lineBreakPlaceholder) {
+				buf.WriteString(newlineStr)
+				continue
+			}
+			buf.WriteString(txt)
+			buf.WriteString(newlineStr)
+		}
+		return buf.Bytes(), scanner.Err()
+	}
+}
diff --git a/internal/hotfix/unicode.go b/internal/hotfix/unicode.go
index 1f98e2e..f219dda 100644
--- a/internal/hotfix/unicode.go
+++ b/internal/hotfix/unicode.go
@@ -1,3 +1,17 @@
+// 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 hotfix
 
 import (
@@ -7,9 +21,10 @@ import (
 	"github.com/RageCage64/go-utf8-codepoint-converter/codepoint"
 )
 
-func ParseUnicodePoints(content []byte) []byte {
+// yamlfmt.FeatureFunc
+func ParseUnicodePoints(content []byte) ([]byte, error) {
 	if len(content) == 0 {
-		return []byte{}
+		return []byte{}, errors.New("no content")
 	}
 
 	p := unicodeParser{
@@ -28,7 +43,7 @@ func ParseUnicodePoints(content []byte) []byte {
 		err = p.next()
 	}
 
-	return p.out
+	return p.out, nil
 }
 
 type unicodeParser struct {
diff --git a/yamlfmt.go b/yamlfmt.go
index 14c3835..75135ff 100644
--- a/yamlfmt.go
+++ b/yamlfmt.go
@@ -14,7 +14,9 @@
 
 package yamlfmt
 
-import "fmt"
+import (
+	"fmt"
+)
 
 type Formatter interface {
 	Type() string
@@ -65,3 +67,65 @@ const (
 	LineBreakStyleLF   = "lf"
 	LineBreakStyleCRLF = "crlf"
 )
+
+type FeatureFunc func([]byte) ([]byte, error)
+
+type Feature struct {
+	Name         string
+	BeforeAction FeatureFunc
+	AfterAction  FeatureFunc
+}
+
+type FeatureList []Feature
+
+type FeatureApplyMode string
+
+var (
+	FeatureApplyBefore FeatureApplyMode = "before"
+	FeatureApplyAfter  FeatureApplyMode = "after"
+)
+
+type FeatureApplyError struct {
+	err         error
+	featureName string
+	mode        FeatureApplyMode
+}
+
+func (e *FeatureApplyError) Error() string {
+	action := "Before"
+	if e.mode == FeatureApplyAfter {
+		action = "After"
+	}
+	return fmt.Sprintf("Feature %s %sAction failed with error: %v", e.featureName, action, e.err)
+}
+
+func (e *FeatureApplyError) Unwrap() error {
+	return e.err
+}
+
+func (fl FeatureList) ApplyFeatures(input []byte, mode FeatureApplyMode) ([]byte, error) {
+	// Declare err here so the result variable doesn't get shadowed in the loop
+	var err error
+	result := make([]byte, len(input))
+	copy(result, input)
+	for _, feature := range fl {
+		if mode == FeatureApplyBefore {
+			if feature.BeforeAction != nil {
+				result, err = feature.BeforeAction(result)
+			}
+		} else {
+			if feature.AfterAction != nil {
+				result, err = feature.AfterAction(result)
+			}
+		}
+
+		if err != nil {
+			return nil, &FeatureApplyError{
+				err:         err,
+				featureName: feature.Name,
+				mode:        mode,
+			}
+		}
+	}
+	return result, nil
+}


Test patch:
diff --git a/formatters/basic/formatter_test.go b/formatters/basic/formatter_test.go
index f5604a3..e5d66ed 100644
--- a/formatters/basic/formatter_test.go
+++ b/formatters/basic/formatter_test.go
@@ -22,8 +22,15 @@ import (
 	"github.com/google/yamlfmt/formatters/basic"
 )
 
+func newFormatter(config *basic.Config) *basic.BasicFormatter {
+	return &basic.BasicFormatter{
+		Config:   config,
+		Features: basic.ConfigureFeaturesFromConfig(config),
+	}
+}
+
 func TestFormatterRetainsComments(t *testing.T) {
-	f := &basic.BasicFormatter{Config: basic.DefaultConfig()}
+	f := newFormatter(basic.DefaultConfig())
 
 	yaml := `x: "y" # foo comment`
 
@@ -72,8 +79,9 @@ a:
 }
 
 func TestWithDocumentStart(t *testing.T) {
-	f := &basic.BasicFormatter{Config: basic.DefaultConfig()}
-	f.Config.IncludeDocumentStart = true
+	config := basic.DefaultConfig()
+	config.IncludeDocumentStart = true
+	f := newFormatter(config)
 
 	yaml := "a:"
 	s, err := f.Format([]byte(yaml))
@@ -86,8 +94,9 @@ func TestWithDocumentStart(t *testing.T) {
 }
 
 func TestCRLFLineEnding(t *testing.T) {
-	f := &basic.BasicFormatter{Config: basic.DefaultConfig()}
-	f.Config.LineEnding = yamlfmt.LineBreakStyleCRLF
+	config := basic.DefaultConfig()
+	config.LineEnding = yamlfmt.LineBreakStyleCRLF
+	f := newFormatter(config)
 
 	yaml := "# comment\r\na:\r\n"
 	result, err := f.Format([]byte(yaml))
@@ -100,8 +109,9 @@ func TestCRLFLineEnding(t *testing.T) {
 }
 
 func TestEmojiSupport(t *testing.T) {
-	f := &basic.BasicFormatter{Config: basic.DefaultConfig()}
-	f.Config.EmojiSupport = true
+	config := basic.DefaultConfig()
+	config.EmojiSupport = true
+	f := newFormatter(config)
 
 	yaml := "a: 😊"
 	result, err := f.Format([]byte(yaml))
@@ -166,7 +176,9 @@ shell: |
 `,
 		},
 	}
-	f := &basic.BasicFormatter{Config: basic.DefaultConfig()}
+	config := basic.DefaultConfig()
+	config.RetainLineBreaks = true
+	f := newFormatter(config)
 	for _, c := range testCases {
 		t.Run(c.desc, func(t *testing.T) {
 			got, err := f.Format([]byte(c.input))
@@ -174,7 +186,7 @@ shell: |
 				t.Fatalf("expected formatting to pass, returned error: %v", err)
 			}
 			if string(got) != c.expect {
-				t.Fatalf("didn't retain line breaks result: %v, expect %s", string(got), c.expect)
+				t.Fatalf("didn't retain line breaks\nresult: %v\nexpect %s", string(got), c.expect)
 			}
 		})
 	}
diff --git a/internal/hotfix/crlf_test.go b/internal/hotfix/crlf_test.go
index ecb94f8..aa9796b 100644
--- a/internal/hotfix/crlf_test.go
+++ b/internal/hotfix/crlf_test.go
@@ -1,3 +1,17 @@
+// 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 hotfix_test
 
 import (
@@ -8,7 +22,7 @@ import (
 
 func TestStripCRBytes(t *testing.T) {
 	crlfContent := []byte("two\r\nlines\r\n")
-	lfContent := hotfix.StripCRBytes(crlfContent)
+	lfContent, _ := hotfix.StripCRBytes(crlfContent)
 	count := countByte(lfContent, '\r')
 	if count != 0 {
 		t.Fatalf("Found %d CR (decimal 13) bytes in %v", count, lfContent)
@@ -17,7 +31,7 @@ func TestStripCRBytes(t *testing.T) {
 
 func TestWriteCRLF(t *testing.T) {
 	lfContent := []byte("two\nlines\n")
-	crlfContent := hotfix.WriteCRLFBytes(lfContent)
+	crlfContent, _ := hotfix.WriteCRLFBytes(lfContent)
 	countCR := countByte(crlfContent, '\r')
 	countLF := countByte(crlfContent, '\n')
 	if countCR != countLF {
diff --git a/internal/hotfix/unicode_test.go b/internal/hotfix/unicode_test.go
index 856ef11..1e9814e 100644
--- a/internal/hotfix/unicode_test.go
+++ b/internal/hotfix/unicode_test.go
@@ -1,3 +1,17 @@
+// 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 hotfix_test
 
 import (
@@ -32,7 +46,10 @@ func TestParseEmoji(t *testing.T) {
 			if err != nil {
 				t.Fatalf("yaml failed to parse: %v", err)
 			}
-			formattedAfter := hotfix.ParseUnicodePoints(formattedBefore)
+			formattedAfter, err := hotfix.ParseUnicodePoints(formattedBefore)
+			if err != nil {
+				t.Fatalf("expected no error, got: %v", err)
+			}
 			formattedStr := string(formattedAfter)
 			if formattedStr != tc.expectedStr {
 				t.Fatalf("parsed string does not match: \nexpected: %s\ngot: %s", tc.expectedStr, string(formattedStr))