blob: 3915328231342023e98a2888e332d5810d09cc97 [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
import (
"fmt"
"strconv"
"strings"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/literal"
"cuelang.org/go/cue/scanner"
"cuelang.org/go/cue/token"
"cuelang.org/go/internal"
)
func printNode(node interface{}, f *printer) error {
s := newFormatter(f)
ls := labelSimplifier{scope: map[string]bool{}}
// format node
f.allowed = nooverride // gobble initial whitespace.
switch x := node.(type) {
case *ast.File:
if f.cfg.simplify {
ls.markReferences(x)
}
s.file(x)
case ast.Expr:
if f.cfg.simplify {
ls.markReferences(x)
}
s.expr(x)
case ast.Decl:
if f.cfg.simplify {
ls.markReferences(x)
}
s.decl(x)
// case ast.Node: // TODO: do we need this?
// s.walk(x)
case []ast.Decl:
if f.cfg.simplify {
ls.processDecls(x)
}
s.walkDeclList(x)
default:
goto unsupported
}
return s.errs
unsupported:
return fmt.Errorf("cue/format: unsupported node type %T", node)
}
func isRegularField(tok token.Token) bool {
return tok == token.ILLEGAL || tok == token.COLON
}
// Helper functions for common node lists. They may be empty.
func nestDepth(f *ast.Field) int {
d := 1
if s, ok := f.Value.(*ast.StructLit); ok {
switch {
case len(s.Elts) != 1:
d = 0
default:
if f, ok := s.Elts[0].(*ast.Field); ok {
d += nestDepth(f)
}
}
}
return d
}
// TODO: be more accurate and move to astutil
func hasDocComments(d ast.Decl) bool {
if len(d.Comments()) > 0 {
return true
}
switch x := d.(type) {
case *ast.Field:
return len(x.Label.Comments()) > 0
case *ast.Alias:
return len(x.Ident.Comments()) > 0
case *ast.LetClause:
return len(x.Ident.Comments()) > 0
}
return false
}
func (f *formatter) walkDeclList(list []ast.Decl) {
f.before(nil)
d := 0
hasEllipsis := false
for i, x := range list {
if i > 0 {
f.print(declcomma)
nd := 0
if f, ok := x.(*ast.Field); ok {
nd = nestDepth(f)
}
if f.current.parentSep == newline && (d == 0 || nd != d) {
f.print(f.formfeed())
}
if hasDocComments(x) {
switch x := list[i-1].(type) {
case *ast.Field:
if x.Token == token.ISA || internal.IsDefinition(x.Label) {
f.print(newsection)
}
default:
f.print(newsection)
}
}
}
if f.printer.cfg.simplify && internal.IsEllipsis(x) {
hasEllipsis = true
continue
}
f.decl(x)
d = 0
if f, ok := x.(*ast.Field); ok {
d = nestDepth(f)
}
if j := i + 1; j < len(list) {
switch x := list[j].(type) {
case *ast.Field:
switch x := x.Value.(type) {
case *ast.StructLit:
// TODO: not entirely correct: could have multiple elements,
// not have a valid Lbrace, and be marked multiline. This
// cannot occur for ASTs resulting from a parse, though.
if x.Lbrace.IsValid() || len(x.Elts) != 1 {
f.print(f.formfeed())
continue
}
case *ast.ListLit:
f.print(f.formfeed())
continue
}
}
}
f.print(f.current.parentSep)
}
if hasEllipsis {
f.decl(&ast.Ellipsis{})
f.print(f.current.parentSep)
}
f.after(nil)
}
func (f *formatter) walkSpecList(list []*ast.ImportSpec) {
f.before(nil)
for _, x := range list {
f.before(x)
f.importSpec(x)
f.after(x)
}
f.after(nil)
}
func (f *formatter) walkClauseList(list []ast.Clause, ws whiteSpace) {
f.before(nil)
for _, x := range list {
f.before(x)
f.print(ws)
f.clause(x)
f.after(x)
}
f.after(nil)
}
func (f *formatter) walkListElems(list []ast.Expr) {
f.before(nil)
for _, x := range list {
f.before(x)
switch n := x.(type) {
case *ast.Comprehension:
f.walkClauseList(n.Clauses, blank)
f.print(blank, nooverride)
f.expr(n.Value)
case *ast.Ellipsis:
f.ellipsis(n)
case *ast.Alias:
f.expr(n.Ident)
f.print(n.Equal, token.BIND)
f.expr(n.Expr)
// TODO: ast.CommentGroup: allows comment groups in ListLits.
case ast.Expr:
f.exprRaw(n, token.LowestPrec, 1)
}
f.print(comma, blank)
f.after(x)
}
f.after(nil)
}
func (f *formatter) walkArgsList(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)
f.walkDeclList(file.Decls)
f.after(file)
f.print(token.EOF)
}
func (f *formatter) inlineField(n *ast.Field) *ast.Field {
regular := internal.IsRegularField(n)
// shortcut single-element structs.
// If the label has a valid position, we assume that an unspecified
// Lbrace signals the intend to collapse fields.
if !n.Label.Pos().IsValid() && !(f.printer.cfg.simplify && regular) {
return nil
}
obj, ok := n.Value.(*ast.StructLit)
if !ok || len(obj.Elts) != 1 ||
(obj.Lbrace.IsValid() && !f.printer.cfg.simplify) ||
(obj.Lbrace.IsValid() && hasDocComments(n)) ||
len(n.Attrs) > 0 {
return nil
}
mem, ok := obj.Elts[0].(*ast.Field)
if !ok || len(mem.Attrs) > 0 {
return nil
}
if hasDocComments(mem) {
// TODO: this inserts curly braces even in spaces where this
// may not be desirable, such as:
// a:
// // foo
// b: 3
return nil
}
return mem
}
func (f *formatter) decl(decl ast.Decl) {
if decl == nil {
return
}
defer f.after(decl)
if !f.before(decl) {
return
}
switch n := decl.(type) {
case *ast.Field:
f.label(n.Label, n.Optional != token.NoPos)
regular := isRegularField(n.Token)
if regular {
f.print(noblank, nooverride, n.TokenPos, token.COLON)
} else {
f.print(blank, nooverride, n.Token)
}
if mem := f.inlineField(n); mem != nil {
switch {
default:
fallthrough
case regular && f.cfg.simplify:
f.print(blank, nooverride)
f.decl(mem)
case mem.Label.Pos().IsNewline():
f.print(indent, formfeed)
f.decl(mem)
f.indent--
}
return
}
nextFF := f.nextNeedsFormfeed(n.Value)
tab := vtab
if nextFF {
tab = blank
}
f.print(tab)
if n.Value != nil {
switch n.Value.(type) {
case *ast.ListComprehension, *ast.ListLit, *ast.StructLit:
f.expr(n.Value)
default:
f.print(indent)
f.expr(n.Value)
f.markUnindentLine()
}
} else {
f.current.pos++
f.visitComments(f.current.pos)
}
space := tab
for _, a := range n.Attrs {
if f.before(a) {
f.print(space, a.At, a)
}
f.after(a)
space = blank
}
if nextFF {
f.print(formfeed)
}
case *ast.BadDecl:
f.print(n.From, "*bad decl*", declcomma)
case *ast.Package:
f.print(n.PackagePos, "package")
f.print(blank, n.Name, newsection, nooverride)
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)
break
}
switch {
case len(n.Specs) == 1 && len(n.Specs[0].Comments()) == 0:
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, nooverride)
case *ast.LetClause:
if !decl.Pos().HasRelPos() || decl.Pos().RelPos() >= token.Newline {
f.print(formfeed)
}
f.print(n.Let, token.LET, blank, nooverride)
f.expr(n.Ident)
f.print(blank, nooverride, n.Equal, token.BIND, blank)
f.expr(n.Expr)
f.print(declcomma) // implied
case *ast.EmbedDecl:
if !n.Pos().HasRelPos() || n.Pos().RelPos() >= token.Newline {
f.print(formfeed)
}
f.expr(n.Expr)
f.print(newline, noblank)
case *ast.Attribute:
f.print(n.At, n)
case *ast.CommentGroup:
f.print(newsection)
f.printComment(n)
f.print(newsection)
case ast.Expr:
f.embedding(n)
}
}
func (f *formatter) embedding(decl ast.Expr) {
switch n := decl.(type) {
case *ast.Comprehension:
if !n.Pos().HasRelPos() || n.Pos().RelPos() >= token.Newline {
f.print(formfeed)
}
f.walkClauseList(n.Clauses, blank)
f.print(blank, nooverride)
f.expr(n.Value)
case *ast.Ellipsis:
f.ellipsis(n)
case *ast.Alias:
if !decl.Pos().HasRelPos() || decl.Pos().RelPos() >= token.Newline {
f.print(formfeed)
}
f.expr(n.Ident)
f.print(blank, n.Equal, token.BIND, blank)
f.expr(n.Expr)
f.print(declcomma) // implied
// TODO: ast.CommentGroup: allows comment groups in ListLits.
case ast.Expr:
f.exprRaw(n, token.LowestPrec, 1)
}
}
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, false)
f.print(blank)
} else {
f.current.pos++
f.visitComments(f.current.pos)
}
f.expr(x.Path)
f.print(newline)
}
func isValidIdent(ident string) bool {
var scan scanner.Scanner
scan.Init(token.NewFile("check", -1, len(ident)), []byte(ident), nil, 0)
_, tok, lit := scan.Scan()
if tok == token.IDENT || tok.IsKeyword() {
return lit == ident
}
return false
}
func (f *formatter) label(l ast.Label, optional bool) {
f.before(l)
defer f.after(l)
switch n := l.(type) {
case *ast.Alias:
f.expr(n)
case *ast.Ident:
// Escape an identifier that has invalid characters. This may happen,
// if the AST is not generated by the parser.
name := n.Name
if !ast.IsValidIdent(name) {
name = strconv.Quote(n.Name)
}
f.print(n.NamePos, name)
case *ast.BasicLit:
str := n.Value
// Allow any CUE string in the AST, but ensure it is formatted
// according to spec.
if strings.HasPrefix(str, `"""`) || strings.HasPrefix(str, "#") {
if u, err := literal.Unquote(str); err == nil {
str = strconv.Quote(u)
}
}
f.print(n.ValuePos, str)
case *ast.TemplateLabel:
f.print(n.Langle, token.LSS, indent)
f.label(n.Ident, false)
f.print(unindent, n.Rangle, token.GTR)
case *ast.ListLit:
f.expr(n)
case *ast.Interpolation:
f.expr(n)
default:
panic(fmt.Sprintf("unknown label type %T", n))
}
if optional {
f.print(token.OPTION)
}
}
func (f *formatter) ellipsis(x *ast.Ellipsis) {
f.print(x.Ellipsis, token.ELLIPSIS)
if x.Type != nil && !isTop(x.Type) {
f.expr(x.Type)
}
}
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.Alias:
// Aliases in expression positions are printed in short form.
f.label(x.Ident, false)
f.print(x.Equal, token.BIND)
f.expr(x.Expr)
case *ast.Ident:
f.print(x.NamePos, x)
case *ast.BinaryExpr:
if depth < 1 {
f.internalError("depth < 1:", depth)
depth = 1
}
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.walkArgsList(x.Args, depth)
f.print(trailcomma, noblank, x.Rparen, token.RPAREN)
if wasIndented {
f.print(unindent)
}
case *ast.StructLit:
var l line
ws := noblank
ff := f.formfeed()
switch {
case len(x.Elts) == 0:
if !x.Rbrace.HasRelPos() {
// collapse curly braces if the body is empty.
ffAlt := blank | nooverride
for _, c := range x.Comments() {
if c.Position == 1 {
ffAlt = ff
}
}
ff = ffAlt
}
case !x.Rbrace.HasRelPos() || !x.Elts[0].Pos().HasRelPos():
ws |= newline | nooverride
}
f.print(x.Lbrace, token.LBRACE, &l, ws, ff, indent)
f.walkDeclList(x.Elts)
f.matchUnindent()
ws = noblank
if f.lineout != l {
ws |= newline
if f.lastTok != token.RBRACE && f.lastTok != token.RBRACK {
ws |= nooverride
}
}
f.print(ws, x.Rbrace, token.RBRACE)
case *ast.ListLit:
f.print(x.Lbrack, token.LBRACK, indent)
f.walkListElems(x.Elts)
f.print(trailcomma, noblank)
f.visitComments(f.current.pos)
f.matchUnindent()
f.print(noblank, x.Rbrack, token.RBRACK)
case *ast.Ellipsis:
f.ellipsis(x)
case *ast.ListComprehension:
f.print(x.Lbrack, token.LBRACK, blank, indent)
f.print(blank)
f.walkClauseList(x.Clauses, blank)
f.print(blank, nooverride)
if _, ok := x.Expr.(*ast.StructLit); ok {
f.expr(x.Expr)
} else {
f.print(token.LBRACE, blank)
f.expr(x.Expr)
f.print(blank, token.RBRACE)
}
f.print(unindent, f.wsOverride(blank), x.Rbrack, token.RBRACK)
default:
panic(fmt.Sprintf("unimplemented type %T", x))
}
}
func (f *formatter) clause(clause ast.Clause) {
switch n := clause.(type) {
case *ast.ForClause:
f.print(n.For, "for", blank)
f.print(indent)
if n.Key != nil {
f.label(n.Key, false)
f.print(n.Colon, token.COMMA, blank)
} else {
f.current.pos++
f.visitComments(f.current.pos)
}
f.label(n.Value, false)
f.print(blank, n.In, "in", blank)
f.expr(n.Source)
f.markUnindentLine()
case *ast.IfClause:
f.print(n.If, "if", blank)
f.print(indent)
f.expr(n.Condition)
f.markUnindentLine()
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:
// 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
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)
}
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 == "_"
}