tools/fix: allow fixing non-essential simplification

First implementation is rewriting `_ | x` to `_`.

Change-Id: Idda745f5493dc092dd0a3677c52e7e79eee74aa3
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/8231
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Paul Jolly <paul@myitcv.org.uk>
diff --git a/cmd/cue/cmd/fix.go b/cmd/cue/cmd/fix.go
index 30f4be2..2026654 100644
--- a/cmd/cue/cmd/fix.go
+++ b/cmd/cue/cmd/fix.go
@@ -54,6 +54,11 @@
 		return err
 	}
 
+	var opts []fix.Option
+	if flagSimplify.Bool(cmd) {
+		opts = append(opts, fix.Simplify())
+	}
+
 	if len(args) == 0 {
 		args = []string{"./..."}
 
@@ -81,7 +86,7 @@
 		Tools: true,
 	})
 
-	errs := fix.Instances(instances)
+	errs := fix.Instances(instances, opts...)
 
 	if errs != nil && flagForce.Bool(cmd) {
 		return errs
diff --git a/tools/fix/fix.go b/tools/fix/fix.go
index 98f5ffd..b464270 100644
--- a/tools/fix/fix.go
+++ b/tools/fix/fix.go
@@ -28,8 +28,25 @@
 	"cuelang.org/go/cue/token"
 )
 
+type Option func(*options)
+
+type options struct {
+	simplify bool
+}
+
+// Simplify enables fixes that simplify the code, but are not strictly
+// necessary.
+func Simplify() Option {
+	return func(o *options) { o.simplify = true }
+}
+
 // File applies fixes to f and returns it. It alters the original f.
-func File(f *ast.File) *ast.File {
+func File(f *ast.File, o ...Option) *ast.File {
+	var options options
+	for _, f := range o {
+		f(&options)
+	}
+
 	// Rewrite integer division operations to use builtins.
 	f = astutil.Apply(f, func(c astutil.Cursor) bool {
 		n := c.Node()
@@ -248,5 +265,38 @@
 	// 	return true
 	// }, nil).(*ast.File)
 
+	if !options.simplify {
+		return f
+	}
+
+	// Rewrite disjunctions with _ to _.
+	f = astutil.Apply(f, func(c astutil.Cursor) bool {
+		if x := findTop(c.Node()); x != nil {
+			c.Replace(x)
+		}
+		return true
+	}, nil).(*ast.File)
+
 	return f
 }
+
+func findTop(x ast.Node) ast.Expr {
+	switch x := x.(type) {
+	case *ast.BinaryExpr:
+		if x.Op != token.OR {
+			break
+		}
+		if v := findTop(x.X); v != nil {
+			return v
+		}
+		if v := findTop(x.Y); v != nil {
+			return v
+		}
+
+	case *ast.Ident:
+		if x.Name == "_" {
+			return x
+		}
+	}
+	return nil
+}
diff --git a/tools/fix/fix_test.go b/tools/fix/fix_test.go
index 83c1976..63f7f99 100644
--- a/tools/fix/fix_test.go
+++ b/tools/fix/fix_test.go
@@ -23,9 +23,10 @@
 
 func TestFile(t *testing.T) {
 	testCases := []struct {
-		name string
-		in   string
-		out  string
+		name     string
+		in       string
+		out      string
+		simplify bool
 	}{{
 		name: "rewrite integer division",
 		in: `package foo
@@ -85,6 +86,14 @@
 		out: `
 let y = foo
 `,
+	}, {
+		simplify: true,
+		in: `
+		y: _ | {[string]: int}
+		`,
+		out: `y: _
+`,
+
 		// 	}, {
 		// 		name: "slice",
 		// 		in: `package foo
@@ -135,7 +144,13 @@
 			if err != nil {
 				t.Fatal(err)
 			}
-			n := File(f)
+
+			var opts []Option
+			if tc.simplify {
+				opts = append(opts, Simplify())
+			}
+			n := File(f, opts...)
+
 			b, err := format.Node(n)
 			if err != nil {
 				t.Fatal(err)
diff --git a/tools/fix/fixall.go b/tools/fix/fixall.go
index c7a94b0..c2fa2ae 100644
--- a/tools/fix/fixall.go
+++ b/tools/fix/fixall.go
@@ -25,7 +25,7 @@
 // Instances modifies all files contained in the given build instances at once.
 //
 // It also applies fix.File.
-func Instances(a []*build.Instance) errors.Error {
+func Instances(a []*build.Instance, o ...Option) errors.Error {
 	cwd, _ := os.Getwd()
 
 	// Collect all
@@ -34,7 +34,7 @@
 		cwd:       cwd,
 	}
 
-	p.visitAll(func(f *ast.File) { File(f) })
+	p.visitAll(func(f *ast.File) { File(f, o...) })
 
 	return p.err
 }