| // 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) |
| } |
| }) |
| } |
| } |