cuelang.org/go@v0.10.1/cue/format/format.go (about)

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