cue/format: add package
Change-Id: I09091c053009fc29d1426e1037f67933c805d158
diff --git a/cue/format/format.go b/cue/format/format.go
new file mode 100644
index 0000000..e581df5
--- /dev/null
+++ b/cue/format/format.go
@@ -0,0 +1,302 @@
+// Copyright 2018 The CUE Authors
+//
+// 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 format implements standard formatting of CUE configurations.
+package format // import "cuelang.org/go/cue/format"
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "strings"
+ "text/tabwriter"
+
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/parser"
+ "cuelang.org/go/cue/token"
+)
+
+// An Option sets behavior of the formatter.
+type Option func(c *config)
+
+// FileSet sets the file set that was used for creating a node. The formatter
+// generally formats fine without it.
+func FileSet(fset *token.FileSet) Option {
+ return func(c *config) { c.fset = fset }
+}
+
+// Simplify allows the formatter to simplify output, such as removing
+// unnecessary quotes.
+func Simplify() Option {
+ return func(c *config) { c.simplify = true }
+}
+
+// TODO: other options:
+//
+// const (
+// RawFormat Mode = 1 << iota // do not use a tabwriter; if set, UseSpaces is ignored
+// TabIndent // use tabs for indentation independent of UseSpaces
+// UseSpaces // use spaces instead of tabs for alignment
+// SourcePos // emit //line comments to preserve original source positions
+// )
+
+// Node formats node in canonical cue fmt style and writes the result to dst.
+//
+// The node type must be *ast.File, []syntax.Decl, syntax.Expr, syntax.Decl, or
+// syntax.Spec. Node does not modify node. Imports are not sorted for nodes
+// representing partial source files (for instance, if the node is not an
+// *ast.File).
+//
+// The function may return early (before the entire result is written) and
+// return a formatting error, for instance due to an incorrect AST.
+//
+func Node(dst io.Writer, node ast.Node, opt ...Option) error {
+ cfg := newConfig(opt)
+ return cfg.fprint(dst, node)
+}
+
+// Source formats src in canonical cue fmt style and returns the result or an
+// (I/O or syntax) error. src is expected to be a syntactically correct CUE
+// source file, or a list of CUE declarations or statements.
+//
+// If src is a partial source file, the leading and trailing space of src is
+// applied to the result (such that it has the same leading and trailing space
+// as src), and the result is indented by the same amount as the first line of
+// src containing code. Imports are not sorted for partial source files.
+//
+// Caution: Tools relying on consistent formatting based on the installed
+// version of cue (for instance, such as for presubmit checks) should execute
+// that cue binary instead of calling Source.
+//
+func Source(b []byte, opt ...Option) ([]byte, error) {
+ cfg := newConfig(opt)
+ if cfg.fset == nil {
+ cfg.fset = token.NewFileSet()
+ }
+
+ f, err := parser.ParseFile(cfg.fset, "", b,
+ parser.ParseComments, parser.ParseLambdas)
+ if err != nil {
+ return nil, fmt.Errorf("parse: %s", err)
+ }
+
+ // print AST
+ var buf bytes.Buffer
+ if err := cfg.fprint(&buf, f); err != nil {
+ return nil, fmt.Errorf("print: %s", err)
+ }
+ return buf.Bytes(), nil
+}
+
+type config struct {
+ fset *token.FileSet
+
+ UseSpaces bool
+ TabIndent bool
+ Tabwidth int // default: 8
+ Indent int // default: 0 (all code is indented at least by this much)
+
+ simplify bool
+}
+
+func newConfig(opt []Option) *config {
+ cfg := &config{Tabwidth: 8, TabIndent: true, UseSpaces: true}
+ for _, o := range opt {
+ o(cfg)
+ }
+ return cfg
+}
+
+// Config defines the output of Fprint.
+func (cfg *config) fprint(output io.Writer, node interface{}) (err error) {
+ // print node
+ var p printer
+ p.init(cfg)
+ if err = printNode(node, &p); err != nil {
+ return err
+ }
+
+ minwidth := cfg.Tabwidth
+
+ padchar := byte('\t')
+ if cfg.UseSpaces {
+ padchar = ' '
+ }
+
+ twmode := tabwriter.DiscardEmptyColumns | tabwriter.StripEscape
+ if cfg.TabIndent {
+ minwidth = 0
+ twmode |= tabwriter.TabIndent
+ }
+
+ tw := tabwriter.NewWriter(output, minwidth, cfg.Tabwidth, 1, padchar, twmode)
+
+ // write printer result via tabwriter/trimmer to output
+ if _, err = tw.Write(p.output); err != nil {
+ return
+ }
+
+ err = tw.Flush()
+ return
+}
+
+// A formatter walks a syntax.Node, interspersed with comments and spacing
+// directives, in the order that they would occur in printed form.
+type formatter struct {
+ *printer
+
+ stack []frame
+ current frame
+ nestExpr int
+
+ labelBuf []ast.Label
+}
+
+func newFormatter(p *printer) *formatter {
+ f := &formatter{
+ printer: p,
+ current: frame{
+ settings: settings{
+ nodeSep: newline,
+ parentSep: newline,
+ },
+ },
+ }
+ return f
+}
+
+type whiteSpace int
+
+const (
+ ignore whiteSpace = 0
+
+ // write a space, or disallow it
+ blank whiteSpace = 1 << iota
+ vtab // column marker
+ noblank
+
+ nooverride
+
+ comma // print a comma, unless trailcomma overrides it
+ trailcomma // print a trailing comma unless closed on same line
+ declcomma // write a comma when not at the end of line
+
+ newline // write a line in a table
+ formfeed // next line is not part of the table
+ newsection // add two newlines
+
+ indent // request indent an extra level after the next newline
+ unindent // unindent a level after the next newline
+ indented // element was indented.
+)
+
+type frame struct {
+ cg []*ast.CommentGroup
+ pos int8
+
+ settings
+ commentNewline bool
+}
+
+type settings struct {
+ // separator is blank if the current node spans a single line and newline
+ // otherwise.
+ nodeSep whiteSpace
+ parentSep whiteSpace
+ override whiteSpace
+}
+
+func (f *formatter) print(a ...interface{}) {
+ for _, x := range a {
+ f.Print(x)
+ switch x.(type) {
+ case string, token.Token: // , *syntax.BasicLit, *syntax.Ident:
+ f.current.pos++
+ }
+ }
+ f.visitComments(f.current.pos)
+}
+
+func (f *formatter) formfeed() whiteSpace {
+ if f.current.nodeSep == blank {
+ return blank
+ }
+ return formfeed
+}
+
+func (f *formatter) wsOverride(def whiteSpace) whiteSpace {
+ if f.current.override == ignore {
+ return def
+ }
+ return f.current.override
+}
+
+func (f *formatter) onOneLine(node ast.Node) bool {
+ a := node.Pos()
+ b := node.End()
+ if f.fset != nil && a.IsValid() && b.IsValid() {
+ return f.lineFor(a) == f.lineFor(b)
+ }
+ // TODO: walk and look at relative positions to determine the same?
+ return false
+}
+
+func (f *formatter) before(node ast.Node) bool {
+ f.stack = append(f.stack, f.current)
+ f.current = frame{settings: f.current.settings}
+ f.current.parentSep = f.current.nodeSep
+
+ if node != nil {
+ if f.current.nodeSep != blank && f.onOneLine(node) {
+ f.current.nodeSep = blank
+ }
+ f.current.cg = node.Comments()
+ f.visitComments(f.current.pos)
+ return true
+ }
+ return false
+}
+
+func (f *formatter) after(node ast.Node) {
+ f.visitComments(127)
+ p := len(f.stack) - 1
+ f.current = f.stack[p]
+ f.stack = f.stack[:p]
+ f.current.pos++
+ f.visitComments(f.current.pos)
+}
+
+func (f *formatter) visitComments(until int8) {
+ c := &f.current
+
+ for ; len(c.cg) > 0 && c.cg[0].Position <= until; c.cg = c.cg[1:] {
+ f.Print(c.cg[0])
+
+ printBlank := false
+ if c.cg[0].Doc {
+ f.Print(newline)
+ printBlank = true
+ }
+ for _, c := range c.cg[0].List {
+ if !printBlank {
+ f.Print(vtab)
+ }
+ f.Print(c.Slash)
+ f.Print(c)
+ if strings.HasPrefix(c.Text, "//") {
+ f.Print(newline)
+ }
+ }
+ }
+}
diff --git a/cue/format/format_test.go b/cue/format/format_test.go
new file mode 100644
index 0000000..c77130f
--- /dev/null
+++ b/cue/format/format_test.go
@@ -0,0 +1,415 @@
+// Copyright 2018 The CUE Authors
+//
+// 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 format
+
+// TODO: port more of the tests of go/printer
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/parser"
+ "cuelang.org/go/cue/token"
+)
+
+var (
+ defaultConfig = newConfig([]Option{FileSet(fset)})
+ Fprint = defaultConfig.fprint
+)
+
+const (
+ dataDir = "testdata"
+ tabwidth = 8
+)
+
+var update = flag.Bool("update", false, "update golden files")
+
+var fset = token.NewFileSet()
+
+type checkMode uint
+
+const (
+ export checkMode = 1 << iota
+ rawFormat
+ idempotent
+ simplify
+)
+
+// format parses src, prints the corresponding AST, verifies the resulting
+// src is syntactically correct, and returns the resulting src or an error
+// if any.
+func format(src []byte, mode checkMode) ([]byte, error) {
+ // parse src
+ var opts []Option
+ if mode&simplify != 0 {
+ opts = append(opts, Simplify())
+ }
+
+ res, err := Source(src, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ // make sure formatted output is syntactically correct
+ if _, err := parser.ParseFile(fset, "", res,
+ parser.ParseLambdas, parser.AllErrors); err != nil {
+ return nil, fmt.Errorf("re-parse: %s\n%s", err, res)
+ }
+
+ return res, nil
+}
+
+// lineAt returns the line in text starting at offset offs.
+func lineAt(text []byte, offs int) []byte {
+ i := offs
+ for i < len(text) && text[i] != '\n' {
+ i++
+ }
+ return text[offs:i]
+}
+
+// diff compares a and b.
+func diff(aname, bname string, a, b []byte) error {
+ var buf bytes.Buffer // holding long error message
+
+ // compare lengths
+ if len(a) != len(b) {
+ fmt.Fprintf(&buf, "\nlength changed: len(%s) = %d, len(%s) = %d", aname, len(a), bname, len(b))
+ }
+
+ // compare contents
+ line := 1
+ offs := 1
+ for i := 0; i < len(a) && i < len(b); i++ {
+ ch := a[i]
+ if ch != b[i] {
+ fmt.Fprintf(&buf, "\n%s:%d:%d: %s", aname, line, i-offs+1, lineAt(a, offs))
+ fmt.Fprintf(&buf, "\n%s:%d:%d: %s", bname, line, i-offs+1, lineAt(b, offs))
+ fmt.Fprintf(&buf, "\n\n")
+ break
+ }
+ if ch == '\n' {
+ line++
+ offs = i + 1
+ }
+ }
+
+ if buf.Len() > 0 {
+ return errors.New(buf.String())
+ }
+ return nil
+}
+
+func runcheck(t *testing.T, source, golden string, mode checkMode) {
+ src, err := ioutil.ReadFile(source)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ res, err := format(src, mode)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // update golden files if necessary
+ if *update {
+ if err := ioutil.WriteFile(golden, res, 0644); err != nil {
+ t.Error(err)
+ }
+ return
+ }
+
+ // get golden
+ gld, err := ioutil.ReadFile(golden)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ // formatted source and golden must be the same
+ if err := diff(source, golden, res, gld); err != nil {
+ t.Error(err)
+ return
+ }
+
+ if mode&idempotent != 0 {
+ // formatting golden must be idempotent
+ // (This is very difficult to achieve in general and for now
+ // it is only checked for files explicitly marked as such.)
+ res, err = format(gld, mode)
+ if err := diff(golden, fmt.Sprintf("format(%s)", golden), gld, res); err != nil {
+ t.Errorf("golden is not idempotent: %s", err)
+ }
+ }
+}
+
+func check(t *testing.T, source, golden string, mode checkMode) {
+ // run the test
+ cc := make(chan int)
+ go func() {
+ runcheck(t, source, golden, mode)
+ cc <- 0
+ }()
+
+ // wait with timeout
+ select {
+ case <-time.After(100000 * time.Second): // plenty of a safety margin, even for very slow machines
+ // test running past time out
+ t.Errorf("%s: running too slowly", source)
+ case <-cc:
+ // test finished within allotted time margin
+ }
+}
+
+type entry struct {
+ source, golden string
+ mode checkMode
+}
+
+// Use go test -update to create/update the respective golden files.
+var data = []entry{
+ {"comments.input", "comments.golden", 0},
+ {"simplify.input", "simplify.golden", simplify},
+ {"expressions.input", "expressions.golden", 0},
+}
+
+func TestFiles(t *testing.T) {
+ t.Parallel()
+ for _, e := range data {
+ source := filepath.Join(dataDir, e.source)
+ golden := filepath.Join(dataDir, e.golden)
+ mode := e.mode
+ t.Run(e.source, func(t *testing.T) {
+ t.Parallel()
+ check(t, source, golden, mode)
+ // TODO(gri) check that golden is idempotent
+ //check(t, golden, golden, e.mode)
+ })
+ }
+}
+
+// Verify that the printer can be invoked during initialization.
+func init() {
+ const name = "foobar"
+ var buf bytes.Buffer
+ if err := Fprint(&buf, &ast.Ident{Name: name}); err != nil {
+ panic(err) // error in test
+ }
+ // in debug mode, the result contains additional information;
+ // ignore it
+ if s := buf.String(); !debug && s != name {
+ panic("got " + s + ", want " + name)
+ }
+}
+
+// Verify that the printer doesn't crash if the AST contains BadXXX nodes.
+func TestBadNodes(t *testing.T) {
+ const src = "package p\n("
+ const res = "package p\n\n(BadExpr)\n"
+ f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
+ if err == nil {
+ t.Error("expected illegal program") // error in test
+ }
+ var buf bytes.Buffer
+ Fprint(&buf, f)
+ if buf.String() != res {
+ t.Errorf("got %q, expected %q", buf.String(), res)
+ }
+}
+
+// idents is an iterator that returns all idents in f via the result channel.
+func idents(f *ast.File) <-chan *ast.Ident {
+ v := make(chan *ast.Ident)
+ go func() {
+ ast.Walk(f, func(n ast.Node) bool {
+ if ident, ok := n.(*ast.Ident); ok {
+ v <- ident
+ }
+ return true
+ }, nil)
+ close(v)
+ }()
+ return v
+}
+
+// identCount returns the number of identifiers found in f.
+func identCount(f *ast.File) int {
+ n := 0
+ for range idents(f) {
+ n++
+ }
+ return n
+}
+
+// Verify that the SourcePos mode emits correct //line comments
+// by testing that position information for matching identifiers
+// is maintained.
+func TestSourcePos(t *testing.T) {
+ const src = `package p
+
+import (
+ "go/printer"
+ "math"
+ "regexp"
+)
+
+pi = 3.14 // TODO: allow on same line
+xx = 0
+t: {
+ x: int
+ y: int
+ z: int
+ u: number
+ v: number
+ w: number
+}
+e: a*t.x + b*t.y
+
+// two extra lines here // ...
+e2: c*t.z
+`
+
+ // parse original
+ f1, err := parser.ParseFile(fset, "src", src, parser.ParseComments)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // pretty-print original
+ var buf bytes.Buffer
+ err = (&config{UseSpaces: true, Tabwidth: 8}).fprint(&buf, f1)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // parse pretty printed original
+ // (//line comments must be interpreted even w/o syntax.ParseComments set)
+ f2, err := parser.ParseFile(fset, "", buf.Bytes(),
+ parser.AllErrors, parser.ParseLambdas, parser.ParseComments)
+ if err != nil {
+ t.Fatalf("%s\n%s", err, buf.Bytes())
+ }
+
+ // At this point the position information of identifiers in f2 should
+ // match the position information of corresponding identifiers in f1.
+
+ // number of identifiers must be > 0 (test should run) and must match
+ n1 := identCount(f1)
+ n2 := identCount(f2)
+ if n1 == 0 {
+ t.Fatal("got no idents")
+ }
+ if n2 != n1 {
+ t.Errorf("got %d idents; want %d", n2, n1)
+ }
+
+ // verify that all identifiers have correct line information
+ i2range := idents(f2)
+ for i1 := range idents(f1) {
+ i2 := <-i2range
+
+ if i2 == nil || i1 == nil {
+ t.Fatal("non nil identifiers")
+ }
+ if i2.Name != i1.Name {
+ t.Errorf("got ident %s; want %s", i2.Name, i1.Name)
+ }
+
+ l1 := fset.Position(i1.Pos()).Line
+ l2 := fset.Position(i2.Pos()).Line
+ if l2 != l1 {
+ t.Errorf("got line %d; want %d for %s", l2, l1, i1.Name)
+ }
+ }
+
+ if t.Failed() {
+ t.Logf("\n%s", buf.Bytes())
+ }
+}
+
+var decls = []string{
+ `import "fmt"`,
+ "pi = 3.1415\ne = 2.71828\n\nx = pi",
+}
+
+func TestDeclLists(t *testing.T) {
+ for _, src := range decls {
+ file, err := parser.ParseFile(fset, "", "package p\n"+src, parser.ParseComments)
+ if err != nil {
+ panic(err) // error in test
+ }
+
+ var buf bytes.Buffer
+ err = Fprint(&buf, file.Decls) // only print declarations
+ if err != nil {
+ panic(err) // error in test
+ }
+
+ out := buf.String()
+
+ if out != src {
+ t.Errorf("\ngot : %q\nwant: %q\n", out, src)
+ }
+ }
+}
+
+var stmts = []string{
+ "i := 0",
+ "select {}\nvar a, b = 1, 2\nreturn a + b",
+ "go f()\ndefer func() {}()",
+}
+
+type limitWriter struct {
+ remaining int
+ errCount int
+}
+
+func (l *limitWriter) Write(buf []byte) (n int, err error) {
+ n = len(buf)
+ if n >= l.remaining {
+ n = l.remaining
+ err = io.EOF
+ l.errCount++
+ }
+ l.remaining -= n
+ return n, err
+}
+
+// TextX is a skeleton test that can be filled in for debugging one-off cases.
+// Do not remove.
+func TestX(t *testing.T) {
+ const src = `
+ { e: k <-
+ for a, v in s}
+ a: b
+
+`
+ b, err := format([]byte(src), 0)
+ if err != nil {
+ t.Error(err)
+ }
+ _ = b
+ // t.Error("\n", string(b))
+}
diff --git a/cue/format/node.go b/cue/format/node.go
new file mode 100644
index 0000000..cb138c7
--- /dev/null
+++ b/cue/format/node.go
@@ -0,0 +1,688 @@
+// Copyright 2018 The CUE Authors
+//
+// 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 format
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/parser"
+ "cuelang.org/go/cue/token"
+)
+
+func printNode(node interface{}, f *printer) error {
+ s := newFormatter(f)
+
+ // format node
+ f.allowed = nooverride // gobble initial whitespace.
+ switch x := node.(type) {
+ case *ast.File:
+ s.file(x)
+ case ast.Expr:
+ s.expr(x)
+ case ast.Decl:
+ s.decl(x)
+ // case ast.Node: // TODO: do we need this?
+ // s.walk(x)
+ case []ast.Decl:
+ s.walkDeclList(x)
+ default:
+ goto unsupported
+ }
+
+ return nil
+
+unsupported:
+ return fmt.Errorf("cue/format: unsupported node type %T", node)
+}
+
+// Helper functions for common node lists. They may be empty.
+
+func (f *formatter) walkDeclList(list []ast.Decl) {
+ f.before(nil)
+ for i, x := range list {
+ if i > 0 {
+ f.print(declcomma)
+ }
+ f.decl(x)
+ f.print(f.current.parentSep)
+ }
+ f.after(nil)
+}
+
+func (f *formatter) walkSpecList(list []*ast.ImportSpec) {
+ f.before(nil)
+ for _, x := range list {
+ f.importSpec(x)
+ }
+ f.after(nil)
+}
+
+func (f *formatter) walkClauseList(list []ast.Clause) {
+ f.before(nil)
+ for _, x := range list {
+ f.clause(x)
+ }
+ f.after(nil)
+}
+
+func (f *formatter) walkExprList(list []ast.Expr, depth int) {
+ f.before(nil)
+ for _, x := range list {
+ f.before(x)
+ f.exprRaw(x, token.LowestPrec, depth)
+ f.print(comma, blank)
+ f.after(x)
+ }
+ f.after(nil)
+}
+
+func (f *formatter) file(file *ast.File) {
+ f.before(file)
+ if file.Name != nil {
+ f.print(file.Package, "package")
+ f.print(blank, file.Name, newline, newsection)
+ }
+ f.current.pos = 3
+ f.visitComments(3)
+ f.walkDeclList(file.Decls)
+ f.after(file)
+ f.print(token.EOF)
+}
+func (f *formatter) decl(decl ast.Decl) {
+ if decl == nil {
+ return
+ }
+ if !f.before(decl) {
+ goto after
+ }
+ switch n := decl.(type) {
+ case *ast.Field:
+ // shortcut single-element structs.
+ lastSize := len(f.labelBuf)
+ f.labelBuf = f.labelBuf[:0]
+ first := n.Label
+ for {
+ obj, ok := n.Value.(*ast.StructLit)
+ if !ok || len(obj.Elts) != 1 || (obj.Lbrace.IsValid() && !f.printer.cfg.simplify) {
+ break
+ }
+
+ // Verify that struct doesn't have inside comments and that
+ // element doesn't have doc comments.
+ hasComments := len(obj.Elts[0].Comments()) > 0
+ for _, c := range obj.Comments() {
+ if c.Position == 1 || c.Position == 2 {
+ hasComments = true
+ }
+ }
+ if hasComments {
+ break
+ }
+
+ mem, ok := obj.Elts[0].(*ast.Field)
+ if !ok {
+ break
+ }
+ f.labelBuf = append(f.labelBuf, mem.Label)
+ n = mem
+ }
+
+ if lastSize != len(f.labelBuf) {
+ f.print(formfeed)
+ }
+
+ f.before(nil)
+ f.label(first)
+ for _, x := range f.labelBuf {
+ f.print(blank, nooverride)
+ f.label(x)
+ }
+ f.after(nil)
+
+ nextFF := f.nextNeedsFormfeed(n.Value)
+ tab := vtab
+ if nextFF || f.nestExpr >= 1 {
+ tab = blank
+ }
+
+ f.print(n.Colon, token.COLON, tab)
+ if n.Value != nil {
+ f.expr(n.Value)
+ } else {
+ f.current.pos++
+ f.visitComments(f.current.pos)
+ }
+
+ if nextFF {
+ f.print(formfeed)
+ }
+
+ case *ast.ComprehensionDecl:
+ f.decl(n.Field)
+ f.print(blank)
+ if n.Select != token.NoPos {
+ f.print(n.Select, token.ARROW, blank)
+ }
+ f.print(indent)
+ f.walkClauseList(n.Clauses)
+ f.print(unindent)
+ f.print("") // force whitespace to be written
+
+ case *ast.BadDecl:
+ f.print(n.From, "*bad decl*", declcomma)
+
+ case *ast.ImportDecl:
+ f.print(n.Import, "import")
+ if len(n.Specs) == 0 {
+ f.print(blank, n.Lparen, token.LPAREN, n.Rparen, token.RPAREN, newline)
+ }
+ switch {
+ case len(n.Specs) == 1:
+ if !n.Lparen.IsValid() {
+ f.print(blank)
+ f.walkSpecList(n.Specs)
+ break
+ }
+ fallthrough
+ default:
+ f.print(blank, n.Lparen, token.LPAREN, newline, indent)
+ f.walkSpecList(n.Specs)
+ f.print(unindent, newline, n.Rparen, token.RPAREN, newline)
+ }
+ f.print(newsection)
+
+ case *ast.EmitDecl:
+ f.expr(n.Expr)
+ f.print(newline, newsection) // force newline
+
+ case *ast.Alias:
+ f.expr(n.Ident)
+ f.print(blank, n.Equal, token.BIND, blank)
+ f.expr(n.Expr)
+ f.print(declcomma, newline) // implied
+ }
+after:
+ f.after(decl)
+}
+
+func (f *formatter) nextNeedsFormfeed(n ast.Expr) bool {
+ switch x := n.(type) {
+ case *ast.StructLit:
+ return true
+ case *ast.BasicLit:
+ return strings.IndexByte(x.Value, '\n') >= 0
+ case *ast.ListLit:
+ return true
+ }
+ return false
+}
+
+func (f *formatter) importSpec(x *ast.ImportSpec) {
+ if x.Name != nil {
+ f.label(x.Name)
+ f.print(blank)
+ } else {
+ f.current.pos++
+ f.visitComments(f.current.pos)
+ }
+ f.expr(x.Path)
+ f.print(newline)
+}
+
+func (f *formatter) label(l ast.Label) {
+ switch n := l.(type) {
+ case *ast.Ident:
+ f.print(n.NamePos, n)
+
+ case *ast.BasicLit:
+ if f.cfg.simplify && n.Kind == token.STRING && len(n.Value) > 2 {
+ s := n.Value
+ unquoted, err := strconv.Unquote(s)
+ fset := token.NewFileSet()
+ if err == nil {
+ e, _ := parser.ParseExpr(fset, "check", unquoted)
+ if _, ok := e.(*ast.Ident); ok {
+ f.print(n.ValuePos, unquoted)
+ break
+ }
+ }
+ }
+ f.print(n.ValuePos, n.Value)
+
+ case *ast.TemplateLabel:
+ f.print(n.Langle, token.LSS, indent)
+ f.label(n.Ident)
+ f.print(unindent, n.Rangle, token.GTR)
+
+ case *ast.Interpolation:
+ f.expr(n)
+
+ case *ast.ExprLabel:
+ f.print(n.Lbrack, token.LBRACK, indent)
+ f.expr(n.Label)
+ f.print(unindent, n.Rbrack, token.RBRACK)
+
+ default:
+ panic(fmt.Sprintf("unknown label type %T", n))
+ }
+}
+
+func (f *formatter) expr(x ast.Expr) {
+ const depth = 1
+ f.expr1(x, token.LowestPrec, depth)
+}
+
+func (f *formatter) expr0(x ast.Expr, depth int) {
+ f.expr1(x, token.LowestPrec, depth)
+}
+
+func (f *formatter) expr1(expr ast.Expr, prec1, depth int) {
+ if f.before(expr) {
+ f.exprRaw(expr, prec1, depth)
+ }
+ f.after(expr)
+}
+
+func (f *formatter) exprRaw(expr ast.Expr, prec1, depth int) {
+
+ switch x := expr.(type) {
+ case *ast.BadExpr:
+ f.print(x.From, "BadExpr")
+
+ case *ast.BottomLit:
+ f.print(x.Bottom, token.BOTTOM)
+
+ case *ast.Ident:
+ f.print(x.NamePos, x)
+
+ case *ast.BinaryExpr:
+ if depth < 1 {
+ f.internalError("depth < 1:", depth)
+ depth = 1
+ }
+ if prec1 == 8 { // ..
+ prec1 = 9 // always parentheses for values of ranges
+ }
+ f.binaryExpr(x, prec1, cutoff(x, depth), depth)
+
+ case *ast.UnaryExpr:
+ const prec = token.UnaryPrec
+ if prec < prec1 {
+ // parenthesis needed
+ f.print(token.LPAREN, nooverride)
+ f.expr(x)
+ f.print(token.RPAREN)
+ } else {
+ // no parenthesis needed
+ f.print(x.OpPos, x.Op, nooverride)
+ f.expr1(x.X, prec, depth)
+ }
+
+ case *ast.BasicLit:
+ f.print(x.ValuePos, x)
+
+ case *ast.Interpolation:
+ f.before(nil)
+ for _, x := range x.Elts {
+ f.expr0(x, depth+1)
+ }
+ f.after(nil)
+
+ case *ast.ParenExpr:
+ if _, hasParens := x.X.(*ast.ParenExpr); hasParens {
+ // don't print parentheses around an already parenthesized expression
+ // TODO: consider making this more general and incorporate precedence levels
+ f.expr0(x.X, depth)
+ } else {
+ f.print(x.Lparen, token.LPAREN)
+ f.expr0(x.X, reduceDepth(depth)) // parentheses undo one level of depth
+ f.print(x.Rparen, token.RPAREN)
+ }
+
+ case *ast.SelectorExpr:
+ f.selectorExpr(x, depth)
+
+ case *ast.IndexExpr:
+ f.expr1(x.X, token.HighestPrec, 1)
+ f.print(x.Lbrack, token.LBRACK)
+ f.expr0(x.Index, depth+1)
+ f.print(x.Rbrack, token.RBRACK)
+
+ case *ast.SliceExpr:
+ f.expr1(x.X, token.HighestPrec, 1)
+ f.print(x.Lbrack, token.LBRACK)
+ indices := []ast.Expr{x.Low, x.High}
+ for i, y := range indices {
+ if i > 0 {
+ // blanks around ":" if both sides exist and either side is a binary expression
+ x := indices[i-1]
+ if depth <= 1 && x != nil && y != nil && (isBinary(x) || isBinary(y)) {
+ f.print(blank, token.COLON, blank)
+ } else {
+ f.print(token.COLON)
+ }
+ }
+ if y != nil {
+ f.expr0(y, depth+1)
+ }
+ }
+ f.print(x.Rbrack, token.RBRACK)
+
+ case *ast.CallExpr:
+ if len(x.Args) > 1 {
+ depth++
+ }
+ wasIndented := f.possibleSelectorExpr(x.Fun, token.HighestPrec, depth)
+ f.print(x.Lparen, token.LPAREN)
+ f.walkExprList(x.Args, depth)
+ f.print(trailcomma, noblank, x.Rparen, token.RPAREN)
+ if wasIndented {
+ f.print(unindent)
+ }
+
+ case *ast.Ellipsis:
+ f.print(x.Ellipsis, token.ELLIPSIS)
+ if x.Elt != nil {
+ f.expr(x.Elt) // TODO
+ }
+
+ case *ast.StructLit:
+ f.print(x.Lbrace, token.LBRACE, noblank, f.formfeed(), indent)
+ f.walkDeclList(x.Elts)
+ f.matchUnindent()
+ f.print(noblank, x.Rbrace, token.RBRACE)
+
+ case *ast.ListLit:
+ f.print(x.Lbrack, token.LBRACK, indent)
+ f.walkExprList(x.Elts, 1)
+ if x.Ellipsis != token.NoPos || x.Type != nil {
+ f.print(x.Ellipsis, token.ELLIPSIS)
+ if x.Type != nil && !isTop(x.Type) {
+ f.expr(x.Type)
+ }
+ } else {
+ f.print(trailcomma, noblank)
+ f.current.pos += 2
+ f.visitComments(f.current.pos)
+ }
+ f.matchUnindent()
+ f.print(noblank, x.Rbrack, token.RBRACK)
+
+ case *ast.ListComprehension:
+ f.print(x.Lbrack, token.LBRACK, blank, indent)
+ f.expr(x.Expr)
+ f.print(blank)
+ f.walkClauseList(x.Clauses)
+ f.print(unindent, f.wsOverride(blank), x.Rbrack, token.RBRACK)
+
+ case *ast.LambdaExpr:
+ f.print(x.Lparen, token.LPAREN, indent, noblank)
+
+ f.before(nil)
+ for _, x := range x.Params {
+ f.label(x.Label)
+ if x.Colon.IsValid() {
+ f.print(x.Colon, token.COLON, blank)
+ f.expr(x.Value)
+ }
+ f.print(comma, blank)
+ }
+ f.print(trailcomma, noblank)
+ f.after(nil)
+
+ f.print(trailcomma, noblank, unindent)
+ f.print(x.Rparen, token.RPAREN, blank)
+ f.print(token.LAMBDA, blank)
+ f.expr(x.Expr)
+
+ default:
+ panic(fmt.Sprintf("unimplemented type %T", x))
+ }
+ return
+}
+
+func (f *formatter) clause(clause ast.Clause) {
+ switch n := clause.(type) {
+ case *ast.ForClause:
+ f.print(blank, n.For, "for", blank)
+ if n.Key != nil {
+ f.label(n.Key)
+ f.print(n.Colon, token.COMMA, blank)
+ } else {
+ f.current.pos++
+ f.visitComments(f.current.pos)
+ }
+ f.label(n.Value)
+ f.print(blank, n.In, "in", blank)
+ f.expr(n.Source)
+
+ case *ast.IfClause:
+ f.print(blank, n.If, "if", blank)
+ f.expr(n.Condition)
+
+ default:
+ panic("unknown clause type")
+ }
+}
+
+func walkBinary(e *ast.BinaryExpr) (has6, has7, has8 bool, maxProblem int) {
+ switch e.Op.Precedence() {
+ case 6:
+ has6 = true
+ case 7:
+ has7 = true
+ case 8:
+ has8 = true
+ }
+
+ switch l := e.X.(type) {
+ case *ast.BinaryExpr:
+ if l.Op.Precedence() < e.Op.Precedence() {
+ // parens will be inserted.
+ // pretend this is an *syntax.ParenExpr and do nothing.
+ break
+ }
+ h6, h7, h8, mp := walkBinary(l)
+ has6 = has6 || h6
+ has7 = has7 || h7
+ has8 = has8 || h8
+ if maxProblem < mp {
+ maxProblem = mp
+ }
+ }
+
+ switch r := e.Y.(type) {
+ case *ast.BinaryExpr:
+ if r.Op.Precedence() <= e.Op.Precedence() {
+ // parens will be inserted.
+ // pretend this is an *syntax.ParenExpr and do nothing.
+ break
+ }
+ h6, h7, h8, mp := walkBinary(r)
+ has6 = has6 || h6
+ has7 = has7 || h7
+ has8 = has8 || h8
+ if maxProblem < mp {
+ maxProblem = mp
+ }
+
+ case *ast.UnaryExpr:
+ switch e.Op.String() + r.Op.String() {
+ case "/*":
+ maxProblem = 8
+ case "++", "--":
+ if maxProblem < 6 {
+ maxProblem = 6
+ }
+ }
+ }
+ return
+}
+
+func cutoff(e *ast.BinaryExpr, depth int) int {
+ has6, has7, has8, maxProblem := walkBinary(e)
+ if maxProblem > 0 {
+ return maxProblem + 1
+ }
+ if (has6 || has7) && has8 {
+ if depth == 1 {
+ return 8
+ }
+ if has7 {
+ return 7
+ }
+ return 6
+ }
+ if has6 && has7 {
+ if depth == 1 {
+ return 7
+ }
+ return 6
+ }
+ if depth == 1 {
+ return 8
+ }
+ return 6
+}
+
+func diffPrec(expr ast.Expr, prec int) int {
+ x, ok := expr.(*ast.BinaryExpr)
+ if !ok || prec != x.Op.Precedence() {
+ return 1
+ }
+ return 0
+}
+
+func reduceDepth(depth int) int {
+ depth--
+ if depth < 1 {
+ depth = 1
+ }
+ return depth
+}
+
+// Format the binary expression: decide the cutoff and then format.
+// Let's call depth == 1 Normal mode, and depth > 1 Compact mode.
+// (Algorithm suggestion by Russ Cox.)
+//
+// The precedences are:
+// 8 ..
+// 7 * / % quo rem div mod
+// 6 + -
+// 5 == != < <= > >=
+// 4 &&
+// 3 ||
+// 2 &
+// 1 |
+//
+// The only decision is whether there will be spaces around levels 6 and 7.
+// There are never spaces at level 8 (unary), and always spaces at levels 5 and below.
+//
+// To choose the cutoff, look at the whole expression but excluding primary
+// expressions (function calls, parenthesized exprs), and apply these rules:
+//
+// 1) If there is a binary operator with a right side unary operand
+// that would clash without a space, the cutoff must be (in order):
+//
+// /* 8
+// ++ 7 // not necessary, but to avoid confusion
+// -- 7
+//
+// (Comparison operators always have spaces around them.)
+//
+// 2) If there is a mix of level 7 and level 6 operators, then the cutoff
+// is 7 (use spaces to distinguish precedence) in Normal mode
+// and 6 (never use spaces) in Compact mode.
+//
+// 3) If there are no level 6 operators or no level 7 operators, then the
+// cutoff is 8 (always use spaces) in Normal mode
+// and 6 (never use spaces) in Compact mode.
+//
+func (f *formatter) binaryExpr(x *ast.BinaryExpr, prec1, cutoff, depth int) {
+ f.nestExpr++
+ defer func() { f.nestExpr-- }()
+
+ prec := x.Op.Precedence()
+ if prec < prec1 {
+ // parenthesis needed
+ // Note: The parser inserts an syntax.ParenExpr node; thus this case
+ // can only occur if the AST is created in a different way.
+ // defer p.pushComment(nil).pop()
+ f.print(token.LPAREN, nooverride)
+ f.expr0(x, reduceDepth(depth)) // parentheses undo one level of depth
+ f.print(token.RPAREN)
+ return
+ }
+
+ printBlank := prec < cutoff
+
+ ws := indent
+ f.expr1(x.X, prec, depth+diffPrec(x.X, prec))
+ f.print(nooverride)
+ if printBlank {
+ f.print(blank)
+ }
+ f.print(x.OpPos, x.Op)
+ if x.Y.Pos().IsNewline() {
+ // at least one line break, but respect an extra empty line
+ // in the source
+ f.print(formfeed)
+ printBlank = false // no blank after line break
+ } else {
+ f.print(nooverride)
+ }
+ if printBlank {
+ f.print(blank)
+ }
+ f.expr1(x.Y, prec+1, depth+1)
+ if ws == ignore {
+ f.print(unindent)
+ }
+}
+
+func isBinary(expr ast.Expr) bool {
+ _, ok := expr.(*ast.BinaryExpr)
+ return ok
+}
+
+func (f *formatter) possibleSelectorExpr(expr ast.Expr, prec1, depth int) bool {
+ if x, ok := expr.(*ast.SelectorExpr); ok {
+ return f.selectorExpr(x, depth)
+ }
+ f.expr1(expr, prec1, depth)
+ return false
+}
+
+// selectorExpr handles an *syntax.SelectorExpr node and returns whether x spans
+// multiple lines.
+func (f *formatter) selectorExpr(x *ast.SelectorExpr, depth int) bool {
+ f.expr1(x.X, token.HighestPrec, depth)
+ f.print(token.PERIOD)
+ if x.Sel.Pos().IsNewline() {
+ f.print(indent, formfeed, x.Sel.Pos(), x.Sel)
+ f.print(unindent)
+ return true
+ }
+ f.print(x.Sel.Pos(), x.Sel)
+ return false
+}
+
+func isTop(e ast.Expr) bool {
+ ident, ok := e.(*ast.Ident)
+ return ok && ident.Name == "_"
+}
diff --git a/cue/format/printer.go b/cue/format/printer.go
new file mode 100644
index 0000000..10a2b7b
--- /dev/null
+++ b/cue/format/printer.go
@@ -0,0 +1,369 @@
+// Copyright 2018 The CUE Authors
+//
+// 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 format
+
+import (
+ "fmt"
+ "os"
+ "text/tabwriter"
+
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/token"
+)
+
+// A printer takes the stream of formatting tokens and spacing directives
+// produced by the formatter and adjusts the spacing based on the original
+// source code.
+type printer struct {
+ w tabwriter.Writer
+
+ fset *token.FileSet
+ cfg *config
+
+ allowed whiteSpace
+ requested whiteSpace
+ indentStack []whiteSpace
+ indentPos int
+
+ pos token.Position // current pos in AST
+
+ lastTok token.Token // last token printed (syntax.ILLEGAL if it's whitespace)
+
+ output []byte
+ indent int
+ spaceBefore bool
+}
+
+func (p *printer) init(cfg *config) {
+ p.cfg = cfg
+ p.pos = token.Position{Line: 1, Column: 1}
+}
+
+const debug = false
+
+func (p *printer) internalError(msg ...interface{}) {
+ if debug {
+ fmt.Print(p.pos.String() + ": ")
+ fmt.Println(msg...)
+ panic("go/printer")
+ }
+}
+
+func (p *printer) lineFor(pos token.Pos) int {
+ if p.fset == nil {
+ return 0
+ }
+ return p.fset.Position(pos).Line
+}
+
+func (p *printer) Print(v interface{}) {
+ var (
+ impliedComma = false
+ isLit bool
+ data string
+ nextWS whiteSpace
+ )
+ switch x := v.(type) {
+ case token.Token:
+ s := x.String()
+ before, after := mayCombine(p.lastTok, x)
+ if before && !p.spaceBefore {
+ // the previous and the current token must be
+ // separated by a blank otherwise they combine
+ // into a different incorrect token sequence
+ // (except for syntax.INT followed by a '.' this
+ // should never happen because it is taken care
+ // of via binary expression formatting)
+ if p.allowed&blank != 0 {
+ p.internalError("whitespace buffer not empty")
+ }
+ p.allowed |= blank
+ }
+ if after {
+ nextWS = blank
+ }
+ data = s
+ switch x {
+ case token.EOF:
+ data = ""
+ p.allowed = newline
+ p.allowed &^= newsection
+ case token.LPAREN, token.LBRACK, token.LBRACE:
+ case token.RPAREN, token.RBRACK, token.RBRACE:
+ impliedComma = true
+ }
+ p.lastTok = x
+
+ case *ast.BasicLit:
+ data = x.Value
+ isLit = true
+ impliedComma = true
+ p.lastTok = x.Kind
+
+ case *ast.Ident:
+ data = x.Name
+ impliedComma = true
+ p.lastTok = token.IDENT
+
+ case string:
+ data = x
+ impliedComma = true
+ p.lastTok = token.STRING
+
+ case *ast.CommentGroup:
+ rel := x.Pos().RelPos()
+ if x.Line { // TODO: we probably don't need this.
+ rel = token.Blank
+ }
+ switch rel {
+ case token.NoRelPos:
+ case token.Newline, token.NewSection:
+ case token.Blank, token.Elided:
+ p.allowed |= blank
+ fallthrough
+ case token.NoSpace:
+ p.allowed &^= newline | newsection | formfeed | declcomma
+ }
+ return
+
+ case *ast.Comment:
+ // TODO: if implied comma, postpone comment
+ data = x.Text
+ p.lastTok = token.COMMENT
+ break
+
+ case whiteSpace:
+ p.allowed |= x
+ return
+
+ case token.Pos:
+ // TODO: should we use a known file position to synchronize? Go does,
+ // but we don't really have to.
+ // pos := p.fset.Position(x)
+ if x.HasRelPos() {
+ if p.allowed&nooverride == 0 {
+ requested := p.allowed
+ switch x.RelPos() {
+ case token.NoSpace:
+ requested &^= newline | newsection | formfeed
+ case token.Blank:
+ requested |= blank
+ requested &^= newline | newsection | formfeed
+ case token.Newline:
+ requested |= newline
+ case token.NewSection:
+ requested |= newsection
+ }
+ p.writeWhitespace(requested)
+ p.allowed = 0
+ p.requested = 0
+ }
+ // p.pos = pos
+ }
+ return
+
+ default:
+ fmt.Fprintf(os.Stderr, "print: unsupported argument %v (%T)\n", x, x)
+ panic("go/printer type")
+ }
+
+ p.writeWhitespace(p.allowed)
+ p.allowed = 0
+ p.requested = 0
+ p.writeString(data, isLit)
+ p.allowed = nextWS
+ _ = impliedComma // TODO: delay comment printings
+}
+
+func (p *printer) writeWhitespace(ws whiteSpace) {
+ if debug {
+ p.output = append(p.output, fmt.Sprintf("/*=%x=*/", p.allowed)...) // do not update f.pos!
+ }
+
+ if ws&comma != 0 {
+ switch {
+ case ws&(newsection|newline|formfeed) != 0,
+ ws&trailcomma == 0:
+ p.writeByte(',', 1)
+ }
+ }
+ if ws&indent != 0 {
+ p.markLineIndent(ws)
+ }
+ if ws&unindent != 0 {
+ p.markUnindentLine()
+ }
+ switch {
+ case ws&newsection != 0:
+ p.maybeIndentLine(ws)
+ p.writeByte('\f', 2)
+ p.spaceBefore = true
+ case ws&formfeed != 0:
+ p.maybeIndentLine(ws)
+ p.writeByte('\f', 1)
+ p.spaceBefore = true
+ case ws&newline != 0:
+ p.maybeIndentLine(ws)
+ p.writeByte('\n', 1)
+ p.spaceBefore = true
+ case ws&declcomma != 0:
+ p.writeByte(',', 1)
+ p.writeByte(' ', 1)
+ p.spaceBefore = true
+ case ws&noblank != 0:
+ case ws&vtab != 0:
+ p.writeByte('\v', 1)
+ p.spaceBefore = true
+ case ws&blank != 0:
+ p.writeByte(' ', 1)
+ p.spaceBefore = true
+ }
+}
+
+func (p *printer) markLineIndent(ws whiteSpace) {
+ p.indentStack = append(p.indentStack, ws)
+}
+
+func (p *printer) markUnindentLine() (wasUnindented bool) {
+ last := len(p.indentStack) - 1
+ if ws := p.indentStack[last]; ws&indented != 0 {
+ p.indent--
+ wasUnindented = true
+ }
+ p.indentStack = p.indentStack[:last]
+ return wasUnindented
+}
+
+func (p *printer) maybeIndentLine(ws whiteSpace) {
+ if ws&unindent == 0 && len(p.indentStack) > 0 {
+ last := len(p.indentStack) - 1
+ if ws := p.indentStack[last]; ws&indented != 0 || ws&indent == 0 {
+ return
+ }
+ p.indentStack[last] |= indented
+ p.indent++
+ }
+}
+
+func (f *formatter) matchUnindent() whiteSpace {
+ f.allowed |= unindent
+ // TODO: make this work. Whitespace from closing bracket should match that
+ // of opening if there is no position information.
+ // f.allowed &^= nooverride | newline | newsection | formfeed | blank | noblank
+ // ws := f.indentStack[len(f.indentStack)-1]
+ // mask := blank | noblank | vtab
+ // f.allowed |= unindent | blank | noblank
+ // if ws&newline != 0 || ws*indented != 0 {
+ // f.allowed |= newline
+ // }
+ return 0
+}
+
+// writeString writes the string s to p.output and updates p.pos, p.out,
+// and p.last. If isLit is set, s is escaped w/ tabwriter.Escape characters
+// to protect s from being interpreted by the tabwriter.
+//
+// Note: writeString is only used to write Go tokens, literals, and
+// comments, all of which must be written literally. Thus, it is correct
+// to always set isLit = true. However, setting it explicitly only when
+// needed (i.e., when we don't know that s contains no tabs or line breaks)
+// avoids processing extra escape characters and reduces run time of the
+// printer benchmark by up to 10%.
+//
+func (p *printer) writeString(s string, isLit bool) {
+ if s != "" {
+ p.spaceBefore = false
+ }
+
+ if isLit {
+ // Protect s such that is passes through the tabwriter
+ // unchanged. Note that valid Go programs cannot contain
+ // tabwriter.Escape bytes since they do not appear in legal
+ // UTF-8 sequences.
+ p.output = append(p.output, tabwriter.Escape)
+ }
+
+ if debug {
+ p.output = append(p.output, fmt.Sprintf("/*%s*/", p.pos)...) // do not update f.pos!
+ }
+ p.output = append(p.output, s...)
+
+ if isLit {
+ p.output = append(p.output, tabwriter.Escape)
+ }
+ // update positions
+ nLines := 0
+ var li int // index of last newline; valid if nLines > 0
+ for i := 0; i < len(s); i++ {
+ // CUE tokens cannot contain '\f' - no need to look for it
+ if s[i] == '\n' {
+ nLines++
+ li = i
+ }
+ }
+ p.pos.Offset += len(s)
+ if nLines > 0 {
+ p.pos.Line += nLines
+ c := len(s) - li
+ p.pos.Column = c
+ } else {
+ p.pos.Column += len(s)
+ }
+}
+
+func (p *printer) writeByte(ch byte, n int) {
+ for i := 0; i < n; i++ {
+ p.output = append(p.output, ch)
+ }
+
+ // update positions
+ p.pos.Offset += n
+ if ch == '\n' || ch == '\f' {
+ p.pos.Line += n
+ p.pos.Column = 1
+
+ n := p.cfg.Indent + p.indent // include base indentation
+ for i := 0; i < n; i++ {
+ p.output = append(p.output, '\t')
+ }
+
+ // update positions
+ p.pos.Offset += n
+ p.pos.Column += n
+
+ return
+ }
+ p.pos.Column += n
+}
+
+func mayCombine(prev, next token.Token) (before, after bool) {
+ s := next.String()
+ if 'a' <= s[0] && s[0] < 'z' {
+ return true, true
+ }
+ switch prev {
+ case token.IQUO, token.IREM, token.IDIV, token.IMOD:
+ return false, false
+ case token.INT:
+ before = next == token.PERIOD // 1.
+ case token.ADD:
+ before = s[0] == '+' // ++
+ case token.SUB:
+ before = s[0] == '-' // --
+ case token.QUO:
+ before = s[0] == '*' // /*
+ }
+ return false, false
+}
diff --git a/cue/format/testdata/comments.golden b/cue/format/testdata/comments.golden
new file mode 100644
index 0000000..53c3b01
--- /dev/null
+++ b/cue/format/testdata/comments.golden
@@ -0,0 +1,19 @@
+// Package line 1 group 1
+// Package line 2 group 1
+
+// Package line 1 - group 2
+// Package line 2 - group 2
+package comments
+
+// Emit line 1 group 1
+
+// Emit line 1 group 2
+// Emit line 2 group 2
+{
+ // Inside Emit
+}
+
+a: 3 // a line comment
+
+b: 4 // line comment that is last in the file.
+cc: 555 // align comments
diff --git a/cue/format/testdata/comments.input b/cue/format/testdata/comments.input
new file mode 100644
index 0000000..8aafcc9
--- /dev/null
+++ b/cue/format/testdata/comments.input
@@ -0,0 +1,19 @@
+// Package line 1 group 1
+// Package line 2 group 1
+
+// Package line 1 - group 2
+// Package line 2 - group 2
+package comments
+
+// Emit line 1 group 1
+
+// Emit line 1 group 2
+// Emit line 2 group 2
+{
+ // Inside Emit
+}
+
+a: 3 // a line comment
+
+b: 4 // line comment that is last in the file.
+cc: 555 // align comments
\ No newline at end of file
diff --git a/cue/format/testdata/expressions.golden b/cue/format/testdata/expressions.golden
new file mode 100644
index 0000000..deaf030
--- /dev/null
+++ b/cue/format/testdata/expressions.golden
@@ -0,0 +1,163 @@
+package expressions
+
+{
+ a: 1
+ aaa: 2
+
+ b: 3
+
+ c b a: 4
+ c bb aaa: 5
+ c b <Name> a: int
+ alias = 3.14
+
+ alias2 = foo
+ aaalias = foo
+ b: bar
+
+ bottom1: _|_
+ bottom2: _|_ // converted to compact symbol
+
+ empty: {}
+ emptyNewLine: {
+
+ }
+ someObject: {
+ a: 8
+ aa: 9
+ aaa: 10
+ }
+
+ e: 1 + 2*3
+ e: 1 * 2 * 3 // error
+ e: 2..3
+ e: 2..(3 + 4)
+ ex: 2..3 + 4*5
+ e: (2..3)..4
+ e: 1 + 2 + 3 // error
+
+ e: s[1+2]
+ e: s[1:2]
+ e: s[1+2 : 2+4]
+ e: s[2]
+ e: s[2*3]
+ e: s[1+2*3]
+
+ e: f(3 + 4 + 5)
+ e: f(3 * 4 * 5)
+ e: f(3 + 4*5)
+
+ e: f(3 + 4 div 5)
+
+ e: 3 < 4 && 5 > 4
+ e: a || b && c || d
+
+ e: a + +b*3
+ e: -a - -b
+
+ e: b + c
+ e: b*c + d
+ e: a*b + c
+ e: a - b - c
+ e: a - (b - c)
+ e: a - b*c
+ e: a - (b * c)
+ e: a * b / c
+ e: a div b + 5
+ e: a / b
+ e: x[a | b]
+ e: x[a/b]
+ e: a & b
+ e: a + +b
+ e: a - -b
+ e: a div -b
+ e: x[a*-b]
+ e: x[a + +b]
+ e: len(longVariableName) * 2
+
+ e: "\(a)"
+ e: 'aa \(aaa) aa'
+ e: "aa \(aaa)"
+
+ e: [1, 2]
+ e: [1, 2, 3, 4,
+ 5, 6, 7, 8]
+ e: [1, 2, 3, 4,
+ 5, 6, 7, 8, // maybe force additional comma
+ ]
+ e: [...]
+ e: [
+ ...]
+ e: [...
+ ]
+ e: [1, 2, ...]
+ e: [1, 2,
+ ...]
+ e: [...int]
+ e: [...int]
+ e: [...int | float]
+ e: [ x for x in someObject if x > 9 ]
+ e: [ x
+ for x in someObject
+ if x > 9 ]
+ e: [ x
+ for x in someObject
+ if x > 9
+ ]
+
+ [k]: v for k, v in someObject
+ [k]: v <-
+ for k, v in someObject
+
+ e: {[k]: v <-
+ for k, v in someObject
+ if k > "a"
+ }
+
+ e: {[k]: v for k, v in someObject if k > "a"}
+ e: {[k]: v <-
+ for k, v in someObject if k > "a"}
+
+ e: {[k]: v <-
+ for k, v in someObject
+ if k > "a"}
+
+ e: [{
+ a: 1, b: 2
+ }]
+
+ e: [{
+ a: 1, b: 2
+ },
+ ]
+
+ e: [{
+ a: 1, b: 2
+ }, {
+ c: 1, d: 2
+ }]
+
+ e: [{
+ a: 1, b: 2
+ },
+ 3,
+ 4,
+ ]
+
+ e: (a, b) -> a + b
+ e: (a,
+ b) -> a + b
+ e: (a, b) -> {
+ a: b
+ }
+ e: (a,
+ b,
+ ) ->
+ {
+ a: b
+ }
+
+ e: e.f(1, 2)
+
+ e: (3 + 4)
+}
diff --git a/cue/format/testdata/expressions.input b/cue/format/testdata/expressions.input
new file mode 100644
index 0000000..0f104e0
--- /dev/null
+++ b/cue/format/testdata/expressions.input
@@ -0,0 +1,163 @@
+package expressions
+
+{
+ a: 1
+ aaa: 2
+
+ b: 3
+
+ c b a: 4
+ c bb aaa: 5
+ c b <Name> a: int
+ alias = 3.14
+
+ alias2 = foo
+ aaalias = foo
+ b: bar
+
+ bottom1: _|_
+ bottom2: _|_ // converted to compact symbol
+
+ empty: {}
+ emptyNewLine: {
+
+ }
+ someObject: {
+ a: 8
+ aa: 9
+ aaa: 10
+ }
+
+ e: 1+2*3
+ e: 1*2*3 // error
+ e: 2..3
+ e: 2..(3 + 4)
+ ex: 2..3+4*5
+ e: 2..3..4
+ e: 1 + 2 + 3 // error
+
+ e: s[1+2]
+ e: s[1:2]
+ e: s[1+2:2+4]
+ e: s[2]
+ e: s[2*3]
+ e: s[1+2*3]
+
+ e: f(3+4+5)
+ e: f(3*4*5)
+ e: f(3+4*5)
+
+ e: f(3 + 4 div 5)
+
+ e: 3<4&&5>4
+ e: a || b && c || d
+
+ e: a + +b * 3
+ e: -a - -b
+
+ e: b + c
+ e: b*c + d
+ e: a*b + c
+ e: a - b - c
+ e: a - (b - c)
+ e: a - b*c
+ e: a - (b * c)
+ e: a * b / c
+ e: a div b + 5
+ e: a / b
+ e: x[a|b]
+ e: x[a /b]
+ e: a & b
+ e: a + +b
+ e: a - -b
+ e: a div - b
+ e: x[a*-b]
+ e: x[a + +b]
+ e: len(longVariableName) * 2
+
+ e: "\(a)"
+ e: 'aa \(aaa) aa'
+ e: "aa \(aaa)"
+
+ e: [1, 2]
+ e: [1, 2, 3, 4,
+ 5, 6, 7, 8]
+ e: [1, 2, 3, 4,
+ 5, 6, 7, 8 // maybe force additional comma
+ ]
+ e: [...]
+ e: [
+ ...]
+ e: [...
+ ]
+ e: [1, 2, ...]
+ e: [1, 2,
+ ...]
+ e: [...int]
+ e: [...int,]
+ e: [...int | float]
+ e: [ x for x in someObject if x > 9 ]
+ e: [ x
+ for x in someObject
+ if x > 9 ]
+ e: [ x
+ for x in someObject
+ if x > 9
+ ]
+
+ [k]: v for k, v in someObject
+ [k]: v <-
+ for k, v in someObject
+
+ e: { [k]:v <-
+ for k, v in someObject
+ if k > "a"
+ }
+
+ e: { [k]:v for k, v in someObject if k > "a"}
+ e: { [k]:v <-
+ for k, v in someObject if k > "a"}
+
+ e: { [k]:v <-
+ for k, v in someObject
+ if k > "a"}
+
+ e: [{
+ a: 1, b: 2,
+ }]
+
+ e: [{
+ a: 1, b: 2,
+ },
+ ]
+
+ e: [{
+ a: 1, b: 2,
+ }, {
+ c: 1, d: 2,
+ }]
+
+ e: [{
+ a: 1, b: 2,
+ },
+ 3,
+ 4,
+ ]
+
+ e: (a, b) -> a + b
+ e: (a,
+ b) -> a + b
+ e: (a, b) -> {
+ a: b
+ }
+ e: (a,
+ b
+ ) ->
+ {
+ a: b
+ }
+
+ e: e.f(1, 2)
+
+ e: ((3 + 4))
+}
\ No newline at end of file
diff --git a/cue/format/testdata/simplify.golden b/cue/format/testdata/simplify.golden
new file mode 100644
index 0000000..edb8b27
--- /dev/null
+++ b/cue/format/testdata/simplify.golden
@@ -0,0 +1,6 @@
+
+foo bar: "str"
+
+a B: 42
+
+"a.b" "foo-" cc_dd: x
diff --git a/cue/format/testdata/simplify.input b/cue/format/testdata/simplify.input
new file mode 100644
index 0000000..08e374d
--- /dev/null
+++ b/cue/format/testdata/simplify.input
@@ -0,0 +1,5 @@
+"foo" "bar": "str"
+
+a "B": 42
+
+"a.b" "foo-" "cc_dd": x
\ No newline at end of file