github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/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 // import "github.com/joomcode/cue/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  	"github.com/joomcode/cue/cue/ast"
    32  	"github.com/joomcode/cue/cue/parser"
    33  	"github.com/joomcode/cue/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  //
    91  func Node(node ast.Node, opt ...Option) ([]byte, error) {
    92  	cfg := newConfig(opt)
    93  	return cfg.fprint(node)
    94  }
    95  
    96  // Source formats src in canonical cue fmt style and returns the result or an
    97  // (I/O or syntax) error. src is expected to be a syntactically correct CUE
    98  // source file, or a list of CUE declarations or statements.
    99  //
   100  // If src is a partial source file, the leading and trailing space of src is
   101  // applied to the result (such that it has the same leading and trailing space
   102  // as src), and the result is indented by the same amount as the first line of
   103  // src containing code. Imports are not sorted for partial source files.
   104  //
   105  // Caution: Tools relying on consistent formatting based on the installed
   106  // version of cue (for instance, such as for presubmit checks) should execute
   107  // that cue binary instead of calling Source.
   108  //
   109  func Source(b []byte, opt ...Option) ([]byte, error) {
   110  	cfg := newConfig(opt)
   111  
   112  	f, err := parser.ParseFile("", b, parser.ParseComments)
   113  	if err != nil {
   114  		return nil, fmt.Errorf("parse: %s", err)
   115  	}
   116  
   117  	// print AST
   118  	return cfg.fprint(f)
   119  }
   120  
   121  type config struct {
   122  	UseSpaces bool
   123  	TabIndent bool
   124  	Tabwidth  int // default: 4
   125  	Indent    int // default: 0 (all code is indented at least by this much)
   126  
   127  	simplify    bool
   128  	sortImports bool
   129  }
   130  
   131  func newConfig(opt []Option) *config {
   132  	cfg := &config{
   133  		Tabwidth:  8,
   134  		TabIndent: true,
   135  		UseSpaces: true,
   136  	}
   137  	for _, o := range opt {
   138  		o(cfg)
   139  	}
   140  	return cfg
   141  }
   142  
   143  // Config defines the output of Fprint.
   144  func (cfg *config) fprint(node interface{}) (out []byte, err error) {
   145  	var p printer
   146  	p.init(cfg)
   147  	if err = printNode(node, &p); err != nil {
   148  		return p.output, err
   149  	}
   150  
   151  	padchar := byte('\t')
   152  	if cfg.UseSpaces {
   153  		padchar = byte(' ')
   154  	}
   155  
   156  	twmode := tabwriter.StripEscape | tabwriter.TabIndent | tabwriter.DiscardEmptyColumns
   157  	if cfg.TabIndent {
   158  		twmode |= tabwriter.TabIndent
   159  	}
   160  
   161  	buf := &bytes.Buffer{}
   162  	tw := tabwriter.NewWriter(buf, 0, cfg.Tabwidth, 1, padchar, twmode)
   163  
   164  	// write printer result via tabwriter/trimmer to output
   165  	if _, err = tw.Write(p.output); err != nil {
   166  		return
   167  	}
   168  
   169  	err = tw.Flush()
   170  	if err != nil {
   171  		return buf.Bytes(), err
   172  	}
   173  
   174  	b := buf.Bytes()
   175  	if !cfg.TabIndent {
   176  		b = bytes.ReplaceAll(b, []byte{'\t'}, bytes.Repeat([]byte{' '}, cfg.Tabwidth))
   177  	}
   178  	return b, nil
   179  }
   180  
   181  // A formatter walks a syntax.Node, interspersed with comments and spacing
   182  // directives, in the order that they would occur in printed form.
   183  type formatter struct {
   184  	*printer
   185  
   186  	stack    []frame
   187  	current  frame
   188  	nestExpr int
   189  }
   190  
   191  func newFormatter(p *printer) *formatter {
   192  	f := &formatter{
   193  		printer: p,
   194  		current: frame{
   195  			settings: settings{
   196  				nodeSep:   newline,
   197  				parentSep: newline,
   198  			},
   199  		},
   200  	}
   201  	return f
   202  }
   203  
   204  type whiteSpace int
   205  
   206  const (
   207  	ignore whiteSpace = 0
   208  
   209  	// write a space, or disallow it
   210  	blank whiteSpace = 1 << iota
   211  	vtab             // column marker
   212  	noblank
   213  
   214  	nooverride
   215  
   216  	comma      // print a comma, unless trailcomma overrides it
   217  	trailcomma // print a trailing comma unless closed on same line
   218  	declcomma  // write a comma when not at the end of line
   219  
   220  	newline    // write a line in a table
   221  	formfeed   // next line is not part of the table
   222  	newsection // add two newlines
   223  
   224  	indent   // request indent an extra level after the next newline
   225  	unindent // unindent a level after the next newline
   226  	indented // element was indented.
   227  )
   228  
   229  type frame struct {
   230  	cg  []*ast.CommentGroup
   231  	pos int8
   232  
   233  	settings
   234  }
   235  
   236  type settings struct {
   237  	// separator is blank if the current node spans a single line and newline
   238  	// otherwise.
   239  	nodeSep   whiteSpace
   240  	parentSep whiteSpace
   241  	override  whiteSpace
   242  }
   243  
   244  // suppress spurious linter warning: field is actually used.
   245  func init() {
   246  	s := settings{}
   247  	_ = s.override
   248  }
   249  
   250  func (f *formatter) print(a ...interface{}) {
   251  	for _, x := range a {
   252  		f.Print(x)
   253  		switch x.(type) {
   254  		case string, token.Token: // , *syntax.BasicLit, *syntax.Ident:
   255  			f.current.pos++
   256  		}
   257  	}
   258  	f.visitComments(f.current.pos)
   259  }
   260  
   261  func (f *formatter) formfeed() whiteSpace {
   262  	if f.current.nodeSep == blank {
   263  		return blank
   264  	}
   265  	return formfeed
   266  }
   267  
   268  func (f *formatter) wsOverride(def whiteSpace) whiteSpace {
   269  	if f.current.override == ignore {
   270  		return def
   271  	}
   272  	return f.current.override
   273  }
   274  
   275  func (f *formatter) onOneLine(node ast.Node) bool {
   276  	a := node.Pos()
   277  	b := node.End()
   278  	if a.IsValid() && b.IsValid() {
   279  		return f.lineFor(a) == f.lineFor(b)
   280  	}
   281  	// TODO: walk and look at relative positions to determine the same?
   282  	return false
   283  }
   284  
   285  func (f *formatter) before(node ast.Node) bool {
   286  	f.stack = append(f.stack, f.current)
   287  	f.current = frame{settings: f.current.settings}
   288  	f.current.parentSep = f.current.nodeSep
   289  
   290  	if node != nil {
   291  		s, ok := node.(*ast.StructLit)
   292  		if ok && len(s.Elts) <= 1 && f.current.nodeSep != blank && f.onOneLine(node) {
   293  			f.current.nodeSep = blank
   294  		}
   295  		f.current.cg = node.Comments()
   296  		f.visitComments(f.current.pos)
   297  		return true
   298  	}
   299  	return false
   300  }
   301  
   302  func (f *formatter) after(node ast.Node) {
   303  	f.visitComments(127)
   304  	p := len(f.stack) - 1
   305  	f.current = f.stack[p]
   306  	f.stack = f.stack[:p]
   307  	f.current.pos++
   308  	f.visitComments(f.current.pos)
   309  }
   310  
   311  func (f *formatter) visitComments(until int8) {
   312  	c := &f.current
   313  
   314  	printed := false
   315  	for ; len(c.cg) > 0 && c.cg[0].Position <= until; c.cg = c.cg[1:] {
   316  		if printed {
   317  			f.Print(newsection)
   318  		}
   319  		printed = true
   320  		f.printComment(c.cg[0])
   321  	}
   322  }
   323  
   324  func (f *formatter) printComment(cg *ast.CommentGroup) {
   325  	f.Print(cg)
   326  
   327  	printBlank := false
   328  	if cg.Doc && len(f.output) > 0 {
   329  		f.Print(newline)
   330  		printBlank = true
   331  	}
   332  	for _, c := range cg.List {
   333  		isEnd := strings.HasPrefix(c.Text, "//")
   334  		if !printBlank {
   335  			if isEnd {
   336  				f.Print(vtab)
   337  			} else {
   338  				f.Print(blank)
   339  			}
   340  		}
   341  		f.Print(c.Slash)
   342  		f.Print(c)
   343  		if isEnd {
   344  			f.Print(newline)
   345  			if cg.Doc {
   346  				f.Print(nooverride)
   347  			}
   348  		}
   349  	}
   350  }