| // Copyright 2023 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 parsego_test |
| |
| import ( |
| "context" |
| "fmt" |
| "go/ast" |
| "go/parser" |
| "go/token" |
| "reflect" |
| "slices" |
| "testing" |
| |
| "golang.org/x/tools/gopls/internal/cache/parsego" |
| "golang.org/x/tools/gopls/internal/util/safetoken" |
| "golang.org/x/tools/internal/analysisinternal" |
| "golang.org/x/tools/internal/tokeninternal" |
| ) |
| |
| // TODO(golang/go#64335): we should have many more tests for fixed syntax. |
| |
| func TestFixPosition_Issue64488(t *testing.T) { |
| // This test reproduces the conditions of golang/go#64488, where a type error |
| // on fixed syntax overflows the token.File. |
| const src = ` |
| package foo |
| |
| func _() { |
| type myThing struct{} |
| var foo []myThing |
| for ${1:}, ${2:} := range foo { |
| $0 |
| } |
| } |
| ` |
| |
| pgf, _ := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", []byte(src), parsego.Full, false) |
| fset := tokeninternal.FileSetFor(pgf.Tok) |
| ast.Inspect(pgf.File, func(n ast.Node) bool { |
| if n != nil { |
| posn := safetoken.StartPosition(fset, n.Pos()) |
| if !posn.IsValid() { |
| t.Fatalf("invalid position for %T (%v): %v not in [%d, %d]", n, n, n.Pos(), pgf.Tok.Base(), pgf.Tok.Base()+pgf.Tok.Size()) |
| } |
| } |
| return true |
| }) |
| } |
| |
| func TestFixGoAndDefer(t *testing.T) { |
| var testCases = []struct { |
| source string |
| fixes []parsego.FixType |
| wantFix string |
| }{ |
| {source: "", fixes: nil}, // keyword alone |
| {source: "a.b(", fixes: nil}, |
| {source: "a.b()", fixes: nil}, |
| {source: "func {", fixes: nil}, |
| { |
| source: "f", |
| fixes: []parsego.FixType{parsego.FixedDeferOrGo}, |
| wantFix: "f()", |
| }, |
| { |
| source: "func", |
| fixes: []parsego.FixType{parsego.FixedDeferOrGo}, |
| wantFix: "(func())()", |
| }, |
| { |
| source: "func {}", |
| fixes: []parsego.FixType{parsego.FixedDeferOrGo}, |
| wantFix: "(func())()", |
| }, |
| { |
| source: "func {}(", |
| fixes: []parsego.FixType{parsego.FixedDeferOrGo}, |
| wantFix: "(func())()", |
| }, |
| { |
| source: "func {}()", |
| fixes: []parsego.FixType{parsego.FixedDeferOrGo}, |
| wantFix: "(func())()", |
| }, |
| { |
| source: "a.", |
| fixes: []parsego.FixType{parsego.FixedDeferOrGo, parsego.FixedDanglingSelector, parsego.FixedDeferOrGo}, |
| wantFix: "a._()", |
| }, |
| { |
| source: "a.b", |
| fixes: []parsego.FixType{parsego.FixedDeferOrGo}, |
| wantFix: "a.b()", |
| }, |
| } |
| |
| for _, keyword := range []string{"go", "defer"} { |
| for _, tc := range testCases { |
| source := fmt.Sprintf("%s %s", keyword, tc.source) |
| t.Run(source, func(t *testing.T) { |
| src := filesrc(source) |
| pgf, fixes := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", src, parsego.Full, false) |
| if !slices.Equal(fixes, tc.fixes) { |
| t.Fatalf("got %v want %v", fixes, tc.fixes) |
| } |
| if tc.fixes == nil { |
| return |
| } |
| |
| fset := tokeninternal.FileSetFor(pgf.Tok) |
| inspect(t, pgf, func(stmt ast.Stmt) { |
| var call *ast.CallExpr |
| switch stmt := stmt.(type) { |
| case *ast.DeferStmt: |
| call = stmt.Call |
| case *ast.GoStmt: |
| call = stmt.Call |
| default: |
| return |
| } |
| |
| if got := analysisinternal.Format(fset, call); got != tc.wantFix { |
| t.Fatalf("got %v want %v", got, tc.wantFix) |
| } |
| }) |
| }) |
| } |
| } |
| } |
| |
| // TestFixInit tests the init stmt after if/for/switch which is put under cond after parsing |
| // will be fixed and moved to Init. |
| func TestFixInit(t *testing.T) { |
| var testCases = []struct { |
| name string |
| source string |
| fixes []parsego.FixType |
| wantInitFix string |
| }{ |
| { |
| name: "simple define", |
| source: "i := 0", |
| fixes: []parsego.FixType{parsego.FixedInit}, |
| wantInitFix: "i := 0", |
| }, |
| { |
| name: "simple assign", |
| source: "i = 0", |
| fixes: []parsego.FixType{parsego.FixedInit}, |
| wantInitFix: "i = 0", |
| }, |
| { |
| name: "define with function call", |
| source: "i := f()", |
| fixes: []parsego.FixType{parsego.FixedInit}, |
| wantInitFix: "i := f()", |
| }, |
| { |
| name: "assign with function call", |
| source: "i = f()", |
| fixes: []parsego.FixType{parsego.FixedInit}, |
| wantInitFix: "i = f()", |
| }, |
| { |
| name: "assign with receiving chan", |
| source: "i = <-ch", |
| fixes: []parsego.FixType{parsego.FixedInit}, |
| wantInitFix: "i = <-ch", |
| }, |
| |
| // fixInitStmt won't fix the following cases. |
| { |
| name: "call in if", |
| source: `fmt.Println("helloworld")`, |
| fixes: nil, |
| }, |
| { |
| name: "receive chan", |
| source: `<- ch`, |
| fixes: nil, |
| }, |
| } |
| |
| // currently, switch will leave its Tag empty after fix because it allows empty, |
| // and if and for will leave an underscore in Cond. |
| getWantCond := func(keyword string) string { |
| if keyword == "switch" { |
| return "" |
| } |
| return "_" |
| } |
| |
| for _, keyword := range []string{"if", "for", "switch"} { |
| for _, tc := range testCases { |
| caseName := fmt.Sprintf("%s %s", keyword, tc.name) |
| t.Run(caseName, func(t *testing.T) { |
| // the init stmt is treated as a cond. |
| src := filesrc(fmt.Sprintf("%s %s {}", keyword, tc.source)) |
| pgf, fixes := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", src, parsego.Full, false) |
| if !slices.Equal(fixes, tc.fixes) { |
| t.Fatalf("TestFixArrayType(): got %v want %v", fixes, tc.fixes) |
| } |
| if tc.fixes == nil { |
| return |
| } |
| |
| // ensure the init stmt is parsed to a BadExpr. |
| ensureSource(t, src, func(bad *ast.BadExpr) {}) |
| |
| info := func(n ast.Node, wantStmt string) (init ast.Stmt, cond ast.Expr, has bool) { |
| switch wantStmt { |
| case "if": |
| if e, ok := n.(*ast.IfStmt); ok { |
| return e.Init, e.Cond, true |
| } |
| case "switch": |
| if e, ok := n.(*ast.SwitchStmt); ok { |
| return e.Init, e.Tag, true |
| } |
| case "for": |
| if e, ok := n.(*ast.ForStmt); ok { |
| return e.Init, e.Cond, true |
| } |
| } |
| return nil, nil, false |
| } |
| fset := tokeninternal.FileSetFor(pgf.Tok) |
| inspect(t, pgf, func(n ast.Stmt) { |
| if init, cond, ok := info(n, keyword); ok { |
| if got := analysisinternal.Format(fset, init); got != tc.wantInitFix { |
| t.Fatalf("%s: Init got %v want %v", tc.source, got, tc.wantInitFix) |
| } |
| |
| wantCond := getWantCond(keyword) |
| if got := analysisinternal.Format(fset, cond); got != wantCond { |
| t.Fatalf("%s: Cond got %v want %v", tc.source, got, wantCond) |
| } |
| } |
| }) |
| }) |
| } |
| } |
| } |
| |
| func TestFixPhantomSelector(t *testing.T) { |
| wantFixes := []parsego.FixType{parsego.FixedPhantomSelector} |
| var testCases = []struct { |
| source string |
| fixes []parsego.FixType |
| }{ |
| {source: "a.break", fixes: wantFixes}, |
| {source: "_.break", fixes: wantFixes}, |
| {source: "a.case", fixes: wantFixes}, |
| {source: "a.chan", fixes: wantFixes}, |
| {source: "a.const", fixes: wantFixes}, |
| {source: "a.continue", fixes: wantFixes}, |
| {source: "a.default", fixes: wantFixes}, |
| {source: "a.defer", fixes: wantFixes}, |
| {source: "a.else", fixes: wantFixes}, |
| {source: "a.fallthrough", fixes: wantFixes}, |
| {source: "a.for", fixes: wantFixes}, |
| {source: "a.func", fixes: wantFixes}, |
| {source: "a.go", fixes: wantFixes}, |
| {source: "a.goto", fixes: wantFixes}, |
| {source: "a.if", fixes: wantFixes}, |
| {source: "a.import", fixes: wantFixes}, |
| {source: "a.interface", fixes: wantFixes}, |
| {source: "a.map", fixes: wantFixes}, |
| {source: "a.package", fixes: wantFixes}, |
| {source: "a.range", fixes: wantFixes}, |
| {source: "a.return", fixes: wantFixes}, |
| {source: "a.select", fixes: wantFixes}, |
| {source: "a.struct", fixes: wantFixes}, |
| {source: "a.switch", fixes: wantFixes}, |
| {source: "a.type", fixes: wantFixes}, |
| {source: "a.var", fixes: wantFixes}, |
| |
| {source: "break.break"}, |
| {source: "a.BREAK"}, |
| {source: "a.break_"}, |
| {source: "a.breaka"}, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.source, func(t *testing.T) { |
| src := filesrc(tc.source) |
| pgf, fixes := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", src, parsego.Full, false) |
| if !slices.Equal(fixes, tc.fixes) { |
| t.Fatalf("got %v want %v", fixes, tc.fixes) |
| } |
| |
| // some fixes don't fit the fix scenario, but we want to confirm it. |
| if fixes == nil { |
| return |
| } |
| |
| // ensure the selector has been converted to underscore by parser. |
| ensureSource(t, src, func(sel *ast.SelectorExpr) { |
| if sel.Sel.Name != "_" { |
| t.Errorf("%s: the input doesn't cause a blank selector after parser", tc.source) |
| } |
| }) |
| |
| fset := tokeninternal.FileSetFor(pgf.Tok) |
| inspect(t, pgf, func(sel *ast.SelectorExpr) { |
| // the fix should restore the selector as is. |
| if got, want := fmt.Sprintf("%s", analysisinternal.Format(fset, sel)), tc.source; got != want { |
| t.Fatalf("got %v want %v", got, want) |
| } |
| }) |
| }) |
| } |
| } |
| |
| // inspect helps to go through each node of pgf and trigger checkFn if the type matches T. |
| func inspect[T ast.Node](t *testing.T, pgf *parsego.File, checkFn func(n T)) { |
| fset := tokeninternal.FileSetFor(pgf.Tok) |
| var visited bool |
| ast.Inspect(pgf.File, func(node ast.Node) bool { |
| if node != nil { |
| posn := safetoken.StartPosition(fset, node.Pos()) |
| if !posn.IsValid() { |
| t.Fatalf("invalid position for %T (%v): %v not in [%d, %d]", node, node, node.Pos(), pgf.Tok.Base(), pgf.Tok.Base()+pgf.Tok.Size()) |
| } |
| if n, ok := node.(T); ok { |
| visited = true |
| checkFn(n) |
| } |
| } |
| return true |
| }) |
| if !visited { |
| var n T |
| t.Fatalf("got no %s node but want at least one", reflect.TypeOf(n)) |
| } |
| } |
| |
| // ensureSource helps to parse src into an ast.File by go/parser and trigger checkFn if the type matches T. |
| func ensureSource[T ast.Node](t *testing.T, src []byte, checkFn func(n T)) { |
| // tolerate error as usually the src is problematic. |
| originFile, _ := parser.ParseFile(token.NewFileSet(), "file://foo.go", src, parsego.Full) |
| var visited bool |
| ast.Inspect(originFile, func(node ast.Node) bool { |
| if n, ok := node.(T); ok { |
| visited = true |
| checkFn(n) |
| } |
| return true |
| }) |
| |
| if !visited { |
| var n T |
| t.Fatalf("got no %s node but want at least one", reflect.TypeOf(n)) |
| } |
| } |
| |
| func filesrc(expressions string) []byte { |
| const srcTmpl = `package foo |
| |
| func _() { |
| %s |
| }` |
| return fmt.Appendf(nil, srcTmpl, expressions) |
| } |