tools/fix: move fix functionality to separate package
This will allow users to apply fixes programmatically.
Also handy for tests.
Change-Id: Iec662c773d0957abf3aa79e7473a3408bb971c80
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6202
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/fix.go b/cmd/cue/cmd/fix.go
index be53700..eccab53 100644
--- a/cmd/cue/cmd/fix.go
+++ b/cmd/cue/cmd/fix.go
@@ -15,20 +15,17 @@
package cmd
import (
- "fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"cuelang.org/go/cue/ast"
- "cuelang.org/go/cue/ast/astutil"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/format"
"cuelang.org/go/cue/load"
- "cuelang.org/go/cue/parser"
"cuelang.org/go/cue/token"
- "cuelang.org/go/internal"
+ "cuelang.org/go/tools/fix"
"github.com/spf13/cobra"
)
@@ -87,7 +84,7 @@
}
}
- errs := fixAll(instances)
+ errs := fix.Instances(instances)
if errs != nil && flagForce.Bool(cmd) {
return errs
@@ -129,238 +126,3 @@
})
return a
}
-
-func fix(f *ast.File) *ast.File {
- // Isolate bulk optional fields into a single struct.
- ast.Walk(f, func(n ast.Node) bool {
- var decls []ast.Decl
- switch x := n.(type) {
- case *ast.StructLit:
- decls = x.Elts
- case *ast.File:
- decls = x.Decls
- }
-
- if len(decls) <= 1 {
- return true
- }
-
- for i, d := range decls {
- if internal.IsBulkField(d) {
- decls[i] = internal.EmbedStruct(ast.NewStruct(d))
- }
- }
-
- return true
- }, nil)
-
- // Rewrite an old-style alias to a let clause.
- ast.Walk(f, func(n ast.Node) bool {
- var decls []ast.Decl
- switch x := n.(type) {
- case *ast.StructLit:
- decls = x.Elts
- case *ast.File:
- decls = x.Decls
- }
- for i, d := range decls {
- if a, ok := d.(*ast.Alias); ok {
- x := &ast.LetClause{
- Ident: a.Ident,
- Equal: a.Equal,
- Expr: a.Expr,
- }
- astutil.CopyMeta(x, a)
- decls[i] = x
- }
- }
- return true
- }, nil)
-
- // Rewrite block comments to regular comments.
- ast.Walk(f, func(n ast.Node) bool {
- switch x := n.(type) {
- case *ast.CommentGroup:
- comments := []*ast.Comment{}
- for _, c := range x.List {
- s := c.Text
- if !strings.HasPrefix(s, "/*") || !strings.HasSuffix(s, "*/") {
- comments = append(comments, c)
- continue
- }
- if x.Position > 0 {
- // Moving to the end doesn't work, as it still
- // may inject at a false line break position.
- x.Position = 0
- x.Doc = true
- }
- s = strings.TrimSpace(s[2 : len(s)-2])
- for _, s := range strings.Split(s, "\n") {
- for i := 0; i < 3; i++ {
- if strings.HasPrefix(s, " ") || strings.HasPrefix(s, "*") {
- s = s[1:]
- }
- }
- comments = append(comments, &ast.Comment{Text: "// " + s})
- }
- }
- x.List = comments
- return false
- }
- return true
- }, nil)
-
- // Referred nodes and used identifiers.
- referred := map[ast.Node]string{}
- used := map[string]bool{}
- replacement := map[ast.Node]string{}
-
- ast.Walk(f, func(n ast.Node) bool {
- if i, ok := n.(*ast.Ident); ok {
- str, err := ast.ParseIdent(i)
- if err != nil {
- return false
- }
- referred[i.Node] = str
- used[str] = true
- }
- return true
- }, nil)
-
- num := 0
- newIdent := func() string {
- for num++; ; num++ {
- str := fmt.Sprintf("X%d", num)
- if !used[str] {
- used[str] = true
- return str
- }
- }
- }
-
- // Rewrite TemplateLabel to ListLit.
- // Note: there is a chance that the name will clash with the
- // scope in which it is defined. We drop the alias if it is not
- // used to mitigate this issue.
- f = astutil.Apply(f, func(c astutil.Cursor) bool {
- n := c.Node()
- switch x := n.(type) {
- case *ast.TemplateLabel:
- var expr ast.Expr = ast.NewIdent("string")
- if _, ok := referred[x]; ok {
- expr = &ast.Alias{
- Ident: x.Ident,
- Expr: ast.NewIdent("_"),
- }
- }
- c.Replace(ast.NewList(expr))
- }
- return true
- }, nil).(*ast.File)
-
- // Rewrite quoted identifier fields that are referenced.
- f = astutil.Apply(f, func(c astutil.Cursor) bool {
- n := c.Node()
- switch x := n.(type) {
- case *ast.Field:
- m, ok := referred[x.Value]
- if !ok {
- break
- }
-
- if b, ok := x.Label.(*ast.Ident); ok {
- str, err := ast.ParseIdent(b)
- var expr ast.Expr = b
-
- switch {
- case token.Lookup(str) != token.IDENT:
- // quote keywords
- expr = ast.NewString(b.Name)
-
- case err != nil || str != m || str == b.Name:
- return true
-
- case ast.IsValidIdent(str):
- x.Label = astutil.CopyMeta(ast.NewIdent(str), x.Label).(ast.Label)
- return true
- }
-
- ident := newIdent()
- replacement[x.Value] = ident
- expr = &ast.Alias{Ident: ast.NewIdent(ident), Expr: expr}
- ast.SetRelPos(x.Label, token.NoRelPos)
- x.Label = astutil.CopyMeta(expr, x.Label).(ast.Label)
- }
- }
- return true
- }, nil).(*ast.File)
-
- // Replace quoted references with their alias identifier.
- astutil.Apply(f, func(c astutil.Cursor) bool {
- n := c.Node()
- switch x := n.(type) {
- case *ast.Ident:
- if r, ok := replacement[x.Node]; ok {
- c.Replace(astutil.CopyMeta(ast.NewIdent(r), n))
- break
- }
- str, err := ast.ParseIdent(x)
- if err != nil || str == x.Name {
- break
- }
- // Either the identifier is valid, in which can be replaced simply
- // as here, or it is a complicated identifier and the original
- // destination must have been quoted, in which case it is handled
- // above.
- if ast.IsValidIdent(str) && token.Lookup(str) == token.IDENT {
- c.Replace(astutil.CopyMeta(ast.NewIdent(str), n))
- }
- }
- return true
- }, nil)
-
- // TODO: we are probably reintroducing slices. Disable for now.
- //
- // Rewrite slice expression.
- // f = astutil.Apply(f, func(c astutil.Cursor) bool {
- // n := c.Node()
- // getVal := func(n ast.Expr) ast.Expr {
- // if n == nil {
- // return nil
- // }
- // if id, ok := n.(*ast.Ident); ok && id.Name == "_" {
- // return nil
- // }
- // return n
- // }
- // switch x := n.(type) {
- // case *ast.SliceExpr:
- // ast.SetRelPos(x.X, token.NoRelPos)
-
- // lo := getVal(x.Low)
- // hi := getVal(x.High)
- // if lo == nil { // a[:j]
- // lo = mustParseExpr("0")
- // astutil.CopyMeta(lo, x.Low)
- // }
- // if hi == nil { // a[i:]
- // hi = ast.NewCall(ast.NewIdent("len"), x.X)
- // astutil.CopyMeta(lo, x.High)
- // }
- // if pkg := c.Import("list"); pkg != nil {
- // c.Replace(ast.NewCall(ast.NewSel(pkg, "Slice"), x.X, lo, hi))
- // }
- // }
- // return true
- // }, nil).(*ast.File)
-
- return f
-}
-
-func mustParseExpr(expr string) ast.Expr {
- ex, err := parser.ParseExpr("fix", expr)
- if err != nil {
- panic(err)
- }
- return ex
-}
diff --git a/cmd/cue/cmd/fmt.go b/cmd/cue/cmd/fmt.go
index 5b92705..def1d97 100644
--- a/cmd/cue/cmd/fmt.go
+++ b/cmd/cue/cmd/fmt.go
@@ -22,6 +22,7 @@
"cuelang.org/go/cue/format"
"cuelang.org/go/cue/load"
"cuelang.org/go/internal/encoding"
+ "cuelang.org/go/tools/fix"
)
func newFmtCmd(c *Command) *cobra.Command {
@@ -67,7 +68,7 @@
f := d.File()
if file.Encoding == build.CUE {
- f = fix(f)
+ f = fix.File(f)
}
files = append(files, f)
diff --git a/tools/fix/fix.go b/tools/fix/fix.go
new file mode 100644
index 0000000..e617edf
--- /dev/null
+++ b/tools/fix/fix.go
@@ -0,0 +1,258 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package fix contains functionality for writing CUE files with legacy
+// syntax to newer ones.
+//
+// Note: the transformations that are supported in this package will change
+// over time.
+package fix
+
+import (
+ "fmt"
+ "strings"
+
+ "cuelang.org/go/cue/ast"
+ "cuelang.org/go/cue/ast/astutil"
+ "cuelang.org/go/cue/token"
+ "cuelang.org/go/internal"
+)
+
+// File applies fixes to f and returns it. It alters the original f.
+func File(f *ast.File) *ast.File {
+ // Isolate bulk optional fields into a single struct.
+ ast.Walk(f, func(n ast.Node) bool {
+ var decls []ast.Decl
+ switch x := n.(type) {
+ case *ast.StructLit:
+ decls = x.Elts
+ case *ast.File:
+ decls = x.Decls
+ }
+
+ if len(decls) <= 1 {
+ return true
+ }
+
+ for i, d := range decls {
+ if internal.IsBulkField(d) {
+ decls[i] = internal.EmbedStruct(ast.NewStruct(d))
+ }
+ }
+
+ return true
+ }, nil)
+
+ // Rewrite an old-style alias to a let clause.
+ ast.Walk(f, func(n ast.Node) bool {
+ var decls []ast.Decl
+ switch x := n.(type) {
+ case *ast.StructLit:
+ decls = x.Elts
+ case *ast.File:
+ decls = x.Decls
+ }
+ for i, d := range decls {
+ if a, ok := d.(*ast.Alias); ok {
+ x := &ast.LetClause{
+ Ident: a.Ident,
+ Equal: a.Equal,
+ Expr: a.Expr,
+ }
+ astutil.CopyMeta(x, a)
+ decls[i] = x
+ }
+ }
+ return true
+ }, nil)
+
+ // Rewrite block comments to regular comments.
+ ast.Walk(f, func(n ast.Node) bool {
+ switch x := n.(type) {
+ case *ast.CommentGroup:
+ comments := []*ast.Comment{}
+ for _, c := range x.List {
+ s := c.Text
+ if !strings.HasPrefix(s, "/*") || !strings.HasSuffix(s, "*/") {
+ comments = append(comments, c)
+ continue
+ }
+ if x.Position > 0 {
+ // Moving to the end doesn't work, as it still
+ // may inject at a false line break position.
+ x.Position = 0
+ x.Doc = true
+ }
+ s = strings.TrimSpace(s[2 : len(s)-2])
+ for _, s := range strings.Split(s, "\n") {
+ for i := 0; i < 3; i++ {
+ if strings.HasPrefix(s, " ") || strings.HasPrefix(s, "*") {
+ s = s[1:]
+ }
+ }
+ comments = append(comments, &ast.Comment{Text: "// " + s})
+ }
+ }
+ x.List = comments
+ return false
+ }
+ return true
+ }, nil)
+
+ // Referred nodes and used identifiers.
+ referred := map[ast.Node]string{}
+ used := map[string]bool{}
+ replacement := map[ast.Node]string{}
+
+ ast.Walk(f, func(n ast.Node) bool {
+ if i, ok := n.(*ast.Ident); ok {
+ str, err := ast.ParseIdent(i)
+ if err != nil {
+ return false
+ }
+ referred[i.Node] = str
+ used[str] = true
+ }
+ return true
+ }, nil)
+
+ num := 0
+ newIdent := func() string {
+ for num++; ; num++ {
+ str := fmt.Sprintf("X%d", num)
+ if !used[str] {
+ used[str] = true
+ return str
+ }
+ }
+ }
+
+ // Rewrite TemplateLabel to ListLit.
+ // Note: there is a chance that the name will clash with the
+ // scope in which it is defined. We drop the alias if it is not
+ // used to mitigate this issue.
+ f = astutil.Apply(f, func(c astutil.Cursor) bool {
+ n := c.Node()
+ switch x := n.(type) {
+ case *ast.TemplateLabel:
+ var expr ast.Expr = ast.NewIdent("string")
+ if _, ok := referred[x]; ok {
+ expr = &ast.Alias{
+ Ident: x.Ident,
+ Expr: ast.NewIdent("_"),
+ }
+ }
+ c.Replace(ast.NewList(expr))
+ }
+ return true
+ }, nil).(*ast.File)
+
+ // Rewrite quoted identifier fields that are referenced.
+ f = astutil.Apply(f, func(c astutil.Cursor) bool {
+ n := c.Node()
+ switch x := n.(type) {
+ case *ast.Field:
+ m, ok := referred[x.Value]
+ if !ok {
+ break
+ }
+
+ if b, ok := x.Label.(*ast.Ident); ok {
+ str, err := ast.ParseIdent(b)
+ var expr ast.Expr = b
+
+ switch {
+ case token.Lookup(str) != token.IDENT:
+ // quote keywords
+ expr = ast.NewString(b.Name)
+
+ case err != nil || str != m || str == b.Name:
+ return true
+
+ case ast.IsValidIdent(str):
+ x.Label = astutil.CopyMeta(ast.NewIdent(str), x.Label).(ast.Label)
+ return true
+ }
+
+ ident := newIdent()
+ replacement[x.Value] = ident
+ expr = &ast.Alias{Ident: ast.NewIdent(ident), Expr: expr}
+ ast.SetRelPos(x.Label, token.NoRelPos)
+ x.Label = astutil.CopyMeta(expr, x.Label).(ast.Label)
+ }
+ }
+ return true
+ }, nil).(*ast.File)
+
+ // Replace quoted references with their alias identifier.
+ astutil.Apply(f, func(c astutil.Cursor) bool {
+ n := c.Node()
+ switch x := n.(type) {
+ case *ast.Ident:
+ if r, ok := replacement[x.Node]; ok {
+ c.Replace(astutil.CopyMeta(ast.NewIdent(r), n))
+ break
+ }
+ str, err := ast.ParseIdent(x)
+ if err != nil || str == x.Name {
+ break
+ }
+ // Either the identifier is valid, in which can be replaced simply
+ // as here, or it is a complicated identifier and the original
+ // destination must have been quoted, in which case it is handled
+ // above.
+ if ast.IsValidIdent(str) && token.Lookup(str) == token.IDENT {
+ c.Replace(astutil.CopyMeta(ast.NewIdent(str), n))
+ }
+ }
+ return true
+ }, nil)
+
+ // TODO: we are probably reintroducing slices. Disable for now.
+ //
+ // Rewrite slice expression.
+ // f = astutil.Apply(f, func(c astutil.Cursor) bool {
+ // n := c.Node()
+ // getVal := func(n ast.Expr) ast.Expr {
+ // if n == nil {
+ // return nil
+ // }
+ // if id, ok := n.(*ast.Ident); ok && id.Name == "_" {
+ // return nil
+ // }
+ // return n
+ // }
+ // switch x := n.(type) {
+ // case *ast.SliceExpr:
+ // ast.SetRelPos(x.X, token.NoRelPos)
+
+ // lo := getVal(x.Low)
+ // hi := getVal(x.High)
+ // if lo == nil { // a[:j]
+ // lo = mustParseExpr("0")
+ // astutil.CopyMeta(lo, x.Low)
+ // }
+ // if hi == nil { // a[i:]
+ // hi = ast.NewCall(ast.NewIdent("len"), x.X)
+ // astutil.CopyMeta(lo, x.High)
+ // }
+ // if pkg := c.Import("list"); pkg != nil {
+ // c.Replace(ast.NewCall(ast.NewSel(pkg, "Slice"), x.X, lo, hi))
+ // }
+ // }
+ // return true
+ // }, nil).(*ast.File)
+
+ return f
+}
diff --git a/cmd/cue/cmd/fix_test.go b/tools/fix/fix_test.go
similarity index 97%
rename from cmd/cue/cmd/fix_test.go
rename to tools/fix/fix_test.go
index f4d5eb9..06d2bd4 100644
--- a/cmd/cue/cmd/fix_test.go
+++ b/tools/fix/fix_test.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package cmd
+package fix
import (
"testing"
@@ -21,7 +21,7 @@
"cuelang.org/go/cue/parser"
)
-func TestFix(t *testing.T) {
+func TestFile(t *testing.T) {
testCases := []struct {
name string
in string
@@ -161,7 +161,7 @@
if err != nil {
t.Fatal(err)
}
- n := fix(f)
+ n := File(f)
b, err := format.Node(n)
if err != nil {
t.Fatal(err)
diff --git a/cmd/cue/cmd/fixall.go b/tools/fix/fixall.go
similarity index 96%
rename from cmd/cue/cmd/fixall.go
rename to tools/fix/fixall.go
index 6209659..9069f20 100644
--- a/cmd/cue/cmd/fixall.go
+++ b/tools/fix/fixall.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package cmd
+package fix
import (
"fmt"
@@ -31,7 +31,10 @@
"cuelang.org/go/internal"
)
-func fixAll(a []*build.Instance) errors.Error {
+// Instances modifies all files contained in the given build instances at once.
+//
+// It also applies fix.File.
+func Instances(a []*build.Instance) errors.Error {
cwd, _ := os.Getwd()
// Collect all
@@ -44,7 +47,7 @@
ambiguous: map[string][]token.Pos{},
}
- p.visitAll(func(f *ast.File) { fix(f) })
+ p.visitAll(func(f *ast.File) { File(f) })
instances := cue.Build(a)
p.updateValues(instances)
diff --git a/cmd/cue/cmd/fixall_test.go b/tools/fix/fixall_test.go
similarity index 85%
rename from cmd/cue/cmd/fixall_test.go
rename to tools/fix/fixall_test.go
index 83f2d14..2180e2e 100644
--- a/cmd/cue/cmd/fixall_test.go
+++ b/tools/fix/fixall_test.go
@@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package cmd
+package fix
import (
+ "flag"
"fmt"
"testing"
@@ -22,16 +23,18 @@
"cuelang.org/go/internal/cuetxtar"
)
-func TestFixAll(t *testing.T) {
+var update = flag.Bool("update", false, "update the test files")
+
+func TestInstances(t *testing.T) {
test := cuetxtar.TxTarTest{
- Root: "./testdata/fix",
+ Root: "./testdata",
Name: "fixmod",
Update: *update,
}
test.Run(t, func(t *cuetxtar.Test) {
a := t.ValidInstances("./...")
- err := fixAll(a)
+ err := Instances(a)
t.WriteErrors(err)
for _, b := range a {
for _, f := range b.Files {
diff --git a/cmd/cue/cmd/testdata/fix/alias.txtar b/tools/fix/testdata/alias.txtar
similarity index 100%
rename from cmd/cue/cmd/testdata/fix/alias.txtar
rename to tools/fix/testdata/alias.txtar
diff --git a/cmd/cue/cmd/testdata/fix/basic.txtar b/tools/fix/testdata/basic.txtar
similarity index 100%
rename from cmd/cue/cmd/testdata/fix/basic.txtar
rename to tools/fix/testdata/basic.txtar
diff --git a/cmd/cue/cmd/testdata/fix/incompatible.txtar b/tools/fix/testdata/incompatible.txtar
similarity index 100%
rename from cmd/cue/cmd/testdata/fix/incompatible.txtar
rename to tools/fix/testdata/incompatible.txtar
diff --git a/cmd/cue/cmd/testdata/fix/invalid.txtar b/tools/fix/testdata/invalid.txtar
similarity index 100%
rename from cmd/cue/cmd/testdata/fix/invalid.txtar
rename to tools/fix/testdata/invalid.txtar
diff --git a/cmd/cue/cmd/testdata/fix/template.txtar b/tools/fix/testdata/template.txtar
similarity index 100%
rename from cmd/cue/cmd/testdata/fix/template.txtar
rename to tools/fix/testdata/template.txtar
diff --git a/cmd/cue/cmd/testdata/fix/unsupported.txtar b/tools/fix/testdata/unsupported.txtar
similarity index 87%
rename from cmd/cue/cmd/testdata/fix/unsupported.txtar
rename to tools/fix/testdata/unsupported.txtar
index 4fd5f0a..560e652 100644
--- a/cmd/cue/cmd/testdata/fix/unsupported.txtar
+++ b/tools/fix/testdata/unsupported.txtar
@@ -21,8 +21,8 @@
package foo
// Possible references to this location:
-// testdata/fix/foo/foo.cue:8:4
-// testdata/fix/foo/foo.cue:9:7
+// testdata/foo/foo.cue:8:4
+// testdata/foo/foo.cue:9:7
#Def: {
a: int
b: "foo"
diff --git a/cmd/cue/cmd/testdata/fix/unused.txtar b/tools/fix/testdata/unused.txtar
similarity index 100%
rename from cmd/cue/cmd/testdata/fix/unused.txtar
rename to tools/fix/testdata/unused.txtar