Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
1a09799
Phase 1: bootstrap skeleton with naive renderer
janezpodhostnik Apr 29, 2026
ccdb008
Phase 1: bootstrap skeleton with naive renderer
janezpodhostnik Apr 29, 2026
406370d
Phase 2: comment scanner with grouping
janezpodhostnik Apr 29, 2026
40cf561
Phase 3: comment attachment via position-based CommentMap
janezpodhostnik Apr 29, 2026
1012fb7
Phase 4: comment interleaving in renderer
janezpodhostnik Apr 29, 2026
cac2bb5
Phase 4: comment interleaving in renderer
janezpodhostnik Apr 29, 2026
45696c5
Phase 5: rewrite passes with import sorting
janezpodhostnik Apr 29, 2026
00ff364
Phase 5: rewrite passes with import sorting
janezpodhostnik Apr 29, 2026
1ba867d
Phase 6: style overrides in renderer
janezpodhostnik Apr 29, 2026
088950d
Phase 6: style overrides in renderer
janezpodhostnik Apr 29, 2026
4b513fe
Phase 8: round-trip AST verification pass
janezpodhostnik Apr 29, 2026
568d185
Phase 10: fuzz testing and hardening
janezpodhostnik Apr 29, 2026
8f95815
Fix orphaned comments on real-world contracts
janezpodhostnik Apr 29, 2026
9b457f2
Fix continuation line indentation for ?? and multi-line expressions
janezpodhostnik Apr 29, 2026
4a4a9a8
Fix continuation line indentation for ?? and multi-line expressions
janezpodhostnik Apr 29, 2026
b032344
Stage 1: Fix comment displacement inside for/while/if loop bodies
janezpodhostnik Apr 29, 2026
b9bc5e5
Stage 1: Fix comment displacement inside for/while/if loop bodies
janezpodhostnik Apr 29, 2026
f01a271
Stage 2: Fix variable declaration value over-indentation
janezpodhostnik Apr 29, 2026
eef3a14
Stage 2: Fix variable declaration value over-indentation
janezpodhostnik Apr 29, 2026
62cbb74
Stage 3: Fix assignment statement value over-indentation
janezpodhostnik Apr 29, 2026
206d8cc
Stage 3: Fix assignment statement value over-indentation
janezpodhostnik Apr 29, 2026
467f406
Stage 4: Fix long if-condition line breaking and expression comment d…
janezpodhostnik Apr 29, 2026
f17425b
Stage 4: Fix long if-condition line breaking and expression comment d…
janezpodhostnik Apr 29, 2026
4ac05d2
Add developer tooling, corpus tests, README, and clean up repo
janezpodhostnik Apr 29, 2026
f86b51b
fix: resolve three fuzz-found idempotence bugs and fix lint
janezpodhostnik Apr 29, 2026
8aaded1
fix: strip blank-line whitespace, join entitlement decls, fix invocat…
janezpodhostnik Apr 29, 2026
b2da29b
fix: strip blank-line whitespace, join entitlement decls, fix invocat…
janezpodhostnik Apr 29, 2026
390f62d
fix(render): indent casting operator on continuation lines
janezpodhostnik Apr 29, 2026
3547e59
test: add snapshot test for casting continuation indentation
janezpodhostnik Apr 29, 2026
aa7edfa
chore: upgrade to cadence PR #4485 and fix binary expression indent
janezpodhostnik Apr 29, 2026
0ef5522
chore: upgrade to cadence PR #4485 and fix binary expression indent
janezpodhostnik Apr 29, 2026
9227095
fix(render): preserve comments between event parameters
janezpodhostnik Apr 29, 2026
fed5182
fix(render): preserve comments between event parameters
janezpodhostnik Apr 29, 2026
c0d7ba8
fix(render): preserve same-line comments on invocation arguments
janezpodhostnik Apr 29, 2026
7098da4
fix(render): preserve same-line comments on invocation arguments
janezpodhostnik Apr 29, 2026
f0b042f
test: unskip FlowEpoch.cdc and FlowTransactionScheduler.cdc corpus tests
janezpodhostnik Apr 29, 2026
7bfbf98
fix(render): preserve comments on function/init parameters
janezpodhostnik Apr 29, 2026
7a98d7b
fix: keep string interpolations flat, add transaction renderer, docum…
janezpodhostnik Apr 30, 2026
878189c
fix: keep string interpolations flat, add transaction renderer, docum…
janezpodhostnik Apr 30, 2026
4303c2f
fix: guard drainDescendantComments against nil output slice
janezpodhostnik Apr 30, 2026
b13c72b
fix: post-process to rejoin broken string interpolations
janezpodhostnik Apr 30, 2026
09acaf4
test: add flow-ft and flow-nft to corpus tests
janezpodhostnik Apr 30, 2026
2be4da1
chore: upgrade to cadence PR #4485 second commit (048f0af)
janezpodhostnik Apr 30, 2026
e02273a
chore: upgrade to cadence PR #4485 second commit (048f0af)
janezpodhostnik Apr 30, 2026
a91abeb
refactor: comprehensive cleanup, implement Options, fix orphaned comm…
janezpodhostnik Apr 30, 2026
e681b2a
refactor: comprehensive cleanup, implement Options, fix orphaned comm…
janezpodhostnik Apr 30, 2026
043e7a9
fix(trivia): exclude access modifier NominalType from comment attachment
janezpodhostnik Apr 30, 2026
e4c59ec
docs: clarify minor comment wording in render and trivia packages
janezpodhostnik Apr 30, 2026
1547d45
feat: add benchmarks, fix --no-verify flag, update README
janezpodhostnik Apr 30, 2026
e94639b
fix(render): preserve blank lines between statements in function bodies
janezpodhostnik Apr 30, 2026
15023c1
fix(render): preserve blank lines between statements in function bodies
janezpodhostnik Apr 30, 2026
fe57985
refactor: replace Indent/UseTabs with IndentCharacter/IndentCount
janezpodhostnik Apr 30, 2026
f7d17e4
feat: wire up SortImports option, add CLI integration tests
janezpodhostnik Apr 30, 2026
3dc6533
fix(render): hoist line comments off non-terminal type annotation
janezpodhostnik Apr 30, 2026
e8a2e1c
fix(render): hoist line comments off non-terminal type annotation
janezpodhostnik Apr 30, 2026
0f2e08c
fix(trivia): clip end positions to compensate for parser quirks
janezpodhostnik Apr 30, 2026
0f8855b
fix(trivia): clip end positions to compensate for parser quirks
janezpodhostnik Apr 30, 2026
5969f8d
refactor: cleanup pass — sentinel errors, CLI guards, renderer struct
janezpodhostnik Apr 30, 2026
b3e739a
Add 'formatter/' from commit '5969f8dcb9ebb730039d01f94eab7bbd70373010'
turbolent May 19, 2026
cd5bdc3
rename package and adjust imports
turbolent May 19, 2026
a6ab12c
remove corpus tests
turbolent May 19, 2026
1d148b1
load testdata relative to package directory
turbolent May 19, 2026
4d656d3
Add 'formatter/testdata/format/' from commit '0f8855be8f4b0ba4be47321…
turbolent May 19, 2026
a0f48c8
add license headers
turbolent May 19, 2026
e6d9c3f
Merge branch 'master' into bastian/janez-formatter
turbolent May 20, 2026
aa7f487
lint
turbolent May 20, 2026
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
87 changes: 87 additions & 0 deletions formatter/bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Cadence - The resource-oriented smart contract programming language
*
* Copyright Flow Foundation
*
* 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 formatter_test

