blob: f1274e1e9ef18dfb6bc3b1f5998f02be398ad926 [file] [log] [blame]
// 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"
"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)
// Simplify allows the formatter to simplify output, such as removing
// unnecessary quotes.
func Simplify() Option {
return func(c *config) { c.simplify = true }
}
// UseSpaces specifies that tabs should be converted to spaces and sets the
// default tab width.
func UseSpaces(tabwidth int) Option {
return func(c *config) {
c.UseSpaces = true
c.Tabwidth = tabwidth
}
}
// TabIndent specifies whether to use tabs for indentation independent of
// UseSpaces.
func TabIndent(indent bool) Option {
return func(c *config) { c.TabIndent = indent }
}
// TODO: make public
// sortImportsOption causes import declarations to be sorted.
func sortImportsOption() Option {
return func(c *config) { c.sortImports = 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(node ast.Node, opt ...Option) ([]byte, error) {
cfg := newConfig(opt)
return cfg.fprint(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)
f, err := parser.ParseFile("", b, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("parse: %s", err)
}
// print AST
return cfg.fprint(f)
}
type config struct {
UseSpaces bool
TabIndent bool
Tabwidth int // default: 4
Indent int // default: 0 (all code is indented at least by this much)
simplify bool
sortImports 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(node interface{}) (out []byte, err error) {
var p printer
p.init(cfg)
if err = printNode(node, &p); err != nil {
return p.output, err
}
padchar := byte('\t')
if cfg.UseSpaces {
padchar = byte(' ')
}
twmode := tabwriter.StripEscape | tabwriter.TabIndent | tabwriter.DiscardEmptyColumns
if cfg.TabIndent {
twmode |= tabwriter.TabIndent
}
buf := &bytes.Buffer{}
tw := tabwriter.NewWriter(buf, 0, 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()
if err != nil {
return buf.Bytes(), err
}
b := buf.Bytes()
if !cfg.TabIndent {
b = bytes.ReplaceAll(b, []byte{'\t'}, bytes.Repeat([]byte{' '}, cfg.Tabwidth))
}
return b, nil
}
// 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 []labelEntry
}
type labelEntry struct {
label ast.Label
optional bool
}
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
}
type settings struct {
// separator is blank if the current node spans a single line and newline
// otherwise.
nodeSep whiteSpace
parentSep whiteSpace
override whiteSpace
}
// suppress spurious linter warning: field is actually used.
func init() {
s := settings{}
_ = s.override
}
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 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 {
s, ok := node.(*ast.StructLit)
if ok && len(s.Elts) <= 1 && 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
printed := false
for ; len(c.cg) > 0 && c.cg[0].Position <= until; c.cg = c.cg[1:] {
if printed {
f.Print(newsection)
}
printed = true
f.printComment(c.cg[0])
}
}
func (f *formatter) printComment(cg *ast.CommentGroup) {
f.Print(cg)
printBlank := false
if cg.Doc {
f.Print(newline)
printBlank = true
}
for _, c := range cg.List {
isEnd := strings.HasPrefix(c.Text, "//")
if !printBlank {
if isEnd {
f.Print(vtab)
} else {
f.Print(blank)
}
}
f.Print(c.Slash)
f.Print(c)
if isEnd {
f.Print(newline)
}
}
}