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  }