blob: 997396f7724bd4483df929f192ab8bcbb90f3255 [file] [log] [blame]
// 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/stretchr/testify/assert"
)
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)
assert.Equal(t, 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))
}