...

Source file src/cuelang.org/go/cue/format/format.go

Documentation: cuelang.org/go/cue/format

     1  // Copyright 2018 The CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package format implements standard formatting of CUE configurations.
    16  package format // import "cuelang.org/go/cue/format"
    17  
    18  // TODO: this package is in need of a rewrite. When doing so, the API should
    19  // allow for reformatting an AST, without actually writing bytes.
    20  //
    21  // In essence, formatting determines the relative spacing to tokens. It should
    22  // be possible to have an abstract implementation providing such information
    23  // that can be used to either format or update an AST in a single walk.
    24  
    25  import (
    26  	"bytes"
    27  	"fmt"
    28  	"strings"
    29  	"text/tabwriter"
    30  
    31  	"cuelang.org/go/cue/ast"
    32  	"cuelang.org/go/cue/parser"
    33  	"cuelang.org/go/cue/token"
    34  )
    35  
    36  // An Option sets behavior of the formatter.
    37  type Option func(c *config)
    38  
    39  // Simplify allows the formatter to simplify output, such as removing
    40  // unnecessary quotes.
    41  func Simplify() Option {
    42  	return func(c *config) { c.simplify = true }
    43  }
    44  
    45  // UseSpaces specifies that tabs should be converted to spaces and sets the
    46  // default tab width.
    47  func UseSpaces(tabwidth int) Option {
    48  	return func(c *config) {
    49  		c.UseSpaces = true
    50  		c.Tabwidth = tabwidth
    51  	}
    52  }
    53  
    54  // TabIndent specifies whether to use tabs for indentation independent of
    55  // UseSpaces.
    56  func TabIndent(indent bool) Option {
    57  	return func(c *config) { c.TabIndent = indent }
    58  }
    59  
    60  // IndentPrefix specifies the number of tabstops to use as a prefix for every
    61  // line.
    62  func IndentPrefix(n int) Option {
    63  	return func(c *config) { c.Indent = n }
    64  }
    65  
    66  // TODO: make public
    67  // sortImportsOption causes import declarations to be sorted.
    68  func sortImportsOption() Option {
    69  	return func(c *config) { c.sortImports = true }
    70  }
    71  
    72  // TODO: other options:
    73  //
    74  // const (
    75  // 	RawFormat Mode = 1 << iota // do not use a tabwriter; if set, UseSpaces is ignored
    76  // 	TabIndent                  // use tabs for indentation independent of UseSpaces
    77  // 	UseSpaces                  // use spaces instead of tabs for alignment
    78  // 	SourcePos                  // emit //line comments to preserve original source positions
    79  // )
    80  
    81  // Node formats node in canonical cue fmt style and writes the result to dst.
    82  //
    83  // The node type must be *ast.File, []syntax.Decl, syntax.Expr, syntax.Decl, or
    84  // syntax.Spec. Node does not modify node. Imports are not sorted for nodes
    85  // representing partial source files (for instance, if the node is not an
    86  // *ast.File).
    87  //
    88  // The function may return early (before the entire result is written) and
    89  // return a formatting error, for instance due to an incorrect AST.
    90  func Node(node ast.Node, opt ...Option) ([]byte, error) {
    91  	cfg := newConfig(opt)
    92  	return cfg.fprint(node)
    93  }
    94  
    95  // Source formats src in canonical cue fmt style and returns the result or an
    96  // (I/O or syntax) error. src is expected to be a syntactically correct CUE
    97  // source file, or a list of CUE declarations or statements.
    98  //
    99  // If src is a partial source file, the leading and trailing space of src is
   100  // applied to the result (such that it has the same leading and trailing space
   101  // as src), and the result is indented by the same amount as the first line of
   102  // src containing code. Imports are not sorted for partial source files.
   103  //
   104  // Caution: Tools relying on consistent formatting based on the installed
   105  // version of cue (for instance, such as for presubmit checks) should execute
   106  // that cue binary instead of calling Source.
   107  func Source(b []byte, opt ...Option) ([]byte, error) {
   108  	cfg := newConfig(opt)
   109  
   110  	f, err := parser.ParseFile("", b, parser.ParseComments)
   111  	if err != nil {
   112  		return nil, fmt.Errorf("parse: %s", err)
   113  	}
   114  
   115  	// print AST
   116  	return cfg.fprint(f)
   117  }
   118  
   119  type config struct {
   120  	UseSpaces bool
   121  	TabIndent bool
   122  	Tabwidth  int // default: 4
   123  	Indent    int // default: 0 (all code is indented at least by this much)
   124  
   125  	simplify    bool
   126  	sortImports bool
   127  }
   128  
   129  func newConfig(opt []Option) *config {
   130  	cfg := &config{
   131  		Tabwidth:  8,
   132  		TabIndent: true,
   133  		UseSpaces: true,
   134  	}
   135  	for _, o := range opt {
   136  		o(cfg)
   137  	}
   138  	return cfg
   139  }
   140  
   141  // Config defines the output of Fprint.
   142  func (cfg *config) fprint(node interface{}) (out []byte, err error) {
   143  	var p printer
   144  	p.init(cfg)
   145  	if err = printNode(node, &p); err != nil {
   146  		return p.output, err
   147  	}
   148  
   149  	padchar := byte('\t')
   150  	if cfg.UseSpaces {
   151  		padchar = byte(' ')
   152  	}
   153  
   154  	twmode := tabwriter.StripEscape | tabwriter.TabIndent | tabwriter.DiscardEmptyColumns
   155  	if cfg.TabIndent {
   156  		twmode |= tabwriter.TabIndent
   157  	}
   158  
   159  	buf := &bytes.Buffer{}
   160  	tw := tabwriter.NewWriter(buf, 0, cfg.Tabwidth, 1, padchar, twmode)
   161  
   162  	// write printer result via tabwriter/trimmer to output
   163  	if _, err = tw.Write(p.output); err != nil {
   164  		return
   165  	}
   166  
   167  	err = tw.Flush()
   168  	if err != nil {
   169  		return buf.Bytes(), err
   170  	}
   171  
   172  	b := buf.Bytes()
   173  	if !cfg.TabIndent {
   174  		b = bytes.ReplaceAll(b, []byte{'\t'}, bytes.Repeat([]byte{' '}, cfg.Tabwidth))
   175  	}
   176  	return b, nil
   177  }
   178  
   179  // A formatter walks a syntax.Node, interspersed with comments and spacing
   180  // directives, in the order that they would occur in printed form.
   181  type formatter struct {
   182  	*printer
   183  
   184  	stack    []frame
   185  	current  frame
   186  	nestExpr int
   187  }
   188  
   189  func newFormatter(p *printer) *formatter {
   190  	f := &formatter{
   191  		printer: p,
   192  		current: frame{
   193  			settings: settings{
   194  				nodeSep:   newline,
   195  				parentSep: newline,
   196  			},
   197  		},
   198  	}
   199  	return f
   200  }
   201  
   202  type whiteSpace int
   203  
   204  const (
   205  	ignore whiteSpace = 0
   206  
   207  	// write a space, or disallow it
   208  	blank whiteSpace = 1 << iota
   209  	vtab             // column marker
   210  	noblank
   211  
   212  	nooverride
   213  
   214  	comma      // print a comma, unless trailcomma overrides it
   215  	trailcomma // print a trailing comma unless closed on same line
   216  	declcomma  // write a comma when not at the end of line
   217  
   218  	newline    // write a line in a table
   219  	formfeed   // next line is not part of the table
   220  	newsection // add two newlines
   221  
   222  	indent   // request indent an extra level after the next newline
   223  	unindent // unindent a level after the next newline
   224  	indented // element was indented.
   225  )
   226  
   227  type frame struct {
   228  	cg  []*ast.CommentGroup
   229  	pos int8
   230  
   231  	settings
   232  }
   233  
   234  type settings struct {
   235  	// separator is blank if the current node spans a single line and newline
   236  	// otherwise.
   237  	nodeSep   whiteSpace
   238  	parentSep whiteSpace
   239  	override  whiteSpace
   240  }
   241  
   242  // suppress spurious linter warning: field is actually used.
   243  func init() {
   244  	s := settings{}
   245  	_ = s.override
   246  }
   247  
   248  func (f *formatter) print(a ...interface{}) {
   249  	for _, x := range a {
   250  		f.Print(x)
   251  		switch x.(type) {
   252  		case string, token.Token: // , *syntax.BasicLit, *syntax.Ident:
   253  			f.current.pos++
   254  		}
   255  	}
   256  }
   257  
   258  func (f *formatter) formfeed() whiteSpace {
   259  	if f.current.nodeSep == blank {
   260  		return blank
   261  	}
   262  	return formfeed
   263  }
   264  
   265  func (f *formatter) wsOverride(def whiteSpace) whiteSpace {
   266  	if f.current.override == ignore {
   267  		return def
   268  	}
   269  	return f.current.override
   270  }
   271  
   272  func (f *formatter) onOneLine(node ast.Node) bool {
   273  	a := node.Pos()
   274  	b := node.End()
   275  	if a.IsValid() && b.IsValid() {
   276  		return f.lineFor(a) == f.lineFor(b)
   277  	}
   278  	// TODO: walk and look at relative positions to determine the same?
   279  	return false
   280  }
   281  
   282  func (f *formatter) before(node ast.Node) bool {
   283  	f.stack = append(f.stack, f.current)
   284  	f.current = frame{settings: f.current.settings}
   285  	f.current.parentSep = f.current.nodeSep
   286  
   287  	if node != nil {
   288  		s, ok := node.(*ast.StructLit)
   289  		if ok && len(s.Elts) <= 1 && f.current.nodeSep != blank && f.onOneLine(node) {
   290  			f.current.nodeSep = blank
   291  		}
   292  		f.current.cg = node.Comments()
   293  		f.visitComments(f.current.pos)
   294  		return true
   295  	}
   296  	return false
   297  }
   298  
   299  func (f *formatter) after(node ast.Node) {
   300  	f.visitComments(127)
   301  	p := len(f.stack) - 1
   302  	f.current = f.stack[p]
   303  	f.stack = f.stack[:p]
   304  	f.current.pos++
   305  	f.visitComments(f.current.pos)
   306  }
   307  
   308  func (f *formatter) visitComments(until int8) {
   309  	c := &f.current
   310  
   311  	printed := false
   312  	for ; len(c.cg) > 0 && c.cg[0].Position <= until; c.cg = c.cg[1:] {
   313  		if printed {
   314  			f.Print(newsection)
   315  		}
   316  		printed = true
   317  		f.printComment(c.cg[0])
   318  	}
   319  }
   320  
   321  func (f *formatter) printComment(cg *ast.CommentGroup) {
   322  	f.Print(cg)
   323  
   324  	printBlank := false
   325  	if cg.Doc && len(f.output) > 0 {
   326  		f.Print(newline)
   327  		printBlank = true
   328  	}
   329  	for _, c := range cg.List {
   330  		isEnd := strings.HasPrefix(c.Text, "//")
   331  		if !printBlank {
   332  			if isEnd {
   333  				f.Print(vtab)
   334  			} else {
   335  				f.Print(blank)
   336  			}
   337  		}
   338  		f.Print(c.Slash)
   339  		f.Print(c)
   340  		if isEnd {
   341  			f.Print(newline)
   342  			if cg.Doc {
   343  				f.Print(nooverride)
   344  			}
   345  		}
   346  	}
   347  }
   348  

View as plain text