Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions formatters/basic/formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package basic_test

import (
"fmt"
"strings"
"testing"

"github.com/google/yamlfmt/formatters/basic"
Expand Down Expand Up @@ -371,6 +373,28 @@ c: 3
}
}

// TestRetainLineBreaksLongLine is a regression test for a line longer than
// bufio.Scanner's default 64KiB token limit. The retain-line-breaks feature
// scans the document line by line; before the fix this failed with
// "bufio.Scanner: token too long" on real YAML carrying a large single-line
// value (e.g. SwiftPM .build manifests). See internal/linescan.
func TestRetainLineBreaksLongLine(t *testing.T) {
longValue := strings.Repeat("x", 200*1024) // 200KiB, well past the 64KiB limit
input := "a: 1\n\nbig: " + longValue + "\nb: 2\n"
for _, single := range []bool{false, true} {
t.Run(fmt.Sprintf("single=%t", single), func(t *testing.T) {
f, err := factory.NewFormatter(map[string]any{
"retain_line_breaks": true,
"retain_line_breaks_single": single,
})
require.NoError(t, err)
got, err := f.Format([]byte(input))
require.NoError(t, err)
require.Contains(t, string(got), longValue, "the long single-line value must survive formatting")
})
}
}

func stripTrailingNewline(s string) string {
// strip trailing \n or \r\n characters
if len(s) > 0 {
Expand Down
6 changes: 2 additions & 4 deletions internal/features/trim_whitespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@
package features

import (
"bufio"
"bytes"
"context"
"strings"

"github.com/google/yamlfmt"
"github.com/google/yamlfmt/internal/linescan"
)

func MakeFeatureTrimTrailingWhitespace(linebreakStr string) yamlfmt.Feature {
Expand All @@ -32,8 +31,7 @@ func MakeFeatureTrimTrailingWhitespace(linebreakStr string) yamlfmt.Feature {

func trimTrailingWhitespaceFeature(linebreakStr string) yamlfmt.FeatureFunc {
return func(_ context.Context, content []byte) (context.Context, []byte, error) {
buf := bytes.NewBuffer(content)
s := bufio.NewScanner(buf)
s := linescan.NewScanner(content)
newLines := []string{}
for s.Scan() {
newLines = append(newLines, strings.TrimRight(s.Text(), " "))
Expand Down
8 changes: 3 additions & 5 deletions internal/hotfix/retain_line_break.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
package hotfix

import (
"bufio"
"bytes"
"context"
"strings"

"github.com/google/yamlfmt"
"github.com/google/yamlfmt/internal/linescan"
)

const lineBreakPlaceholder = "#magic___^_^___line"
Expand Down Expand Up @@ -54,8 +54,7 @@ func MakeFeatureRetainLineBreak(linebreakStr string, chomp bool) yamlfmt.Feature
func replaceLineBreakFeature(newlineStr string, chomp bool) yamlfmt.FeatureFunc {
return func(_ context.Context, content []byte) (context.Context, []byte, error) {
var buf bytes.Buffer
reader := bytes.NewReader(content)
scanner := bufio.NewScanner(reader)
scanner := linescan.NewScanner(content)
var inLineBreaks bool
var padding paddinger
for scanner.Scan() {
Expand All @@ -82,8 +81,7 @@ func replaceLineBreakFeature(newlineStr string, chomp bool) yamlfmt.FeatureFunc
func restoreLineBreakFeature(newlineStr string) yamlfmt.FeatureFunc {
return func(_ context.Context, content []byte) (context.Context, []byte, error) {
var buf bytes.Buffer
reader := bytes.NewReader(content)
scanner := bufio.NewScanner(reader)
scanner := linescan.NewScanner(content)
for scanner.Scan() {
txt := scanner.Text()
if strings.TrimSpace(txt) == "" {
Expand Down
9 changes: 3 additions & 6 deletions internal/hotfix/strip_directives.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@
package hotfix

import (
"bufio"
"bytes"
"context"
"strings"

"github.com/google/yamlfmt"
"github.com/google/yamlfmt/internal/linescan"
)

type directiveKey string
Expand Down Expand Up @@ -51,8 +50,7 @@ func MakeFeatureStripDirectives(lineSepChar string) yamlfmt.Feature {
func stripDirectivesFeature(lineSepChar string) yamlfmt.FeatureFunc {
return func(ctx context.Context, content []byte) (context.Context, []byte, error) {
directives := []Directive{}
reader := bytes.NewReader(content)
scanner := bufio.NewScanner(reader)
scanner := linescan.NewScanner(content)
result := ""
currLine := 1
for scanner.Scan() {
Expand All @@ -76,8 +74,7 @@ func restoreDirectivesFeature(lineSepChar string) yamlfmt.FeatureFunc {
directives := DirectivesFromContext(ctx)
directiveIdx := 0
doneDirectives := directiveIdx == len(directives)
reader := bytes.NewReader(content)
scanner := bufio.NewScanner(reader)
scanner := linescan.NewScanner(content)
result := ""
currLine := 1
for scanner.Scan() {
Expand Down
29 changes: 29 additions & 0 deletions internal/linescan/linescan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2026 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 linescan provides a line scanner that is not subject to bufio's
// default token size limit.
package linescan

import (
"bufio"
"bytes"
)

// NewScanner returns a bufio.Scanner over content that can read a line of any length.
func NewScanner(content []byte) *bufio.Scanner {
scanner := bufio.NewScanner(bytes.NewReader(content))
scanner.Buffer(nil, len(content)+1)
return scanner
}
60 changes: 60 additions & 0 deletions internal/linescan/linescan_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2026 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 linescan_test

import (
"bufio"
"strings"
"testing"

"github.com/google/yamlfmt/internal/linescan"
)

// A line longer than bufio's default token limit must scan without error,
// which a plain bufio.NewScanner cannot do.
func TestNewScannerLongLine(t *testing.T) {
long := strings.Repeat("x", 10*bufio.MaxScanTokenSize) // 640KiB, one line
content := []byte("a\n" + long + "\nb\n")

var got []string
scanner := linescan.NewScanner(content)
for scanner.Scan() {
got = append(got, scanner.Text())
}
if err := scanner.Err(); err != nil {
t.Fatalf("unexpected scan error: %v", err)
}

want := []string{"a", long, "b"}
if len(got) != len(want) {
t.Fatalf("got %d lines, want %d", len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("line %d mismatch: len(got)=%d len(want)=%d", i, len(got[i]), len(want[i]))
}
}
}

// Empty content is a valid (zero-line) scan, not an error.
func TestNewScannerEmpty(t *testing.T) {
scanner := linescan.NewScanner(nil)
for scanner.Scan() {
t.Fatalf("empty content should yield no lines, got %q", scanner.Text())
}
if err := scanner.Err(); err != nil {
t.Fatalf("unexpected scan error: %v", err)
}
}