import (
"os"
"path/filepath"
"testing"

"github.com/onflow/cadence/formatter"
)

func loadSnapshotInputs(b *testing.B) map[string][]byte {
b.Helper()
root := findRepoRoot(b)
dir := filepath.Join(root, "testdata", "format")
entries, err := os.ReadDir(dir)
if err != nil {
b.Fatalf("reading testdata dir: %v", err)
}
inputs := make(map[string][]byte, len(entries))
for _, e := range entries {
if !e.IsDir() {
continue
}
data, err := os.ReadFile(filepath.Join(dir, e.Name(), "input.cdc"))
if err != nil {
b.Fatalf("reading input %s: %v", e.Name(), err)
}
inputs[e.Name()] = data
}
return inputs
}

// --- End-to-end benchmarks ---

func BenchmarkFormat_Snapshot(b *testing.B) {
inputs := loadSnapshotInputs(b)
opts := formatter.Default()

var totalBytes int64
for _, data := range inputs {
totalBytes += int64(len(data))
}

b.ResetTimer()
b.SetBytes(totalBytes)
for b.Loop() {
for name, data := range inputs {
if _, err := formatter.Format(data, name+".cdc", opts); err != nil {
b.Fatalf("format %s: %v", name, err)
}
}
}
}

func BenchmarkFormat_PerCase(b *testing.B) {
inputs := loadSnapshotInputs(b)
opts := formatter.Default()

for name, data := range inputs {
b.Run(name, func(b *testing.B) {
b.SetBytes(int64(len(data)))
for b.Loop() {
if _, err := formatter.Format(data, name+".cdc", opts); err != nil {
b.Fatalf("format: %v", err)
}
}
})
}
}
221 changes: 221 additions & 0 deletions formatter/formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* Cadence - The resource-oriented smart contract programming language
*
* Copyright Flow Foundation
*
* 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 formatter

import (
"bytes"
"errors"
"fmt"
"strings"

"github.com/turbolent/prettier"

"github.com/onflow/cadence/ast"
"github.com/onflow/cadence/formatter/render"
"github.com/onflow/cadence/formatter/rewrite"
"github.com/onflow/cadence/formatter/trivia"
"github.com/onflow/cadence/formatter/verify"
"github.com/onflow/cadence/parser"
)

