// Copyright 2020 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 astutil_test import ( "testing" "cuelang.org/go/cue/ast" "cuelang.org/go/cue/ast/astutil" "cuelang.org/go/cue/format" "cuelang.org/go/internal" "github.com/go-quicktest/qt" ) func TestSanitize(t *testing.T) { testCases := []struct { desc string file *ast.File want string }{{ desc: "Take existing import and rename it", file: func() *ast.File { spec := ast.NewImport(nil, "list") spec.AddComment(internal.NewComment(true, "will be renamed")) return &ast.File{Decls: []ast.Decl{ &ast.ImportDecl{Specs: []*ast.ImportSpec{spec}}, &ast.EmbedDecl{ Expr: ast.NewStruct( ast.NewIdent("list"), ast.NewCall( ast.NewSel(&ast.Ident{Name: "list", Node: spec}, "Min")), )}, }} }(), want: `import ( // will be renamed list_1 "list" ) { list: list_1.Min() } `, }, { desc: "Take existing import and rename it", file: func() *ast.File { spec := ast.NewImport(nil, "list") return &ast.File{Decls: []ast.Decl{ &ast.ImportDecl{Specs: []*ast.ImportSpec{spec}}, &ast.Field{ Label: ast.NewIdent("a"), Value: ast.NewStruct( ast.NewIdent("list"), ast.NewCall( ast.NewSel(&ast.Ident{Name: "list", Node: spec}, "Min")), ), }, }} }(), want: `import list_1 "list" a: { list: list_1.Min() } `, }, { desc: "One import added, one removed", file: &ast.File{Decls: []ast.Decl{ &ast.ImportDecl{Specs: []*ast.ImportSpec{ {Path: ast.NewString("foo")}, }}, &ast.Field{ Label: ast.NewIdent("a"), Value: ast.NewCall( ast.NewSel(&ast.Ident{ Name: "bar", Node: &ast.ImportSpec{Path: ast.NewString("bar")}, }, "Min")), }, }}, want: `import "bar" a: bar.Min() `, }, { desc: "Rename duplicate import", file: func() *ast.File { spec1 := ast.NewImport(nil, "bar") spec2 := ast.NewImport(nil, "foo/bar") spec3 := ast.NewImport(ast.NewIdent("bar"), "foo") return &ast.File{Decls: []ast.Decl{ internal.NewComment(false, "File comment"), &ast.Package{Name: ast.NewIdent("pkg")}, &ast.Field{ Label: ast.NewIdent("a"), Value: ast.NewStruct( ast.NewIdent("b"), ast.NewCall( ast.NewSel(&ast.Ident{Name: "bar", Node: spec1}, "A")), ast.NewIdent("c"), ast.NewCall( ast.NewSel(&ast.Ident{Name: "bar", Node: spec2}, "A")), ast.NewIdent("d"), ast.NewCall( ast.NewSel(&ast.Ident{Name: "bar", Node: spec3}, "A")), ), }, }} }(), want: `// File comment package pkg import ( "bar" bar_1 "foo/bar" bar_5 "foo" ) a: { b: bar.A() c: bar_1.A() d: bar_5.A() } `, }, { desc: "Rename duplicate import, reuse and drop", file: func() *ast.File { spec1 := ast.NewImport(nil, "bar") spec2 := ast.NewImport(nil, "foo/bar") spec3 := ast.NewImport(ast.NewIdent("bar"), "foo") return &ast.File{Decls: []ast.Decl{ &ast.ImportDecl{Specs: []*ast.ImportSpec{ spec3, ast.NewImport(nil, "foo"), }}, &ast.Field{ Label: ast.NewIdent("a"), Value: ast.NewStruct( ast.NewIdent("b"), ast.NewCall( ast.NewSel(&ast.Ident{Name: "bar", Node: spec1}, "A")), ast.NewIdent("c"), ast.NewCall( ast.NewSel(&ast.Ident{Name: "bar", Node: spec2}, "A")), ast.NewIdent("d"), ast.NewCall( ast.NewSel(&ast.Ident{Name: "bar", Node: spec3}, "A")), ), }, }} }(), want: `import ( bar "foo" bar_1 "bar" bar_5 "foo/bar" ) a: { b: bar_1.A() c: bar_5.A() d: bar.A() } `, }, { desc: "Reuse different import", file: &ast.File{Decls: []ast.Decl{ &ast.Package{Name: ast.NewIdent("pkg")}, &ast.ImportDecl{Specs: []*ast.ImportSpec{ {Path: ast.NewString("bar")}, }}, &ast.Field{ Label: ast.NewIdent("a"), Value: ast.NewStruct( ast.NewIdent("list"), ast.NewCall( ast.NewSel(&ast.Ident{ Name: "bar", Node: &ast.ImportSpec{Path: ast.NewString("bar")}, }, "Min")), ), }, }}, want: `package pkg import "bar" a: { list: bar.Min() } `, }, { desc: "Clear reference that does not exist in scope", file: &ast.File{Decls: []ast.Decl{ &ast.Field{ Label: ast.NewIdent("a"), Value: ast.NewStruct( ast.NewIdent("b"), &ast.Ident{ Name: "c", Node: ast.NewString("foo"), }, ast.NewIdent("d"), ast.NewIdent("e"), ), }, }}, want: `a: { b: c d: e } `, }, { desc: "Unshadow possible reference to other file", file: &ast.File{Decls: []ast.Decl{ &ast.Field{ Label: ast.NewIdent("a"), Value: ast.NewStruct( ast.NewIdent("b"), &ast.Ident{ Name: "c", Node: ast.NewString("foo"), }, ast.NewIdent("c"), ast.NewIdent("d"), ), }, }}, want: `a: { b: c_1 c: d } let c_1 = c `, }, { desc: "Add alias to shadowed field", file: func() *ast.File { field := &ast.Field{ Label: ast.NewIdent("a"), Value: ast.NewString("b"), } return &ast.File{Decls: []ast.Decl{ field, &ast.Field{ Label: ast.NewIdent("c"), Value: ast.NewStruct( ast.NewIdent("a"), ast.NewStruct(), ast.NewIdent("b"), &ast.Ident{ Name: "a", Node: field.Value, }, ast.NewIdent("c"), ast.NewIdent("d"), ), }, }} }(), want: `a_1=a: "b" c: { a: {} b: a_1 c: d } `, }, { desc: "Add let clause to shadowed field", // Resolve both identifiers to same clause. file: func() *ast.File { field := &ast.Field{ Label: ast.NewIdent("a"), Value: ast.NewString("b"), } return &ast.File{Decls: []ast.Decl{ field, &ast.Field{ Label: ast.NewIdent("c"), Value: ast.NewStruct( ast.NewIdent("a"), ast.NewStruct(), // Remove this reference. ast.NewIdent("b"), &ast.Ident{ Name: "a", Node: field.Value, }, ast.NewIdent("c"), ast.NewIdent("d"), ast.NewIdent("e"), &ast.Ident{ Name: "a", Node: field.Value, }, ), }, }} }(), want: `a_1=a: "b" c: { a: {} b: a_1 c: d e: a_1 } `, }, { desc: "Add let clause to shadowed field", // Resolve both identifiers to same clause. file: func() *ast.File { fieldX := &ast.Field{ Label: &ast.Alias{ Ident: ast.NewIdent("X"), Expr: ast.NewIdent("a"), // shadowed }, Value: ast.NewString("b"), } fieldY := &ast.Field{ Label: &ast.Alias{ Ident: ast.NewIdent("Y"), // shadowed Expr: ast.NewIdent("q"), // not shadowed }, Value: ast.NewString("b"), } return &ast.File{Decls: []ast.Decl{ fieldX, fieldY, &ast.Field{ Label: ast.NewIdent("c"), Value: ast.NewStruct( ast.NewIdent("a"), ast.NewStruct(), ast.NewIdent("b"), &ast.Ident{ Name: "X", Node: fieldX, }, ast.NewIdent("c"), ast.NewIdent("d"), ast.NewIdent("e"), &ast.Ident{ Name: "a", Node: fieldX.Value, }, ast.NewIdent("f"), &ast.Ident{ Name: "Y", Node: fieldY, }, ), }, }} }(), want: ` let X_1 = X X=a: "b" Y=q: "b" c: { a: {} b: X c: d e: X_1 f: Y } `, }, { desc: "Add let clause to nested shadowed field", // Resolve both identifiers to same clause. file: func() *ast.File { field := &ast.Field{ Label: ast.NewIdent("a"), Value: ast.NewString("b"), } return &ast.File{Decls: []ast.Decl{ &ast.Field{ Label: ast.NewIdent("b"), Value: ast.NewStruct( field, ast.NewIdent("b"), ast.NewStruct( ast.NewIdent("a"), ast.NewString("bar"), ast.NewIdent("b"), &ast.Ident{ Name: "a", Node: field.Value, }, ast.NewIdent("e"), &ast.Ident{ Name: "a", Node: field.Value, }, ), ), }, }} }(), want: `b: { a_1=a: "b" b: { a: "bar" b: a_1 e: a_1 } } `, }, { desc: "Add let clause to nested shadowed field with alias", // Resolve both identifiers to same clause. file: func() *ast.File { field := &ast.Field{ Label: &ast.Alias{ Ident: ast.NewIdent("X"), Expr: ast.NewIdent("a"), }, Value: ast.NewString("b"), } return &ast.File{Decls: []ast.Decl{ &ast.Field{ Label: ast.NewIdent("b"), Value: ast.NewStruct( field, ast.NewIdent("b"), ast.NewStruct( ast.NewIdent("a"), ast.NewString("bar"), ast.NewIdent("b"), &ast.Ident{ Name: "a", Node: field.Value, }, ast.NewIdent("e"), &ast.Ident{ Name: "a", Node: field.Value, }, ), ), }, }} }(), want: `b: { let X_1 = X X=a: "b" b: { a: "bar" b: X_1 e: X_1 } } `, }} for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { err := astutil.Sanitize(tc.file) if err != nil { t.Fatal(err) } b, errs := format.Node(tc.file) if errs != nil { t.Fatal(errs) } got := string(b) qt.Assert(t, qt.Equals(got, tc.want)) }) } } // For testing purposes: do not remove. func TestX(t *testing.T) { t.Skip() field := &ast.Field{ Label: &ast.Alias{ Ident: ast.NewIdent("X"), Expr: ast.NewIdent("a"), }, Value: ast.NewString("b"), } file := &ast.File{Decls: []ast.Decl{ &ast.Field{ Label: ast.NewIdent("b"), Value: ast.NewStruct( field, ast.NewIdent("b"), ast.NewStruct( ast.NewIdent("a"), ast.NewString("bar"), ast.NewIdent("b"), &ast.Ident{ Name: "a", Node: field.Value, }, ast.NewIdent("e"), &ast.Ident{ Name: "a", Node: field.Value, }, ), ), }, }} err := astutil.Sanitize(file) if err != nil { t.Fatal(err) } b, errs := format.Node(file) if errs != nil { t.Fatal(errs) } t.Error(string(b)) }