blob: e0771d05648035efe50a96bd9d0cc3c7f7c6981e [file] [log] [blame] [edit]
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package refactor_test
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"go/types"
"slices"
"testing"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/refactor"
)
func TestDeleteStmt(t *testing.T) {
type testCase struct {
in string
which int // count of ast.Stmt in ast.Inspect traversal to remove
want string
name string // should contain exactly one of [block,switch,case,comm,for,type]
}
tests := []testCase{
{
in: "package p; func f() {/*a*/x := 12/*b*/}",
which: 1,
want: "package p; func f() {}",
name: "var0",
},
{
in: "package p; func f() {\n/*a*/x := 12 //foo\n//keep\n}",
which: 1,
want: "package p; func f() {\n//keep\n}",
name: "var1",
},
{ // for code coverage
in: "package p; func f() {/*a*/x:=12/*b*/; y:=11}",
which: 1,
want: "package p; func f() {/*b*/; y:=11}",
name: "var2",
},
{ // do nothing when asked to remove a function body
in: "package p; func f() { }",
which: 0,
want: "package p; func f() { }",
name: "block0",
},
{
in: "package p; func f() { abcd()}",
which: 1,
want: "package p; func f() { }",
name: "block1",
},
{ // keep white space
in: "package p; func f() { a() }",
which: 1,
want: "package p; func f() { }",
name: "block2",
},
{ // keep semicolon
in: "package p; func f() { a();}",
which: 1,
want: "package p; func f() { ;}",
name: "block3",
},
{ // remove whole line
in: "package p; func f() {\n a() \n\n}",
which: 1,
want: "package p; func f() {\n\n}",
name: "block4",
},
{ // preserve whitespace and remove comment
in: "package p; func f() { a()// comment\n}",
which: 1,
want: "package p; func f() { \n}",
name: "block5",
},
{ // preserve whitespace, remove preceding comment
in: "package p; func f() { /*c*/a() \n}",
which: 1,
want: "package p; func f() { \n}",
name: "block6",
},
{
in: "package p; func f() { a();b();}",
which: 2,
want: "package p; func f() { a();;}",
name: "block7",
},
{ // remove whole line
in: "package p; func f() {\n\ta()\n\tb()\n}",
which: 2,
want: "package p; func f() {\n\ta()\n}",
name: "block8",
},
{ // remove whole line
in: "package p; func f() {\n\ta()\n\tb()\n\tc()\n}",
which: 2,
want: "package p; func f() {\n\ta()\n\tc()\n}",
name: "block9",
},
{ // remove expression statement
in: "package p\nfunc f() {a()+b()}",
which: 1,
want: "package p\nfunc f() {}",
name: "block10",
},
{ // even in parentheses
in: "package p\nfunc f() {(a()+b())}",
which: 1,
want: "package p\nfunc f() {}",
name: "block11",
},
{ // should it also remove the semicolon? No, can't find it.
in: "package p; func f() { switch a(); b() {}}",
which: 2, // 0 is the func body, 1 is the switch statement
want: "package p; func f() { switch ; b() {}}",
name: "switch0",
},
{ // remove preceding comment too
in: "package p; func f() { switch /*c*/a(); {}}",
which: 2, // 0 is the func body, 1 is the switch statement
want: "package p; func f() { switch ; {}}",
name: "switch1",
},
{ // remove subsequent comment too
in: "package p; func f() { switch a()/*c*/; {}}",
which: 2, // 0 is the func body, 1 is the switch statement
want: "package p; func f() { switch ; {}}",
name: "switch2",
},
{ // statement inside a case
in: "package p; func f() { select {default: a()}}",
which: 4, // 0 is the func body, 1 is the select statement, 2 is its body, 3 is the comm clause
want: "package p; func f() { select {default: }}",
name: "comm0",
},
{ // statement in comm clause
in: "package p; func f(x chan any) { select {case x <- a: a(x)}}",
which: 5, // 0 is the func body, 1 is the select statement, 2 is its body, 3 is the comm clause
want: "package p; func f(x chan any) { select {case x <- a: }}",
name: "comm1",
},
{ // don't remove whole clause
in: "package p; func f(x chan any) { select {case x <- a: a(x)}}",
which: 4, // 0 is the func body, 1 is the select statement, 2 is its body, 3 is the comm clause
want: "package p; func f(x chan any) { select {case x <- a: a(x)}}",
name: "comm2",
},
{
in: "package p; func f() { switch {default: a()}}",
which: 4, // 0 is the func body, 1 is the select statement, 2 is its body
want: "package p; func f() { switch {default: }}",
name: "case0",
},
{
in: "package p; func f() { switch {case 3: /*A*/ a() /*B*/}}",
which: 4, // 0 is the func body, 1 is the select statement, 2 is its body
want: "package p; func f() { switch {case 3: }}",
name: "case1",
},
{ // init in for statement
in: "package p; func f() {for /*a*/ a();;b() {}}",
which: 2,
want: "package p; func f() {for ;;b() {}}",
name: "for0",
},
{ // step in for statement
in: "package p; func f() {for a();c();b() /*B*/ {}}",
which: 3,
want: "package p; func f() {for a();c(); {}}",
name: "for1",
},
{ // one of two func calls on. line
in: "package p; func f() {for\na();c()\nb() {}}",
which: 2,
want: "package p; func f() {for\n;c()\nb() {}}",
name: "for2",
},
{ // step in for, with strange \n
in: "package p; func f() {for a();\nc();b() {}}",
which: 3,
want: "package p; func f() {for a();\nc(); {}}",
name: "for3",
},
{ // in type switch
in: "package p; func f() {switch a();b().(type){}}",
which: 2,
want: "package p; func f() {switch ;b().(type){}}",
name: "type0",
},
{ // but not the type cast
in: "package p; func f() {switch a();b().(type){}}",
which: 3,
want: "package p; func f() {switch a();b().(type){}}",
name: "type1",
},
{ // if
in: "package p; func f() {if a();b() {}}",
which: 2,
want: "package p; func f() {if ;b() {}}",
name: "if0",
},
{ // cannot remove the else
in: "package p\nfunc _() { if a() {} else {b()}}",
which: 3,
want: "package p\nfunc _() { if a() {} else {b()}}",
name: "else0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, tt.name, tt.in, parser.ParseComments)
if err != nil {
t.Fatalf("%s: %v", tt.name, err)
}
insp := inspector.New([]*ast.File{f})
root := insp.Root()
var stmt inspector.Cursor
cnt := 0
for cn := range root.Preorder() { // Preorder(ast.Stmt(nil)) doesn't work
if _, ok := cn.Node().(ast.Stmt); !ok {
continue
}
if cnt == tt.which {
stmt = cn
break
}
cnt++
}
if cnt != tt.which {
t.Fatalf("test %s does not contain desired statement %d", tt.name, tt.which)
}
tokFile := fset.File(f.Pos())
edits := refactor.DeleteStmt(tokFile, stmt)
if tt.want == tt.in {
if len(edits) != 0 {
t.Fatalf("%s: got %d edits, expected 0", tt.name, len(edits))
}
return
}
if len(edits) != 1 {
t.Fatalf("%s: got %d edits, expected 1", tt.name, len(edits))
}
left := tokFile.Offset(edits[0].Pos)
right := tokFile.Offset(edits[0].End)
got := tt.in[:left] + tt.in[right:]
if got != tt.want {
t.Errorf("%s: got\n%q, want\n%q", tt.name, got, tt.want)
}
})
}
}
func TestDeleteVar(t *testing.T) {
// Each example deletes var v.
for i, test := range []struct {
src string
want string
}{
// package-level GenDecl > ValueSpec
{
"package p; var v int",
"package p; ",
},
{
"package p; var x, v int",
"package p; var x int",
},
{
"package p; var v, x int",
"package p; var x int",
},
{
"package p; var ( v int )",
"package p;",
},
{
"package p; var ( x, v int )",
"package p; var ( x int )",
},
{
"package p; var ( v, x int )",
"package p; var ( x int )",
},
{
"package p; var v, x = 1, 2",
"package p; var x = 2",
},
{
"package p; var x, v = 1, 2",
"package p; var x = 1",
},
{
"package p; var v, x = fx(), fx()",
"package p; var _, x = fx(), fx()",
},
{
"package p; var v, _ = fx(), fx()",
"package p; var _, _ = fx(), fx()",
},
{
"package p; var _, v = fx(), fx()",
"package p; var _, _ = fx(), fx()",
},
{
"package p; var v = fx()",
"package p; var _ = fx()",
},
{
"package p; var ( a int; v int; c int )",
"package p; var ( a int; c int )",
},
{
"package p; var ( a int; v int = 2; c int )",
"package p; var ( a int; c int )",
},
// GenDecl doc comments are not deleted unless decl is deleted.
{
"package p\n// comment\nvar ( v int )",
"package p",
},
{
"package p\n// comment\nvar v int",
"package p",
},
{
"package p\n/* comment */\nvar v int",
"package p",
},
{
"package p\n// comment\nvar ( v, x int )",
"package p\n// comment\nvar ( x int )",
},
{
"package p\n// comment\nvar v, x int",
"package p\n// comment\nvar x int",
},
{
"package p\n/* comment */\nvar x, v int",
"package p\n/* comment */\nvar x int",
},
// ValueSpec leading doc comments
{
"package p\nvar (\n// comment\nv int; x int )",
"package p\nvar (\nx int )",
},
{
"package p\nvar (\n// comment\nx int; v int )",
"package p\nvar (\n// comment\nx int )",
},
// ValueSpec trailing line comments
{
"package p; var ( v int // comment\nx int )",
"package p; var ( x int )",
},
{
"package p; var ( x int // comment\nv int )",
"package p; var ( x int // comment\n )",
},
{
"package p; var ( v int /* comment */)",
"package p;",
},
{
"package p; var ( v int // comment\n)",
"package p;",
},
{
"package p; var ( v int ) // comment",
"package p;",
},
{
"package p; var ( x, v int /* comment */ )",
"package p; var ( x int /* comment */ )",
},
{
"package p; var ( v, x int /* comment */ )",
"package p; var ( x int /* comment */ )",
},
{
"package p; var ( x, v int // comment\n)",
"package p; var ( x int // comment\n)",
},
{
"package p; var ( v, x int // comment\n)",
"package p; var ( x int // comment\n)",
},
{
"package p; var ( v, x int ) // comment",
"package p; var ( x int ) // comment",
},
{
"package p; var ( x int; v int // comment\n)",
"package p; var ( x int )",
},
{
"package p; var ( v int // comment\n x int )",
"package p; var ( x int )",
},
// local DeclStmt > GenDecl > ValueSpec
// (The only interesting cases
// here are the total deletions.)
{
"package p; func _() { var v int }",
"package p; func _() {}",
},
{
"package p; func _() { var ( v int ) }",
"package p; func _() {}",
},
{
"package p; func _() { var ( v int // comment\n) }",
"package p; func _() {}",
},
{
"package p; func _() { var ( v int ) // comment\n }",
"package p; func _() { \n}",
},
{
"package p; func _() { var v int // comment\n }",
"package p; func _() { \n}",
},
// AssignStmt
{
"package p; func _() { v := 0 }",
"package p; func _() {}",
},
{
"package p; func _() { x, v := 0, 1 }",
"package p; func _() { x := 0 }",
},
{
"package p; func _() { v, x := 0, 1 }",
"package p; func _() { x := 1 }",
},
{
"package p; func _() { v, x := f() }",
"package p; func _() { _, x := f() }",
},
{
"package p; func _() { v, x := fx(), fx() }",
"package p; func _() { _, x := fx(), fx() }",
},
{
"package p; func _() { v, _ := fx(), fx() }",
"package p; func _() { _, _ = fx(), fx() }",
},
{
"package p; func _() { _, v := fx(), fx() }",
"package p; func _() { _, _ = fx(), fx() }",
},
{
"package p; func _() { v := fx() }",
"package p; func _() { _ = fx() }",
},
{
"package p; func _() { v := 1 // comment\n }",
"package p; func _() { \n}",
},
{
"package p; func _() { v, x := 0, 1 // comment\n }",
"package p; func _() { x := 1 // comment\n }",
},
{
"package p; func _() { if v := 1; cond {} }",
"package p; func _() { if ; cond {} }",
},
{
"package p; func _() { if v, x := 1, 2; cond {} }",
"package p; func _() { if x := 2; cond {} }",
},
{
"package p; func _() { switch v := 0; cond {} }",
"package p; func _() { switch cond {} }",
},
{
"package p; func _() { switch v := fx(); cond {} }",
"package p; func _() { switch _ = fx(); cond {} }",
},
{
"package p; func _() { for v := 0; ; {} }",
"package p; func _() { for {} }",
},
// unhandled cases
{
"package p; func _(v int) {}", // parameter
"package p; func _(v int) {}",
},
{
"package p; func _() (v int) {}", // result
"package p; func _() (v int) {}",
},
{
"package p; type T int; func _(v T) {}", // receiver
"package p; type T int; func _(v T) {}",
},
// There is no defining Ident in this case.
// {
// "package p; func _() { switch v := any(nil).(type) {} }",
// "package p; func _() { switch v := any(nil).(type) {} }",
// },
} {
t.Run(fmt.Sprint(i), func(t *testing.T) {
t.Logf("src: %s", test.src)
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "p", test.src, parser.ParseComments) // allow errors
conf := types.Config{
Error: func(err error) {}, // allow errors
}
info := &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
}
files := []*ast.File{f}
conf.Check("p", fset, files, info) // ignore error
curId := func() inspector.Cursor {
for curId := range inspector.New(files).Root().Preorder((*ast.Ident)(nil)) {
id := curId.Node().(*ast.Ident)
if id.Name == "v" && info.Defs[id] != nil {
return curId
}
}
t.Fatalf("can't find Defs[v]")
panic("unreachable")
}()
tokFile := fset.File(f.Pos())
edits := refactor.DeleteVar(tokFile, info, curId)
// TODO(adonovan): extract this helper for
// applying TextEdits and comparing against
// expectations. (This code was mostly copied
// from analysistest.)
var dedits []diff.Edit
for _, edit := range edits {
file := fset.File(edit.Pos)
dedits = append(dedits, diff.Edit{
Start: file.Offset(edit.Pos),
End: file.Offset(edit.End),
New: string(edit.NewText),
})
}
fixed, err := diff.ApplyBytes([]byte(test.src), dedits)
if err != nil {
t.Fatalf("diff.Apply: %v", err)
}
t.Logf("fixed: %s", fixed)
fixed, err = format.Source(fixed)
if err != nil {
t.Fatalf("format: %v", err)
}
want, err := format.Source([]byte(test.want))
if err != nil {
t.Fatalf("formatting want: %v", err)
}
t.Logf("want: %s", want)
unified := func(xlabel, ylabel string, x, y []byte) string {
x = append(slices.Clip(bytes.TrimSpace(x)), '\n')
y = append(slices.Clip(bytes.TrimSpace(y)), '\n')
return diff.Unified(xlabel, ylabel, string(x), string(y))
}
if diff := unified("fixed", "want", fixed, want); diff != "" {
t.Errorf("-- diff original fixed --\n%s\n"+
"-- diff fixed want --\n%s",
unified("original", "fixed", []byte(test.src), fixed),
diff)
}
})
}
}