// ErrParse marks errors caused by malformed input source.
// ErrInternal marks errors caused by formatter bugs (rewrite failure,
// orphaned comments, round-trip verification failure). Callers can
// distinguish these via errors.Is to choose an appropriate exit code.
var (
ErrParse = errors.New("parse error")
ErrInternal = errors.New("internal error")
)

// Format parses Cadence source and returns deterministically formatted output.
// filename is used for diagnostics only; the file need not exist on disk.
func Format(src []byte, filename string, opts Options) ([]byte, error) {
if err := opts.Validate(); err != nil {
return nil, err
}

program, err := parser.ParseProgram(nil, src, parser.Config{})
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrParse, err)
}

// Extract and attach comments
comments := trivia.Scan(src)
groups := trivia.Group(comments, src)
cm := trivia.Attach(program, groups, src)

// Apply AST rewrites (import sorting, etc.)
if err := rewrite.Apply(program, cm, opts.SortImports); err != nil {
return nil, fmt.Errorf("%w: rewrite failed: %w", ErrInternal, err)
}

indent := strings.Repeat(opts.IndentCharacter, opts.IndentCount)

// Render AST with interleaved comments
var semicolons map[ast.Element]bool
if !opts.StripSemicolons {
semicolons = trivia.ScanSemicolons(src, program)
}
doc := render.Program(program, cm, src, semicolons)

var buf bytes.Buffer
prettier.Prettier(&buf, doc, opts.LineWidth, indent)

result := collapseBlankLines(
rejoinStringInterpolations(stripTrailingLineWhitespace(buf.Bytes())),
opts.KeepBlankLines,
)

// Verify no orphaned comments remain
if !cm.IsEmpty() {
details := cm.OrphanDetails()
return result, fmt.Errorf("%w: orphaned comments remain in CommentMap\n%s", ErrInternal, details)
}

// Round-trip verification: re-parse and compare ASTs
if !opts.SkipVerify {
if err := verify.RoundTrip(src, result); err != nil {
return result, fmt.Errorf("%w: round-trip verification failed: %w", ErrInternal, err)
}
}

return result, nil
}

// rejoinStringInterpolations collapses line breaks inside string template
// interpolations \(...). The prettier library may break expressions inside
// interpolations across lines; this rejoins them into a single line.
// Tracks paren depth to find the matching ) for each \(.
func rejoinStringInterpolations(data []byte) []byte {
result := make([]byte, 0, len(data))
i := 0
inString := false

for i < len(data) {
b := data[i]

// Track string boundaries (handle escaped quotes)
if b == '"' && !inString {
inString = true
result = append(result, b)
i++
continue
}
if b == '"' && inString {
inString = false
result = append(result, b)
i++
continue
}

// Handle escape sequences inside strings
if inString && b == '\\' && i+1 < len(data) {
if data[i+1] == '(' {
// Start of interpolation \( — scan to matching )
result = append(result, '\\', '(')
i += 2
depth := 1
for i < len(data) && depth > 0 {
c := data[i]
if c == '(' {
depth++
result = append(result, c)
} else if c == ')' {
depth--
result = append(result, c)
} else if c == '\n' {
// Collapse newline + following whitespace. The expression
// content already has operators/dots that provide spacing.
i++
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
i++
}
// Add a space unless the next char is . (member access)
if i < len(data) && data[i] != '.' {
result = append(result, ' ')
}
continue
} else if c == '"' {
// Nested string inside interpolation — copy until closing "
result = append(result, c)
i++
for i < len(data) && data[i] != '"' {
if data[i] == '\\' && i+1 < len(data) {
result = append(result, data[i], data[i+1])
i += 2
continue
}
result = append(result, data[i])
i++
}
if i < len(data) {
result = append(result, data[i]) // closing "
}
} else {
result = append(result, c)
}
i++
}
continue
}
// Other escape: copy both bytes
result = append(result, b, data[i+1])
i += 2
continue
}

result = append(result, b)
i++
}

return result
}

// collapseBlankLines limits consecutive blank lines to at most max.
func collapseBlankLines(data []byte, max int) []byte {
lines := bytes.Split(data, []byte("\n"))
result := make([][]byte, 0, len(lines))
consecutive := 0
for _, line := range lines {
if len(bytes.TrimSpace(line)) == 0 {
consecutive++
if consecutive > max {
continue
}
} else {
consecutive = 0
}
result = append(result, line)
}
return bytes.Join(result, []byte("\n"))
}

// stripTrailingLineWhitespace strips indent whitespace from blank lines.
// The prettier library emits indent prefixes on blank lines inside Indent
// blocks (e.g. " \n" instead of "\n"); this cleans that up.
// Only whitespace-only lines are affected — content lines are not touched.
func stripTrailingLineWhitespace(data []byte) []byte {
lines := bytes.Split(data, []byte("\n"))
for i, line := range lines {
if len(bytes.TrimRight(line, " \t")) == 0 {
lines[i] = nil
}
}
return bytes.Join(lines, []byte("\n"))
}
Loading
Loading