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 }