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