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 }