blob: c9f43a35416f55d56bc8c6a0cef5dd97da48403b [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"
"os"
"strings"
"text/tabwriter"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
"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 {
cfg *config
allowed whiteSpace
requested whiteSpace
indentStack []whiteSpace
pos token.Position // current pos in AST
lineout line
lastTok token.Token // last token printed (syntax.ILLEGAL if it's whitespace)
output []byte
indent int
spaceBefore bool
errs errors.Error
}
type line int
func (p *printer) init(cfg *config) {
p.cfg = cfg
p.pos = token.Position{Line: 1, Column: 1}
}
func (p *printer) errf(n ast.Node, format string, args ...interface{}) {
p.errs = errors.Append(p.errs, errors.Newf(n.Pos(), format, args...))
}
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 {
return pos.Line()
}
func (p *printer) Print(v interface{}) {
var (
impliedComma = false
isLit bool
data string
nextWS whiteSpace
)
switch x := v.(type) {
case *line:
*x = p.lineout
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
switch x.Kind {
case token.INT:
if len(data) > 1 &&
data[0] == '0' &&
data[1] >= '0' && data[1] <= '9' {
data = "0o" + data[1:]
}
case token.FLOAT:
if strings.IndexByte(data, 'E') != -1 {
data = strings.ToLower(data)
}
}
isLit = true
impliedComma = true
p.lastTok = x.Kind
case *ast.Ident:
data = x.Name
if !ast.IsValidIdent(data) {
p.errf(x, "invalid identifier %q", x.Name)
data = "*bad identifier*"
}
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.Attribute:
data = x.Text
impliedComma = true
p.lastTok = token.ATTRIBUTE
case *ast.Comment:
// TODO: if implied comma, postpone comment
data = x.Text
p.lastTok = token.COMMENT
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 := 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 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.lineout += 2
p.spaceBefore = true
case ws&formfeed != 0:
p.maybeIndentLine(ws)
p.writeByte('\f', 1)
p.lineout++
p.spaceBefore = true
case ws&newline != 0:
p.maybeIndentLine(ws)
p.writeByte('\n', 1)
p.lineout++
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)
}
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 before, false
}