github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/opt/optgen/cmd/optfmt/main.go (about) 1 // Copyright 2020 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 // optfmt pretty prints .opt files. 12 package main 13 14 import ( 15 "bytes" 16 "flag" 17 "fmt" 18 "io" 19 "io/ioutil" 20 "os" 21 "strings" 22 23 "github.com/cockroachdb/cockroach/pkg/sql/opt/optgen/lang" 24 "github.com/cockroachdb/cockroach/pkg/util/pretty" 25 "github.com/pmezard/go-difflib/difflib" 26 ) 27 28 var ( 29 write = flag.Bool("w", false, "write result to (source) file instead of stdout") 30 list = flag.Bool("l", false, "list diffs when formatting differs from optfmt's") 31 verify = flag.Bool("verify", false, "verify output order") 32 exprgen = flag.Bool("e", false, "format an exprgen expression") 33 ) 34 35 func main() { 36 flag.Usage = func() { 37 fmt.Fprintf(flag.CommandLine.Output(), "usage of %s [flags] [path ...]:\n", os.Args[0]) 38 flag.PrintDefaults() 39 } 40 flag.Parse() 41 42 args := flag.Args() 43 switch len(args) { 44 case 0: 45 orig, err := ioutil.ReadAll(os.Stdin) 46 if err != nil { 47 fmt.Fprintln(os.Stderr, err) 48 os.Exit(1) 49 } 50 s, err := prettyify(bytes.NewReader(orig), defaultWidth, *exprgen) 51 if err != nil { 52 fmt.Fprintln(os.Stderr, err) 53 os.Exit(1) 54 } 55 if *verify { 56 err := verifyOutput(string(orig), s) 57 if err != nil { 58 fmt.Fprintln(os.Stderr, s) 59 fmt.Fprintf(os.Stderr, "verify failed: %s\n", err) 60 os.Exit(1) 61 } 62 } 63 fmt.Print(s) 64 return 65 } 66 67 for _, name := range args { 68 orig, prettied, err := prettyFile(name) 69 if err != nil { 70 fmt.Fprintf(os.Stderr, "%s: %s\n", name, err) 71 os.Exit(1) 72 } 73 if bytes.Equal(orig, []byte(prettied)) { 74 continue 75 } 76 if *verify { 77 err := verifyOutput(string(orig), prettied) 78 if err != nil { 79 fmt.Fprintf(os.Stderr, "verify failed: %s: %s\n", name, err) 80 os.Exit(1) 81 } 82 } 83 if *write { 84 err := ioutil.WriteFile(name, []byte(prettied), 0666) 85 if err != nil { 86 fmt.Fprintf(os.Stderr, "%s: %s\n", name, err) 87 os.Exit(1) 88 } 89 } 90 if *list { 91 diff := difflib.UnifiedDiff{ 92 A: difflib.SplitLines(string(orig)), 93 FromFile: name, 94 B: difflib.SplitLines(prettied), 95 ToFile: name, 96 Context: 4, 97 } 98 diffText, _ := difflib.GetUnifiedDiffString(diff) 99 fmt.Print(diffText) 100 } else if !*write { 101 fmt.Print(prettied) 102 } 103 } 104 } 105 106 func verifyOutput(orig, prettied string) error { 107 origToks := toTokens(orig) 108 prettyToks := toTokens(prettied) 109 for i, tok := range origToks { 110 if i >= len(prettyToks) { 111 return fmt.Errorf("pretty ended early after %d tokens", i+1) 112 } 113 if prettyToks[i] != tok { 114 return fmt.Errorf("token %d didn't match:\nnew: %q\norig: %q", i+1, prettyToks[i], tok) 115 } 116 } 117 if len(prettyToks) > len(origToks) { 118 return fmt.Errorf("orig ended early after %d tokens", len(origToks)) 119 } 120 return nil 121 } 122 123 func toTokens(input string) []string { 124 scanner := lang.NewScanner(strings.NewReader(input)) 125 var ret []string 126 for { 127 tok := scanner.Scan() 128 lit := scanner.Literal() 129 switch tok { 130 case lang.WHITESPACE: 131 // ignore 132 case lang.EOF, lang.ILLEGAL, lang.ERROR: 133 ret = append(ret, fmt.Sprintf("%s: %q", tok, lit)) 134 return ret 135 default: 136 ret = append(ret, fmt.Sprintf("%s: %s", tok, lit)) 137 } 138 } 139 } 140 141 const defaultWidth = 65 142 143 type pp struct { 144 p *lang.Parser 145 } 146 147 func prettyFile(name string) (orig []byte, pretty string, err error) { 148 orig, err = ioutil.ReadFile(name) 149 if err != nil { 150 return orig, "", err 151 } 152 pretty, err = prettyify(bytes.NewReader(orig), defaultWidth, *exprgen) 153 return orig, pretty, err 154 } 155 156 func prettyify(r io.Reader, n int, exprgen bool) (string, error) { 157 parser := lang.NewParser("") 158 parser.SetFileResolver(func(name string) (io.Reader, error) { 159 if exprgen { 160 return io.MultiReader(strings.NewReader(exprgenPrefix), r), nil 161 } 162 return r, nil 163 }) 164 parsed := parser.Parse() 165 errs := parser.Errors() 166 if len(errs) > 0 { 167 return "", errs[0] 168 } 169 p := pp{p: parser} 170 var exprs []lang.Expr 171 if exprgen { 172 exprs = []lang.Expr{parsed.Rules[0].Replace} 173 } else { 174 exprs = parser.Exprs() 175 } 176 d := p.toDoc(exprs) 177 s := pretty.Pretty(d, n, false, 4, nil) 178 179 // Remove any whitespace at EOL. This can happen in define rules where 180 // we always insert a blank line above comments which are nested with 181 // a tab, or when comments force a pretty.HardLine with a pretty.Line 182 // (which flattens to a space) before them. 183 var sb strings.Builder 184 delim := "" 185 for _, line := range strings.Split(s, "\n") { 186 sb.WriteString(delim) 187 delim = "\n" 188 // Keep comment whitespace. 189 if strings.HasPrefix(line, "#") { 190 sb.WriteString(line) 191 } else { 192 sb.WriteString(strings.TrimRight(line, " \t")) 193 } 194 } 195 return sb.String(), nil 196 } 197 198 const exprgenPrefix = ` 199 [R] 200 (F) 201 => 202 ` 203 204 func (p *pp) toDoc(exprs []lang.Expr) pretty.Doc { 205 doc := pretty.Nil 206 for _, expr := range exprs { 207 var next pretty.Doc 208 switch expr := expr.(type) { 209 case *lang.CommentsExpr: 210 next = p.docComments(expr, pretty.Nil) 211 default: 212 next = pretty.Concat(p.docExpr(expr), pretty.Line) 213 } 214 doc = pretty.ConcatDoc(doc, next, pretty.Line) 215 } 216 return doc 217 } 218 219 func softstack(docs ...pretty.Doc) pretty.Doc { 220 return pretty.Fold(concatSoftBreak, docs...) 221 } 222 223 func concatSoftBreak(a, b pretty.Doc) pretty.Doc { 224 return pretty.ConcatDoc(a, b, pretty.SoftBreak) 225 } 226 227 func (p *pp) docDefine(e *lang.DefineExpr) pretty.Doc { 228 var docs []pretty.Doc 229 for _, com := range p.p.GetComments(e) { 230 docs = append(docs, pretty.Text(string(com))) 231 } 232 if len(e.Tags) > 0 { 233 var tags []pretty.Doc 234 for _, tag := range e.Tags { 235 tags = append(tags, pretty.Text(string(tag))) 236 } 237 docs = append(docs, pretty.BracketDoc( 238 pretty.Text("["), 239 pretty.Join(",", tags...), 240 pretty.Text("]"), 241 )) 242 } 243 name := pretty.Text(fmt.Sprintf("define %s {", e.Name)) 244 if len(e.Fields) == 0 { 245 docs = append(docs, name, pretty.Text("}")) 246 } else { 247 name = pretty.Concat(name, pretty.SoftBreak) 248 docs = append(docs, 249 pretty.NestT(pretty.Concat( 250 name, 251 p.docFields(e.Fields), 252 )), 253 pretty.Text("}"), 254 ) 255 } 256 return softstack(docs...) 257 } 258 259 func (p *pp) docComments(e *lang.CommentsExpr, start pretty.Doc) pretty.Doc { 260 doc := start 261 for _, com := range *e { 262 doc = pretty.Fold(pretty.Concat, 263 doc, 264 pretty.Text(string(com)), 265 pretty.HardLine, 266 ) 267 } 268 return doc 269 } 270 271 func (p *pp) docFields(e lang.DefineFieldsExpr) pretty.Doc { 272 var docs []pretty.Doc 273 for fi, f := range e { 274 for comi, com := range p.p.GetComments(f) { 275 if fi > 0 && comi == 0 { 276 // Insert blank line above a comment. 277 docs = append(docs, pretty.Text("")) 278 } 279 docs = append(docs, pretty.Text(string(com))) 280 } 281 docs = append(docs, pretty.Text(fmt.Sprintf("%s %s", f.Name, f.Type))) 282 } 283 return softstack(docs...) 284 } 285 286 func (p *pp) docRule(e *lang.RuleExpr) pretty.Doc { 287 var docs []pretty.Doc 288 for _, com := range p.p.GetComments(e) { 289 docs = append(docs, pretty.Text(string(com))) 290 } 291 tags := []pretty.Doc{pretty.Text(string(e.Name))} 292 for _, tag := range e.Tags { 293 tags = append(tags, pretty.Text(string(tag))) 294 } 295 docs = append(docs, pretty.BracketDoc( 296 pretty.Text("["), 297 pretty.Join(",", tags...), 298 pretty.Text("]"), 299 )) 300 for _, com := range p.p.GetComments(e.Match) { 301 docs = append(docs, pretty.Text(string(com))) 302 } 303 docs = append(docs, p.docOnlyExpr(e.Match)) 304 docs = append(docs, pretty.Text("=>")) 305 for _, com := range p.p.GetComments(e.Replace) { 306 docs = append(docs, pretty.Text(string(com))) 307 } 308 docs = append(docs, p.docOnlyExpr(e.Replace)) 309 return softstack(docs...) 310 } 311 312 func (p *pp) docExpr(e lang.Expr) pretty.Doc { 313 doc := p.docOnlyExpr(e) 314 // These expressions add their own comments. 315 switch e.(type) { 316 case *lang.DefineExpr, *lang.RuleExpr: 317 return doc 318 } 319 if coms := p.p.GetComments(e); len(coms) > 0 { 320 doc = pretty.Fold(pretty.Concat, 321 p.docComments(&coms, pretty.Line), 322 doc, 323 ) 324 } 325 return doc 326 } 327 328 // docSlice returns a doc for a SliceExpr. Its first doc is guaranteed to be a 329 // Line or HardLine. 330 func (p *pp) docSlice(e lang.SliceExpr) pretty.Doc { 331 doc := pretty.Nil 332 for _, x := range e { 333 coms := p.p.GetComments(x) 334 if len(coms) > 0 { 335 doc = pretty.Concat(doc, p.docComments(&coms, pretty.Line)) 336 } else { 337 doc = pretty.Concat(doc, pretty.Line) 338 } 339 doc = pretty.Concat(doc, p.docOnlyExpr(x)) 340 } 341 return doc 342 } 343 344 func (p *pp) docOnlyExpr(e lang.Expr) pretty.Doc { 345 switch e := e.(type) { 346 case *lang.ListExpr: 347 if len(e.Items) == 0 { 348 return pretty.Text("[]") 349 } 350 return pretty.Group(pretty.Fold(pretty.Concat, 351 pretty.Text("["), 352 pretty.NestT(p.docSlice(e.Items)), 353 pretty.Line, 354 pretty.Text("]"), 355 )) 356 case *lang.FuncExpr: 357 docs := []pretty.Doc{ 358 pretty.Text("("), 359 p.docExpr(e.Name), 360 p.docSlice(e.Args), 361 } 362 return pretty.Group(concatSoftBreak( 363 pretty.NestT(pretty.Fold(pretty.Concat, docs...)), 364 pretty.Text(")"), 365 )) 366 case *lang.NamesExpr: 367 var docs []pretty.Doc 368 for i, x := range *e { 369 d := p.docExpr(&x) 370 if i > 0 { 371 d = pretty.ConcatSpace(pretty.Text("|"), d) 372 } 373 docs = append(docs, d) 374 } 375 return pretty.NestT(pretty.Fillwords(docs...)) 376 case *lang.BindExpr: 377 return pretty.Concat( 378 pretty.Text(fmt.Sprintf("$%s:", e.Label)), 379 p.docExpr(e.Target), 380 ) 381 case *lang.AnyExpr: 382 return pretty.Text("*") 383 case *lang.ListAnyExpr: 384 return pretty.Text("...") 385 case *lang.RefExpr: 386 return pretty.Text(fmt.Sprintf("$%s", e.Label)) 387 case *lang.AndExpr: 388 // In order to prevent nested And expressions, find all of the 389 // adjacent Ands and join them. 390 var docs []pretty.Doc 391 check := []lang.Expr{e.Left, e.Right} 392 for len(check) > 0 { 393 e := check[0] 394 check = check[1:] 395 if and, ok := e.(*lang.AndExpr); ok { 396 // Append to front to preserve order. 397 check = append([]lang.Expr{and.Left, and.Right}, check...) 398 } else { 399 docs = append(docs, p.docExpr(e)) 400 } 401 } 402 return pretty.Group(pretty.NestT(pretty.Join(" &", docs...))) 403 case *lang.NotExpr: 404 return pretty.Concat(pretty.Text("^"), p.docExpr(e.Input)) 405 case *lang.NameExpr: 406 return pretty.Text(string(*e)) 407 case *lang.NumberExpr: 408 return pretty.Text(fmt.Sprint(*e)) 409 case *lang.RuleExpr: 410 return p.docRule(e) 411 case *lang.DefineExpr: 412 return p.docDefine(e) 413 case *lang.CommentsExpr: 414 return p.docComments(e, pretty.Line) 415 case *lang.StringExpr: 416 return pretty.Text(fmt.Sprintf(`"%s"`, string(*e))) 417 default: 418 panic(fmt.Errorf("unknown: %T)", e)) 419 } 420 }