| // 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 implements standard formatting of CUE configurations. |
| package format // import "cuelang.org/go/cue/format" |
| |
| import ( |
| "bytes" |
| "fmt" |
| "strings" |
| "text/tabwriter" |
| |
| "cuelang.org/go/cue/ast" |
| "cuelang.org/go/cue/parser" |
| "cuelang.org/go/cue/token" |
| ) |
| |
| // An Option sets behavior of the formatter. |
| type Option func(c *config) |
| |
| // Simplify allows the formatter to simplify output, such as removing |
| // unnecessary quotes. |
| func Simplify() Option { |
| return func(c *config) { c.simplify = true } |
| } |
| |
| // UseSpaces specifies that tabs should be converted to spaces and sets the |
| // default tab width. |
| func UseSpaces(tabwidth int) Option { |
| return func(c *config) { |
| c.UseSpaces = true |
| c.Tabwidth = tabwidth |
| } |
| } |
| |
| // TabIndent specifies whether to use tabs for indentation independent of |
| // UseSpaces. |
| func TabIndent(indent bool) Option { |
| return func(c *config) { c.TabIndent = indent } |
| } |
| |
| // TODO: make public |
| // sortImportsOption causes import declarations to be sorted. |
| func sortImportsOption() Option { |
| return func(c *config) { c.sortImports = true } |
| } |
| |
| // TODO: other options: |
| // |
| // const ( |
| // RawFormat Mode = 1 << iota // do not use a tabwriter; if set, UseSpaces is ignored |
| // TabIndent // use tabs for indentation independent of UseSpaces |
| // UseSpaces // use spaces instead of tabs for alignment |
| // SourcePos // emit //line comments to preserve original source positions |
| // ) |
| |
| // Node formats node in canonical cue fmt style and writes the result to dst. |
| // |
| // The node type must be *ast.File, []syntax.Decl, syntax.Expr, syntax.Decl, or |
| // syntax.Spec. Node does not modify node. Imports are not sorted for nodes |
| // representing partial source files (for instance, if the node is not an |
| // *ast.File). |
| // |
| // The function may return early (before the entire result is written) and |
| // return a formatting error, for instance due to an incorrect AST. |
| // |
| func Node(node ast.Node, opt ...Option) ([]byte, error) { |
| cfg := newConfig(opt) |
| return cfg.fprint(node) |
| } |
| |
| // Source formats src in canonical cue fmt style and returns the result or an |
| // (I/O or syntax) error. src is expected to be a syntactically correct CUE |
| // source file, or a list of CUE declarations or statements. |
| // |
| // If src is a partial source file, the leading and trailing space of src is |
| // applied to the result (such that it has the same leading and trailing space |
| // as src), and the result is indented by the same amount as the first line of |
| // src containing code. Imports are not sorted for partial source files. |
| // |
| // Caution: Tools relying on consistent formatting based on the installed |
| // version of cue (for instance, such as for presubmit checks) should execute |
| // that cue binary instead of calling Source. |
| // |
| func Source(b []byte, opt ...Option) ([]byte, error) { |
| cfg := newConfig(opt) |
| |
| f, err := parser.ParseFile("", b, parser.ParseComments) |
| if err != nil { |
| return nil, fmt.Errorf("parse: %s", err) |
| } |
| |
| // print AST |
| return cfg.fprint(f) |
| } |
| |
| type config struct { |
| UseSpaces bool |
| TabIndent bool |
| Tabwidth int // default: 4 |
| Indent int // default: 0 (all code is indented at least by this much) |
| |
| simplify bool |
| sortImports bool |
| } |
| |
| func newConfig(opt []Option) *config { |
| cfg := &config{ |
| Tabwidth: 8, |
| TabIndent: true, |
| UseSpaces: true, |
| } |
| for _, o := range opt { |
| o(cfg) |
| } |
| return cfg |
| } |
| |
| // Config defines the output of Fprint. |
| func (cfg *config) fprint(node interface{}) (out []byte, err error) { |
| var p printer |
| p.init(cfg) |
| if err = printNode(node, &p); err != nil { |
| return p.output, err |
| } |
| |
| padchar := byte('\t') |
| if cfg.UseSpaces { |
| padchar = byte(' ') |
| } |
| |
| twmode := tabwriter.StripEscape | tabwriter.TabIndent | tabwriter.DiscardEmptyColumns |
| if cfg.TabIndent { |
| twmode |= tabwriter.TabIndent |
| } |
| |
| buf := &bytes.Buffer{} |
| tw := tabwriter.NewWriter(buf, 0, cfg.Tabwidth, 1, padchar, twmode) |
| |
| // write printer result via tabwriter/trimmer to output |
| if _, err = tw.Write(p.output); err != nil { |
| return |
| } |
| |
| err = tw.Flush() |
| if err != nil { |
| return buf.Bytes(), err |
| } |
| |
| b := buf.Bytes() |
| if !cfg.TabIndent { |
| b = bytes.ReplaceAll(b, []byte{'\t'}, bytes.Repeat([]byte{' '}, cfg.Tabwidth)) |
| } |
| return b, nil |
| } |
| |
| // A formatter walks a syntax.Node, interspersed with comments and spacing |
| // directives, in the order that they would occur in printed form. |
| type formatter struct { |
| *printer |
| |
| stack []frame |
| current frame |
| nestExpr int |
| |
| labelBuf []labelEntry |
| } |
| |
| type labelEntry struct { |
| label ast.Label |
| optional bool |
| } |
| |
| func newFormatter(p *printer) *formatter { |
| f := &formatter{ |
| printer: p, |
| current: frame{ |
| settings: settings{ |
| nodeSep: newline, |
| parentSep: newline, |
| }, |
| }, |
| } |
| return f |
| } |
| |
| type whiteSpace int |
| |
| const ( |
| ignore whiteSpace = 0 |
| |
| // write a space, or disallow it |
| blank whiteSpace = 1 << iota |
| vtab // column marker |
| noblank |
| |
| nooverride |
| |
| comma // print a comma, unless trailcomma overrides it |
| trailcomma // print a trailing comma unless closed on same line |
| declcomma // write a comma when not at the end of line |
| |
| newline // write a line in a table |
| formfeed // next line is not part of the table |
| newsection // add two newlines |
| |
| indent // request indent an extra level after the next newline |
| unindent // unindent a level after the next newline |
| indented // element was indented. |
| ) |
| |
| type frame struct { |
| cg []*ast.CommentGroup |
| pos int8 |
| |
| settings |
| } |
| |
| type settings struct { |
| // separator is blank if the current node spans a single line and newline |
| // otherwise. |
| nodeSep whiteSpace |
| parentSep whiteSpace |
| override whiteSpace |
| } |
| |
| // suppress spurious linter warning: field is actually used. |
| func init() { |
| s := settings{} |
| _ = s.override |
| } |
| |
| func (f *formatter) print(a ...interface{}) { |
| for _, x := range a { |
| f.Print(x) |
| switch x.(type) { |
| case string, token.Token: // , *syntax.BasicLit, *syntax.Ident: |
| f.current.pos++ |
| } |
| } |
| f.visitComments(f.current.pos) |
| } |
| |
| func (f *formatter) formfeed() whiteSpace { |
| if f.current.nodeSep == blank { |
| return blank |
| } |
| return formfeed |
| } |
| |
| func (f *formatter) wsOverride(def whiteSpace) whiteSpace { |
| if f.current.override == ignore { |
| return def |
| } |
| return f.current.override |
| } |
| |
| func (f *formatter) onOneLine(node ast.Node) bool { |
| a := node.Pos() |
| b := node.End() |
| if a.IsValid() && b.IsValid() { |
| return f.lineFor(a) == f.lineFor(b) |
| } |
| // TODO: walk and look at relative positions to determine the same? |
| return false |
| } |
| |
| func (f *formatter) before(node ast.Node) bool { |
| f.stack = append(f.stack, f.current) |
| f.current = frame{settings: f.current.settings} |
| f.current.parentSep = f.current.nodeSep |
| |
| if node != nil { |
| s, ok := node.(*ast.StructLit) |
| if ok && len(s.Elts) <= 1 && f.current.nodeSep != blank && f.onOneLine(node) { |
| f.current.nodeSep = blank |
| } |
| f.current.cg = node.Comments() |
| f.visitComments(f.current.pos) |
| return true |
| } |
| return false |
| } |
| |
| func (f *formatter) after(node ast.Node) { |
| f.visitComments(127) |
| p := len(f.stack) - 1 |
| f.current = f.stack[p] |
| f.stack = f.stack[:p] |
| f.current.pos++ |
| f.visitComments(f.current.pos) |
| } |
| |
| func (f *formatter) visitComments(until int8) { |
| c := &f.current |
| |
| printed := false |
| for ; len(c.cg) > 0 && c.cg[0].Position <= until; c.cg = c.cg[1:] { |
| if printed { |
| f.Print(newsection) |
| } |
| printed = true |
| f.printComment(c.cg[0]) |
| } |
| } |
| |
| func (f *formatter) printComment(cg *ast.CommentGroup) { |
| f.Print(cg) |
| |
| printBlank := false |
| if cg.Doc { |
| f.Print(newline) |
| printBlank = true |
| } |
| for _, c := range cg.List { |
| isEnd := strings.HasPrefix(c.Text, "//") |
| if !printBlank { |
| if isEnd { |
| f.Print(vtab) |
| } else { |
| f.Print(blank) |
| } |
| } |
| f.Print(c.Slash) |
| f.Print(c) |
| if isEnd { |
| f.Print(newline) |
| } |
| } |
| } |