blob: db78b59604218e3abe4cfeb43f06a31bdf4c2c64 [file] [log] [blame]
// 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)
}