From 70fe6af9fea2ffadc70a9adb14584eee9ac9a9f3 Mon Sep 17 00:00:00 2001 From: visualfc Date: Sat, 29 Aug 2026 17:39:58 +0800 Subject: [PATCH 1/6] support Go 1.27 and expand syntax tests --- .github/workflows/go.yml | 6 +- astview/_testdata/basic/generic.go | 32 +++ astview/_testdata/basic/types.go | 14 ++ astview/_testdata/basic/usage.go | 10 + astview/_testdata/basic/want.txt | 29 +++ astview/astview.go | 16 +- astview/astview_test.go | 50 ++++ docview/_testdata/basic/doc.go | 18 ++ docview/_testdata/basic/extra.go | 10 + docview/_testdata/basic/want.txt | 9 + docview/docview_test.go | 103 +++++++++ docview/docx.go | 11 +- go.mod | 10 +- go.sum | 41 +--- goapi/_testdata/generic/methods.go | 5 + goapi/_testdata/generic/types.go | 5 + goapi/_testdata/generic/want.txt | 4 + goapi/base_type_go117.go | 20 ++ goapi/base_type_go118.go | 24 ++ goapi/goapi.go | 61 ++--- goapi/goapi_test.go | 282 +++++++++++++++++++++++ gofmt/_testdata/fiximports/input.go | 11 + gofmt/_testdata/fiximports/want.txt | 7 + gofmt/_testdata/nonstd/input.go | 7 + gofmt/_testdata/nonstd/want.txt | 11 + gofmt/_testdata/nonstd_existing/input.go | 14 ++ gofmt/_testdata/nonstd_existing/want.txt | 16 ++ gofmt/gofmt.go | 28 ++- gofmt/gofmt_test.go | 51 ++++ pkg/pkgwalk/pkgwalk.go | 3 + pkg/stdlib/pkglist.go | 141 +++++++----- pkg/stdlib/pkglist_test.go | 24 ++ types/_testdata/generic/methods.go | 23 ++ types/_testdata/generic/types.go | 16 ++ types/_testdata/generic/want.txt | 20 ++ types/_testdata/syntax/types.go | 40 ++++ types/_testdata/syntax/usage.go | 30 +++ types/_testdata/syntax/want.txt | 23 ++ types/go123_test.go | 17 ++ types/syntax_test.go | 187 +++++++++++++++ types/types.go | 3 + 41 files changed, 1296 insertions(+), 136 deletions(-) create mode 100644 astview/_testdata/basic/generic.go create mode 100644 astview/_testdata/basic/types.go create mode 100644 astview/_testdata/basic/usage.go create mode 100644 astview/_testdata/basic/want.txt create mode 100644 astview/astview_test.go create mode 100644 docview/_testdata/basic/doc.go create mode 100644 docview/_testdata/basic/extra.go create mode 100644 docview/_testdata/basic/want.txt create mode 100644 docview/docview_test.go create mode 100644 goapi/_testdata/generic/methods.go create mode 100644 goapi/_testdata/generic/types.go create mode 100644 goapi/_testdata/generic/want.txt create mode 100644 goapi/base_type_go117.go create mode 100644 goapi/base_type_go118.go create mode 100644 goapi/goapi_test.go create mode 100644 gofmt/_testdata/fiximports/input.go create mode 100644 gofmt/_testdata/fiximports/want.txt create mode 100644 gofmt/_testdata/nonstd/input.go create mode 100644 gofmt/_testdata/nonstd/want.txt create mode 100644 gofmt/_testdata/nonstd_existing/input.go create mode 100644 gofmt/_testdata/nonstd_existing/want.txt create mode 100644 gofmt/gofmt_test.go create mode 100644 pkg/stdlib/pkglist_test.go create mode 100644 types/_testdata/generic/methods.go create mode 100644 types/_testdata/generic/types.go create mode 100644 types/_testdata/generic/want.txt create mode 100644 types/_testdata/syntax/types.go create mode 100644 types/_testdata/syntax/usage.go create mode 100644 types/_testdata/syntax/want.txt create mode 100644 types/go123_test.go create mode 100644 types/syntax_test.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 2fc9b2d..6a8e5d5 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -10,14 +10,14 @@ jobs: Test: strategy: matrix: - go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x] + go-version: [1.25.x, 1.26.x, 1.27.x] os: [ubuntu-latest, windows-latest, macos-11] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} diff --git a/astview/_testdata/basic/generic.go b/astview/_testdata/basic/generic.go new file mode 100644 index 0000000..4167354 --- /dev/null +++ b/astview/_testdata/basic/generic.go @@ -0,0 +1,32 @@ +package fixture + +type Integer interface { + ~int | ~int64 +} + +type Pair[A, B any] struct { + First A + Second B +} + +type List[T any] []T + +type Alias[T any] = List[T] + +type Combiner[T, U any] interface { + Combine(T, U) T +} + +func Identity[T any](v T) T { return v } + +func Map[T, U any](xs []T, f func(T) U) []U { + out := make([]U, len(xs)) + for i, x := range xs { + out[i] = f(x) + } + return out +} + +func (p Pair[A, B]) FirstValue() A { return p.First } + +var _ = Pair[string, int]{First: "x", Second: 1} diff --git a/astview/_testdata/basic/types.go b/astview/_testdata/basic/types.go new file mode 100644 index 0000000..ae83661 --- /dev/null +++ b/astview/_testdata/basic/types.go @@ -0,0 +1,14 @@ +package fixture + +// Box stores a value of any type. +type Box[T any] struct { + Value T +} + +type Reader interface { + Read() string +} + +type Record struct { + Name string +} diff --git a/astview/_testdata/basic/usage.go b/astview/_testdata/basic/usage.go new file mode 100644 index 0000000..d67938c --- /dev/null +++ b/astview/_testdata/basic/usage.go @@ -0,0 +1,10 @@ +package fixture + +import "fmt" + +func (b Box[T]) Get() T { return b.Value } + +func Use() string { + b := Box[string]{Value: "ok"} + return fmt.Sprint(Record{Name: b.Get()}) +} diff --git a/astview/_testdata/basic/want.txt b/astview/_testdata/basic/want.txt new file mode 100644 index 0000000..8c0fe91 --- /dev/null +++ b/astview/_testdata/basic/want.txt @@ -0,0 +1,29 @@ +@_testdata/basic/generic.go +@_testdata/basic/types.go +@_testdata/basic/usage.go +0,p,fixture +1,+m,Imports +2,mm,fmt,2:3:8 +1,+v,Variables +2,v,_,0:32:5 +1,+f,Functions +2,f,Identity[T any],0:20:1@func(v T) T +2,f,Map[T any, U any],0:22:1@func(xs []T, f func(T) U) []U +2,f,Use,2:7:1@func() string +1,t,Alias[T any],0:14:1 +1,s,Box[T any],1:4:1 +2,tm,Get,2:5:1@func() T +2,tv,Value,1:5:2@T +1,i,Combiner[T any, U any],0:16:1 +2,tm,Combine,0:17:9@Combine +1,i,Integer,0:3:1 +2,t,~int | ~int64,0:4:2@~int | ~int64 +1,t,List[T any],0:12:1 +1,s,Pair[A any, B any],0:7:1 +2,tm,FirstValue,0:30:1@func() A +2,tv,First,0:8:2@A +2,tv,Second,0:9:2@B +1,i,Reader,1:8:1 +2,tm,Read,1:9:6@Read +1,s,Record,1:12:1 +2,tv,Name,1:13:2@string diff --git a/astview/astview.go b/astview/astview.go index e6104c9..d50e0b4 100644 --- a/astview/astview.go +++ b/astview/astview.go @@ -138,10 +138,10 @@ func NewFilePackage(filename string) (*PackageView, error) { } m := make(map[string]*ast.File) m[filename] = file - pkg, err := ast.NewPackage(p.fset, m, nil, nil) - if err != nil { - return nil, err - } + // ast.NewPackage reports predeclared identifiers such as `any` as + // undeclared when no universe scope is supplied. The documentation view + // only needs the parsed files, so avoid the deprecated resolver here. + pkg := &ast.Package{Name: file.Name.Name, Files: m} p.pkg = pkg p.pdoc = NewPackageDoc(pkg, pkg.Name, true) return p, nil @@ -221,10 +221,10 @@ func NewFilePackageSource(filename string, f io.Reader, expr bool) (*PackageView } m := make(map[string]*ast.File) m[filename] = file - pkg, err := ast.NewPackage(p.fset, m, nil, nil) - if err != nil { - return nil, err - } + // ast.NewPackage reports predeclared identifiers such as `any` as + // undeclared when no universe scope is supplied. The documentation view + // only needs the parsed files, so avoid the deprecated resolver here. + pkg := &ast.Package{Name: file.Name.Name, Files: m} p.pdoc = NewPackageDoc(pkg, pkg.Name, true) return p, nil diff --git a/astview/astview_test.go b/astview/astview_test.go new file mode 100644 index 0000000..37e3197 --- /dev/null +++ b/astview/astview_test.go @@ -0,0 +1,50 @@ +//go:build go1.18 +// +build go1.18 + +package astview + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestFiles(t *testing.T) { + dir := filepath.Join("_testdata", "basic") + files, err := filepath.Glob(filepath.Join(dir, "*.go")) + if err != nil { + t.Fatal(err) + } + if len(files) == 0 { + t.Fatal("no testdata Go files") + } + var got bytes.Buffer + AllFiles = nil + astViewShowTypeParams = true + defer func() { astViewShowTypeParams = false }() + if err := PrintFilesTree(files, &got, true); err != nil { + t.Fatalf("print files tree: %v", err) + } + want, err := os.ReadFile(filepath.Join(dir, "want.txt")) + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(got.String()) != strings.TrimSpace(string(want)) { + t.Fatalf("file tree mismatch\n got:\n%s\nwant:\n%s", got.String(), want) + } +} + +func TestGenericSource(t *testing.T) { + src := strings.NewReader("package p\n\ntype Pair[A, B any] struct { A A; B B }\n") + view, err := NewFilePackageSource("stdin.go", src, true) + if err != nil { + t.Fatalf("parse generic source: %v", err) + } + var out strings.Builder + view.PrintTree(&out) + if !strings.Contains(out.String(), "Pair") { + t.Fatalf("generic type missing from output: %s", out.String()) + } +} diff --git a/docview/_testdata/basic/doc.go b/docview/_testdata/basic/doc.go new file mode 100644 index 0000000..68050d2 --- /dev/null +++ b/docview/_testdata/basic/doc.go @@ -0,0 +1,18 @@ +// Package fixture demonstrates documentation extraction. +package fixture + +import "fmt" + +// Identity returns its argument. +func Identity[T any](v T) T { return v } + +// Box stores one value. +type Box[T any] struct { + Value T +} + +// Get returns the stored value. +func (b Box[T]) Get() T { return b.Value } + +// Format formats a value. +func Format[T any](v T) string { return fmt.Sprint(v) } diff --git a/docview/_testdata/basic/extra.go b/docview/_testdata/basic/extra.go new file mode 100644 index 0000000..fdc36d4 --- /dev/null +++ b/docview/_testdata/basic/extra.go @@ -0,0 +1,10 @@ +package fixture + +// Pair contains two values. +type Pair[A, B any] struct { + First A + Second B +} + +// NewPair constructs a Pair. +func NewPair[A, B any](a A, b B) Pair[A, B] { return Pair[A, B]{a, b} } diff --git a/docview/_testdata/basic/want.txt b/docview/_testdata/basic/want.txt new file mode 100644 index 0000000..b2630aa --- /dev/null +++ b/docview/_testdata/basic/want.txt @@ -0,0 +1,9 @@ +package|fixture +doc|Package fixture demonstrates documentation extraction. +import|fmt +type|Box|Box stores one value. +method|Box|Get|Get returns the stored value. +type|Pair|Pair contains two values. +func|Format|Format formats a value. +func|Identity|Identity returns its argument. +factory|Pair|NewPair|NewPair constructs a Pair. diff --git a/docview/docview_test.go b/docview/docview_test.go new file mode 100644 index 0000000..5afe5bc --- /dev/null +++ b/docview/docview_test.go @@ -0,0 +1,103 @@ +package docview + +import ( + "bufio" + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestPackageDocFiles(t *testing.T) { + dir := filepath.Join("_testdata", "basic") + fset := token.NewFileSet() + pkg, err := parser.ParseDir(fset, dir, func(info os.FileInfo) bool { + return strings.HasSuffix(info.Name(), ".go") + }, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + astPkg := pkg["fixture"] + if astPkg == nil { + t.Fatal("fixture package not parsed") + } + doc := NewPackageDoc(astPkg, "fixture", true) + checkDocWant(t, filepath.Join(dir, "want.txt"), doc) +} + +func checkDocWant(t *testing.T, filename string, doc *PackageDoc) { + t.Helper() + f, err := os.Open(filename) + if err != nil { + t.Fatal(err) + } + defer f.Close() + byName := func(name string) *TypeDoc { + for _, typ := range doc.Types { + if typ.Type.Name.Name == name { + return typ + } + } + return nil + } + funcDoc := func(name string) *FuncDoc { + for _, fn := range doc.Funcs { + if fn.Name == name { + return fn + } + } + return nil + } + sc := bufio.NewScanner(f) + for sc.Scan() { + parts := strings.Split(sc.Text(), "|") + if len(parts) < 2 { + t.Fatalf("invalid want line %q", sc.Text()) + } + var found bool + switch parts[0] { + case "package": + found = doc.PackageName == parts[1] + case "doc": + found = strings.TrimSpace(doc.Doc) == parts[1] + case "import": + for _, imp := range doc.Imports { + found = imp == parts[1] || found + } + case "type": + typ := byName(parts[1]) + found = typ != nil && strings.TrimSpace(typ.Doc) == parts[2] + case "method": + typ := byName(parts[1]) + if typ != nil { + for _, method := range typ.Methods { + if method.Name == parts[2] && strings.TrimSpace(method.Doc) == parts[3] { + found = true + } + } + } + case "factory": + typ := byName(parts[1]) + if typ != nil { + for _, fn := range typ.Funcs { + if fn.Name == parts[2] && strings.TrimSpace(fn.Doc) == parts[3] { + found = true + } + } + } + case "func": + fn := funcDoc(parts[1]) + found = fn != nil && strings.TrimSpace(fn.Doc) == parts[2] + default: + t.Fatalf("unknown want category %q", parts[0]) + } + if !found { + t.Fatalf("missing doc entry %q", sc.Text()) + } + } + if err := sc.Err(); err != nil { + t.Fatal(err) + } +} diff --git a/docview/docx.go b/docview/docx.go index 01a9355..1455cfb 100644 --- a/docview/docx.go +++ b/docview/docx.go @@ -31,7 +31,6 @@ type typeDoc struct { // in the respective AST nodes so that they are not printed // twice (once when printing the documentation and once when // printing the corresponding AST node). -// type docReader struct { doc *ast.CommentGroup // package documentation, if any pkgName string @@ -107,6 +106,10 @@ func docBaseTypeName(typ ast.Expr, showAll bool) string { } case *ast.StarExpr: return docBaseTypeName(t.X, showAll) + case *ast.IndexExpr: + return docBaseTypeName(t.X, showAll) + case *ast.IndexListExpr: + return docBaseTypeName(t.X, showAll) } return "" } @@ -285,7 +288,6 @@ var ( // addFile adds the AST for a source file to the docReader. // Adding the same AST multiple times is a no-op. -// func (doc *docReader) addFile(src *ast.File) { // add package documentation if src.Doc != nil { @@ -339,7 +341,6 @@ func NewPackageDoc(pkg *ast.Package, importpath string, showAll bool) *PackageDo // ValueDoc is the documentation for a group of declared // values, either vars or consts. -// type ValueDoc struct { Doc string Decl *ast.GenDecl @@ -393,7 +394,6 @@ func makeValueDocs(list []*ast.GenDecl, tok token.Token) []*ValueDoc { // FuncDoc is the documentation for a func declaration, // either a top-level function or a method function. -// type FuncDoc struct { Doc string Recv ast.Expr // TODO(rsc): Would like string here @@ -520,7 +520,6 @@ func makeBugDocs(list []*ast.CommentGroup) []string { } // PackageDoc is the documentation for an entire package. -// type PackageDoc struct { PackageName string ImportPath string @@ -535,7 +534,6 @@ type PackageDoc struct { } // newDoc returns the accumulated documentation for the package. -// func (doc *docReader) newDoc(importpath string, filenames []string) *PackageDoc { p := new(PackageDoc) p.PackageName = doc.pkgName @@ -658,7 +656,6 @@ func filterTypeDocs(a []*TypeDoc, f Filter) []*TypeDoc { // Filter eliminates documentation for names that don't pass through the filter f. // TODO: Recognize "Type.Method" as a name. -// func (p *PackageDoc) Filter(f Filter) { p.Consts = filterValueDocs(p.Consts, f) p.Vars = filterValueDocs(p.Vars, f) diff --git a/go.mod b/go.mod index 8a8c0b3..cb32bb5 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,17 @@ module github.com/visualfc/gotools -go 1.13 +go 1.25.0 require ( github.com/creack/pty v1.1.21 github.com/pmezard/go-difflib v1.0.0 github.com/visualfc/gomod v0.1.2 github.com/visualfc/goversion v1.1.0 - golang.org/x/tools v0.5.0 + golang.org/x/tools v0.49.0 +) + +require ( + github.com/yuin/goldmark v1.4.13 // indirect + golang.org/x/mod v0.39.0 // indirect + golang.org/x/sync v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 3521c54..3b01e71 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/visualfc/gomod v0.1.2 h1:7qfPmifcA8r/0ZTpTPZQqsm5aJUiQ/EeyHEENPyywDg= @@ -8,36 +10,9 @@ github.com/visualfc/goversion v1.1.0 h1:EN0YQGRkeGoWTPxPNTnbhyNQyas5leKH5U5lL4t8 github.com/visualfc/goversion v1.1.0/go.mod h1:Gr3s6bW8NTomhheImwAttqno97Mw6pAnFn2dU8/EMa8= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= -golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= +golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= diff --git a/goapi/_testdata/generic/methods.go b/goapi/_testdata/generic/methods.go new file mode 100644 index 0000000..9229476 --- /dev/null +++ b/goapi/_testdata/generic/methods.go @@ -0,0 +1,5 @@ +package fixture + +func (b Box[T]) Get() T { return b.Value } // cursor:Get + +func Identity[T comparable](v T) T { return v } diff --git a/goapi/_testdata/generic/types.go b/goapi/_testdata/generic/types.go new file mode 100644 index 0000000..6663b44 --- /dev/null +++ b/goapi/_testdata/generic/types.go @@ -0,0 +1,5 @@ +package fixture + +type Box[T any] struct { + Value T +} diff --git a/goapi/_testdata/generic/want.txt b/goapi/_testdata/generic/want.txt new file mode 100644 index 0000000..b6b484f --- /dev/null +++ b/goapi/_testdata/generic/want.txt @@ -0,0 +1,4 @@ +pkg fixture, func Identity(T) T +pkg fixture, method (Box[T]) Get() T +pkg fixture, type Box struct +pkg fixture, type Box struct, Value T diff --git a/goapi/base_type_go117.go b/goapi/base_type_go117.go new file mode 100644 index 0000000..4e5f4dd --- /dev/null +++ b/goapi/base_type_go117.go @@ -0,0 +1,20 @@ +//go:build !go1.18 +// +build !go1.18 + +package goapi + +import "go/ast" + +func baseTypeName(x ast.Expr) (name string, imported bool) { + switch t := x.(type) { + case *ast.Ident: + return t.Name, false + case *ast.SelectorExpr: + if _, ok := t.X.(*ast.Ident); ok { + return t.Sel.Name, true + } + case *ast.StarExpr: + return baseTypeName(t.X) + } + return +} diff --git a/goapi/base_type_go118.go b/goapi/base_type_go118.go new file mode 100644 index 0000000..c623769 --- /dev/null +++ b/goapi/base_type_go118.go @@ -0,0 +1,24 @@ +//go:build go1.18 +// +build go1.18 + +package goapi + +import "go/ast" + +func baseTypeName(x ast.Expr) (name string, imported bool) { + switch t := x.(type) { + case *ast.Ident: + return t.Name, false + case *ast.SelectorExpr: + if _, ok := t.X.(*ast.Ident); ok { + return t.Sel.Name, true + } + case *ast.StarExpr: + return baseTypeName(t.X) + case *ast.IndexExpr: + return baseTypeName(t.X) + case *ast.IndexListExpr: + return baseTypeName(t.X) + } + return +} diff --git a/goapi/goapi.go b/goapi/goapi.go index 6e05e14..2cd3c0a 100644 --- a/goapi/goapi.go +++ b/goapi/goapi.go @@ -433,7 +433,7 @@ type pkgSymbol struct { symbol string // "RoundTripper" } -//expression kind +// expression kind type Kind int const ( @@ -499,7 +499,7 @@ func (k Kind) String() string { return fmt.Sprint("unknown-kind") } -//expression type +// expression type type TypeInfo struct { Kind Kind Name string @@ -2163,6 +2163,13 @@ func (w *Walker) lookupExpr(vi ast.Expr, p token.Pos) (string, *TypeInfo, error) return w.lookupExpr(v.Index, p) } return w.lookupExpr(v.X, p) + case *ast.IndexListExpr: + for _, index := range v.Indices { + if inRange(index, p) { + return w.lookupExpr(index, p) + } + } + return w.lookupExpr(v.X, p) case *ast.ParenExpr: return w.lookupExpr(v.X, p) case *ast.FuncLit: @@ -2962,6 +2969,11 @@ func (w *Walker) varValueType(vi ast.Expr, index int) (string, error) { return w.varSelectorType(typ[2:], v.Sel.Name) } } + case *ast.IndexListExpr: + typ, err := w.varValueType(st.X, index) + if err == nil && strings.HasPrefix(typ, "[]") { + return w.varSelectorType(typ[2:], v.Sel.Name) + } case *ast.CompositeLit: typ, err := w.varValueType(st.Type, 0) if err == nil { @@ -3113,6 +3125,11 @@ func (w *Walker) varValueType(vi ast.Expr, index int) (string, error) { return w.varFunctionType(typ[2:], ft.Sel.Name, index) } } + case *ast.IndexListExpr: + typ, err := w.varValueType(st.X, index) + if err == nil && strings.HasPrefix(typ, "[]") { + return w.varFunctionType(typ[2:], ft.Sel.Name, index) + } case *ast.TypeAssertExpr: typ := w.nodeString(w.namelessType(st.Type)) typ = strings.TrimLeft(typ, "*") @@ -3168,6 +3185,12 @@ func (w *Walker) varValueType(vi ast.Expr, index int) (string, error) { } } return "", fmt.Errorf("unknown index %v %v %v %v", typ, v.X, index, err) + case *ast.IndexListExpr: + typ, err := w.varValueType(v.X, index) + if err == nil { + return typ, nil + } + return "", fmt.Errorf("unknown index list %v %v", v.X, err) case *ast.SliceExpr: return w.varValueType(v.X, index) case *ast.ChanType: @@ -3502,7 +3525,10 @@ func (w *Walker) interfaceMethods(pkg, iname string) (methods []typeMethod, comp methods = append(methods, m...) complete = complete && c default: - log.Fatalf("unknown type %T in interface field", typ) + // Go 1.18 added type-set expressions to interfaces. They are + // represented as *ast.BinaryExpr (for example, ~int | ~string) + // and do not contribute methods to the exported API. + continue } } return @@ -3541,22 +3567,6 @@ func (w *Walker) walkInterfaceType(name string, t *ast.InterfaceType) { } } -func baseTypeName(x ast.Expr) (name string, imported bool) { - switch t := x.(type) { - case *ast.Ident: - return t.Name, false - case *ast.SelectorExpr: - if _, ok := t.X.(*ast.Ident); ok { - // only possible for qualified type names; - // assume type is imported - return t.Sel.Name, true - } - case *ast.StarExpr: - return baseTypeName(t.X) - } - return -} - func (w *Walker) peekFuncDecl(f *ast.FuncDecl) { var fname = f.Name.Name var recv ast.Expr @@ -3733,13 +3743,12 @@ const goarchList = "386 amd64 arm " // suffix which does not match the current system. // The recognized name formats are: // -// name_$(GOOS).* -// name_$(GOARCH).* -// name_$(GOOS)_$(GOARCH).* -// name_$(GOOS)_test.* -// name_$(GOARCH)_test.* -// name_$(GOOS)_$(GOARCH)_test.* -// +// name_$(GOOS).* +// name_$(GOARCH).* +// name_$(GOOS)_$(GOARCH).* +// name_$(GOOS)_test.* +// name_$(GOARCH)_test.* +// name_$(GOOS)_$(GOARCH)_test.* func isOSArchFile(ctxt *build.Context, name string) bool { if dot := strings.Index(name, "."); dot != -1 { name = name[:dot] diff --git a/goapi/goapi_test.go b/goapi/goapi_test.go new file mode 100644 index 0000000..210fe05 --- /dev/null +++ b/goapi/goapi_test.go @@ -0,0 +1,282 @@ +//go:build go1.23 +// +build go1.23 + +package goapi + +import ( + "bytes" + "go/build" + "go/token" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestGenericConstraintPackage(t *testing.T) { + w := NewWalker() + w.context = &build.Default + w.WalkPackage("cmp") + if p := w.findPackage("cmp"); p == nil { + t.Fatal("cmp package was not loaded") + } +} + +func TestGenericConstraintFeatures(t *testing.T) { + w := NewWalker() + w.context = &build.Default + w.wantedPkg["cmp"] = true + w.WalkPackage("cmp") + features := strings.Join(w.Features(""), "\n") + for _, want := range []string{ + "pkg cmp, func Compare(T, T) int", + "pkg cmp, func Less(T, T) bool", + "pkg cmp, type Ordered interface {}", + } { + if !strings.Contains(features, want) { + t.Errorf("features do not contain %q:\n%s", want, features) + } + } +} + +func TestCompareAPI(t *testing.T) { + tests := []struct { + name string + features []string + required []string + optional []string + except []string + allowNew bool + ok bool + want string + }{ + { + name: "compatible", + features: []string{"pkg p, func F()"}, + required: []string{"pkg p, func F()"}, + ok: true, + }, + { + name: "missing required", + required: []string{"pkg p, func F()"}, + want: "-pkg p, func F()\n", + }, + { + name: "optional addition", + features: []string{"pkg p, func F()"}, + optional: []string{"pkg p, func F()"}, + allowNew: false, + ok: true, + }, + { + name: "exception", + required: []string{"pkg p, func F()"}, + except: []string{"pkg p, func F()"}, + want: "~pkg p, func F()\n", + ok: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got bytes.Buffer + if ok := compareAPI(&got, tt.features, tt.required, tt.optional, tt.except, tt.allowNew); ok != tt.ok { + t.Fatalf("compareAPI ok = %v, want %v", ok, tt.ok) + } + if got.String() != tt.want { + t.Fatalf("compareAPI output = %q, want %q", got.String(), tt.want) + } + }) + } +} + +func TestContextAndFeatureHelpers(t *testing.T) { + c := parseContext("linux-amd64-cgo") + if c.GOOS != "linux" || c.GOARCH != "amd64" || !c.CgoEnabled { + t.Fatalf("parseContext returned %+v", c) + } + if got := contextName(c); got != "linux-amd64-cgo" { + t.Fatalf("contextName = %q", got) + } + if got := featureWithoutContext("func F (linux-amd64), x"); got != "func F, x" { + t.Fatalf("featureWithoutContext = %q", got) + } +} + +func TestCursorInfo(t *testing.T) { + dir := t.TempDir() + src := "package sample\n\nimport \"fmt\"\n\ntype Item struct { Name string }\n\nfunc (i Item) String() string { return i.Name }\n\nfunc Hello() { value := Item{Name: \"hi\"}; fmt.Println(value.Name) }\n" + if err := os.WriteFile(filepath.Join(dir, "sample.go"), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + text string + kind Kind + sym string + }{ + {name: "package", text: "sample", kind: KindPackage, sym: "sample"}, + {name: "import", text: "fmt", kind: KindImport, sym: "fmt"}, + {name: "type", text: "Item", kind: KindStruct, sym: "Item"}, + {name: "field", text: "Name string", kind: KindField, sym: "Item.Name"}, + {name: "method", text: "String()", kind: KindMethod, sym: "Item.String"}, + {name: "function", text: "Hello", kind: KindFunc, sym: "Hello"}, + {name: "local", text: "value :=", kind: KindVar, sym: "value"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pos := strings.Index(src, tt.text) + 1 + w := NewWalker() + w.context = &build.Default + w.cursorInfo = &CursorInfo{pkg: "sample", file: "sample.go", pos: tokenPos(pos)} + w.WalkPackage(dir) + if w.cursorInfo.info == nil { + t.Fatalf("no cursor info for %q", tt.text) + } + if w.cursorInfo.info.Kind != tt.kind || w.cursorInfo.info.Name != tt.sym { + t.Fatalf("cursor info = %s %q, want %s %q", w.cursorInfo.info.Kind, w.cursorInfo.info.Name, tt.kind, tt.sym) + } + }) + } +} + +func TestCursorInfoFromStandardInput(t *testing.T) { + src := "package iter\n\ntype Seq[V any] func(yield func(V) bool)\n" + w := NewWalker() + w.context = &build.Default + w.cursorInfo = &CursorInfo{ + pkg: "iter", + file: "iter.go", + pos: tokenPos(strings.Index(src, "Seq") + 1), + src: []byte(src), + std: true, + } + w.WalkPackage("iter") + if w.cursorInfo.info == nil { + t.Fatal("no cursor info from standard input") + } + if w.cursorInfo.info.Kind != KindType || w.cursorInfo.info.Name != "Seq" { + t.Fatalf("cursor info = %s %q, want type Seq", w.cursorInfo.info.Kind, w.cursorInfo.info.Name) + } +} + +func TestGenericMethodFeaturesAndCursor(t *testing.T) { + dir := t.TempDir() + src := "package generic\n\ntype Box[T any] struct { Value T }\n\nfunc (b Box[T]) Get() T { return b.Value }\n\nfunc Identity[T comparable](v T) T { return v }\n" + if err := os.WriteFile(filepath.Join(dir, "generic.go"), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + + w := NewWalker() + w.context = &build.Default + w.wantedPkg["generic"] = true + w.WalkPackage(dir) + features := strings.Join(w.Features(""), "\n") + for _, want := range []string{ + "pkg generic, func Identity(T) T", + "pkg generic, method (Box[T]) Get() T", + } { + if !strings.Contains(features, want) { + t.Errorf("generic features do not contain %q:\n%s", want, features) + } + } + + pos := strings.Index(src, "Get") + 1 + w = NewWalker() + w.context = &build.Default + w.cursorInfo = &CursorInfo{pkg: "generic", file: "generic.go", pos: tokenPos(pos)} + w.WalkPackage(dir) + if w.cursorInfo.info == nil { + t.Fatal("no cursor info for generic method") + } + if w.cursorInfo.info.Kind != KindMethod || w.cursorInfo.info.Name != "Box.Get" { + t.Fatalf("generic method cursor info = %s %q", w.cursorInfo.info.Kind, w.cursorInfo.info.Name) + } +} + +func TestCursorInfoAcrossFileSet(t *testing.T) { + dir := t.TempDir() + files := map[string]string{ + "types.go": "package split\n\ntype Box[T any] struct { Value T }\n", + "methods.go": "package split\n\nfunc (b Box[T]) Get() T {\n\treturn b.Value\n}\n", + } + for name, src := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + } + + src := files["methods.go"] + pos := strings.Index(src, "Get") + 1 + w := NewWalker() + w.context = &build.Default + w.cursorInfo = &CursorInfo{pkg: "split", file: "methods.go", pos: tokenPos(pos)} + w.WalkPackage(dir) + info := w.cursorInfo.info + if info == nil { + t.Fatal("no cursor info in second file") + } + if info.Kind != KindMethod || info.Name != "Box.Get" { + t.Fatalf("cursor info = %s %q, want method Box.Get", info.Kind, info.Name) + } + if info.T == nil { + t.Fatal("cursor info has no AST position") + } + file := w.fset.File(info.T.Pos()) + if file == nil || filepath.Base(file.Name()) != "methods.go" { + t.Fatalf("AST position file = %v, want methods.go", file) + } + if got := w.fset.Position(info.T.Pos()); got.Line != 3 { + t.Fatalf("method position = %v, want line 3", got) + } +} + +func TestFiles(t *testing.T) { + testFiles(t, "generic") +} + +func testFiles(t *testing.T, name string) { + t.Helper() + dir, err := filepath.Abs(filepath.Join("_testdata", name)) + if err != nil { + t.Fatal(err) + } + w := NewWalker() + w.context = &build.Default + w.wantedPkg["fixture"] = true + w.WalkPackage(dir) + + wantBytes, err := os.ReadFile(filepath.Join(dir, "want.txt")) + if err != nil { + t.Fatal(err) + } + want := strings.TrimSpace(string(wantBytes)) + got := strings.TrimSpace(strings.Join(w.Features(""), "\n")) + if got != want { + t.Fatalf("API features mismatch:\n got:\n%s\nwant:\n%s", got, want) + } + + src, err := os.ReadFile(filepath.Join(dir, "methods.go")) + if err != nil { + t.Fatal(err) + } + marker := "// cursor:Get" + markerPos := strings.Index(string(src), marker) + if markerPos < 0 { + t.Fatalf("missing cursor marker %q", marker) + } + w = NewWalker() + w.context = &build.Default + w.cursorInfo = &CursorInfo{ + pkg: "fixture", + file: "methods.go", + pos: tokenPos(strings.Index(string(src[:markerPos]), "Get") + 1), + } + w.WalkPackage(dir) + if w.cursorInfo.info == nil || w.cursorInfo.info.Kind != KindMethod || w.cursorInfo.info.Name != "Box.Get" { + t.Fatalf("cursor info = %#v, want method Box.Get", w.cursorInfo.info) + } +} + +// tokenPos keeps cursor offsets in the same representation used by runApi. +func tokenPos(pos int) token.Pos { return token.Pos(pos) } diff --git a/gofmt/_testdata/fiximports/input.go b/gofmt/_testdata/fiximports/input.go new file mode 100644 index 0000000..3a93c4c --- /dev/null +++ b/gofmt/_testdata/fiximports/input.go @@ -0,0 +1,11 @@ +package fixture + + + +import "strings" + + + +func Trim(s string) string { + return strings.TrimSpace(s) +} diff --git a/gofmt/_testdata/fiximports/want.txt b/gofmt/_testdata/fiximports/want.txt new file mode 100644 index 0000000..07f434d --- /dev/null +++ b/gofmt/_testdata/fiximports/want.txt @@ -0,0 +1,7 @@ +package fixture + +import "strings" + +func Trim(s string) string { + return strings.TrimSpace(s) +} diff --git a/gofmt/_testdata/nonstd/input.go b/gofmt/_testdata/nonstd/input.go new file mode 100644 index 0000000..8c4d6c3 --- /dev/null +++ b/gofmt/_testdata/nonstd/input.go @@ -0,0 +1,7 @@ +package fixture + +import "fmt" + +func Use() { + fmt.Println(gomod.Module{}) +} diff --git a/gofmt/_testdata/nonstd/want.txt b/gofmt/_testdata/nonstd/want.txt new file mode 100644 index 0000000..389d783 --- /dev/null +++ b/gofmt/_testdata/nonstd/want.txt @@ -0,0 +1,11 @@ +package fixture + +import ( + "fmt" + + "github.com/visualfc/gotools/pkg/gomod" +) + +func Use() { + fmt.Println(gomod.Module{}) +} diff --git a/gofmt/_testdata/nonstd_existing/input.go b/gofmt/_testdata/nonstd_existing/input.go new file mode 100644 index 0000000..1f1f6f2 --- /dev/null +++ b/gofmt/_testdata/nonstd_existing/input.go @@ -0,0 +1,14 @@ +package fixture + +import ( + "fmt" + "github.com/visualfc/gotools/pkg/godiff" + "github.com/visualfc/gotools/pkg/gomod" +) + +func Use() string { + _ = gomod.Module{} + _ = pkgutil.IsVendorExperiment() + fmt.Print("") + return godiff.UnifiedDiffString("a\n", "b\n") +} diff --git a/gofmt/_testdata/nonstd_existing/want.txt b/gofmt/_testdata/nonstd_existing/want.txt new file mode 100644 index 0000000..6f040ef --- /dev/null +++ b/gofmt/_testdata/nonstd_existing/want.txt @@ -0,0 +1,16 @@ +package fixture + +import ( + "fmt" + + "github.com/visualfc/gotools/pkg/godiff" + "github.com/visualfc/gotools/pkg/gomod" + "github.com/visualfc/gotools/pkg/pkgutil" +) + +func Use() string { + _ = gomod.Module{} + _ = pkgutil.IsVendorExperiment() + fmt.Print("") + return godiff.UnifiedDiffString("a\n", "b\n") +} diff --git a/gofmt/gofmt.go b/gofmt/gofmt.go index 84c0524..a65fd2e 100644 --- a/gofmt/gofmt.go +++ b/gofmt/gofmt.go @@ -7,6 +7,8 @@ package gofmt import ( "bytes" "fmt" + "go/ast" + "go/parser" "go/token" "io" "io/ioutil" @@ -43,7 +45,7 @@ var ( gofmtTabIndent bool ) -//func init +// func init func init() { Command.Flag.BoolVar(&gofmtList, "l", false, "list files whose formatting differs from goimport's") Command.Flag.BoolVar(&gofmtWrite, "w", false, "write result to (source) file instead of stdout") @@ -129,6 +131,9 @@ func processFile(filename string, in io.Reader, out io.Writer, stdin bool) error if err != nil { return err } + if gofmtFixImports { + res = collapseBlankLines(res) + } if !bytes.Equal(src, res) { // formatting has changed @@ -166,6 +171,27 @@ func processFile(filename string, in io.Reader, out io.Writer, stdin bool) error return err } +func collapseBlankLines(src []byte) []byte { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "", src, parser.ParseComments) + if err != nil || len(file.Decls) == 0 { + return src + } + limit := len(src) + for _, decl := range file.Decls { + if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.IMPORT { + continue + } + limit = fset.Position(decl.Pos()).Offset + break + } + prefix := src[:limit] + for bytes.Contains(prefix, []byte("\n\n\n")) { + prefix = bytes.ReplaceAll(prefix, []byte("\n\n\n"), []byte("\n\n")) + } + return append(prefix, src[limit:]...) +} + func visitFile(path string, f os.FileInfo, err error) error { if err == nil && isGoFile(f) { err = processFile(path, nil, os.Stdout, false) diff --git a/gofmt/gofmt_test.go b/gofmt/gofmt_test.go new file mode 100644 index 0000000..f76d155 --- /dev/null +++ b/gofmt/gofmt_test.go @@ -0,0 +1,51 @@ +package gofmt + +import ( + "bytes" + "os" + "strings" + "testing" + + "golang.org/x/tools/imports" +) + +func TestFixImportsFile(t *testing.T) { + testFixImportsCase(t, "fiximports") + testFixImportsCase(t, "nonstd") + testFixImportsCase(t, "nonstd_existing") +} + +func testFixImportsCase(t *testing.T, name string) { + t.Helper() + src, err := os.ReadFile("_testdata/" + name + "/input.go") + if err != nil { + t.Fatal(err) + } + want, err := os.ReadFile("_testdata/" + name + "/want.txt") + if err != nil { + t.Fatal(err) + } + oldFix, oldOptions := gofmtFixImports, options + defer func() { gofmtFixImports, options = oldFix, oldOptions }() + gofmtFixImports = true + options = &imports.Options{FormatOnly: false, TabWidth: 8, TabIndent: true, Comments: true, Fragment: true} + var out bytes.Buffer + if err := processFile("_testdata/"+name+"/input.go", bytes.NewReader(src), &out, false); err != nil { + t.Fatal(err) + } + if !bytes.Equal(out.Bytes(), want) { + t.Fatalf("fiximports output mismatch\n got:\n%s\nwant:\n%s", out.Bytes(), want) + } + var again bytes.Buffer + if err := processFile("_testdata/"+name+"/input.go", bytes.NewReader(out.Bytes()), &again, false); err != nil { + t.Fatal(err) + } + if !bytes.Equal(again.Bytes(), out.Bytes()) { + t.Fatalf("fiximports is not idempotent\n first:\n%s\nsecond:\n%s", out.Bytes(), again.Bytes()) + } + for _, line := range strings.Split(string(out.Bytes()), "\n") { + if strings.TrimRight(line, " \t") != line { + t.Fatalf("fiximports left trailing whitespace in %q", line) + } + } +} diff --git a/pkg/pkgwalk/pkgwalk.go b/pkg/pkgwalk/pkgwalk.go index 9d28132..a1ecb86 100644 --- a/pkg/pkgwalk/pkgwalk.go +++ b/pkg/pkgwalk/pkgwalk.go @@ -81,7 +81,10 @@ var builtinInfoMap = map[string]string{ "delete": "func delete(m map[Type]Type1, key Type)", "len": "func len(v Type) int", "cap": "func cap(v Type) int", + "clear": "func clear[T ~[]Type | ~map[Type]Type | ~chan Type](x T)", "make": "func make(Type, size IntegerType) Type", + "max": "func max[T cmp.Ordered](x T, y ...T) T", + "min": "func min[T cmp.Ordered](x T, y ...T) T", "new": "func new(Type) *Type", "complex": "func complex(r, i FloatType) ComplexType", "real": "func real(c ComplexType) FloatType", diff --git a/pkg/stdlib/pkglist.go b/pkg/stdlib/pkglist.go index 846b65e..2144091 100644 --- a/pkg/stdlib/pkglist.go +++ b/pkg/stdlib/pkglist.go @@ -4,66 +4,95 @@ package stdlib var Packages = []string{ "archive/tar", "archive/zip", "bufio", "bytes", - "compress/bzip2", "compress/flate", "compress/gzip", "compress/lzw", - "compress/zlib", "container/heap", "container/list", "container/ring", - "context", "crypto", "crypto/aes", "crypto/cipher", - "crypto/des", "crypto/dsa", "crypto/ecdh", "crypto/ecdsa", - "crypto/ed25519", "crypto/elliptic", "crypto/hmac", "crypto/internal/alias", - "crypto/internal/bigmod", "crypto/internal/boring", "crypto/internal/boring/bbig", "crypto/internal/boring/bcache", - "crypto/internal/boring/sig", "crypto/internal/edwards25519", "crypto/internal/edwards25519/field", "crypto/internal/nistec", - "crypto/internal/nistec/fiat", "crypto/internal/randutil", "crypto/md5", "crypto/rand", - "crypto/rc4", "crypto/rsa", "crypto/sha1", "crypto/sha256", - "crypto/sha512", "crypto/subtle", "crypto/tls", "crypto/x509", - "crypto/x509/internal/macos", "crypto/x509/pkix", "database/sql", "database/sql/driver", + "cmp", "compress/bzip2", "compress/flate", "compress/gzip", + "compress/lzw", "compress/zlib", "container/heap", "container/list", + "container/ring", "context", "crypto", "crypto/aes", + "crypto/cipher", "crypto/des", "crypto/dsa", "crypto/ecdh", + "crypto/ecdsa", "crypto/ed25519", "crypto/elliptic", "crypto/fips140", + "crypto/hkdf", "crypto/hmac", "crypto/hpke", "crypto/internal/boring", + "crypto/internal/boring/bbig", "crypto/internal/boring/bcache", "crypto/internal/boring/sig", "crypto/internal/constanttime", + "crypto/internal/cryptotest", "crypto/internal/cryptotest/wycheproof", "crypto/internal/cryptotest/x509limbo", "crypto/internal/entropy", + "crypto/internal/entropy/v1.0.0", "crypto/internal/fips140", "crypto/internal/fips140/aes", "crypto/internal/fips140/aes/gcm", + "crypto/internal/fips140/alias", "crypto/internal/fips140/bigmod", "crypto/internal/fips140/check", "crypto/internal/fips140/check/checktest", + "crypto/internal/fips140/drbg", "crypto/internal/fips140/ecdh", "crypto/internal/fips140/ecdsa", "crypto/internal/fips140/ed25519", + "crypto/internal/fips140/edwards25519", "crypto/internal/fips140/edwards25519/field", "crypto/internal/fips140/hkdf", "crypto/internal/fips140/hmac", + "crypto/internal/fips140/mldsa", "crypto/internal/fips140/mlkem", "crypto/internal/fips140/nistec", "crypto/internal/fips140/nistec/fiat", + "crypto/internal/fips140/pbkdf2", "crypto/internal/fips140/rsa", "crypto/internal/fips140/sha256", "crypto/internal/fips140/sha3", + "crypto/internal/fips140/sha512", "crypto/internal/fips140/ssh", "crypto/internal/fips140/subtle", "crypto/internal/fips140/tls12", + "crypto/internal/fips140/tls13", "crypto/internal/fips140cache", "crypto/internal/fips140deps", "crypto/internal/fips140deps/byteorder", + "crypto/internal/fips140deps/cpu", "crypto/internal/fips140deps/godebug", "crypto/internal/fips140deps/time", "crypto/internal/fips140hash", + "crypto/internal/fips140only", "crypto/internal/fips140test", "crypto/internal/impl", "crypto/internal/rand", + "crypto/internal/randutil", "crypto/internal/sysrand", "crypto/internal/sysrand/internal/seccomp", "crypto/md5", + "crypto/mldsa", "crypto/mlkem", "crypto/mlkem/mlkemtest", "crypto/pbkdf2", + "crypto/rand", "crypto/rc4", "crypto/rsa", "crypto/sha1", + "crypto/sha256", "crypto/sha3", "crypto/sha512", "crypto/subtle", + "crypto/tls", "crypto/tls/internal/fips140tls", "crypto/x509", "crypto/x509/internal/macos", + "crypto/x509/pkix", "database/sql", "database/sql/driver", "database/sql/internal", "debug/buildinfo", "debug/dwarf", "debug/elf", "debug/gosym", "debug/macho", "debug/pe", "debug/plan9obj", "embed", "embed/internal/embedtest", "encoding", "encoding/ascii85", "encoding/asn1", "encoding/base32", "encoding/base64", "encoding/binary", "encoding/csv", - "encoding/gob", "encoding/hex", "encoding/json", "encoding/pem", - "encoding/xml", "errors", "expvar", "flag", - "fmt", "go/ast", "go/build", "go/build/constraint", - "go/constant", "go/doc", "go/doc/comment", "go/format", - "go/importer", "go/internal/gccgoimporter", "go/internal/gcimporter", "go/internal/srcimporter", - "go/internal/typeparams", "go/parser", "go/printer", "go/scanner", - "go/token", "go/types", "hash", "hash/adler32", - "hash/crc32", "hash/crc64", "hash/fnv", "hash/maphash", - "html", "html/template", "image", "image/color", - "image/color/palette", "image/draw", "image/gif", "image/internal/imageutil", - "image/jpeg", "image/png", "index/suffixarray", "internal/abi", - "internal/buildcfg", "internal/bytealg", "internal/cfg", "internal/coverage", - "internal/coverage/calloc", "internal/coverage/cformat", "internal/coverage/cmerge", "internal/coverage/decodecounter", - "internal/coverage/decodemeta", "internal/coverage/encodecounter", "internal/coverage/encodemeta", "internal/coverage/pods", - "internal/coverage/rtcov", "internal/coverage/slicereader", "internal/coverage/slicewriter", "internal/coverage/stringtab", - "internal/coverage/test", "internal/coverage/uleb128", "internal/cpu", "internal/dag", - "internal/diff", "internal/fmtsort", "internal/fuzz", "internal/goarch", - "internal/godebug", "internal/goexperiment", "internal/goos", "internal/goroot", - "internal/goversion", "internal/intern", "internal/itoa", "internal/lazyregexp", - "internal/lazytemplate", "internal/nettrace", "internal/obscuretestdata", "internal/oserror", - "internal/pkgbits", "internal/platform", "internal/poll", "internal/profile", - "internal/race", "internal/reflectlite", "internal/safefilepath", "internal/saferio", - "internal/singleflight", "internal/syscall/execenv", "internal/syscall/unix", "internal/sysinfo", - "internal/testenv", "internal/testlog", "internal/testpty", "internal/trace", - "internal/txtar", "internal/types/errors", "internal/unsafeheader", "internal/xcoff", - "io", "io/fs", "io/ioutil", "log", - "log/syslog", "math", "math/big", "math/bits", - "math/cmplx", "math/rand", "mime", "mime/multipart", - "mime/quotedprintable", "net", "net/http", "net/http/cgi", - "net/http/cookiejar", "net/http/fcgi", "net/http/httptest", "net/http/httptrace", - "net/http/httputil", "net/http/internal", "net/http/internal/ascii", "net/http/internal/testcert", - "net/http/pprof", "net/internal/socktest", "net/mail", "net/netip", - "net/rpc", "net/rpc/jsonrpc", "net/smtp", "net/textproto", - "net/url", "os", "os/exec", "os/exec/internal/fdtest", - "os/signal", "os/user", "path", "path/filepath", - "plugin", "reflect", "reflect/internal/example1", "reflect/internal/example2", - "regexp", "regexp/syntax", "runtime", "runtime/cgo", - "runtime/coverage", "runtime/debug", "runtime/internal/atomic", "runtime/internal/math", - "runtime/internal/startlinetest", "runtime/internal/sys", "runtime/metrics", "runtime/pprof", - "runtime/race", "runtime/race/internal/amd64v1", "runtime/trace", "sort", - "strconv", "strings", "sync", "sync/atomic", - "syscall", "testing", "testing/fstest", "testing/internal/testdeps", - "testing/iotest", "testing/quick", "text/scanner", "text/tabwriter", - "text/template", "text/template/parse", "time", "time/tzdata", - "unicode", "unicode/utf16", "unicode/utf8", "unsafe"} + "encoding/gob", "encoding/hex", "encoding/json", "encoding/json/internal", + "encoding/json/internal/jsonflags", "encoding/json/internal/jsonopts", "encoding/json/internal/jsontest", "encoding/json/internal/jsonwire", + "encoding/json/jsontext", "encoding/json/v2", "encoding/pem", "encoding/xml", + "errors", "expvar", "flag", "fmt", + "go/ast", "go/build", "go/build/constraint", "go/constant", + "go/doc", "go/doc/comment", "go/format", "go/importer", + "go/internal/gccgoimporter", "go/internal/gcimporter", "go/internal/srcimporter", "go/parser", + "go/printer", "go/scanner", "go/token", "go/types", + "go/version", "hash", "hash/adler32", "hash/crc32", + "hash/crc64", "hash/fnv", "hash/maphash", "html", + "html/template", "image", "image/color", "image/color/palette", + "image/draw", "image/gif", "image/internal/imageutil", "image/jpeg", + "image/png", "index/suffixarray", "internal/abi", "internal/asan", + "internal/bisect", "internal/buildcfg", "internal/bytealg", "internal/byteorder", + "internal/cfg", "internal/chacha8rand", "internal/copyright", "internal/coverage", + "internal/coverage/calloc", "internal/coverage/cfile", "internal/coverage/cformat", "internal/coverage/cmerge", + "internal/coverage/decodecounter", "internal/coverage/decodemeta", "internal/coverage/encodecounter", "internal/coverage/encodemeta", + "internal/coverage/pods", "internal/coverage/rtcov", "internal/coverage/slicereader", "internal/coverage/slicewriter", + "internal/coverage/stringtab", "internal/coverage/test", "internal/coverage/uleb128", "internal/cpu", + "internal/dag", "internal/diff", "internal/exportdata", "internal/filepathlite", + "internal/fmtsort", "internal/fuzz", "internal/gate", "internal/goarch", + "internal/godebug", "internal/godebugs", "internal/goexperiment", "internal/goos", + "internal/goroot", "internal/gover", "internal/goversion", "internal/lazyregexp", + "internal/lazytemplate", "internal/msan", "internal/nettest", "internal/nettrace", + "internal/obscuretestdata", "internal/oserror", "internal/pkgbits", "internal/platform", + "internal/poll", "internal/profile", "internal/profilerecord", "internal/race", + "internal/reflectlite", "internal/routebsd", "internal/runtime/atomic", "internal/runtime/cgobench", + "internal/runtime/cgroup", "internal/runtime/exithook", "internal/runtime/gc", "internal/runtime/gc/internal/gen", + "internal/runtime/gc/scan", "internal/runtime/maps", "internal/runtime/math", "internal/runtime/pprof/label", + "internal/runtime/sys", "internal/runtime/wasitest", "internal/saferio", "internal/singleflight", + "internal/strconv", "internal/stringslite", "internal/sync", "internal/synctest", + "internal/syscall/execenv", "internal/syscall/unix", "internal/sysinfo", "internal/syslist", + "internal/testenv", "internal/testhash", "internal/testlog", "internal/testpty", + "internal/trace", "internal/trace/internal/testgen", "internal/trace/internal/tracev1", "internal/trace/raw", + "internal/trace/testtrace", "internal/trace/tracev2", "internal/trace/traceviewer", "internal/trace/traceviewer/format", + "internal/trace/version", "internal/txtar", "internal/types/errors", "internal/unsafeheader", + "internal/xcoff", "internal/zstd", "io", "io/fs", + "io/ioutil", "iter", "log", "log/internal", + "log/slog", "log/slog/internal", "log/slog/internal/benchmarks", "log/slog/internal/buffer", + "log/syslog", "maps", "math", "math/big", + "math/big/internal/asmgen", "math/bits", "math/cmplx", "math/rand", + "math/rand/v2", "mime", "mime/multipart", "mime/quotedprintable", + "net", "net/http", "net/http/cgi", "net/http/cookiejar", + "net/http/fcgi", "net/http/httptest", "net/http/httptrace", "net/http/httputil", + "net/http/internal", "net/http/internal/ascii", "net/http/internal/http2", "net/http/internal/httpcommon", + "net/http/internal/httpsfv", "net/http/internal/testcert", "net/http/pprof", "net/internal/cgotest", + "net/internal/socktest", "net/mail", "net/netip", "net/rpc", + "net/rpc/jsonrpc", "net/smtp", "net/textproto", "net/url", + "os", "os/exec", "os/exec/internal/fdtest", "os/signal", + "os/user", "path", "path/filepath", "plugin", + "reflect", "reflect/internal/example1", "reflect/internal/example2", "regexp", + "regexp/syntax", "runtime", "runtime/cgo", "runtime/coverage", + "runtime/debug", "runtime/metrics", "runtime/pprof", "runtime/race", + "runtime/trace", "slices", "sort", "strconv", + "strings", "structs", "sync", "sync/atomic", + "syscall", "testing", "testing/cryptotest", "testing/fstest", + "testing/internal/testdeps", "testing/iotest", "testing/quick", "testing/slogtest", + "testing/synctest", "text/scanner", "text/tabwriter", "text/template", + "text/template/parse", "time", "time/tzdata", "unicode", + "unicode/utf16", "unicode/utf8", "unique", "unsafe", + "uuid", "weak"} func IsStdPkg(pkg string) bool { for _, v := range Packages { diff --git a/pkg/stdlib/pkglist_test.go b/pkg/stdlib/pkglist_test.go new file mode 100644 index 0000000..050b4ae --- /dev/null +++ b/pkg/stdlib/pkglist_test.go @@ -0,0 +1,24 @@ +//go:build go1.23 +// +build go1.23 + +package stdlib + +import "testing" + +func TestGo123AndLaterPackages(t *testing.T) { + for _, pkg := range []string{ + "cmp", + "encoding/json/v2", + "iter", + "log/slog", + "math/rand/v2", + "slices", + "testing/synctest", + "unique", + "weak", + } { + if !IsStdPkg(pkg) { + t.Errorf("IsStdPkg(%q) = false", pkg) + } + } +} diff --git a/types/_testdata/generic/methods.go b/types/_testdata/generic/methods.go new file mode 100644 index 0000000..d575719 --- /dev/null +++ b/types/_testdata/generic/methods.go @@ -0,0 +1,23 @@ +package fixture + +func (b Box[T]) Get() T { return b.Value } + +func Identity[T comparable](v T) T { return v } + +func Map[T, U any](values []T, fn func(T) U) []U { + out := make([]U, len(values)) + for i, value := range values { + out[i] = fn(value) + } + return out +} + +func Sum[T Numeric](a, b T) T { return a + b } + +func Use() int { // cursor:Use + b := Box[int]{Value: 1} + p := Pair[string, int]{First: "x", Second: b.Get()} + values := Slice[int]{p.Second} + result := Map(values, func(v int) int { return Identity(v) }) + return int(Sum(p.Second, len(result))) +} diff --git a/types/_testdata/generic/types.go b/types/_testdata/generic/types.go new file mode 100644 index 0000000..6a6c29a --- /dev/null +++ b/types/_testdata/generic/types.go @@ -0,0 +1,16 @@ +package fixture + +type Box[T any] struct { + Value T +} + +type Pair[A, B any] struct { + First A + Second B +} + +type Numeric interface { + ~int | ~int64 +} + +type Slice[T any] []T diff --git a/types/_testdata/generic/want.txt b/types/_testdata/generic/want.txt new file mode 100644 index 0000000..b0b9783 --- /dev/null +++ b/types/_testdata/generic/want.txt @@ -0,0 +1,20 @@ +defs|types.go:3:6|Box +defs|types.go:7:6|Pair +defs|types.go:12:6|Numeric +defs|types.go:16:6|Slice +defs|methods.go:5:6|Identity +defs|methods.go:7:6|Map +defs|methods.go:15:6|Sum +uses|methods.go:18:7|Box +uses|methods.go:19:7|Pair +uses|methods.go:20:12|Slice +uses|methods.go:21:12|Map +uses|methods.go:22:13|Sum +info|methods.go:18:7|github.com/visualfc/gotools/types/_testdata/generic.Box[int] +info|methods.go:20:12|github.com/visualfc/gotools/types/_testdata/generic.Slice[int] +instances|methods.go:18:7|Box|[int] +instances|methods.go:19:7|Pair|[string, int] +instances|methods.go:20:12|Slice|[int] +instances|methods.go:21:12|Map|[int, int] +instances|methods.go:21:49|Identity|[int] +instances|methods.go:22:13|Sum|[int] diff --git a/types/_testdata/syntax/types.go b/types/_testdata/syntax/types.go new file mode 100644 index 0000000..006c7f6 --- /dev/null +++ b/types/_testdata/syntax/types.go @@ -0,0 +1,40 @@ +package fixture + +import "errors" + +const ( + First = iota + Second +) + +type Alias = string +type Number int + +type Reader interface { + Read([]byte) (int, error) +} + +type Embedded interface { + Reader + Close() error +} + +type Record struct { + Name Alias + Values []Number + Next *Record +} + +func (r Record) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, errors.New("empty") + } + return len(p), nil +} + +func (r *Record) Close() error { + r.Next = nil + return nil +} + +var Global = map[string]Number{} diff --git a/types/_testdata/syntax/usage.go b/types/_testdata/syntax/usage.go new file mode 100644 index 0000000..7becb0d --- /dev/null +++ b/types/_testdata/syntax/usage.go @@ -0,0 +1,30 @@ +package fixture + +func Use(ch chan<- Number) (Number, error) { + r := Record{Name: "x", Values: []Number{1, 2, 3}} + total := Number(0) +outer: + for i, value := range r.Values { + switch { + case i == 1: + continue + case value > 2: + break outer + default: + total += value + } + } + defer func() { Global[r.Name] = total }() + select { + case ch <- total: + default: + } + return total, nil +} + +func Builtins() int { + values := make([]int, 0) + values = append(values, 1) + clear(values) + return min(max(len(values), 1), 2) +} diff --git a/types/_testdata/syntax/want.txt b/types/_testdata/syntax/want.txt new file mode 100644 index 0000000..7da03cf --- /dev/null +++ b/types/_testdata/syntax/want.txt @@ -0,0 +1,23 @@ +defs|types.go:10:6|Alias +defs|types.go:11:6|Number +defs|types.go:13:6|Reader +defs|types.go:17:6|Embedded +defs|types.go:22:6|Record +defs|types.go:40:5|Global +uses|usage.go:4:7|Record +uses|usage.go:3:20|Number +uses|usage.go:3:29|Number +uses|usage.go:4:35|Number +uses|usage.go:17:17|Global +uses|types.go:25:10|Record +uses|types.go:40:25|Number +types|usage.go:4:7 +info|usage.go:4:7|github.com/visualfc/gotools/types/_testdata/syntax.Record +selections|usage.go:7:26|Values +selections|usage.go:17:26|Name +builtins|usage.go:26:12|make +builtins|usage.go:27:11|append +builtins|usage.go:28:2|clear +builtins|usage.go:29:9|min +builtins|usage.go:29:13|max +builtins|usage.go:29:17|len diff --git a/types/go123_test.go b/types/go123_test.go new file mode 100644 index 0000000..60751de --- /dev/null +++ b/types/go123_test.go @@ -0,0 +1,17 @@ +//go:build go1.23 +// +build go1.23 + +package types + +import ( + "go/build" + "testing" +) + +func TestGo123StdlibTypeCheck(t *testing.T) { + w := NewPkgWalker(&build.Default) + conf := DefaultPkgConfig() + if _, _, err := w.Check("iter", conf, nil); err != nil { + t.Fatalf("type checking iter: %v", err) + } +} diff --git a/types/syntax_test.go b/types/syntax_test.go new file mode 100644 index 0000000..620d9f7 --- /dev/null +++ b/types/syntax_test.go @@ -0,0 +1,187 @@ +//go:build go1.18 +// +build go1.18 + +package types + +import ( + "bytes" + "go/ast" + "go/build" + "go/token" + "go/types" + "os" + "path/filepath" + "strconv" + "strings" + "testing" +) + +func TestFiles(t *testing.T) { + for _, name := range []string{"syntax", "generic"} { + t.Run(name, func(t *testing.T) { testFiles(t, name) }) + } +} + +func testFiles(t *testing.T, name string) { + dir, err := filepath.Abs(filepath.Join("_testdata", name)) + if err != nil { + t.Fatal(err) + } + w := NewPkgWalker(&build.Default) + var out bytes.Buffer + w.SetOutput(&out, &out) + conf := DefaultPkgConfig() + pkg, conf, err := w.Check(dir, conf, nil) + if err != nil { + t.Fatalf("%s package check: %v", name, err) + } + if pkg == nil || pkg.Name() != "fixture" { + t.Fatalf("checked package = %v", pkg) + } + if len(conf.Files) == 0 { + t.Fatal("parsed files = 0") + } + if len(conf.Info.Defs) == 0 || len(conf.Info.Uses) == 0 { + t.Fatalf("type info too small: defs=%d uses=%d", len(conf.Info.Defs), len(conf.Info.Uses)) + } + if len(conf.Info.Types) == 0 || len(conf.Info.Scopes) == 0 { + t.Fatalf("missing expression type/scope information") + } + checkWant(t, filepath.Join(dir, "want.txt"), w.FileSet, conf) +} + +func TestBuiltinInfoMap(t *testing.T) { + for _, name := range types.Universe.Names() { + if _, ok := types.Universe.Lookup(name).(*types.Builtin); !ok { + continue + } + if _, ok := builtinInfoMap[name]; !ok { + t.Errorf("builtinInfoMap missing %q", name) + } + } +} + +func checkWant(t *testing.T, filename string, fset *token.FileSet, conf *PkgConfig) { + t.Helper() + data, err := os.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { + parts := strings.Split(line, "|") + if len(parts) < 2 || len(parts) > 4 { + t.Fatalf("invalid want line %q", line) + } + category := parts[0] + switch category { + case "def": + category = "defs" + case "use": + category = "uses" + case "instance": + category = "instances" + } + file, lineNo, colNo := parseWantPosition(t, parts[1]) + wantPos := token.Pos(0) + var name string + if len(parts) >= 3 { + name = parts[2] + } + match := func(pos token.Pos) bool { + p := fset.Position(pos) + return filepath.Base(p.Filename) == file && p.Line == lineNo && p.Column == colNo + } + switch category { + case "defs": + for ident := range conf.Info.Defs { + if ident.Name == name && match(ident.Pos()) { + wantPos = ident.Pos() + break + } + } + case "uses": + for ident := range conf.Info.Uses { + if ident.Name == name && match(ident.Pos()) { + wantPos = ident.Pos() + break + } + } + case "types": + for expr := range conf.Info.Types { + if match(expr.Pos()) { + wantPos = expr.Pos() + break + } + } + case "info": + for expr, typeAndValue := range conf.Info.Types { + if match(expr.Pos()) && (len(parts) < 3 || types.TypeString(typeAndValue.Type, nil) == parts[2]) { + wantPos = expr.Pos() + break + } + } + case "selections": + for expr := range conf.Info.Selections { + if expr.Sel.Name == name && match(expr.Sel.Pos()) { + wantPos = expr.Sel.Pos() + break + } + } + case "instances": + for ident := range conf.Info.Instances { + instance := conf.Info.Instances[ident] + argsMatch := len(parts) < 4 || typeArgsString(instance.TypeArgs) == parts[3] + if ident.Name == name && match(ident.Pos()) && argsMatch { + wantPos = ident.Pos() + break + } + } + case "builtins": + for _, fileAST := range conf.Files { + ast.Inspect(fileAST, func(node ast.Node) bool { + ident, ok := node.(*ast.Ident) + if ok && ident.Name == name && match(ident.Pos()) { + if _, builtin := types.Universe.Lookup(name).(*types.Builtin); builtin { + wantPos = ident.Pos() + } + return false + } + return true + }) + } + default: + t.Fatalf("unknown want category %q", category) + } + if wantPos == token.NoPos { + t.Fatalf("missing %s entry %s at %s:%d:%d", parts[0], name, file, lineNo, colNo) + } + } +} + +func typeArgsString(args *types.TypeList) string { + if args == nil { + return "[]" + } + items := make([]string, args.Len()) + for i := range items { + items[i] = types.TypeString(args.At(i), nil) + } + return "[" + strings.Join(items, ", ") + "]" +} + +func parseWantPosition(t *testing.T, value string) (string, int, int) { + t.Helper() + parts := strings.Split(value, ":") + if len(parts) != 3 { + t.Fatalf("invalid want position %q", value) + } + lineNo, err := strconv.Atoi(parts[1]) + if err != nil { + t.Fatal(err) + } + colNo, err := strconv.Atoi(parts[2]) + if err != nil { + t.Fatal(err) + } + return parts[0], lineNo, colNo +} diff --git a/types/types.go b/types/types.go index 0089761..26402ce 100644 --- a/types/types.go +++ b/types/types.go @@ -120,7 +120,10 @@ var builtinInfoMap = map[string]string{ "delete": "func delete(m map[Type]Type1, key Type)", "len": "func len(v Type) int", "cap": "func cap(v Type) int", + "clear": "func clear[T ~[]Type | ~map[Type]Type | ~chan Type](x T)", "make": "func make(Type, size IntegerType) Type", + "max": "func max[T cmp.Ordered](x T, y ...T) T", + "min": "func min[T cmp.Ordered](x T, y ...T) T", "new": "func new(Type) *Type", "complex": "func complex(r, i FloatType) ComplexType", "real": "func real(c ComplexType) FloatType", From b7edb185ab99ce99bce88eb817efda60a9533209 Mon Sep 17 00:00:00 2001 From: visualfc Date: Sat, 29 Aug 2026 17:44:47 +0800 Subject: [PATCH 2/6] fix cross-platform golden tests --- astview/astview_test.go | 4 +++- goapi/goapi_test.go | 2 +- types/syntax_test.go | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/astview/astview_test.go b/astview/astview_test.go index 37e3197..60d3675 100644 --- a/astview/astview_test.go +++ b/astview/astview_test.go @@ -31,7 +31,9 @@ func TestFiles(t *testing.T) { if err != nil { t.Fatal(err) } - if strings.TrimSpace(got.String()) != strings.TrimSpace(string(want)) { + gotText := strings.ReplaceAll(strings.TrimSpace(got.String()), "\\", "/") + wantText := strings.TrimSpace(strings.ReplaceAll(string(want), "\\", "/")) + if gotText != wantText { t.Fatalf("file tree mismatch\n got:\n%s\nwant:\n%s", got.String(), want) } } diff --git a/goapi/goapi_test.go b/goapi/goapi_test.go index 210fe05..a6f0a5f 100644 --- a/goapi/goapi_test.go +++ b/goapi/goapi_test.go @@ -250,7 +250,7 @@ func testFiles(t *testing.T, name string) { if err != nil { t.Fatal(err) } - want := strings.TrimSpace(string(wantBytes)) + want := strings.TrimSpace(strings.ReplaceAll(string(wantBytes), "\r\n", "\n")) got := strings.TrimSpace(strings.Join(w.Features(""), "\n")) if got != want { t.Fatalf("API features mismatch:\n got:\n%s\nwant:\n%s", got, want) diff --git a/types/syntax_test.go b/types/syntax_test.go index 620d9f7..681402a 100644 --- a/types/syntax_test.go +++ b/types/syntax_test.go @@ -68,6 +68,7 @@ func checkWant(t *testing.T, filename string, fset *token.FileSet, conf *PkgConf t.Fatal(err) } for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { + line = strings.TrimSpace(line) parts := strings.Split(line, "|") if len(parts) < 2 || len(parts) > 4 { t.Fatalf("invalid want line %q", line) From 80ceb64668e8e7d6353976af629ebec19b065d4f Mon Sep 17 00:00:00 2001 From: visualfc Date: Sat, 29 Aug 2026 17:46:53 +0800 Subject: [PATCH 3/6] drop pre-generics compatibility code --- astview/ast_go117.go | 33 --------------------------------- goapi/base_type_go117.go | 20 -------------------- types/types_go117.go | 36 ------------------------------------ 3 files changed, 89 deletions(-) delete mode 100644 astview/ast_go117.go delete mode 100644 goapi/base_type_go117.go delete mode 100644 types/types_go117.go diff --git a/astview/ast_go117.go b/astview/ast_go117.go deleted file mode 100644 index 23c93d7..0000000 --- a/astview/ast_go117.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build !go1.18 -// +build !go1.18 - -package astview - -import "go/ast" - -func docBaseTypeName(typ ast.Expr, showAll bool) string { - name, _ := recvTypeName(typ, showAll) - return name -} - -func recvTypeName(typ ast.Expr, showAll bool) (string, bool) { - switch t := typ.(type) { - case *ast.Ident: - // if the type is not exported, the effect to - // a client is as if there were no type name - if showAll || t.IsExported() { - return t.Name, false - } - case *ast.StarExpr: - return docBaseTypeName(t.X, showAll), true - } - return "", false -} - -func typeName(ts *ast.TypeSpec, showTypeParams bool) string { - return ts.Name.String() -} - -func funcName(d *ast.FuncDecl, showTypeParams bool) string { - return d.Name.String() -} diff --git a/goapi/base_type_go117.go b/goapi/base_type_go117.go deleted file mode 100644 index 4e5f4dd..0000000 --- a/goapi/base_type_go117.go +++ /dev/null @@ -1,20 +0,0 @@ -//go:build !go1.18 -// +build !go1.18 - -package goapi - -import "go/ast" - -func baseTypeName(x ast.Expr) (name string, imported bool) { - switch t := x.(type) { - case *ast.Ident: - return t.Name, false - case *ast.SelectorExpr: - if _, ok := t.X.(*ast.Ident); ok { - return t.Sel.Name, true - } - case *ast.StarExpr: - return baseTypeName(t.X) - } - return -} diff --git a/types/types_go117.go b/types/types_go117.go deleted file mode 100644 index a675c22..0000000 --- a/types/types_go117.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build !go1.18 -// +build !go1.18 - -package types - -import ( - "go/ast" - "go/types" -) - -const enableTypeParams = false - -func DefaultPkgConfig() *PkgConfig { - conf := &PkgConfig{IgnoreFuncBodies: false, AllowBinary: true, WithTestFiles: true} - conf.Info = &types.Info{ - Uses: make(map[*ast.Ident]types.Object), - Defs: make(map[*ast.Ident]types.Object), - Selections: make(map[*ast.SelectorExpr]*types.Selection), - Types: make(map[ast.Expr]types.TypeAndValue), - Scopes: make(map[ast.Node]*types.Scope), - Implicits: make(map[ast.Node]types.Object), - } - conf.XInfo = &types.Info{ - Uses: make(map[*ast.Ident]types.Object), - Defs: make(map[*ast.Ident]types.Object), - Selections: make(map[*ast.SelectorExpr]*types.Selection), - Types: make(map[ast.Expr]types.TypeAndValue), - Scopes: make(map[ast.Node]*types.Scope), - Implicits: make(map[ast.Node]types.Object), - } - return conf -} - -func sameNamed(n1, n2 *types.Named) bool { - return n1 == n2 -} From 0f6ef6ee713a79111bc7c5aa8cd07a41e979c2d0 Mon Sep 17 00:00:00 2001 From: visualfc Date: Sat, 29 Aug 2026 17:49:27 +0800 Subject: [PATCH 4/6] merge generic support into main sources --- astview/ast_go118.go | 65 ---------------------------------------- astview/astview.go | 47 +++++++++++++++++++++++++++++ goapi/base_type_go118.go | 24 --------------- goapi/goapi.go | 18 +++++++++++ types/types.go | 23 ++++++++++++++ types/types_go118.go | 38 ----------------------- 6 files changed, 88 insertions(+), 127 deletions(-) delete mode 100644 astview/ast_go118.go delete mode 100644 goapi/base_type_go118.go delete mode 100644 types/types_go118.go diff --git a/astview/ast_go118.go b/astview/ast_go118.go deleted file mode 100644 index cd818cc..0000000 --- a/astview/ast_go118.go +++ /dev/null @@ -1,65 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package astview - -import ( - "go/ast" - "go/types" - "strings" -) - -func docBaseTypeName(typ ast.Expr, showAll bool) string { - name, _ := recvTypeName(typ, showAll) - return name -} - -func recvTypeName(typ ast.Expr, showAll bool) (string, bool) { - switch t := typ.(type) { - case *ast.Ident: - // if the type is not exported, the effect to - // a client is as if there were no type name - if showAll || t.IsExported() { - return t.Name, false - } - case *ast.StarExpr: - return docBaseTypeName(t.X, showAll), true - case *ast.IndexExpr: - return docBaseTypeName(t.X, showAll), false - case *ast.IndexListExpr: - return docBaseTypeName(t.X, showAll), false - } - return "", false -} - -func typeName(d *ast.TypeSpec, showTypeParams bool) string { - if showTypeParams && d.TypeParams != nil { - tparams := d.TypeParams - var fs []string - n := len(tparams.List) - for i := 0; i < n; i++ { - f := tparams.List[i] - for _, name := range f.Names { - fs = append(fs, name.String()+" "+types.ExprString(f.Type)) - } - } - return d.Name.String() + "[" + strings.Join(fs, ", ") + "]" - } - return d.Name.String() -} - -func funcName(d *ast.FuncDecl, showTypeParams bool) string { - if showTypeParams && d.Type.TypeParams != nil { - tparams := d.Type.TypeParams - var fs []string - n := len(tparams.List) - for i := 0; i < n; i++ { - f := tparams.List[i] - for _, name := range f.Names { - fs = append(fs, name.String()+" "+types.ExprString(f.Type)) - } - } - return d.Name.String() + "[" + strings.Join(fs, ", ") + "]" - } - return d.Name.String() -} diff --git a/astview/astview.go b/astview/astview.go index d50e0b4..c54ff85 100644 --- a/astview/astview.go +++ b/astview/astview.go @@ -38,6 +38,53 @@ var ( astViewSep string ) +func docBaseTypeName(typ ast.Expr, showAll bool) string { + name, _ := recvTypeName(typ, showAll) + return name +} + +func recvTypeName(typ ast.Expr, showAll bool) (string, bool) { + switch t := typ.(type) { + case *ast.Ident: + if showAll || t.IsExported() { + return t.Name, false + } + case *ast.StarExpr: + return docBaseTypeName(t.X, showAll), true + case *ast.IndexExpr: + return docBaseTypeName(t.X, showAll), false + case *ast.IndexListExpr: + return docBaseTypeName(t.X, showAll), false + } + return "", false +} + +func typeName(d *ast.TypeSpec, showTypeParams bool) string { + if showTypeParams && d.TypeParams != nil { + var params []string + for _, field := range d.TypeParams.List { + for _, name := range field.Names { + params = append(params, name.String()+" "+types.ExprString(field.Type)) + } + } + return d.Name.String() + "[" + strings.Join(params, ", ") + "]" + } + return d.Name.String() +} + +func funcName(d *ast.FuncDecl, showTypeParams bool) string { + if showTypeParams && d.Type.TypeParams != nil { + var params []string + for _, field := range d.Type.TypeParams.List { + for _, name := range field.Names { + params = append(params, name.String()+" "+types.ExprString(field.Type)) + } + } + return d.Name.String() + "[" + strings.Join(params, ", ") + "]" + } + return d.Name.String() +} + func init() { Command.Flag.BoolVar(&astViewStdin, "stdin", false, "input from stdin") Command.Flag.BoolVar(&astViewShowEndPos, "end", false, "show decl end pos") diff --git a/goapi/base_type_go118.go b/goapi/base_type_go118.go deleted file mode 100644 index c623769..0000000 --- a/goapi/base_type_go118.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package goapi - -import "go/ast" - -func baseTypeName(x ast.Expr) (name string, imported bool) { - switch t := x.(type) { - case *ast.Ident: - return t.Name, false - case *ast.SelectorExpr: - if _, ok := t.X.(*ast.Ident); ok { - return t.Sel.Name, true - } - case *ast.StarExpr: - return baseTypeName(t.X) - case *ast.IndexExpr: - return baseTypeName(t.X) - case *ast.IndexListExpr: - return baseTypeName(t.X) - } - return -} diff --git a/goapi/goapi.go b/goapi/goapi.go index 2cd3c0a..313b397 100644 --- a/goapi/goapi.go +++ b/goapi/goapi.go @@ -53,6 +53,24 @@ var apiLookupInfo string var apiLookupStdin bool var apiOutput string +func baseTypeName(x ast.Expr) (name string, imported bool) { + switch t := x.(type) { + case *ast.Ident: + return t.Name, false + case *ast.SelectorExpr: + if _, ok := t.X.(*ast.Ident); ok { + return t.Sel.Name, true + } + case *ast.StarExpr: + return baseTypeName(t.X) + case *ast.IndexExpr: + return baseTypeName(t.X) + case *ast.IndexListExpr: + return baseTypeName(t.X) + } + return +} + func init() { Command.Flag.BoolVar(&apiVerbose, "v", false, "verbose debugging") Command.Flag.BoolVar(&apiAllmethods, "e", true, "extract for all embedded methods") diff --git a/types/types.go b/types/types.go index 26402ce..90a6a9f 100644 --- a/types/types.go +++ b/types/types.go @@ -59,6 +59,29 @@ var ( typesTagList = []string{} // exploded version of tags flag; set in main ) +const enableTypeParams = true + +func DefaultPkgConfig() *PkgConfig { + conf := &PkgConfig{IgnoreFuncBodies: false, AllowBinary: true, WithTestFiles: true} + conf.Info = &types.Info{ + Uses: make(map[*ast.Ident]types.Object), Defs: make(map[*ast.Ident]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), Types: make(map[ast.Expr]types.TypeAndValue), + Scopes: make(map[ast.Node]*types.Scope), Implicits: make(map[ast.Node]types.Object), + Instances: make(map[*ast.Ident]types.Instance), + } + conf.XInfo = &types.Info{ + Uses: make(map[*ast.Ident]types.Object), Defs: make(map[*ast.Ident]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), Types: make(map[ast.Expr]types.TypeAndValue), + Scopes: make(map[ast.Node]*types.Scope), Implicits: make(map[ast.Node]types.Object), + Instances: make(map[*ast.Ident]types.Instance), + } + return conf +} + +func sameNamed(n1, n2 *types.Named) bool { + return n1 != nil && n2 != nil && n1.Origin().String() == n2.Origin().String() +} + // func init func init() { Command.Flag.BoolVar(&typesVerbose, "v", false, "verbose debugging") diff --git a/types/types_go118.go b/types/types_go118.go deleted file mode 100644 index 6fae55f..0000000 --- a/types/types_go118.go +++ /dev/null @@ -1,38 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package types - -import ( - "go/ast" - "go/types" -) - -const enableTypeParams = true - -func DefaultPkgConfig() *PkgConfig { - conf := &PkgConfig{IgnoreFuncBodies: false, AllowBinary: true, WithTestFiles: true} - conf.Info = &types.Info{ - Uses: make(map[*ast.Ident]types.Object), - Defs: make(map[*ast.Ident]types.Object), - Selections: make(map[*ast.SelectorExpr]*types.Selection), - Types: make(map[ast.Expr]types.TypeAndValue), - Scopes: make(map[ast.Node]*types.Scope), - Implicits: make(map[ast.Node]types.Object), - Instances: make(map[*ast.Ident]types.Instance), - } - conf.XInfo = &types.Info{ - Uses: make(map[*ast.Ident]types.Object), - Defs: make(map[*ast.Ident]types.Object), - Selections: make(map[*ast.SelectorExpr]*types.Selection), - Types: make(map[ast.Expr]types.TypeAndValue), - Scopes: make(map[ast.Node]*types.Scope), - Implicits: make(map[ast.Node]types.Object), - Instances: make(map[*ast.Ident]types.Instance), - } - return conf -} - -func sameNamed(n1, n2 *types.Named) bool { - return n1 != nil && n2 != nil && n1.Origin().String() == n2.Origin().String() -} From 20cc7aa1fb5adadeac9d2cffaf938634be65f4e3 Mon Sep 17 00:00:00 2001 From: visualfc Date: Sat, 29 Aug 2026 17:52:42 +0800 Subject: [PATCH 5/6] normalize Windows golden line endings --- astview/astview_test.go | 2 +- gofmt/gofmt_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/astview/astview_test.go b/astview/astview_test.go index 60d3675..b8c104a 100644 --- a/astview/astview_test.go +++ b/astview/astview_test.go @@ -32,7 +32,7 @@ func TestFiles(t *testing.T) { t.Fatal(err) } gotText := strings.ReplaceAll(strings.TrimSpace(got.String()), "\\", "/") - wantText := strings.TrimSpace(strings.ReplaceAll(string(want), "\\", "/")) + wantText := strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(string(want), "\r\n", "\n"), "\\", "/")) if gotText != wantText { t.Fatalf("file tree mismatch\n got:\n%s\nwant:\n%s", got.String(), want) } diff --git a/gofmt/gofmt_test.go b/gofmt/gofmt_test.go index f76d155..db1a790 100644 --- a/gofmt/gofmt_test.go +++ b/gofmt/gofmt_test.go @@ -33,6 +33,7 @@ func testFixImportsCase(t *testing.T, name string) { if err := processFile("_testdata/"+name+"/input.go", bytes.NewReader(src), &out, false); err != nil { t.Fatal(err) } + want = bytes.ReplaceAll(want, []byte("\r\n"), []byte("\n")) if !bytes.Equal(out.Bytes(), want) { t.Fatalf("fiximports output mismatch\n got:\n%s\nwant:\n%s", out.Bytes(), want) } From 266937b01186b8bbffcf8c94e954ff2527f37576 Mon Sep 17 00:00:00 2001 From: visualfc Date: Sat, 29 Aug 2026 17:57:56 +0800 Subject: [PATCH 6/6] use macos-latest in CI --- .github/workflows/go.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6a8e5d5..16b4e92 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: go-version: [1.25.x, 1.26.x, 1.27.x] - os: [ubuntu-latest, windows-latest, macos-11] + os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4