diff --git a/benchmark/parse/parse_test.go b/benchmark/parse/parse_test.go index ca3dfa658f6..219d1dbb32d 100644 --- a/benchmark/parse/parse_test.go +++ b/benchmark/parse/parse_test.go @@ -59,11 +59,11 @@ func TestParseLine(t *testing.T) { // error handling cases { line: "BenchPress 100 19.6 ns/op", // non-benchmark - err: true, + err: true, }, { line: "BenchmarkEncrypt lots 19.6 ns/op", // non-int iterations - err: true, + err: true, }, { line: "BenchmarkBridge 100000000 19.6 smoots", // unknown unit diff --git a/cmd/bisect/main_test.go b/cmd/bisect/main_test.go index 7c10ff0fb4b..bff1bf23c0c 100644 --- a/cmd/bisect/main_test.go +++ b/cmd/bisect/main_test.go @@ -17,7 +17,6 @@ import ( "testing" "golang.org/x/tools/internal/bisect" - "golang.org/x/tools/internal/compat" "golang.org/x/tools/internal/diffp" "golang.org/x/tools/txtar" ) @@ -82,7 +81,7 @@ func Test(t *testing.T) { have[color] = true } if m.ShouldReport(uint64(i)) { - out = compat.Appendf(out, "%s %s\n", color, bisect.Marker(uint64(i))) + out = fmt.Appendf(out, "%s %s\n", color, bisect.Marker(uint64(i))) } } err = nil diff --git a/cmd/callgraph/main.go b/cmd/callgraph/main.go index 33f7dfa8098..7853826b8fc 100644 --- a/cmd/callgraph/main.go +++ b/cmd/callgraph/main.go @@ -109,9 +109,20 @@ Flags: Caller and Callee are *ssa.Function values, which print as "(*sync/atomic.Mutex).Lock", but other attributes may be - derived from them, e.g. Caller.Pkg.Pkg.Path yields the - import path of the enclosing package. Consult the go/ssa - API documentation for details. + derived from them. For example: + + - {{.Caller.Pkg.Pkg.Path}} yields the import path of the + enclosing package; and + + - {{(.Caller.Prog.Fset.Position .Caller.Pos).Filename}} + yields the name of the file that declares the caller. + + - The 'posn' template function returns the token.Position + of an ssa.Function, so the previous example can be + reduced to {{(posn .Caller).Filename}}. + + Consult the documentation for go/token, text/template, and + golang.org/x/tools/go/ssa for more detail. Examples: @@ -238,7 +249,12 @@ func doCallgraph(dir, gopath, algo, format string, tests bool, args []string) er format = ` {{printf "%q" .Caller}} -> {{printf "%q" .Callee}}` } - tmpl, err := template.New("-format").Parse(format) + funcMap := template.FuncMap{ + "posn": func(f *ssa.Function) token.Position { + return f.Prog.Fset.Position(f.Pos()) + }, + } + tmpl, err := template.New("-format").Funcs(funcMap).Parse(format) if err != nil { return fmt.Errorf("invalid -format template: %v", err) } diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go index 8ee439b06d0..e0fce428d08 100644 --- a/cmd/deadcode/deadcode.go +++ b/cmd/deadcode/deadcode.go @@ -26,14 +26,13 @@ import ( "strings" "text/template" - "golang.org/x/telemetry/counter" - "golang.org/x/telemetry/crashmonitor" + "golang.org/x/telemetry" "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/rta" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" - "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) //go:embed doc.go @@ -65,8 +64,7 @@ Flags: } func main() { - counter.Open() // Enable telemetry counter writing. - crashmonitor.Start() // Enable crash reporting watchdog. + telemetry.Start(telemetry.Config{ReportCrashes: true}) log.SetPrefix("deadcode: ") log.SetFlags(0) // no time prefix @@ -132,16 +130,6 @@ func main() { log.Fatalf("packages contain errors") } - // Gather names of generated files. - generated := make(map[string]bool) - packages.Visit(initial, nil, func(p *packages.Package) { - for _, file := range p.Syntax { - if isGenerated(file) { - generated[p.Fset.File(file.Pos()).Name()] = true - } - } - }) - // If -filter is unset, use first module (if available). if *filterFlag == "" { if mod := initial[0].Module; mod != nil && mod.Path != "" { @@ -169,6 +157,32 @@ func main() { roots = append(roots, main.Func("init"), main.Func("main")) } + // Gather all source-level functions, + // as the user interface is expressed in terms of them. + // + // We ignore synthetic wrappers, and nested functions. Literal + // functions passed as arguments to other functions are of + // course address-taken and there exists a dynamic call of + // that signature, so when they are unreachable, it is + // invariably because the parent is unreachable. + var sourceFuncs []*ssa.Function + generated := make(map[string]bool) + packages.Visit(initial, nil, func(p *packages.Package) { + for _, file := range p.Syntax { + for _, decl := range file.Decls { + if decl, ok := decl.(*ast.FuncDecl); ok { + obj := p.TypesInfo.Defs[decl.Name].(*types.Func) + fn := prog.FuncValue(obj) + sourceFuncs = append(sourceFuncs, fn) + } + } + + if isGenerated(file) { + generated[p.Fset.File(file.Pos()).Name()] = true + } + } + }) + // Compute the reachabilty from main. // (Build a call graph only for -whylive.) res := rta.Analyze(roots, *whyLiveFlag != "") @@ -195,10 +209,7 @@ func main() { // is not dead, by showing a path to it from some root. if *whyLiveFlag != "" { targets := make(map[*ssa.Function]bool) - for fn := range ssautil.AllFunctions(prog) { - if fn.Synthetic != "" || fn.Object() == nil { - continue // not a source-level named function - } + for _, fn := range sourceFuncs { if prettyName(fn, true) == *whyLiveFlag { targets[fn] = true } @@ -263,26 +274,7 @@ func main() { // Group unreachable functions by package path. byPkgPath := make(map[string]map[*ssa.Function]bool) - for fn := range ssautil.AllFunctions(prog) { - if fn.Synthetic != "" { - continue // ignore synthetic wrappers etc - } - - // Use generic, as instantiations may not have a Pkg. - if orig := fn.Origin(); orig != nil { - fn = orig - } - - // Ignore unreachable nested functions. - // Literal functions passed as arguments to other - // functions are of course address-taken and there - // exists a dynamic call of that signature, so when - // they are unreachable, it is invariably because the - // parent is unreachable. - if fn.Parent() != nil { - continue - } - + for _, fn := range sourceFuncs { posn := prog.Fset.Position(fn.Pos()) if !reachablePosn[posn] { @@ -385,11 +377,8 @@ func prettyName(fn *ssa.Function, qualified bool) string { // method receiver? if recv := fn.Signature.Recv(); recv != nil { - t := recv.Type() - if ptr, ok := aliases.Unalias(t).(*types.Pointer); ok { - t = ptr.Elem() - } - buf.WriteString(aliases.Unalias(t).(*types.Named).Obj().Name()) + _, named := typesinternal.ReceiverNamed(recv) + buf.WriteString(named.Obj().Name()) buf.WriteByte('.') } diff --git a/cmd/deadcode/testdata/issue65915.txtar b/cmd/deadcode/testdata/issue65915.txtar new file mode 100644 index 00000000000..a7c15630bdd --- /dev/null +++ b/cmd/deadcode/testdata/issue65915.txtar @@ -0,0 +1,44 @@ +# Regression test for issue 65915: the enumeration of source-level +# functions used the flawed ssautil.AllFunctions, causing it to +# miss some unexported ones. + + deadcode -filter= example.com + + want "unreachable func: example.UnUsed" + want "unreachable func: example.unUsed" + want "unreachable func: PublicExample.UnUsed" + want "unreachable func: PublicExample.unUsed" + +-- go.mod -- +module example.com +go 1.18 + +-- main.go -- +package main + +type example struct{} + +func (e example) UnUsed() {} + +func (e example) Used() {} + +func (e example) unUsed() {} + +func (e example) used() {} + +type PublicExample struct{} + +func (p PublicExample) UnUsed() {} + +func (p PublicExample) Used() {} + +func (p PublicExample) unUsed() {} + +func (p PublicExample) used() {} + +func main() { + example{}.Used() + example{}.used() + PublicExample{}.Used() + PublicExample{}.used() +} diff --git a/cmd/getgo/.dockerignore b/cmd/getgo/.dockerignore deleted file mode 100644 index 2b87ad9cd76..00000000000 --- a/cmd/getgo/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -.git -.dockerignore -LICENSE -README.md -.gitignore diff --git a/cmd/getgo/.gitignore b/cmd/getgo/.gitignore deleted file mode 100644 index 47fe98419a0..00000000000 --- a/cmd/getgo/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build -getgo diff --git a/cmd/getgo/Dockerfile b/cmd/getgo/Dockerfile deleted file mode 100644 index 78fd9566799..00000000000 --- a/cmd/getgo/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM golang:latest - -ENV SHELL /bin/bash -ENV HOME /root -WORKDIR $HOME - -COPY . /go/src/golang.org/x/tools/cmd/getgo - -RUN ( \ - cd /go/src/golang.org/x/tools/cmd/getgo \ - && go build \ - && mv getgo /usr/local/bin/getgo \ - ) - -# undo the adding of GOPATH to env for testing -ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -ENV GOPATH "" - -# delete /go and /usr/local/go for testing -RUN rm -rf /go /usr/local/go diff --git a/cmd/getgo/LICENSE b/cmd/getgo/LICENSE deleted file mode 100644 index 32017f8fa1d..00000000000 --- a/cmd/getgo/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2017 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cmd/getgo/README.md b/cmd/getgo/README.md deleted file mode 100644 index e62a6c2b64e..00000000000 --- a/cmd/getgo/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# getgo - -A proof-of-concept command-line installer for Go. - -This installer is designed to both install Go as well as do the initial configuration -of setting up the right environment variables and paths. - -It will install the Go distribution (tools & stdlib) to "/.go" inside your home directory by default. - -It will setup "$HOME/go" as your GOPATH. -This is where third party libraries and apps will be installed as well as where you will write your Go code. - -If Go is already installed via this installer it will upgrade it to the latest version of Go. - -Currently the installer supports Windows, \*nix and macOS on x86 & x64. -It supports Bash and Zsh on all of these platforms as well as powershell & cmd.exe on Windows. - -## Usage - -Windows Powershell/cmd.exe: - -`(New-Object System.Net.WebClient).DownloadFile('https://get.golang.org/installer.exe', 'installer.exe'); Start-Process -Wait -NonewWindow installer.exe; Remove-Item installer.exe` - -Shell (Linux/macOS/Windows): - -`curl -LO https://get.golang.org/$(uname)/go_installer && chmod +x go_installer && ./go_installer && rm go_installer` - -## To Do - -* Check if Go is already installed (via a different method) and update it in place or at least notify the user -* Lots of testing. It's only had limited testing so far. -* Add support for additional shells. - -## Development instructions - -### Testing - -There are integration tests in [`main_test.go`](main_test.go). Please add more -tests there. - -#### On unix/linux with the Dockerfile - -The Dockerfile automatically builds the binary, moves it to -`/usr/local/bin/getgo` and then unsets `$GOPATH` and removes all `$GOPATH` from -`$PATH`. - -```bash -$ docker build --rm --force-rm -t getgo . -... -$ docker run --rm -it getgo bash -root@78425260fad0:~# getgo -v -Welcome to the Go installer! -Downloading Go version go1.8.3 to /usr/local/go -This may take a bit of time... -Adding "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin" to /root/.bashrc -Downloaded! -Setting up GOPATH -Adding "export GOPATH=/root/go" to /root/.bashrc -Adding "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/go/bin" to /root/.bashrc -GOPATH has been setup! -root@78425260fad0:~# which go -/usr/local/go/bin/go -root@78425260fad0:~# echo $GOPATH -/root/go -root@78425260fad0:~# echo $PATH -/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/go/bin -``` - -## Release instructions - -To upload a new release of getgo, run `./make.bash && ./upload.bash`. diff --git a/cmd/getgo/download.go b/cmd/getgo/download.go deleted file mode 100644 index 18e1aec2eef..00000000000 --- a/cmd/getgo/download.go +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright 2017 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. - -//go:build !plan9 -// +build !plan9 - -package main - -import ( - "archive/tar" - "archive/zip" - "compress/gzip" - "crypto/sha256" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strings" -) - -const ( - downloadURLPrefix = "https://dl.google.com/go" -) - -// downloadGoVersion downloads and upacks the specific go version to dest/go. -func downloadGoVersion(version, ops, arch, dest string) error { - suffix := "tar.gz" - if ops == "windows" { - suffix = "zip" - } - uri := fmt.Sprintf("%s/%s.%s-%s.%s", downloadURLPrefix, version, ops, arch, suffix) - - verbosef("Downloading %s", uri) - - req, err := http.NewRequest("GET", uri, nil) - if err != nil { - return err - } - req.Header.Add("User-Agent", fmt.Sprintf("golang.org-getgo/%s", version)) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("Downloading Go from %s failed: %v", uri, err) - } - if resp.StatusCode > 299 { - return fmt.Errorf("Downloading Go from %s failed with HTTP status %s", uri, resp.Status) - } - defer resp.Body.Close() - - tmpf, err := os.CreateTemp("", "go") - if err != nil { - return err - } - defer os.Remove(tmpf.Name()) - - h := sha256.New() - - w := io.MultiWriter(tmpf, h) - if _, err := io.Copy(w, resp.Body); err != nil { - return err - } - - verbosef("Downloading SHA %s.sha256", uri) - - sresp, err := http.Get(uri + ".sha256") - if err != nil { - return fmt.Errorf("Downloading Go sha256 from %s.sha256 failed: %v", uri, err) - } - defer sresp.Body.Close() - if sresp.StatusCode > 299 { - return fmt.Errorf("Downloading Go sha256 from %s.sha256 failed with HTTP status %s", uri, sresp.Status) - } - - shasum, err := io.ReadAll(sresp.Body) - if err != nil { - return err - } - - // Check the shasum. - sum := fmt.Sprintf("%x", h.Sum(nil)) - if sum != string(shasum) { - return fmt.Errorf("Shasum mismatch %s vs. %s", sum, string(shasum)) - } - - unpackFunc := unpackTar - if ops == "windows" { - unpackFunc = unpackZip - } - if err := unpackFunc(tmpf.Name(), dest); err != nil { - return fmt.Errorf("Unpacking Go to %s failed: %v", dest, err) - } - return nil -} - -func unpack(dest, name string, fi os.FileInfo, r io.Reader) error { - if strings.HasPrefix(name, "go/") { - name = name[len("go/"):] - } - - path := filepath.Join(dest, name) - if fi.IsDir() { - return os.MkdirAll(path, fi.Mode()) - } - - f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode()) - if err != nil { - return err - } - defer f.Close() - - _, err = io.Copy(f, r) - return err -} - -func unpackTar(src, dest string) error { - r, err := os.Open(src) - if err != nil { - return err - } - defer r.Close() - - archive, err := gzip.NewReader(r) - if err != nil { - return err - } - defer archive.Close() - - tarReader := tar.NewReader(archive) - - for { - header, err := tarReader.Next() - if err == io.EOF { - break - } else if err != nil { - return err - } - - if err := unpack(dest, header.Name, header.FileInfo(), tarReader); err != nil { - return err - } - } - - return nil -} - -func unpackZip(src, dest string) error { - zr, err := zip.OpenReader(src) - if err != nil { - return err - } - - for _, f := range zr.File { - fr, err := f.Open() - if err != nil { - return err - } - if err := unpack(dest, f.Name, f.FileInfo(), fr); err != nil { - return err - } - fr.Close() - } - - return nil -} - -func getLatestGoVersion() (string, error) { - resp, err := http.Get("https://golang.org/dl/?mode=json") - if err != nil { - return "", fmt.Errorf("Getting current Go version failed: %v", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) - return "", fmt.Errorf("Could not get current Go release: HTTP %d: %q", resp.StatusCode, b) - } - var releases []struct { - Version string - } - err = json.NewDecoder(resp.Body).Decode(&releases) - if err != nil { - return "", err - } - if len(releases) < 1 { - return "", fmt.Errorf("Could not get at least one Go release") - } - return releases[0].Version, nil -} diff --git a/cmd/getgo/download_test.go b/cmd/getgo/download_test.go deleted file mode 100644 index b4f2059d14e..00000000000 --- a/cmd/getgo/download_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2017 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. - -//go:build !plan9 -// +build !plan9 - -package main - -import ( - "os" - "path/filepath" - "testing" -) - -func TestDownloadGoVersion(t *testing.T) { - if testing.Short() { - t.Skipf("Skipping download in short mode") - } - - tmpd, err := os.MkdirTemp("", "go") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpd) - - if err := downloadGoVersion("go1.8.1", "linux", "amd64", filepath.Join(tmpd, "go")); err != nil { - t.Fatal(err) - } - - // Ensure the VERSION file exists. - vf := filepath.Join(tmpd, "go", "VERSION") - if _, err := os.Stat(vf); os.IsNotExist(err) { - t.Fatalf("file %s does not exist and should", vf) - } -} diff --git a/cmd/getgo/main.go b/cmd/getgo/main.go deleted file mode 100644 index e7bb2ee59d9..00000000000 --- a/cmd/getgo/main.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2017 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. - -//go:build !plan9 -// +build !plan9 - -// The getgo command installs Go to the user's system. -package main - -import ( - "bufio" - "context" - "errors" - "flag" - "fmt" - "os" - "os/exec" - "strings" -) - -var ( - interactive = flag.Bool("i", false, "Interactive mode, prompt for inputs.") - verbose = flag.Bool("v", false, "Verbose.") - setupOnly = flag.Bool("skip-dl", false, "Don't download - only set up environment variables") - goVersion = flag.String("version", "", `Version of Go to install (e.g. "1.8.3"). If empty, uses the latest version.`) - - version = "devel" -) - -var errExitCleanly error = errors.New("exit cleanly sentinel value") - -func main() { - flag.Parse() - if *goVersion != "" && !strings.HasPrefix(*goVersion, "go") { - *goVersion = "go" + *goVersion - } - - ctx := context.Background() - - verbosef("version " + version) - - runStep := func(s step) { - err := s(ctx) - if err == errExitCleanly { - os.Exit(0) - } - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(2) - } - } - - if !*setupOnly { - runStep(welcome) - runStep(checkOthers) - runStep(chooseVersion) - runStep(downloadGo) - } - - runStep(setupGOPATH) -} - -func verbosef(format string, v ...interface{}) { - if !*verbose { - return - } - - fmt.Printf(format+"\n", v...) -} - -func prompt(ctx context.Context, query, defaultAnswer string) (string, error) { - if !*interactive { - return defaultAnswer, nil - } - - fmt.Printf("%s [%s]: ", query, defaultAnswer) - - type result struct { - answer string - err error - } - ch := make(chan result, 1) - go func() { - s := bufio.NewScanner(os.Stdin) - if !s.Scan() { - ch <- result{"", s.Err()} - return - } - answer := s.Text() - if answer == "" { - answer = defaultAnswer - } - ch <- result{answer, nil} - }() - - select { - case r := <-ch: - return r.answer, r.err - case <-ctx.Done(): - return "", ctx.Err() - } -} - -func runCommand(ctx context.Context, prog string, args ...string) ([]byte, error) { - verbosef("Running command: %s %v", prog, args) - - cmd := exec.CommandContext(ctx, prog, args...) - out, err := cmd.CombinedOutput() - if err != nil { - return nil, fmt.Errorf("running cmd '%s %s' failed: %s err: %v", prog, strings.Join(args, " "), string(out), err) - } - if out != nil && err == nil && len(out) != 0 { - verbosef("%s", out) - } - - return out, nil -} diff --git a/cmd/getgo/main_test.go b/cmd/getgo/main_test.go deleted file mode 100644 index 878137dd3f4..00000000000 --- a/cmd/getgo/main_test.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2017 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. - -//go:build !plan9 -// +build !plan9 - -package main - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "testing" -) - -func TestMain(m *testing.M) { - if os.Getenv("GO_GETGO_TEST_IS_GETGO") != "" { - main() - os.Exit(0) - } - - if os.Getenv("GOGET_INTEGRATION") == "" { - fmt.Fprintln(os.Stderr, "main_test: Skipping integration tests with GOGET_INTEGRATION unset") - return - } - - // Don't let these environment variables confuse the test. - os.Unsetenv("GOBIN") - os.Unsetenv("GOPATH") - os.Unsetenv("GIT_ALLOW_PROTOCOL") - os.Unsetenv("PATH") - - os.Exit(m.Run()) -} - -func createTmpHome(t *testing.T) string { - tmpd, err := os.MkdirTemp("", "testgetgo") - if err != nil { - t.Fatalf("creating test tempdir failed: %v", err) - } - - os.Setenv("HOME", tmpd) - return tmpd -} - -// doRun runs the test getgo command, recording stdout and stderr and -// returning exit status. -func doRun(t *testing.T, args ...string) error { - exe, err := os.Executable() - if err != nil { - t.Fatal(err) - } - t.Helper() - - t.Logf("running getgo %v", args) - var stdout, stderr bytes.Buffer - cmd := exec.Command(exe, args...) - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = append(os.Environ(), "GO_GETGO_TEST_IS_GETGO=1") - status := cmd.Run() - if stdout.Len() > 0 { - t.Log("standard output:") - t.Log(stdout.String()) - } - if stderr.Len() > 0 { - t.Log("standard error:") - t.Log(stderr.String()) - } - return status -} - -func TestCommandVerbose(t *testing.T) { - tmpd := createTmpHome(t) - defer os.RemoveAll(tmpd) - - err := doRun(t, "-v") - if err != nil { - t.Fatal(err) - } - // make sure things are in path - shellConfig, err := shellConfigFile() - if err != nil { - t.Fatal(err) - } - b, err := os.ReadFile(shellConfig) - if err != nil { - t.Fatal(err) - } - home, err := getHomeDir() - if err != nil { - t.Fatal(err) - } - - expected := fmt.Sprintf(` -export PATH=$PATH:%s/.go/bin - -export GOPATH=%s/go - -export PATH=$PATH:%s/go/bin -`, home, home, home) - - if string(b) != expected { - t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b)) - } -} - -func TestCommandPathExists(t *testing.T) { - tmpd := createTmpHome(t) - defer os.RemoveAll(tmpd) - - // run once - err := doRun(t, "-skip-dl") - if err != nil { - t.Fatal(err) - } - // make sure things are in path - shellConfig, err := shellConfigFile() - if err != nil { - t.Fatal(err) - } - b, err := os.ReadFile(shellConfig) - if err != nil { - t.Fatal(err) - } - home, err := getHomeDir() - if err != nil { - t.Fatal(err) - } - - expected := fmt.Sprintf(` -export GOPATH=%s/go - -export PATH=$PATH:%s/go/bin -`, home, home) - - if string(b) != expected { - t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b)) - } - - // run twice - if err := doRun(t, "-skip-dl"); err != nil { - t.Fatal(err) - } - - b, err = os.ReadFile(shellConfig) - if err != nil { - t.Fatal(err) - } - - if string(b) != expected { - t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b)) - } -} diff --git a/cmd/getgo/make.bash b/cmd/getgo/make.bash deleted file mode 100755 index cbc36857e86..00000000000 --- a/cmd/getgo/make.bash +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# Copyright 2017 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. - -set -e -o -x - -LDFLAGS="-X main.version=$(git describe --always --dirty='*')" - -GOOS=windows GOARCH=386 go build -o build/installer.exe -ldflags="$LDFLAGS" -GOOS=linux GOARCH=386 go build -o build/installer_linux -ldflags="$LDFLAGS" -GOOS=darwin GOARCH=386 go build -o build/installer_darwin -ldflags="$LDFLAGS" diff --git a/cmd/getgo/path.go b/cmd/getgo/path.go deleted file mode 100644 index f1799a85f4e..00000000000 --- a/cmd/getgo/path.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2017 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. - -//go:build !plan9 -// +build !plan9 - -package main - -import ( - "bufio" - "context" - "fmt" - "os" - "os/user" - "path/filepath" - "runtime" - "strings" -) - -const ( - bashConfig = ".bash_profile" - zshConfig = ".zshrc" -) - -// appendToPATH adds the given path to the PATH environment variable and -// persists it for future sessions. -func appendToPATH(value string) error { - if isInPATH(value) { - return nil - } - return persistEnvVar("PATH", pathVar+envSeparator+value) -} - -func isInPATH(dir string) bool { - p := os.Getenv("PATH") - - paths := strings.Split(p, envSeparator) - for _, d := range paths { - if d == dir { - return true - } - } - - return false -} - -func getHomeDir() (string, error) { - home := os.Getenv(homeKey) - if home != "" { - return home, nil - } - - u, err := user.Current() - if err != nil { - return "", err - } - return u.HomeDir, nil -} - -func checkStringExistsFile(filename, value string) (bool, error) { - file, err := os.OpenFile(filename, os.O_RDONLY, 0600) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if line == value { - return true, nil - } - } - - return false, scanner.Err() -} - -func appendToFile(filename, value string) error { - verbosef("Adding %q to %s", value, filename) - - ok, err := checkStringExistsFile(filename, value) - if err != nil { - return err - } - if ok { - // Nothing to do. - return nil - } - - f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) - if err != nil { - return err - } - defer f.Close() - - _, err = f.WriteString(lineEnding + value + lineEnding) - return err -} - -func isShell(name string) bool { - return strings.Contains(currentShell(), name) -} - -// persistEnvVarWindows sets an environment variable in the Windows -// registry. -func persistEnvVarWindows(name, value string) error { - _, err := runCommand(context.Background(), "powershell", "-command", - fmt.Sprintf(`[Environment]::SetEnvironmentVariable("%s", "%s", "User")`, name, value)) - return err -} - -func persistEnvVar(name, value string) error { - if runtime.GOOS == "windows" { - if err := persistEnvVarWindows(name, value); err != nil { - return err - } - - if isShell("cmd.exe") || isShell("powershell.exe") { - return os.Setenv(strings.ToUpper(name), value) - } - // User is in bash, zsh, etc. - // Also set the environment variable in their shell config. - } - - rc, err := shellConfigFile() - if err != nil { - return err - } - - line := fmt.Sprintf("export %s=%s", strings.ToUpper(name), value) - if err := appendToFile(rc, line); err != nil { - return err - } - - return os.Setenv(strings.ToUpper(name), value) -} - -func shellConfigFile() (string, error) { - home, err := getHomeDir() - if err != nil { - return "", err - } - - switch { - case isShell("bash"): - return filepath.Join(home, bashConfig), nil - case isShell("zsh"): - return filepath.Join(home, zshConfig), nil - default: - return "", fmt.Errorf("%q is not a supported shell", currentShell()) - } -} diff --git a/cmd/getgo/path_test.go b/cmd/getgo/path_test.go deleted file mode 100644 index 8195f2e68d5..00000000000 --- a/cmd/getgo/path_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2017 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. - -//go:build !plan9 -// +build !plan9 - -package main - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -func TestAppendPath(t *testing.T) { - tmpd, err := os.MkdirTemp("", "go") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpd) - - if err := os.Setenv("HOME", tmpd); err != nil { - t.Fatal(err) - } - - GOPATH := os.Getenv("GOPATH") - if err := appendToPATH(filepath.Join(GOPATH, "bin")); err != nil { - t.Fatal(err) - } - - shellConfig, err := shellConfigFile() - if err != nil { - t.Fatal(err) - } - b, err := os.ReadFile(shellConfig) - if err != nil { - t.Fatal(err) - } - - expected := "export PATH=" + pathVar + envSeparator + filepath.Join(GOPATH, "bin") - if strings.TrimSpace(string(b)) != expected { - t.Fatalf("expected: %q, got %q", expected, strings.TrimSpace(string(b))) - } - - // Check that appendToPATH is idempotent. - if err := appendToPATH(filepath.Join(GOPATH, "bin")); err != nil { - t.Fatal(err) - } - b, err = os.ReadFile(shellConfig) - if err != nil { - t.Fatal(err) - } - if strings.TrimSpace(string(b)) != expected { - t.Fatalf("expected: %q, got %q", expected, strings.TrimSpace(string(b))) - } -} diff --git a/cmd/getgo/server/.gcloudignore b/cmd/getgo/server/.gcloudignore deleted file mode 100644 index 199e6d9f2f9..00000000000 --- a/cmd/getgo/server/.gcloudignore +++ /dev/null @@ -1,25 +0,0 @@ -# This file specifies files that are *not* uploaded to Google Cloud Platform -# using gcloud. It follows the same syntax as .gitignore, with the addition of -# "#!include" directives (which insert the entries of the given .gitignore-style -# file at that point). -# -# For more information, run: -# $ gcloud topic gcloudignore -# -.gcloudignore -# If you would like to upload your .git directory, .gitignore file or files -# from your .gitignore file, remove the corresponding line -# below: -.git -.gitignore - -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib -# Test binary, build with `go test -c` -*.test -# Output of the go coverage tool, specifically when used with LiteIDE -*.out \ No newline at end of file diff --git a/cmd/getgo/server/README.md b/cmd/getgo/server/README.md deleted file mode 100644 index 0cf629d6e6e..00000000000 --- a/cmd/getgo/server/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# getgo server - -## Deployment - -``` -gcloud app deploy --promote --project golang-org -``` diff --git a/cmd/getgo/server/app.yaml b/cmd/getgo/server/app.yaml deleted file mode 100644 index 5c47312ef1d..00000000000 --- a/cmd/getgo/server/app.yaml +++ /dev/null @@ -1,2 +0,0 @@ -runtime: go112 -service: get diff --git a/cmd/getgo/server/main.go b/cmd/getgo/server/main.go deleted file mode 100644 index bdb0f70cf49..00000000000 --- a/cmd/getgo/server/main.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2017 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. - -// Command server serves get.golang.org, redirecting users to the appropriate -// getgo installer based on the request path. -package main - -import ( - "fmt" - "net/http" - "os" - "strings" - "time" -) - -const ( - base = "https://dl.google.com/go/getgo/" - windowsInstaller = base + "installer.exe" - linuxInstaller = base + "installer_linux" - macInstaller = base + "installer_darwin" -) - -// substring-based redirects. -var stringMatch = map[string]string{ - // via uname, from bash - "MINGW": windowsInstaller, // Reported as MINGW64_NT-10.0 in git bash - "Linux": linuxInstaller, - "Darwin": macInstaller, -} - -func main() { - http.HandleFunc("/", handler) - - port := os.Getenv("PORT") - if port == "" { - port = "8080" - fmt.Printf("Defaulting to port %s", port) - } - - fmt.Printf("Listening on port %s", port) - if err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil); err != nil { - fmt.Fprintf(os.Stderr, "http.ListenAndServe: %v", err) - } -} - -func handler(w http.ResponseWriter, r *http.Request) { - if containsIgnoreCase(r.URL.Path, "installer.exe") { - // cache bust - http.Redirect(w, r, windowsInstaller+cacheBust(), http.StatusFound) - return - } - - for match, redirect := range stringMatch { - if containsIgnoreCase(r.URL.Path, match) { - http.Redirect(w, r, redirect, http.StatusFound) - return - } - } - - http.NotFound(w, r) -} - -func containsIgnoreCase(s, substr string) bool { - return strings.Contains( - strings.ToLower(s), - strings.ToLower(substr), - ) -} - -func cacheBust() string { - return fmt.Sprintf("?%d", time.Now().Nanosecond()) -} diff --git a/cmd/getgo/steps.go b/cmd/getgo/steps.go deleted file mode 100644 index fe69aa63aaf..00000000000 --- a/cmd/getgo/steps.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2017 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. - -//go:build !plan9 -// +build !plan9 - -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" -) - -type step func(context.Context) error - -func welcome(ctx context.Context) error { - fmt.Println("Welcome to the Go installer!") - answer, err := prompt(ctx, "Would you like to install Go? Y/n", "Y") - if err != nil { - return err - } - if strings.ToLower(answer) != "y" { - fmt.Println("Exiting install.") - return errExitCleanly - } - - return nil -} - -func checkOthers(ctx context.Context) error { - // TODO: if go is currently installed install new version over that - path, err := whichGo(ctx) - if err != nil { - fmt.Printf("Cannot check if Go is already installed:\n%v\n", err) - } - if path == "" { - return nil - } - if path != installPath { - fmt.Printf("Go is already installed at %v; remove it from your PATH.\n", path) - } - return nil -} - -func chooseVersion(ctx context.Context) error { - if *goVersion != "" { - return nil - } - - var err error - *goVersion, err = getLatestGoVersion() - if err != nil { - return err - } - - answer, err := prompt(ctx, fmt.Sprintf("The latest Go version is %s, install that? Y/n", *goVersion), "Y") - if err != nil { - return err - } - - if strings.ToLower(answer) != "y" { - // TODO: handle passing a version - fmt.Println("Aborting install.") - return errExitCleanly - } - - return nil -} - -func downloadGo(ctx context.Context) error { - answer, err := prompt(ctx, fmt.Sprintf("Download Go version %s to %s? Y/n", *goVersion, installPath), "Y") - if err != nil { - return err - } - - if strings.ToLower(answer) != "y" { - fmt.Println("Aborting install.") - return errExitCleanly - } - - fmt.Printf("Downloading Go version %s to %s\n", *goVersion, installPath) - fmt.Println("This may take a bit of time...") - - if err := downloadGoVersion(*goVersion, runtime.GOOS, arch, installPath); err != nil { - return err - } - - if err := appendToPATH(filepath.Join(installPath, "bin")); err != nil { - return err - } - - fmt.Println("Downloaded!") - return nil -} - -func setupGOPATH(ctx context.Context) error { - answer, err := prompt(ctx, "Would you like us to setup your GOPATH? Y/n", "Y") - if err != nil { - return err - } - - if strings.ToLower(answer) != "y" { - fmt.Println("Exiting and not setting up GOPATH.") - return errExitCleanly - } - - fmt.Println("Setting up GOPATH") - home, err := getHomeDir() - if err != nil { - return err - } - - gopath := os.Getenv("GOPATH") - if gopath == "" { - // set $GOPATH - gopath = filepath.Join(home, "go") - if err := persistEnvVar("GOPATH", gopath); err != nil { - return err - } - fmt.Println("GOPATH has been set up!") - } else { - verbosef("GOPATH is already set to %s", gopath) - } - - if err := appendToPATH(filepath.Join(gopath, "bin")); err != nil { - return err - } - return persistEnvChangesForSession() -} diff --git a/cmd/getgo/system.go b/cmd/getgo/system.go deleted file mode 100644 index 729983986ba..00000000000 --- a/cmd/getgo/system.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2017 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. - -//go:build !plan9 -// +build !plan9 - -package main - -import ( - "bytes" - "context" - "os/exec" - "runtime" - "strings" -) - -// arch contains either amd64 or 386. -var arch = func() string { - cmd := exec.Command("uname", "-m") // "x86_64" - if runtime.GOOS == "windows" { - cmd = exec.Command("powershell", "-command", "(Get-WmiObject -Class Win32_ComputerSystem).SystemType") // "x64-based PC" - } - - out, err := cmd.Output() - if err != nil { - // a sensible default? - return "amd64" - } - if bytes.Contains(out, []byte("64")) { - return "amd64" - } - return "386" -}() - -func findGo(ctx context.Context, cmd string) (string, error) { - out, err := exec.CommandContext(ctx, cmd, "go").CombinedOutput() - return strings.TrimSpace(string(out)), err -} diff --git a/cmd/getgo/system_unix.go b/cmd/getgo/system_unix.go deleted file mode 100644 index 0b511dbeb4b..00000000000 --- a/cmd/getgo/system_unix.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2017 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. - -//go:build !plan9 && !windows - -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" -) - -const ( - envSeparator = ":" - homeKey = "HOME" - lineEnding = "\n" - pathVar = "$PATH" -) - -var installPath = func() string { - home, err := getHomeDir() - if err != nil { - return "/usr/local/go" - } - - return filepath.Join(home, ".go") -}() - -func whichGo(ctx context.Context) (string, error) { - return findGo(ctx, "which") -} - -func isWindowsXP() bool { - return false -} - -func currentShell() string { - return os.Getenv("SHELL") -} - -func persistEnvChangesForSession() error { - shellConfig, err := shellConfigFile() - if err != nil { - return err - } - fmt.Println() - fmt.Printf("One more thing! Run `source %s` to persist the\n", shellConfig) - fmt.Println("new environment variables to your current session, or open a") - fmt.Println("new shell prompt.") - - return nil -} diff --git a/cmd/getgo/system_windows.go b/cmd/getgo/system_windows.go deleted file mode 100644 index 5b1e2471300..00000000000 --- a/cmd/getgo/system_windows.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2017 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. - -//go:build windows -// +build windows - -package main - -import ( - "context" - "log" - "os" - "syscall" - "unsafe" -) - -const ( - envSeparator = ";" - homeKey = "USERPROFILE" - lineEnding = "/r/n" - pathVar = "$env:Path" -) - -var installPath = `c:\go` - -func isWindowsXP() bool { - v, err := syscall.GetVersion() - if err != nil { - log.Fatalf("GetVersion failed: %v", err) - } - major := byte(v) - return major < 6 -} - -func whichGo(ctx context.Context) (string, error) { - return findGo(ctx, "where") -} - -// currentShell reports the current shell. -// It might be "powershell.exe", "cmd.exe" or any of the *nix shells. -// -// Returns empty string if the shell is unknown. -func currentShell() string { - shell := os.Getenv("SHELL") - if shell != "" { - return shell - } - - pid := os.Getppid() - pe, err := getProcessEntry(pid) - if err != nil { - verbosef("getting shell from process entry failed: %v", err) - return "" - } - - return syscall.UTF16ToString(pe.ExeFile[:]) -} - -func getProcessEntry(pid int) (*syscall.ProcessEntry32, error) { - // From https://go.googlesource.com/go/+/go1.8.3/src/syscall/syscall_windows.go#941 - snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) - if err != nil { - return nil, err - } - defer syscall.CloseHandle(snapshot) - - var procEntry syscall.ProcessEntry32 - procEntry.Size = uint32(unsafe.Sizeof(procEntry)) - if err = syscall.Process32First(snapshot, &procEntry); err != nil { - return nil, err - } - - for { - if procEntry.ProcessID == uint32(pid) { - return &procEntry, nil - } - - if err := syscall.Process32Next(snapshot, &procEntry); err != nil { - return nil, err - } - } -} - -func persistEnvChangesForSession() error { - return nil -} diff --git a/cmd/getgo/upload.bash b/cmd/getgo/upload.bash deleted file mode 100755 index f52bb23c93c..00000000000 --- a/cmd/getgo/upload.bash +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Copyright 2017 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. - -if ! command -v gsutil 2>&1 > /dev/null; then - echo "Install gsutil:" - echo - echo " https://cloud.google.com/storage/docs/gsutil_install#sdk-install" -fi - -if [ ! -d build ]; then - echo "Run make.bash first" -fi - -set -e -o -x - -gsutil -m cp -a public-read build/* gs://golang/getgo diff --git a/cmd/godex/print.go b/cmd/godex/print.go index 1bb5214edfd..da3b2f04e0b 100644 --- a/cmd/godex/print.go +++ b/cmd/godex/print.go @@ -12,6 +12,8 @@ import ( "go/types" "io" "math/big" + + "golang.org/x/tools/internal/aliases" ) // TODO(gri) use tabwriter for alignment? @@ -56,7 +58,7 @@ func (p *printer) printf(format string, args ...interface{}) { // denoted by obj is not an interface and has methods. Otherwise it returns // the zero value. func methodsFor(obj *types.TypeName) (*types.Named, []*types.Selection) { - named, _ := obj.Type().(*types.Named) + named, _ := aliases.Unalias(obj.Type()).(*types.Named) if named == nil { // A type name's type can also be the // exported basic type unsafe.Pointer. diff --git a/cmd/godex/writetype.go b/cmd/godex/writetype.go index 5cbe1b12c84..6ae365d13a3 100644 --- a/cmd/godex/writetype.go +++ b/cmd/godex/writetype.go @@ -12,7 +12,11 @@ package main -import "go/types" +import ( + "go/types" + + "golang.org/x/tools/internal/aliases" +) func (p *printer) writeType(this *types.Package, typ types.Type) { p.writeTypeInternal(this, typ, make([]types.Type, 8)) @@ -173,6 +177,10 @@ func (p *printer) writeTypeInternal(this *types.Package, typ types.Type, visited p.print(")") } + case *aliases.Alias: + // TODO(adonovan): display something aliasy. + p.writeTypeInternal(this, aliases.Unalias(t), visited) + case *types.Named: s := "" if obj := t.Obj(); obj != nil { diff --git a/cmd/guru/describe.go b/cmd/guru/describe.go index 0e4964428d5..e85bc385feb 100644 --- a/cmd/guru/describe.go +++ b/cmd/guru/describe.go @@ -19,6 +19,8 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/loader" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) // describe describes the syntax node denoted by the query position, @@ -357,7 +359,7 @@ func appendNames(names []*types.Named, typ types.Type) []*types.Named { Elem() types.Type } - switch t := typ.(type) { + switch t := aliases.Unalias(typ).(type) { case *types.Named: names = append(names, t) case *types.Map: @@ -468,7 +470,7 @@ func describeType(qpos *queryPos, path []ast.Node) (*describeTypeResult, error) description = "alias of " } else if obj.Pos() == n.Pos() { description = "definition of " // (Named type) - } else if _, ok := typ.(*types.Basic); ok { + } else if _, ok := aliases.Unalias(typ).(*types.Basic); ok { description = "reference to built-in " } else { description = "reference to " // (Named type) @@ -485,7 +487,7 @@ func describeType(qpos *queryPos, path []ast.Node) (*describeTypeResult, error) description = description + "type " + qpos.typeString(typ) // Show sizes for structs and named types (it's fairly obvious for others). - switch typ.(type) { + switch aliases.Unalias(typ).(type) { case *types.Named, *types.Struct: szs := types.StdSizes{WordSize: 8, MaxAlign: 8} // assume amd64 description = fmt.Sprintf("%s (size %d, align %d)", description, @@ -575,7 +577,7 @@ func (r *describeTypeResult) PrintPlain(printf printfFunc) { printf(r.node, "%s", r.description) // Show the underlying type for a reference to a named type. - if nt, ok := r.typ.(*types.Named); ok && r.node.Pos() != nt.Obj().Pos() { + if nt, ok := aliases.Unalias(r.typ).(*types.Named); ok && r.node.Pos() != nt.Obj().Pos() { // TODO(adonovan): improve display of complex struct/interface types. printf(nt.Obj(), "defined as %s", r.qpos.typeString(nt.Underlying())) } @@ -584,7 +586,7 @@ func (r *describeTypeResult) PrintPlain(printf printfFunc) { if len(r.methods) == 0 { // Only report null result for type kinds // capable of bearing methods. - switch r.typ.(type) { + switch aliases.Unalias(r.typ).(type) { case *types.Interface, *types.Struct, *types.Named: printf(r.node, "No methods.") } @@ -595,7 +597,7 @@ func (r *describeTypeResult) PrintPlain(printf printfFunc) { func (r *describeTypeResult) JSON(fset *token.FileSet) []byte { var namePos, nameDef string - if nt, ok := r.typ.(*types.Named); ok { + if nt, ok := aliases.Unalias(r.typ).(*types.Named); ok { namePos = fset.Position(nt.Obj().Pos()).String() nameDef = nt.Underlying().String() } @@ -726,7 +728,7 @@ func formatMember(obj types.Object, maxname int) string { } var typestr string // Abbreviate long aggregate type names. - switch typ := typ.(type) { + switch typ := aliases.Unalias(typ).(type) { case *types.Interface: if typ.NumMethods() > 1 { typestr = "interface{...}" @@ -911,11 +913,7 @@ func accessibleFields(recv types.Type, from *types.Package) []describeField { // Handle recursion through anonymous fields. if f.Anonymous() { - tf := f.Type() - if ptr, ok := tf.(*types.Pointer); ok { - tf = ptr.Elem() - } - if named, ok := tf.(*types.Named); ok { // (be defensive) + if _, named := typesinternal.ReceiverNamed(f); named != nil { // If we've already visited this named type // on this path, break the cycle. for _, x := range stack { diff --git a/cmd/guru/implements.go b/cmd/guru/implements.go index 9e4d0dba6ee..48ab19116ae 100644 --- a/cmd/guru/implements.go +++ b/cmd/guru/implements.go @@ -16,6 +16,7 @@ import ( "golang.org/x/tools/cmd/guru/serial" "golang.org/x/tools/go/loader" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/refactor/importgraph" ) @@ -157,7 +158,7 @@ func implements(q *Query) error { } var pos interface{} = qpos - if nt, ok := deref(T).(*types.Named); ok { + if nt, ok := aliases.Unalias(deref(T)).(*types.Named); ok { pos = nt.Obj() } @@ -230,7 +231,7 @@ func (r *implementsResult) PrintPlain(printf printfFunc) { for i, sub := range r.to { if !isInterface(sub) { if r.method == nil { - printf(deref(sub).(*types.Named).Obj(), "\t%s %s type %s", + printf(aliases.Unalias(deref(sub)).(*types.Named).Obj(), "\t%s %s type %s", relation, typeKind(sub), r.qpos.typeString(sub)) } else { meth(r.toMethod[i]) @@ -240,7 +241,7 @@ func (r *implementsResult) PrintPlain(printf printfFunc) { for i, sub := range r.to { if isInterface(sub) { if r.method == nil { - printf(sub.(*types.Named).Obj(), "\t%s %s type %s", + printf(aliases.Unalias(sub).(*types.Named).Obj(), "\t%s %s type %s", relation, typeKind(sub), r.qpos.typeString(sub)) } else { meth(r.toMethod[i]) @@ -251,7 +252,7 @@ func (r *implementsResult) PrintPlain(printf printfFunc) { relation = "implements" for i, super := range r.from { if r.method == nil { - printf(super.(*types.Named).Obj(), "\t%s %s", + printf(aliases.Unalias(super).(*types.Named).Obj(), "\t%s %s", relation, r.qpos.typeString(super)) } else { meth(r.fromMethod[i]) @@ -270,7 +271,7 @@ func (r *implementsResult) PrintPlain(printf printfFunc) { } for i, super := range r.from { if r.method == nil { - printf(super.(*types.Named).Obj(), "\t%s %s", + printf(aliases.Unalias(super).(*types.Named).Obj(), "\t%s %s", relation, r.qpos.typeString(super)) } else { meth(r.fromMethod[i]) @@ -288,7 +289,7 @@ func (r *implementsResult) PrintPlain(printf printfFunc) { for i, psuper := range r.fromPtr { if r.method == nil { - printf(psuper.(*types.Named).Obj(), "\t%s %s", + printf(aliases.Unalias(psuper).(*types.Named).Obj(), "\t%s %s", relation, r.qpos.typeString(psuper)) } else { meth(r.fromPtrMethod[i]) @@ -332,7 +333,7 @@ func makeImplementsTypes(tt []types.Type, fset *token.FileSet) []serial.Implemen func makeImplementsType(T types.Type, fset *token.FileSet) serial.ImplementsType { var pos token.Pos - if nt, ok := deref(T).(*types.Named); ok { // implementsResult.t may be non-named + if nt, ok := aliases.Unalias(deref(T)).(*types.Named); ok { // implementsResult.t may be non-named pos = nt.Obj().Pos() } return serial.ImplementsType{ diff --git a/copyright/copyright.go b/copyright/copyright.go index b13b56e85f1..556c6e4f69a 100644 --- a/copyright/copyright.go +++ b/copyright/copyright.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - // Package copyright checks that files have the correct copyright notices. package copyright diff --git a/copyright/copyright_test.go b/copyright/copyright_test.go index 7f7892524f5..947fb10c1d1 100644 --- a/copyright/copyright_test.go +++ b/copyright/copyright_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package copyright import ( diff --git a/go.mod b/go.mod index 54b8bd473c5..6791902dae1 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,16 @@ module golang.org/x/tools -go 1.18 +go 1.19 require ( github.com/yuin/goldmark v1.4.13 - golang.org/x/mod v0.15.0 - golang.org/x/net v0.21.0 + golang.org/x/mod v0.16.0 + golang.org/x/net v0.22.0 ) require golang.org/x/sync v0.6.0 require ( - golang.org/x/sys v0.17.0 // indirect - golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 + golang.org/x/sys v0.18.0 // indirect + golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 ) diff --git a/go.sum b/go.sum index 373ed3cdd52..c39ea36a534 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,12 @@ 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/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509 h1:Nr7eTQpQZ/ytesxDJpQgaf0t4sdLnnDtAbmtViTrSUo= -golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509/go.mod h1:ZthVHHkOi8rlMEsfFr3Ie42Ym1NonbFNNRKW3ci0UrU= -golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g= -golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= diff --git a/go/analysis/passes/asmdecl/arches_go118.go b/go/analysis/passes/asmdecl/arches_go118.go deleted file mode 100644 index d8211afdc8d..00000000000 --- a/go/analysis/passes/asmdecl/arches_go118.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2022 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. - -//go:build !go1.19 -// +build !go1.19 - -package asmdecl - -func additionalArches() []*asmArch { - return nil -} diff --git a/go/analysis/passes/asmdecl/arches_go119.go b/go/analysis/passes/asmdecl/arches_go119.go deleted file mode 100644 index 3018383e7f2..00000000000 --- a/go/analysis/passes/asmdecl/arches_go119.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2022 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. - -//go:build go1.19 -// +build go1.19 - -package asmdecl - -var asmArchLoong64 = asmArch{name: "loong64", bigEndian: false, stack: "R3", lr: true} - -func additionalArches() []*asmArch { - return []*asmArch{&asmArchLoong64} -} diff --git a/go/analysis/passes/asmdecl/asmdecl.go b/go/analysis/passes/asmdecl/asmdecl.go index e24dac9865a..f2ca95aa9eb 100644 --- a/go/analysis/passes/asmdecl/asmdecl.go +++ b/go/analysis/passes/asmdecl/asmdecl.go @@ -96,6 +96,7 @@ var ( asmArchRISCV64 = asmArch{name: "riscv64", bigEndian: false, stack: "SP", lr: true, retRegs: []string{"X10", "F10"}} asmArchS390X = asmArch{name: "s390x", bigEndian: true, stack: "R15", lr: true} asmArchWasm = asmArch{name: "wasm", bigEndian: false, stack: "SP", lr: false} + asmArchLoong64 = asmArch{name: "loong64", bigEndian: false, stack: "R3", lr: true} arches = []*asmArch{ &asmArch386, @@ -111,11 +112,11 @@ var ( &asmArchRISCV64, &asmArchS390X, &asmArchWasm, + &asmArchLoong64, } ) func init() { - arches = append(arches, additionalArches()...) for _, arch := range arches { arch.sizes = types.SizesFor("gc", arch.name) if arch.sizes == nil { diff --git a/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go b/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go index 345db277933..fc80410e78a 100644 --- a/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go @@ -4,8 +4,6 @@ // This file contains tests for the useless-assignment checker. -//go:build go1.18 - package testdata import "math/rand" diff --git a/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go.golden b/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go.golden index d9384ed5aab..8c8c4b61f5c 100644 --- a/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go.golden +++ b/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go.golden @@ -4,8 +4,6 @@ // This file contains tests for the useless-assignment checker. -//go:build go1.18 - package testdata import "math/rand" diff --git a/go/analysis/passes/bools/testdata/src/typeparams/typeparams.go b/go/analysis/passes/bools/testdata/src/typeparams/typeparams.go index 718462593a1..3afb56a5d0c 100644 --- a/go/analysis/passes/bools/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/bools/testdata/src/typeparams/typeparams.go @@ -4,8 +4,6 @@ // This file contains tests for the bool checker. -//go:build go1.18 - package typeparams type T[P interface{ ~int }] struct { diff --git a/go/analysis/passes/composite/composite.go b/go/analysis/passes/composite/composite.go index 847063bb326..6b126f897d8 100644 --- a/go/analysis/passes/composite/composite.go +++ b/go/analysis/passes/composite/composite.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -71,7 +72,7 @@ func run(pass *analysis.Pass) (interface{}, error) { return } var structuralTypes []types.Type - switch typ := typ.(type) { + switch typ := aliases.Unalias(typ).(type) { case *types.TypeParam: terms, err := typeparams.StructuralTerms(typ) if err != nil { @@ -84,7 +85,8 @@ func run(pass *analysis.Pass) (interface{}, error) { structuralTypes = append(structuralTypes, typ) } for _, typ := range structuralTypes { - under := deref(typ.Underlying()) + // TODO(adonovan): this operation is questionable. + under := aliases.Unalias(deref(typ.Underlying())) strct, ok := under.(*types.Struct) if !ok { // skip non-struct composite literals @@ -142,9 +144,11 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil } +// Note: this is not the usual deref operator! +// It strips off all Pointer constructors (and their Aliases). func deref(typ types.Type) types.Type { for { - ptr, ok := typ.(*types.Pointer) + ptr, ok := aliases.Unalias(typ).(*types.Pointer) if !ok { break } @@ -153,18 +157,18 @@ func deref(typ types.Type) types.Type { return typ } +// isLocalType reports whether typ belongs to the same package as pass. +// TODO(adonovan): local means "internal to a function"; rename to isSamePackageType. func isLocalType(pass *analysis.Pass, typ types.Type) bool { - switch x := typ.(type) { + switch x := aliases.Unalias(typ).(type) { case *types.Struct: // struct literals are local types return true case *types.Pointer: return isLocalType(pass, x.Elem()) - case *types.Named: + case interface{ Obj() *types.TypeName }: // *Named or *TypeParam (aliases were removed already) // names in package foo are local to foo_test too return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test") - case *types.TypeParam: - return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test") } return false } diff --git a/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go b/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go index 20b652e88dd..00cbd70051e 100644 --- a/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go +++ b/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package a import "testing" diff --git a/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden b/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden index 20b652e88dd..00cbd70051e 100644 --- a/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden +++ b/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package a import "testing" diff --git a/go/analysis/passes/copylock/copylock.go b/go/analysis/passes/copylock/copylock.go index 6cbbc7e8140..8f39159c0f0 100644 --- a/go/analysis/passes/copylock/copylock.go +++ b/go/analysis/passes/copylock/copylock.go @@ -18,6 +18,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -255,7 +256,7 @@ func lockPath(tpkg *types.Package, typ types.Type, seen map[types.Type]bool) typ } seen[typ] = true - if tpar, ok := typ.(*types.TypeParam); ok { + if tpar, ok := aliases.Unalias(typ).(*types.TypeParam); ok { terms, err := typeparams.StructuralTerms(tpar) if err != nil { return nil // invalid type diff --git a/go/analysis/passes/deepequalerrors/deepequalerrors.go b/go/analysis/passes/deepequalerrors/deepequalerrors.go index 5e17bd1ab90..95cd9a061eb 100644 --- a/go/analysis/passes/deepequalerrors/deepequalerrors.go +++ b/go/analysis/passes/deepequalerrors/deepequalerrors.go @@ -102,8 +102,7 @@ func containsError(typ types.Type) bool { return true } } - case *types.Named, - *aliases.Alias: + case *types.Named, *aliases.Alias: return check(t.Underlying()) // We list the remaining valid type kinds for completeness. diff --git a/go/analysis/passes/httpmux/httpmux.go b/go/analysis/passes/httpmux/httpmux.go index fa99296b5ec..d13e8aab37b 100644 --- a/go/analysis/passes/httpmux/httpmux.go +++ b/go/analysis/passes/httpmux/httpmux.go @@ -17,6 +17,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typesinternal" ) const Doc = `report using Go 1.22 enhanced ServeMux patterns in older Go versions @@ -83,11 +84,8 @@ func isServeMuxRegisterCall(pass *analysis.Pass, call *ast.CallExpr) bool { if !isMethodNamed(fn, "net/http", "Handle", "HandleFunc") { return false } - t, ok := fn.Type().(*types.Signature).Recv().Type().(*types.Pointer) - if !ok { - return false - } - return analysisutil.IsNamedType(t.Elem(), "net/http", "ServeMux") + isPtr, named := typesinternal.ReceiverNamed(fn.Type().(*types.Signature).Recv()) + return isPtr && analysisutil.IsNamedType(named, "net/http", "ServeMux") } func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool { diff --git a/go/analysis/passes/httpresponse/httpresponse.go b/go/analysis/passes/httpresponse/httpresponse.go index c6b6c81b420..047ae07cca1 100644 --- a/go/analysis/passes/httpresponse/httpresponse.go +++ b/go/analysis/passes/httpresponse/httpresponse.go @@ -14,6 +14,8 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) const Doc = `check for mistakes using HTTP responses @@ -116,7 +118,8 @@ func isHTTPFuncOrMethodOnClient(info *types.Info, expr *ast.CallExpr) bool { if res.Len() != 2 { return false // the function called does not return two values. } - if ptr, ok := res.At(0).Type().(*types.Pointer); !ok || !analysisutil.IsNamedType(ptr.Elem(), "net/http", "Response") { + isPtr, named := typesinternal.ReceiverNamed(res.At(0)) + if !isPtr || !analysisutil.IsNamedType(named, "net/http", "Response") { return false // the first return type is not *http.Response. } @@ -134,7 +137,7 @@ func isHTTPFuncOrMethodOnClient(info *types.Info, expr *ast.CallExpr) bool { if analysisutil.IsNamedType(typ, "net/http", "Client") { return true // method on http.Client. } - ptr, ok := typ.(*types.Pointer) + ptr, ok := aliases.Unalias(typ).(*types.Pointer) return ok && analysisutil.IsNamedType(ptr.Elem(), "net/http", "Client") // method on *http.Client. } diff --git a/go/analysis/passes/httpresponse/testdata/src/typeparams/typeparams.go b/go/analysis/passes/httpresponse/testdata/src/typeparams/typeparams.go index 65dd58c7f8a..b2515c950ba 100644 --- a/go/analysis/passes/httpresponse/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/httpresponse/testdata/src/typeparams/typeparams.go @@ -4,8 +4,6 @@ // This file contains tests for the httpresponse checker. -//go:build go1.18 - package typeparams import ( diff --git a/go/analysis/passes/ifaceassert/parameterized.go b/go/analysis/passes/ifaceassert/parameterized.go index 12507f9967f..a077d440246 100644 --- a/go/analysis/passes/ifaceassert/parameterized.go +++ b/go/analysis/passes/ifaceassert/parameterized.go @@ -7,6 +7,7 @@ package ifaceassert import ( "go/types" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -94,6 +95,10 @@ func (w *tpWalker) isParameterized(typ types.Type) (res bool) { case *types.Chan: return w.isParameterized(t.Elem()) + case *aliases.Alias: + // TODO(adonovan): think about generic aliases. + return w.isParameterized(aliases.Unalias(t)) + case *types.Named: list := t.TypeArgs() for i, n := 0, list.Len(); i < n; i++ { diff --git a/go/analysis/passes/internal/analysisutil/util.go b/go/analysis/passes/internal/analysisutil/util.go index 3f01b3b55dc..89291602a5b 100644 --- a/go/analysis/passes/internal/analysisutil/util.go +++ b/go/analysis/passes/internal/analysisutil/util.go @@ -14,6 +14,7 @@ import ( "go/types" "os" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/analysisinternal" ) @@ -115,7 +116,7 @@ func Imports(pkg *types.Package, path string) bool { // This function avoids allocating the concatenation of "pkg.Name", // which is important for the performance of syntax matching. func IsNamedType(t types.Type, pkgPath string, names ...string) bool { - n, ok := t.(*types.Named) + n, ok := aliases.Unalias(t).(*types.Named) if !ok { return false } diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index 4724c9f3b1a..fe05eda44e4 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" ) @@ -54,9 +55,8 @@ func run(pass *analysis.Pass) (interface{}, error) { switch n := n.(type) { case *ast.File: // Only traverse the file if its goversion is strictly before go1.22. - goversion := versions.Lang(versions.FileVersions(pass.TypesInfo, n)) - // goversion is empty for older go versions (or the version is invalid). - return goversion == "" || versions.Compare(goversion, "go1.22") < 0 + goversion := versions.FileVersion(pass.TypesInfo, n) + return versions.Before(goversion, versions.Go1_22) case *ast.RangeStmt: body = n.Body addVar(n.Key) @@ -367,9 +367,6 @@ func isMethodCall(info *types.Info, expr ast.Expr, pkgPath, typeName, method str // Check that the receiver is a . or // *.. - rtype := recv.Type() - if ptr, ok := recv.Type().(*types.Pointer); ok { - rtype = ptr.Elem() - } - return analysisutil.IsNamedType(rtype, pkgPath, typeName) + _, named := typesinternal.ReceiverNamed(recv) + return analysisutil.IsNamedType(named, pkgPath, typeName) } diff --git a/go/analysis/passes/loopclosure/loopclosure_test.go b/go/analysis/passes/loopclosure/loopclosure_test.go index 386f53289ce..683f91e7b73 100644 --- a/go/analysis/passes/loopclosure/loopclosure_test.go +++ b/go/analysis/passes/loopclosure/loopclosure_test.go @@ -17,6 +17,9 @@ import ( ) func Test(t *testing.T) { + // legacy loopclosure test expectations are incorrect > 1.21. + testenv.SkipAfterGo1Point(t, 21) + testdata := analysistest.TestData() analysistest.Run(t, testdata, loopclosure.Analyzer, "a", "golang.org/...", "subtests", "typeparams") @@ -30,8 +33,6 @@ func TestVersions22(t *testing.T) { } func TestVersions18(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - testfile := filepath.Join(analysistest.TestData(), "src", "versions", "go18.txtar") runTxtarFile(t, testfile, loopclosure.Analyzer, "golang.org/fake/versions") } diff --git a/go/analysis/passes/loopclosure/testdata/src/a/a.go b/go/analysis/passes/loopclosure/testdata/src/a/a.go index 7a7f05f663f..eb4d2a6cc7a 100644 --- a/go/analysis/passes/loopclosure/testdata/src/a/a.go +++ b/go/analysis/passes/loopclosure/testdata/src/a/a.go @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This file contains tests for the loopclosure checker. +// This file contains legacy tests for the loopclosure checker. +// Legacy expectations are incorrect after go1.22. package testdata diff --git a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go index 50283ec6152..faf98387c5d 100644 --- a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go +++ b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This file contains tests that the loopclosure analyzer detects leaked +// This file contains legacy tests that the loopclosure analyzer detects leaked // references via parallel subtests. +// Legacy expectations are incorrect after go1.22. package subtests diff --git a/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go b/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go index 55e129c0ab0..85976873b9a 100644 --- a/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This file contains tests for the loopclosure checker. +// This file contains legacy tests for the loopclosure checker for GoVersion 3 { + _ = slice[2] // want "index of nil slice" + } + for i := 0; i < len(slice); i++ { + _ = slice[i] // want "index of nil slice" + } + } + + if array == nil { + // (The v var is necessary, otherwise the SSA + // code doesn't dereference the pointer.) + for _, v := range array { // want "nil dereference in array index operation" + _ = v + } + } + + if m == nil { + for range m { // want "range over nil map" + } + m["one"] = 1 // want "nil dereference in map update" + } + + if ch == nil { + for range ch { // want "receive from nil channel" + } + <-ch // want "receive from nil channel" + ch <- 0 // want "send to nil channel" + } +} diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index 070654f0124..32350192583 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -24,6 +24,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -959,6 +960,8 @@ func isStringer(sig *types.Signature) bool { // It is almost always a mistake to print a function value. func isFunctionValue(pass *analysis.Pass, e ast.Expr) bool { if typ := pass.TypesInfo.Types[e].Type; typ != nil { + // Don't call Underlying: a named func type with a String method is ok. + // TODO(adonovan): it would be more precise to check isStringer. _, ok := typ.(*types.Signature) return ok } @@ -1010,7 +1013,7 @@ func checkPrint(pass *analysis.Pass, call *ast.CallExpr, fn *types.Func) { // Skip checking functions with unknown type. return } - if sig, ok := typ.(*types.Signature); ok { + if sig, ok := typ.Underlying().(*types.Signature); ok { if !sig.Variadic() { // Skip checking non-variadic functions. return @@ -1020,7 +1023,7 @@ func checkPrint(pass *analysis.Pass, call *ast.CallExpr, fn *types.Func) { typ := params.At(firstArg).Type() typ = typ.(*types.Slice).Elem() - it, ok := typ.(*types.Interface) + it, ok := aliases.Unalias(typ).(*types.Interface) if !ok || !it.Empty() { // Skip variadic functions accepting non-interface{} args. return diff --git a/go/analysis/passes/printf/printf_test.go b/go/analysis/passes/printf/printf_test.go index e206a3f34b5..853d8619b25 100644 --- a/go/analysis/passes/printf/printf_test.go +++ b/go/analysis/passes/printf/printf_test.go @@ -9,12 +9,9 @@ import ( "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/printf" - "golang.org/x/tools/internal/testenv" ) func Test(t *testing.T) { - testenv.NeedsGo1Point(t, 19) // tests use fmt.Appendf - testdata := analysistest.TestData() printf.Analyzer.Flags.Set("funcs", "Warn,Warnf") diff --git a/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go b/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go index c4d7e530d93..08bdb471dd1 100644 --- a/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go +++ b/go/analysis/passes/printf/testdata/src/typeparams/diagnostics.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package typeparams import "fmt" diff --git a/go/analysis/passes/printf/testdata/src/typeparams/wrappers.go b/go/analysis/passes/printf/testdata/src/typeparams/wrappers.go index df8a6fa6e6a..05487ab4e60 100644 --- a/go/analysis/passes/printf/testdata/src/typeparams/wrappers.go +++ b/go/analysis/passes/printf/testdata/src/typeparams/wrappers.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package typeparams import "fmt" diff --git a/go/analysis/passes/printf/types.go b/go/analysis/passes/printf/types.go index ab98e569980..017c8a247ec 100644 --- a/go/analysis/passes/printf/types.go +++ b/go/analysis/passes/printf/types.go @@ -10,6 +10,7 @@ import ( "go/types" "golang.org/x/tools/go/analysis" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -72,7 +73,7 @@ func (m *argMatcher) match(typ types.Type, topLevel bool) bool { return true } - if typ, _ := typ.(*types.TypeParam); typ != nil { + if typ, _ := aliases.Unalias(typ).(*types.TypeParam); typ != nil { // Avoid infinite recursion through type parameters. if m.seen[typ] { return true @@ -275,7 +276,7 @@ func (m *argMatcher) match(typ types.Type, topLevel bool) bool { } func isConvertibleToString(typ types.Type) bool { - if bt, ok := typ.(*types.Basic); ok && bt.Kind() == types.UntypedNil { + if bt, ok := aliases.Unalias(typ).(*types.Basic); ok && bt.Kind() == types.UntypedNil { // We explicitly don't want untyped nil, which is // convertible to both of the interfaces below, as it // would just panic anyway. diff --git a/go/analysis/passes/shift/shift.go b/go/analysis/passes/shift/shift.go index e272df709f3..d01eb1eebe5 100644 --- a/go/analysis/passes/shift/shift.go +++ b/go/analysis/passes/shift/shift.go @@ -21,6 +21,7 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -89,7 +90,8 @@ func checkLongShift(pass *analysis.Pass, node ast.Node, x, y ast.Expr) { if v == nil { return } - amt, ok := constant.Int64Val(v) + u := constant.ToInt(v) // either an Int or Unknown + amt, ok := constant.Int64Val(u) if !ok { return } @@ -98,7 +100,7 @@ func checkLongShift(pass *analysis.Pass, node ast.Node, x, y ast.Expr) { return } var structuralTypes []types.Type - switch t := t.(type) { + switch t := aliases.Unalias(t).(type) { case *types.TypeParam: terms, err := typeparams.StructuralTerms(t) if err != nil { diff --git a/go/analysis/passes/shift/testdata/src/a/a.go b/go/analysis/passes/shift/testdata/src/a/a.go index 796fcaa6ec4..558ece6bf8f 100644 --- a/go/analysis/passes/shift/testdata/src/a/a.go +++ b/go/analysis/passes/shift/testdata/src/a/a.go @@ -153,3 +153,8 @@ func ShiftDeadCode() { _ = i << 64 // want "too small for shift" } } + +func issue65939() { + a := 1 + println(a << 2.0) +} diff --git a/go/analysis/passes/slog/slog.go b/go/analysis/passes/slog/slog.go index a1323c3e666..b3c683b61cb 100644 --- a/go/analysis/passes/slog/slog.go +++ b/go/analysis/passes/slog/slog.go @@ -20,6 +20,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typesinternal" ) //go:embed doc.go @@ -150,14 +151,10 @@ func isAttr(t types.Type) bool { func shortName(fn *types.Func) string { var r string if recv := fn.Type().(*types.Signature).Recv(); recv != nil { - t := recv.Type() - if pt, ok := t.(*types.Pointer); ok { - t = pt.Elem() - } - if nt, ok := t.(*types.Named); ok { - r = nt.Obj().Name() + if _, named := typesinternal.ReceiverNamed(recv); named != nil { + r = named.Obj().Name() } else { - r = recv.Type().String() + r = recv.Type().String() // anon struct/interface } r += "." } @@ -173,17 +170,12 @@ func kvFuncSkipArgs(fn *types.Func) (int, bool) { return 0, false } var recvName string // by default a slog package function - recv := fn.Type().(*types.Signature).Recv() - if recv != nil { - t := recv.Type() - if pt, ok := t.(*types.Pointer); ok { - t = pt.Elem() - } - if nt, ok := t.(*types.Named); !ok { - return 0, false - } else { - recvName = nt.Obj().Name() + if recv := fn.Type().(*types.Signature).Recv(); recv != nil { + _, named := typesinternal.ReceiverNamed(recv) + if named == nil { + return 0, false // anon struct/interface } + recvName = named.Obj().Name() } skip, ok := kvFuncs[recvName][fn.Name()] return skip, ok diff --git a/go/analysis/passes/stdmethods/testdata/src/a/b.go b/go/analysis/passes/stdmethods/testdata/src/a/b.go index c0a16fb0426..9cf3994858b 100644 --- a/go/analysis/passes/stdmethods/testdata/src/a/b.go +++ b/go/analysis/passes/stdmethods/testdata/src/a/b.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package a type H int diff --git a/go/analysis/passes/stringintconv/string.go b/go/analysis/passes/stringintconv/string.go index 005e2e54b7d..16a4b3e5516 100644 --- a/go/analysis/passes/stringintconv/string.go +++ b/go/analysis/passes/stringintconv/string.go @@ -60,10 +60,12 @@ func describe(typ, inType types.Type, inName string) string { } func typeName(typ types.Type) string { - if v, _ := typ.(interface{ Name() string }); v != nil { + typ = aliases.Unalias(typ) + // TODO(adonovan): don't discard alias type, return its name. + if v, _ := typ.(*types.Basic); v != nil { return v.Name() } - if v, _ := typ.(interface{ Obj() *types.TypeName }); v != nil { + if v, _ := typ.(interface{ Obj() *types.TypeName }); v != nil { // Named, TypeParam return v.Obj().Name() } return "" diff --git a/go/analysis/passes/testinggoroutine/testinggoroutine.go b/go/analysis/passes/testinggoroutine/testinggoroutine.go index dc5307a15d0..828f95bc862 100644 --- a/go/analysis/passes/testinggoroutine/testinggoroutine.go +++ b/go/analysis/passes/testinggoroutine/testinggoroutine.go @@ -17,6 +17,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" ) //go:embed doc.go @@ -270,7 +271,7 @@ func forbiddenMethod(info *types.Info, call *ast.CallExpr) (*types.Var, *types.S func formatMethod(sel *types.Selection, fn *types.Func) string { var ptr string rtype := sel.Recv() - if p, ok := rtype.(*types.Pointer); ok { + if p, ok := aliases.Unalias(rtype).(*types.Pointer); ok { ptr = "*" rtype = p.Elem() } diff --git a/go/analysis/passes/testinggoroutine/util.go b/go/analysis/passes/testinggoroutine/util.go index d156851db17..ad815f19010 100644 --- a/go/analysis/passes/testinggoroutine/util.go +++ b/go/analysis/passes/testinggoroutine/util.go @@ -30,7 +30,7 @@ func localFunctionDecls(info *types.Info, files []*ast.File) func(*types.Func) * } } } - // TODO: once we only support go1.19+, set f = f.Origin() here. + // TODO: set f = f.Origin() here. return fnDecls[f] } } diff --git a/go/analysis/passes/tests/testdata/src/a/go118_test.go b/go/analysis/passes/tests/testdata/src/a/go118_test.go index e2bc3f3a0bd..a2ed9a4496b 100644 --- a/go/analysis/passes/tests/testdata/src/a/go118_test.go +++ b/go/analysis/passes/tests/testdata/src/a/go118_test.go @@ -1,6 +1,3 @@ -//go:build go1.18 -// +build go1.18 - package a import ( diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go index 6db12f3cb9a..39d0d9e429e 100644 --- a/go/analysis/passes/tests/tests.go +++ b/go/analysis/passes/tests/tests.go @@ -252,6 +252,8 @@ func validateFuzzArgs(pass *analysis.Pass, params *types.Tuple, expr ast.Expr) b } func isTestingType(typ types.Type, testingType string) bool { + // No Unalias here: I doubt "go test" recognizes + // "type A = *testing.T; func Test(A) {}" as a test. ptr, ok := typ.(*types.Pointer) if !ok { return false diff --git a/go/analysis/passes/timeformat/timeformat.go b/go/analysis/passes/timeformat/timeformat.go index eb84502bd99..4a6c6b8bc6c 100644 --- a/go/analysis/passes/timeformat/timeformat.go +++ b/go/analysis/passes/timeformat/timeformat.go @@ -107,7 +107,7 @@ func badFormatAt(info *types.Info, e ast.Expr) int { return -1 } - t, ok := tv.Type.(*types.Basic) + t, ok := tv.Type.(*types.Basic) // sic, no unalias if !ok || t.Info()&types.IsString == 0 { return -1 } diff --git a/go/analysis/passes/unmarshal/unmarshal.go b/go/analysis/passes/unmarshal/unmarshal.go index f4e73528b43..a7889fa4590 100644 --- a/go/analysis/passes/unmarshal/unmarshal.go +++ b/go/analysis/passes/unmarshal/unmarshal.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typesinternal" ) //go:embed doc.go @@ -69,12 +70,8 @@ func run(pass *analysis.Pass) (interface{}, error) { // (*"encoding/json".Decoder).Decode // (* "encoding/gob".Decoder).Decode // (* "encoding/xml".Decoder).Decode - t := recv.Type() - if ptr, ok := t.(*types.Pointer); ok { - t = ptr.Elem() - } - tname := t.(*types.Named).Obj() - if tname.Name() == "Decoder" { + _, named := typesinternal.ReceiverNamed(recv) + if tname := named.Obj(); tname.Name() == "Decoder" { switch tname.Pkg().Path() { case "encoding/json", "encoding/xml", "encoding/gob": argidx = 0 // func(interface{}) diff --git a/go/analysis/passes/unsafeptr/unsafeptr.go b/go/analysis/passes/unsafeptr/unsafeptr.go index 32e71ef979d..14e4a6c1e4b 100644 --- a/go/analysis/passes/unsafeptr/unsafeptr.go +++ b/go/analysis/passes/unsafeptr/unsafeptr.go @@ -17,6 +17,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/aliases" ) //go:embed doc.go @@ -88,7 +89,7 @@ func isSafeUintptr(info *types.Info, x ast.Expr) bool { // by the time we get to the conversion at the end. // For now approximate by saying that *Header is okay // but Header is not. - pt, ok := info.Types[x.X].Type.(*types.Pointer) + pt, ok := aliases.Unalias(info.Types[x.X].Type).(*types.Pointer) if ok && isReflectHeader(pt.Elem()) { return true } diff --git a/go/analysis/passes/unusedresult/testdata/src/typeparams/typeparams.go b/go/analysis/passes/unusedresult/testdata/src/typeparams/typeparams.go index 04d0e305842..0add516ac94 100644 --- a/go/analysis/passes/unusedresult/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/unusedresult/testdata/src/typeparams/typeparams.go @@ -1,8 +1,6 @@ // Copyright 2015 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. -// -//go:build go1.18 package typeparams diff --git a/go/analysis/passes/unusedresult/testdata/src/typeparams/userdefs/userdefs.go b/go/analysis/passes/unusedresult/testdata/src/typeparams/userdefs/userdefs.go index 218cc9ac75e..e31c6257469 100644 --- a/go/analysis/passes/unusedresult/testdata/src/typeparams/userdefs/userdefs.go +++ b/go/analysis/passes/unusedresult/testdata/src/typeparams/userdefs/userdefs.go @@ -1,8 +1,6 @@ // Copyright 2015 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. -// -//go:build go1.18 package userdefs @@ -25,4 +23,4 @@ type MultiTypeParam[T any, U any] struct { func (_ *MultiTypeParam[T, U]) String() string { return "MultiTypeParam" -} \ No newline at end of file +} diff --git a/go/analysis/passes/unusedwrite/unusedwrite.go b/go/analysis/passes/unusedwrite/unusedwrite.go index f5d0f116cad..a01cbb8f83a 100644 --- a/go/analysis/passes/unusedwrite/unusedwrite.go +++ b/go/analysis/passes/unusedwrite/unusedwrite.go @@ -13,6 +13,7 @@ import ( "golang.org/x/tools/go/analysis/passes/buildssa" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/aliases" ) //go:embed doc.go @@ -124,10 +125,7 @@ func isDeadStore(store *ssa.Store, obj ssa.Value, addr ssa.Instruction) bool { // isStructOrArray returns whether the underlying type is struct or array. func isStructOrArray(tp types.Type) bool { - if named, ok := tp.(*types.Named); ok { - tp = named.Underlying() - } - switch tp.(type) { + switch tp.Underlying().(type) { case *types.Array: return true case *types.Struct: @@ -145,7 +143,7 @@ func hasStructOrArrayType(v ssa.Value) bool { // func (t T) f() { ...} // the receiver object is of type *T: // t0 = local T (t) *T - if tp, ok := alloc.Type().(*types.Pointer); ok { + if tp, ok := aliases.Unalias(alloc.Type()).(*types.Pointer); ok { return isStructOrArray(tp.Elem()) } return false @@ -159,13 +157,14 @@ func hasStructOrArrayType(v ssa.Value) bool { // // For example, for struct T {x int, y int), getFieldName(*T, 1) returns "y". func getFieldName(tp types.Type, index int) string { - if pt, ok := tp.(*types.Pointer); ok { + // TODO(adonovan): use + // stp, ok := typeparams.Deref(tp).Underlying().(*types.Struct); ok { + // when Deref is defined. But see CL 565456 for a better fix. + + if pt, ok := aliases.Unalias(tp).(*types.Pointer); ok { tp = pt.Elem() } - if named, ok := tp.(*types.Named); ok { - tp = named.Underlying() - } - if stp, ok := tp.(*types.Struct); ok { + if stp, ok := tp.Underlying().(*types.Struct); ok { return stp.Field(index).Name() } return fmt.Sprintf("%d", index) diff --git a/go/analysis/unitchecker/separate_test.go b/go/analysis/unitchecker/separate_test.go index cf0143f8203..37e74e481ec 100644 --- a/go/analysis/unitchecker/separate_test.go +++ b/go/analysis/unitchecker/separate_test.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 - package unitchecker_test // This file illustrates separate analysis with an example. diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go index 9f41c71f9a3..54d8fa81851 100644 --- a/go/analysis/unitchecker/unitchecker_test.go +++ b/go/analysis/unitchecker/unitchecker_test.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 - package unitchecker_test import ( @@ -169,6 +167,13 @@ func _() { cmd.Env = append(exported.Config.Env, "ENTRYPOINT=minivet") cmd.Dir = exported.Config.Dir + // TODO(golang/go#65729): this is unsound: any extra + // logging by the child process (e.g. due to GODEBUG + // options) will add noise to stderr, causing the + // CombinedOutput to be unparseable as JSON. But we + // can't simply use Output here as some of the tests + // look for substrings of stderr. Rework the test to + // be specific about which output stream to match. out, err := cmd.CombinedOutput() exitcode := 0 if exitErr, ok := err.(*exec.ExitError); ok { diff --git a/go/callgraph/rta/rta.go b/go/callgraph/rta/rta.go index 72b383dabe7..cd3afa0be74 100644 --- a/go/callgraph/rta/rta.go +++ b/go/callgraph/rta/rta.go @@ -46,7 +46,6 @@ import ( "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/aliases" - "golang.org/x/tools/internal/compat" ) // A Result holds the results of Rapid Type Analysis, which includes the @@ -375,7 +374,7 @@ func (r *rta) interfaces(C types.Type) []*types.Interface { // and update the 'implements' relation. r.interfaceTypes.Iterate(func(I types.Type, v interface{}) { iinfo := v.(*interfaceTypeInfo) - if I := I.(*types.Interface); implements(cinfo, iinfo) { + if I := aliases.Unalias(I).(*types.Interface); implements(cinfo, iinfo) { iinfo.implementations = append(iinfo.implementations, C) cinfo.implements = append(cinfo.implements, I) } @@ -457,7 +456,7 @@ func (r *rta) addRuntimeType(T types.Type, skip bool) { // Each package maintains its own set of types it has visited. var n *types.Named - switch T := T.(type) { + switch T := aliases.Unalias(T).(type) { case *types.Named: n = T case *types.Pointer: @@ -546,7 +545,7 @@ func fingerprint(mset *types.MethodSet) uint64 { for i := 0; i < mset.Len(); i++ { method := mset.At(i).Obj() sig := method.Type().(*types.Signature) - sum := crc32.ChecksumIEEE(compat.Appendf(space[:], "%s/%d/%d", + sum := crc32.ChecksumIEEE(fmt.Appendf(space[:], "%s/%d/%d", method.Id(), sig.Params().Len(), sig.Results().Len())) diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index 4b5a65b3336..0abeab01bb1 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -12,6 +12,7 @@ import ( "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -410,7 +411,7 @@ func (b *builder) tassert(a *ssa.TypeAssert) { // The case where a is register so there // is a flow from a.X to a[0]. Here, a[0] is represented as an // indexedLocal: an entry into local tuple register a at index 0. - tup := a.Type().Underlying().(*types.Tuple) + tup := a.Type().(*types.Tuple) t := tup.At(0).Type() local := indexedLocal{val: a, typ: t, index: 0} @@ -421,7 +422,7 @@ func (b *builder) tassert(a *ssa.TypeAssert) { // and t1 where the source is indexed local representing a value // from tuple register t2 at index i and the target is t1. func (b *builder) extract(e *ssa.Extract) { - tup := e.Tuple.Type().Underlying().(*types.Tuple) + tup := e.Tuple.Type().(*types.Tuple) t := tup.At(e.Index).Type() local := indexedLocal{val: e.Tuple, typ: t, index: e.Index} @@ -527,7 +528,7 @@ func (b *builder) next(n *ssa.Next) { if n.IsString { return } - tup := n.Type().Underlying().(*types.Tuple) + tup := n.Type().(*types.Tuple) kt := tup.At(1).Type() vt := tup.At(2).Type() @@ -657,7 +658,7 @@ func addReturnFlows(b *builder, r *ssa.Return, site ssa.Value) { return } - tup := site.Type().Underlying().(*types.Tuple) + tup := site.Type().(*types.Tuple) for i, r := range results { local := indexedLocal{val: site, typ: tup.At(i).Type(), index: i} b.addInFlowEdge(b.nodeFromVal(r), local) @@ -671,7 +672,7 @@ func (b *builder) multiconvert(c *ssa.MultiConvert) { // This is a adaptation of x/exp/typeparams.NormalTerms which x/tools cannot depend on. var terms []*types.Term var err error - switch typ := typ.(type) { + switch typ := aliases.Unalias(typ).(type) { case *types.TypeParam: terms, err = typeparams.StructuralTerms(typ) case *types.Union: @@ -692,7 +693,7 @@ func (b *builder) multiconvert(c *ssa.MultiConvert) { } // isValuePreserving returns true if a conversion from ut_src to // ut_dst is value-preserving, i.e. just a change of type. - // Precondition: neither argument is a named type. + // Precondition: neither argument is a named or alias type. isValuePreserving := func(ut_src, ut_dst types.Type) bool { // Identical underlying types? if types.IdenticalIgnoreTags(ut_dst, ut_src) { @@ -740,7 +741,7 @@ func (b *builder) addInFlowEdge(s, d node) { // Creates const, pointer, global, func, and local nodes based on register instructions. func (b *builder) nodeFromVal(val ssa.Value) node { - if p, ok := val.Type().(*types.Pointer); ok && !types.IsInterface(p.Elem()) && !isFunction(p.Elem()) { + if p, ok := aliases.Unalias(val.Type()).(*types.Pointer); ok && !types.IsInterface(p.Elem()) && !isFunction(p.Elem()) { // Nested pointer to interfaces are modeled as a special // nestedPtrInterface node. if i := interfaceUnderPtr(p.Elem()); i != nil { diff --git a/go/callgraph/vta/graph_test.go b/go/callgraph/vta/graph_test.go index da574d71b53..060f67f7ae6 100644 --- a/go/callgraph/vta/graph_test.go +++ b/go/callgraph/vta/graph_test.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/go/callgraph/cha" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/aliases" ) func TestNodeInterface(t *testing.T) { @@ -35,7 +36,7 @@ func TestNodeInterface(t *testing.T) { reg := firstRegInstr(main) // t0 := *gl X := pkg.Type("X").Type() gl := pkg.Var("gl") - glPtrType, ok := gl.Type().(*types.Pointer) + glPtrType, ok := aliases.Unalias(gl.Type()).(*types.Pointer) if !ok { t.Fatalf("could not cast gl variable to pointer type") } diff --git a/go/callgraph/vta/utils.go b/go/callgraph/vta/utils.go index 3471aae3a10..ed248d73e0b 100644 --- a/go/callgraph/vta/utils.go +++ b/go/callgraph/vta/utils.go @@ -9,6 +9,7 @@ import ( "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -24,7 +25,7 @@ func isReferenceNode(n node) bool { return true } - if _, ok := n.Type().(*types.Pointer); ok { + if _, ok := aliases.Unalias(n.Type()).(*types.Pointer); ok { return true } @@ -166,6 +167,7 @@ func siteCallees(c ssa.CallInstruction, callgraph *callgraph.Graph) []*ssa.Funct } func canHaveMethods(t types.Type) bool { + t = aliases.Unalias(t) if _, ok := t.(*types.Named); ok { return true } diff --git a/go/callgraph/vta/vta_go117_test.go b/go/callgraph/vta/vta_go117_test.go deleted file mode 100644 index 6a5af2ced44..00000000000 --- a/go/callgraph/vta/vta_go117_test.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2021 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. - -//go:build go1.17 -// +build go1.17 - -package vta - -import ( - "testing" - - "golang.org/x/tools/go/callgraph/cha" - "golang.org/x/tools/go/ssa" - "golang.org/x/tools/go/ssa/ssautil" -) - -func TestVTACallGraphGo117(t *testing.T) { - file := "testdata/src/go117.go" - prog, want, err := testProg(file, ssa.BuilderMode(0)) - if err != nil { - t.Fatalf("couldn't load test file '%s': %s", file, err) - } - if len(want) == 0 { - t.Fatalf("couldn't find want in `%s`", file) - } - - g, _ := typePropGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) - got := vtaGraphStr(g) - if diff := setdiff(want, got); len(diff) != 0 { - t.Errorf("`%s`: want superset of %v;\n got %v", file, want, got) - } -} diff --git a/go/callgraph/vta/vta_test.go b/go/callgraph/vta/vta_test.go index 76c6611d2dd..76bd85e6fb7 100644 --- a/go/callgraph/vta/vta_test.go +++ b/go/callgraph/vta/vta_test.go @@ -144,3 +144,20 @@ func TestVTACallGraphGenerics(t *testing.T) { }) } } + +func TestVTACallGraphGo117(t *testing.T) { + file := "testdata/src/go117.go" + prog, want, err := testProg(file, ssa.BuilderMode(0)) + if err != nil { + t.Fatalf("couldn't load test file '%s': %s", file, err) + } + if len(want) == 0 { + t.Fatalf("couldn't find want in `%s`", file) + } + + g, _ := typePropGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) + got := vtaGraphStr(g) + if diff := setdiff(want, got); len(diff) != 0 { + t.Errorf("`%s`: want superset of %v;\n got %v", file, want, got) + } +} diff --git a/go/cfg/builder.go b/go/cfg/builder.go index dad6a444d82..ac4d63c4003 100644 --- a/go/cfg/builder.go +++ b/go/cfg/builder.go @@ -16,8 +16,8 @@ type builder struct { cfg *CFG mayReturn func(*ast.CallExpr) bool current *Block - lblocks map[*ast.Object]*lblock // labeled blocks - targets *targets // linked stack of branch targets + lblocks map[string]*lblock // labeled blocks + targets *targets // linked stack of branch targets } func (b *builder) stmt(_s ast.Stmt) { @@ -42,7 +42,7 @@ start: b.add(s) if call, ok := s.X.(*ast.CallExpr); ok && !b.mayReturn(call) { // Calls to panic, os.Exit, etc, never return. - b.current = b.newBlock("unreachable.call") + b.current = b.newBlock(KindUnreachable, s) } case *ast.DeclStmt: @@ -57,7 +57,7 @@ start: } case *ast.LabeledStmt: - label = b.labeledBlock(s.Label) + label = b.labeledBlock(s.Label, s) b.jump(label._goto) b.current = label._goto _s = s.Stmt @@ -65,7 +65,7 @@ start: case *ast.ReturnStmt: b.add(s) - b.current = b.newBlock("unreachable.return") + b.current = b.newBlock(KindUnreachable, s) case *ast.BranchStmt: b.branchStmt(s) @@ -77,11 +77,11 @@ start: if s.Init != nil { b.stmt(s.Init) } - then := b.newBlock("if.then") - done := b.newBlock("if.done") + then := b.newBlock(KindIfThen, s) + done := b.newBlock(KindIfDone, s) _else := done if s.Else != nil { - _else = b.newBlock("if.else") + _else = b.newBlock(KindIfElse, s) } b.add(s.Cond) b.ifelse(then, _else) @@ -128,7 +128,7 @@ func (b *builder) branchStmt(s *ast.BranchStmt) { switch s.Tok { case token.BREAK: if s.Label != nil { - if lb := b.labeledBlock(s.Label); lb != nil { + if lb := b.labeledBlock(s.Label, nil); lb != nil { block = lb._break } } else { @@ -139,7 +139,7 @@ func (b *builder) branchStmt(s *ast.BranchStmt) { case token.CONTINUE: if s.Label != nil { - if lb := b.labeledBlock(s.Label); lb != nil { + if lb := b.labeledBlock(s.Label, nil); lb != nil { block = lb._continue } } else { @@ -155,14 +155,14 @@ func (b *builder) branchStmt(s *ast.BranchStmt) { case token.GOTO: if s.Label != nil { - block = b.labeledBlock(s.Label)._goto + block = b.labeledBlock(s.Label, nil)._goto } } - if block == nil { - block = b.newBlock("undefined.branch") + if block == nil { // ill-typed (e.g. undefined label) + block = b.newBlock(KindUnreachable, s) } b.jump(block) - b.current = b.newBlock("unreachable.branch") + b.current = b.newBlock(KindUnreachable, s) } func (b *builder) switchStmt(s *ast.SwitchStmt, label *lblock) { @@ -172,7 +172,7 @@ func (b *builder) switchStmt(s *ast.SwitchStmt, label *lblock) { if s.Tag != nil { b.add(s.Tag) } - done := b.newBlock("switch.done") + done := b.newBlock(KindSwitchDone, s) if label != nil { label._break = done } @@ -188,13 +188,13 @@ func (b *builder) switchStmt(s *ast.SwitchStmt, label *lblock) { for i, clause := range s.Body.List { body := fallthru if body == nil { - body = b.newBlock("switch.body") // first case only + body = b.newBlock(KindSwitchCaseBody, clause) // first case only } // Preallocate body block for the next case. fallthru = done if i+1 < ncases { - fallthru = b.newBlock("switch.body") + fallthru = b.newBlock(KindSwitchCaseBody, s.Body.List[i+1]) } cc := clause.(*ast.CaseClause) @@ -208,7 +208,7 @@ func (b *builder) switchStmt(s *ast.SwitchStmt, label *lblock) { var nextCond *Block for _, cond := range cc.List { - nextCond = b.newBlock("switch.next") + nextCond = b.newBlock(KindSwitchNextCase, cc) b.add(cond) // one half of the tag==cond condition b.ifelse(body, nextCond) b.current = nextCond @@ -247,7 +247,7 @@ func (b *builder) typeSwitchStmt(s *ast.TypeSwitchStmt, label *lblock) { b.add(s.Assign) } - done := b.newBlock("typeswitch.done") + done := b.newBlock(KindSwitchDone, s) if label != nil { label._break = done } @@ -258,10 +258,10 @@ func (b *builder) typeSwitchStmt(s *ast.TypeSwitchStmt, label *lblock) { default_ = cc continue } - body := b.newBlock("typeswitch.body") + body := b.newBlock(KindSwitchCaseBody, cc) var next *Block for _, casetype := range cc.List { - next = b.newBlock("typeswitch.next") + next = b.newBlock(KindSwitchNextCase, cc) // casetype is a type, so don't call b.add(casetype). // This block logically contains a type assertion, // x.(casetype), but it's unclear how to represent x. @@ -300,7 +300,7 @@ func (b *builder) selectStmt(s *ast.SelectStmt, label *lblock) { } } - done := b.newBlock("select.done") + done := b.newBlock(KindSelectDone, s) if label != nil { label._break = done } @@ -312,8 +312,8 @@ func (b *builder) selectStmt(s *ast.SelectStmt, label *lblock) { defaultBody = &clause.Body continue } - body := b.newBlock("select.body") - next := b.newBlock("select.next") + body := b.newBlock(KindSelectCaseBody, clause) + next := b.newBlock(KindSelectAfterCase, clause) b.ifelse(body, next) b.current = body b.targets = &targets{ @@ -358,15 +358,15 @@ func (b *builder) forStmt(s *ast.ForStmt, label *lblock) { if s.Init != nil { b.stmt(s.Init) } - body := b.newBlock("for.body") - done := b.newBlock("for.done") // target of 'break' - loop := body // target of back-edge + body := b.newBlock(KindForBody, s) + done := b.newBlock(KindForDone, s) // target of 'break' + loop := body // target of back-edge if s.Cond != nil { - loop = b.newBlock("for.loop") + loop = b.newBlock(KindForLoop, s) } cont := loop // target of 'continue' if s.Post != nil { - cont = b.newBlock("for.post") + cont = b.newBlock(KindForPost, s) } if label != nil { label._break = done @@ -414,12 +414,12 @@ func (b *builder) rangeStmt(s *ast.RangeStmt, label *lblock) { // jump loop // done: (target of break) - loop := b.newBlock("range.loop") + loop := b.newBlock(KindRangeLoop, s) b.jump(loop) b.current = loop - body := b.newBlock("range.body") - done := b.newBlock("range.done") + body := b.newBlock(KindRangeBody, s) + done := b.newBlock(KindRangeDone, s) b.ifelse(body, done) b.current = body @@ -461,14 +461,19 @@ type lblock struct { // labeledBlock returns the branch target associated with the // specified label, creating it if needed. -func (b *builder) labeledBlock(label *ast.Ident) *lblock { - lb := b.lblocks[label.Obj] +func (b *builder) labeledBlock(label *ast.Ident, stmt *ast.LabeledStmt) *lblock { + lb := b.lblocks[label.Name] if lb == nil { - lb = &lblock{_goto: b.newBlock(label.Name)} + lb = &lblock{_goto: b.newBlock(KindLabel, nil)} if b.lblocks == nil { - b.lblocks = make(map[*ast.Object]*lblock) + b.lblocks = make(map[string]*lblock) } - b.lblocks[label.Obj] = lb + b.lblocks[label.Name] = lb + } + // Fill in the label later (in case of forward goto). + // Stmt may be set already if labels are duplicated (ill-typed). + if stmt != nil && lb._goto.Stmt == nil { + lb._goto.Stmt = stmt } return lb } @@ -477,11 +482,12 @@ func (b *builder) labeledBlock(label *ast.Ident) *lblock { // slice and returns it. // It does not automatically become the current block. // comment is an optional string for more readable debugging output. -func (b *builder) newBlock(comment string) *Block { +func (b *builder) newBlock(kind BlockKind, stmt ast.Stmt) *Block { g := b.cfg block := &Block{ - Index: int32(len(g.Blocks)), - comment: comment, + Index: int32(len(g.Blocks)), + Kind: kind, + Stmt: stmt, } block.Succs = block.succs2[:0] g.Blocks = append(g.Blocks, block) diff --git a/go/cfg/cfg.go b/go/cfg/cfg.go index e9c48d51daa..01668359af2 100644 --- a/go/cfg/cfg.go +++ b/go/cfg/cfg.go @@ -9,7 +9,10 @@ // // The blocks of the CFG contain all the function's non-control // statements. The CFG does not contain control statements such as If, -// Switch, Select, and Branch, but does contain their subexpressions. +// Switch, Select, and Branch, but does contain their subexpressions; +// also, each block records the control statement (Block.Stmt) that +// gave rise to it and its relationship (Block.Kind) to that statement. +// // For example, this source code: // // if x := f(); x != nil { @@ -20,14 +23,14 @@ // // produces this CFG: // -// 1: x := f() +// 1: x := f() Body // x != nil // succs: 2, 3 -// 2: T() +// 2: T() IfThen // succs: 4 -// 3: F() +// 3: F() IfElse // succs: 4 -// 4: +// 4: IfDone // // The CFG does contain Return statements; even implicit returns are // materialized (at the position of the function's closing brace). @@ -50,6 +53,7 @@ import ( // // The entry point is Blocks[0]; there may be multiple return blocks. type CFG struct { + fset *token.FileSet Blocks []*Block // block[0] is entry; order otherwise undefined } @@ -64,9 +68,63 @@ type Block struct { Succs []*Block // successor nodes in the graph Index int32 // index within CFG.Blocks Live bool // block is reachable from entry + Kind BlockKind // block kind + Stmt ast.Stmt // statement that gave rise to this block (see BlockKind for details) - comment string // for debugging - succs2 [2]*Block // underlying array for Succs + succs2 [2]*Block // underlying array for Succs +} + +// A BlockKind identifies the purpose of a block. +// It also determines the possible types of its Stmt field. +type BlockKind uint8 + +const ( + KindInvalid BlockKind = iota // Stmt=nil + + KindUnreachable // unreachable block after {Branch,Return}Stmt / no-return call ExprStmt + KindBody // function body BlockStmt + KindForBody // body of ForStmt + KindForDone // block after ForStmt + KindForLoop // head of ForStmt + KindForPost // post condition of ForStmt + KindIfDone // block after IfStmt + KindIfElse // else block of IfStmt + KindIfThen // then block of IfStmt + KindLabel // labeled block of BranchStmt (Stmt may be nil for dangling label) + KindRangeBody // body of RangeStmt + KindRangeDone // block after RangeStmt + KindRangeLoop // head of RangeStmt + KindSelectCaseBody // body of SelectStmt + KindSelectDone // block after SelectStmt + KindSelectAfterCase // block after a CommClause + KindSwitchCaseBody // body of CaseClause + KindSwitchDone // block after {Type.}SwitchStmt + KindSwitchNextCase // secondary expression of a multi-expression CaseClause +) + +func (kind BlockKind) String() string { + return [...]string{ + KindInvalid: "Invalid", + KindUnreachable: "Unreachable", + KindBody: "Body", + KindForBody: "ForBody", + KindForDone: "ForDone", + KindForLoop: "ForLoop", + KindForPost: "ForPost", + KindIfDone: "IfDone", + KindIfElse: "IfElse", + KindIfThen: "IfThen", + KindLabel: "Label", + KindRangeBody: "RangeBody", + KindRangeDone: "RangeDone", + KindRangeLoop: "RangeLoop", + KindSelectCaseBody: "SelectCaseBody", + KindSelectDone: "SelectDone", + KindSelectAfterCase: "SelectAfterCase", + KindSwitchCaseBody: "SwitchCaseBody", + KindSwitchDone: "SwitchDone", + KindSwitchNextCase: "SwitchNextCase", + }[kind] } // New returns a new control-flow graph for the specified function body, @@ -82,7 +140,7 @@ func New(body *ast.BlockStmt, mayReturn func(*ast.CallExpr) bool) *CFG { mayReturn: mayReturn, cfg: new(CFG), } - b.current = b.newBlock("entry") + b.current = b.newBlock(KindBody, body) b.stmt(body) // Compute liveness (reachability from entry point), breadth-first. @@ -110,7 +168,15 @@ func New(body *ast.BlockStmt, mayReturn func(*ast.CallExpr) bool) *CFG { } func (b *Block) String() string { - return fmt.Sprintf("block %d (%s)", b.Index, b.comment) + return fmt.Sprintf("block %d (%s)", b.Index, b.comment(nil)) +} + +func (b *Block) comment(fset *token.FileSet) string { + s := b.Kind.String() + if fset != nil && b.Stmt != nil { + s = fmt.Sprintf("%s@L%d", s, fset.Position(b.Stmt.Pos()).Line) + } + return s } // Return returns the return statement at the end of this block if present, nil @@ -129,7 +195,7 @@ func (b *Block) Return() (ret *ast.ReturnStmt) { func (g *CFG) Format(fset *token.FileSet) string { var buf bytes.Buffer for _, b := range g.Blocks { - fmt.Fprintf(&buf, ".%d: # %s\n", b.Index, b.comment) + fmt.Fprintf(&buf, ".%d: # %s\n", b.Index, b.comment(fset)) for _, n := range b.Nodes { fmt.Fprintf(&buf, "\t%s\n", formatNode(fset, n)) } @@ -145,6 +211,35 @@ func (g *CFG) Format(fset *token.FileSet) string { return buf.String() } +// digraph emits AT&T GraphViz (dot) syntax for the CFG. +// TODO(adonovan): publish; needs a proposal. +func (g *CFG) digraph(fset *token.FileSet) string { + var buf bytes.Buffer + buf.WriteString("digraph CFG {\n") + buf.WriteString(" node [shape=box];\n") + for _, b := range g.Blocks { + // node label + var text bytes.Buffer + text.WriteString(b.comment(fset)) + for _, n := range b.Nodes { + fmt.Fprintf(&text, "\n%s", formatNode(fset, n)) + } + + // node and edges + fmt.Fprintf(&buf, " n%d [label=%q];\n", b.Index, &text) + for _, succ := range b.Succs { + fmt.Fprintf(&buf, " n%d -> n%d;\n", b.Index, succ.Index) + } + } + buf.WriteString("}\n") + return buf.String() +} + +// exposed to main.go +func digraph(g *CFG, fset *token.FileSet) string { + return g.digraph(fset) +} + func formatNode(fset *token.FileSet, n ast.Node) string { var buf bytes.Buffer format.Node(&buf, fset, n) diff --git a/go/cfg/cfg_test.go b/go/cfg/cfg_test.go index f22bda34113..536d2fe5df7 100644 --- a/go/cfg/cfg_test.go +++ b/go/cfg/cfg_test.go @@ -2,15 +2,20 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package cfg +package cfg_test import ( "bytes" "fmt" "go/ast" + "go/format" "go/parser" "go/token" "testing" + + "golang.org/x/tools/go/cfg" + "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/testenv" ) const src = `package main @@ -140,7 +145,7 @@ func TestDeadCode(t *testing.T) { } for _, decl := range f.Decls { if decl, ok := decl.(*ast.FuncDecl); ok { - g := New(decl.Body, mayReturn) + g := cfg.New(decl.Body, mayReturn) // Print statements in unreachable blocks // (in order determined by builder). @@ -165,6 +170,57 @@ func TestDeadCode(t *testing.T) { } } +// TestSmoke runs the CFG builder on every FuncDecl in the standard +// library and x/tools. (This is all well-typed code, but it gives +// some coverage.) +func TestSmoke(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + testenv.NeedsTool(t, "go") + + // The Mode API is just hateful. + // https://github.com/golang/go/issues/48226#issuecomment-1948792315 + mode := packages.NeedDeps | packages.NeedImports | packages.NeedSyntax | packages.NeedTypes + pkgs, err := packages.Load(&packages.Config{Mode: mode}, "std", "golang.org/x/tools/...") + if err != nil { + t.Fatal(err) + } + + for _, pkg := range pkgs { + for _, file := range pkg.Syntax { + for _, decl := range file.Decls { + if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body != nil { + g := cfg.New(decl.Body, mayReturn) + + // Run a few quick sanity checks. + failed := false + for i, b := range g.Blocks { + errorf := func(format string, args ...any) { + if !failed { + t.Errorf("%s\n%s", pkg.Fset.Position(decl.Pos()), g.Format(pkg.Fset)) + failed = true + } + msg := fmt.Sprintf(format, args...) + t.Errorf("block %d: %s", i, msg) + } + + if b.Kind == cfg.KindInvalid { + errorf("invalid Block.Kind %v", b.Kind) + } + if b.Stmt == nil && b.Kind != cfg.KindLabel { + errorf("nil Block.Stmt (Kind=%v)", b.Kind) + } + if i != int(b.Index) { + errorf("invalid Block.Index") + } + } + } + } + } + } +} + // A trivial mayReturn predicate that looks only at syntax, not types. func mayReturn(call *ast.CallExpr) bool { switch fun := call.Fun.(type) { @@ -175,3 +231,10 @@ func mayReturn(call *ast.CallExpr) bool { } return true } + +func formatNode(fset *token.FileSet, n ast.Node) string { + var buf bytes.Buffer + format.Node(&buf, fset, n) + // Indent secondary lines by a tab. + return string(bytes.Replace(buf.Bytes(), []byte("\n"), []byte("\n\t"), -1)) +} diff --git a/go/cfg/main.go b/go/cfg/main.go new file mode 100644 index 00000000000..e25b36830be --- /dev/null +++ b/go/cfg/main.go @@ -0,0 +1,72 @@ +//go:build ignore + +// Copyright 2024 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. + +// The cfg command prints the control-flow graph of the first function +// or method whose name matches 'funcname' in the specified package. +// +// Usage: cfg package funcname +// +// Example: +// +// $ go build -o cfg ./go/cfg/main.go +// $ cfg ./go/cfg stmt | dot -Tsvg > cfg.svg && open cfg.svg +package main + +import ( + "flag" + "fmt" + "go/ast" + "go/token" + "log" + "os" + _ "unsafe" // for linkname + + "golang.org/x/tools/go/cfg" + "golang.org/x/tools/go/packages" +) + +func main() { + flag.Parse() + if len(flag.Args()) != 2 { + log.Fatal("Usage: package funcname") + } + pattern, funcname := flag.Args()[0], flag.Args()[1] + pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadSyntax}, pattern) + if err != nil { + log.Fatal(err) + } + if packages.PrintErrors(pkgs) > 0 { + os.Exit(1) + } + for _, pkg := range pkgs { + for _, f := range pkg.Syntax { + for _, decl := range f.Decls { + if decl, ok := decl.(*ast.FuncDecl); ok { + if decl.Name.Name == funcname { + g := cfg.New(decl.Body, mayReturn) + fmt.Println(digraph(g, pkg.Fset)) + os.Exit(0) + } + } + } + } + } + log.Fatalf("no function %q found in %s", funcname, pattern) +} + +// A trivial mayReturn predicate that looks only at syntax, not types. +func mayReturn(call *ast.CallExpr) bool { + switch fun := call.Fun.(type) { + case *ast.Ident: + return fun.Name != "panic" + case *ast.SelectorExpr: + return fun.Sel.Name != "Fatal" + } + return true +} + +//go:linkname digraph golang.org/x/tools/go/cfg.digraph +func digraph(g *cfg.CFG, fset *token.FileSet) string diff --git a/go/gcexportdata/gcexportdata.go b/go/gcexportdata/gcexportdata.go index 03543bd4bb8..137cc8df1d8 100644 --- a/go/gcexportdata/gcexportdata.go +++ b/go/gcexportdata/gcexportdata.go @@ -47,7 +47,7 @@ import ( func Find(importPath, srcDir string) (filename, path string) { cmd := exec.Command("go", "list", "-json", "-export", "--", importPath) cmd.Dir = srcDir - out, err := cmd.CombinedOutput() + out, err := cmd.Output() if err != nil { return "", "" } diff --git a/go/internal/cgo/cgo_pkgconfig.go b/go/internal/cgo/cgo_pkgconfig.go index b5bb95a63e5..2455be54f6e 100644 --- a/go/internal/cgo/cgo_pkgconfig.go +++ b/go/internal/cgo/cgo_pkgconfig.go @@ -15,12 +15,15 @@ import ( // pkgConfig runs pkg-config with the specified arguments and returns the flags it prints. func pkgConfig(mode string, pkgs []string) (flags []string, err error) { cmd := exec.Command("pkg-config", append([]string{mode}, pkgs...)...) - out, err := cmd.CombinedOutput() + out, err := cmd.Output() if err != nil { s := fmt.Sprintf("%s failed: %v", strings.Join(cmd.Args, " "), err) if len(out) > 0 { s = fmt.Sprintf("%s: %s", s, out) } + if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 { + s = fmt.Sprintf("%s\nstderr:\n%s", s, err.Stderr) + } return nil, errors.New(s) } if len(out) > 0 { diff --git a/go/internal/gccgoimporter/importer_test.go b/go/internal/gccgoimporter/importer_test.go index 7adffd0df80..d8c6e42f6ad 100644 --- a/go/internal/gccgoimporter/importer_test.go +++ b/go/internal/gccgoimporter/importer_test.go @@ -140,9 +140,12 @@ func TestObjImporter(t *testing.T) { t.Skip("no support yet for debug/xcoff") } - verout, err := exec.Command(gpath, "--version").CombinedOutput() + verout, err := exec.Command(gpath, "--version").Output() if err != nil { t.Logf("%s", verout) + if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { + t.Logf("stderr:\n%s", exit.Stderr) + } t.Fatal(err) } vers := regexp.MustCompile(`([0-9]+)\.([0-9]+)`).FindSubmatch(verout) @@ -182,8 +185,7 @@ func TestObjImporter(t *testing.T) { afile := filepath.Join(artmpdir, "lib"+test.pkgpath+".a") cmd := exec.Command(gpath, "-fgo-pkgpath="+test.pkgpath, "-c", "-o", ofile, gofile) - out, err := cmd.CombinedOutput() - if err != nil { + if out, err := cmd.CombinedOutput(); err != nil { t.Logf("%s", out) t.Fatalf("gccgo %s failed: %s", gofile, err) } @@ -191,8 +193,7 @@ func TestObjImporter(t *testing.T) { runImporterTest(t, imp, initmap, &test) cmd = exec.Command("ar", "cr", afile, ofile) - out, err = cmd.CombinedOutput() - if err != nil { + if out, err := cmd.CombinedOutput(); err != nil { t.Logf("%s", out) t.Fatalf("ar cr %s %s failed: %s", afile, ofile, err) } diff --git a/go/internal/gccgoimporter/parser.go b/go/internal/gccgoimporter/parser.go index 72bbe793c1e..dc40d217a88 100644 --- a/go/internal/gccgoimporter/parser.go +++ b/go/internal/gccgoimporter/parser.go @@ -20,6 +20,8 @@ import ( "strings" "text/scanner" "unicode/utf8" + + "golang.org/x/tools/internal/aliases" ) type parser struct { @@ -241,7 +243,7 @@ func (p *parser) parseName() string { } func deref(typ types.Type) types.Type { - if p, _ := typ.(*types.Pointer); p != nil { + if p, _ := aliases.Unalias(typ).(*types.Pointer); p != nil { typ = p.Elem() } return typ @@ -260,7 +262,7 @@ func (p *parser) parseField(pkg *types.Package) (field *types.Var, tag string) { if aname, ok := p.aliases[n]; ok { name = aname } else { - switch typ := deref(typ).(type) { + switch typ := aliases.Unalias(deref(typ)).(type) { case *types.Basic: name = typ.Name() case *types.Named: @@ -579,7 +581,7 @@ func (p *parser) parseNamedType(nlist []interface{}) types.Type { t := obj.Type() p.update(t, nlist) - nt, ok := t.(*types.Named) + nt, ok := aliases.Unalias(t).(*types.Named) if !ok { // This can happen for unsafe.Pointer, which is a TypeName holding a Basic type. pt := p.parseType(pkg) @@ -1334,7 +1336,7 @@ func (p *parser) parsePackage() *types.Package { } p.fixups = nil for _, typ := range p.typeList { - if it, ok := typ.(*types.Interface); ok { + if it, ok := aliases.Unalias(typ).(*types.Interface); ok { it.Complete() } } diff --git a/go/packages/gopackages/main.go b/go/packages/gopackages/main.go index bf0b5043fc6..706f13a99a0 100644 --- a/go/packages/gopackages/main.go +++ b/go/packages/gopackages/main.go @@ -104,6 +104,7 @@ func (app *application) Run(ctx context.Context, args ...string) error { default: return tool.CommandLineErrorf("invalid mode: %s", app.Mode) } + cfg.Mode |= packages.NeedModule lpkgs, err := packages.Load(cfg, args...) if err != nil { @@ -162,6 +163,9 @@ func (app *application) print(lpkg *packages.Package) { kind += "package" } fmt.Printf("Go %s %q:\n", kind, lpkg.ID) // unique ID + if mod := lpkg.Module; mod != nil { + fmt.Printf("\tmodule %s@%s\n", mod.Path, mod.Version) + } fmt.Printf("\tpackage %s\n", lpkg.Name) // characterize type info diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index e5687babfa7..f522df135a1 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -2444,9 +2444,6 @@ func testIssue37098(t *testing.T, exporter packagestest.Exporter) { // causes C++ sources to be inadvertently included in // (*Package).CompiledGoFiles. - // This is fixed in Go 1.17, but not earlier. - testenv.NeedsGo1Point(t, 17) - if _, err := exec.LookPath("swig"); err != nil { t.Skip("skipping test: swig not available") } diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 8622dfc53a8..72e906c3850 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -81,16 +81,15 @@ import ( "os" "sync" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/versions" ) -type opaqueType struct { - types.Type - name string -} +type opaqueType struct{ name string } -func (t *opaqueType) String() string { return t.name } +func (t *opaqueType) String() string { return t.name } +func (t *opaqueType) Underlying() types.Type { return t } var ( varOk = newVar("ok", tBool) @@ -103,7 +102,7 @@ var ( tInvalid = types.Typ[types.Invalid] tString = types.Typ[types.String] tUntypedNil = types.Typ[types.UntypedNil] - tRangeIter = &opaqueType{nil, "iter"} // the type of all "range" iterators + tRangeIter = &opaqueType{"iter"} // the type of all "range" iterators tEface = types.NewInterfaceType(nil, nil).Complete() // SSA Value constants. @@ -328,7 +327,7 @@ func (b *builder) builtin(fn *Function, obj *types.Builtin, args []ast.Expr, typ } case "new": - return emitNew(fn, mustDeref(typ), pos, "new") + return emitNew(fn, typeparams.MustDeref(typ), pos, "new") case "len", "cap": // Special case: len or cap of an array or *array is @@ -419,7 +418,7 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { wantAddr := true v := b.receiver(fn, e.X, wantAddr, escaping, sel) index := sel.index[len(sel.index)-1] - fld := fieldOf(mustDeref(v.Type()), index) // v is an addr. + fld := fieldOf(typeparams.MustDeref(v.Type()), index) // v is an addr. // Due to the two phases of resolving AssignStmt, a panic from x.f = p() // when x is nil is required to come after the side-effects of @@ -468,7 +467,7 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { v.setType(et) return fn.emit(v) } - return &lazyAddress{addr: emit, t: mustDeref(et), pos: e.Lbrack, expr: e} + return &lazyAddress{addr: emit, t: typeparams.MustDeref(et), pos: e.Lbrack, expr: e} case *ast.StarExpr: return &address{addr: b.expr(fn, e.X), pos: e.Star, expr: e} @@ -802,7 +801,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { if types.IsInterface(rt) { // If v may be an interface type I (after instantiating), // we must emit a check that v is non-nil. - if recv, ok := sel.recv.(*types.TypeParam); ok { + if recv, ok := aliases.Unalias(sel.recv).(*types.TypeParam); ok { // Emit a nil check if any possible instantiation of the // type parameter is an interface type. if typeSetOf(recv).Len() > 0 { @@ -1253,7 +1252,7 @@ func (b *builder) compLit(fn *Function, addr Value, e *ast.CompositeLit, isZero case *types.Array, *types.Slice: var at *types.Array var array Value - switch t := t.(type) { + switch t := aliases.Unalias(t).(type) { case *types.Slice: at = types.NewArray(t.Elem(), b.arrayLen(fn, e.Elts)) array = emitNew(fn, at, e.Lbrace, "slicelit") @@ -1748,8 +1747,7 @@ func (b *builder) forStmt(fn *Function, s *ast.ForStmt, label *lblock) { // Use forStmtGo122 instead if it applies. if s.Init != nil { if assign, ok := s.Init.(*ast.AssignStmt); ok && assign.Tok == token.DEFINE { - afterGo122 := versions.Compare(fn.goversion, "go1.21") > 0 - if afterGo122 { + if versions.AtLeast(fn.goversion, versions.Go1_22) { b.forStmtGo122(fn, s, label) return } @@ -2244,7 +2242,7 @@ func (b *builder) rangeStmt(fn *Function, s *ast.RangeStmt, label *lblock) { } } - afterGo122 := versions.Compare(fn.goversion, "go1.21") > 0 + afterGo122 := versions.AtLeast(fn.goversion, versions.Go1_22) if s.Tok == token.DEFINE && !afterGo122 { // pre-go1.22: If iteration variables are defined (:=), this // occurs once outside the loop. diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index a15ab97aca9..829ddc7afe7 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -25,6 +25,7 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/txtar" ) @@ -821,8 +822,6 @@ var indirect = R[int].M // TestTypeparamTest builds SSA over compilable examples in $GOROOT/test/typeparam/*.go. func TestTypeparamTest(t *testing.T) { - testenv.NeedsGo1Point(t, 19) // fails with infinite recursion at 1.18 -- not investigated - // Tests use a fake goroot to stub out standard libraries with delcarations in // testdata/src. Decreases runtime from ~80s to ~1s. @@ -999,7 +998,6 @@ func TestGenericFunctionSelector(t *testing.T) { func TestIssue58491(t *testing.T) { // Test that a local type reaches type param in instantiation. - testenv.NeedsGo1Point(t, 18) src := ` package p @@ -1057,7 +1055,6 @@ func TestIssue58491(t *testing.T) { func TestIssue58491Rec(t *testing.T) { // Roughly the same as TestIssue58491 but with a recursive type. - testenv.NeedsGo1Point(t, 18) src := ` package p @@ -1091,7 +1088,7 @@ func TestIssue58491Rec(t *testing.T) { // Find the local type result instantiated with int. var found bool for _, rt := range p.Prog.RuntimeTypes() { - if n, ok := rt.(*types.Named); ok { + if n, ok := aliases.Unalias(rt).(*types.Named); ok { if u, ok := n.Underlying().(*types.Struct); ok { found = true if got, want := n.String(), "p.result"; got != want { diff --git a/go/ssa/const.go b/go/ssa/const.go index 2a6ac5882a0..e0d79f5ef72 100644 --- a/go/ssa/const.go +++ b/go/ssa/const.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -47,7 +48,7 @@ func soleTypeKind(typ types.Type) types.BasicInfo { state := types.IsBoolean | types.IsInteger | types.IsString underIs(typeSetOf(typ), func(t types.Type) bool { var c types.BasicInfo - if t, ok := t.(*types.Basic); ok { + if t, ok := aliases.Unalias(t).(*types.Basic); ok { c = t.Info() } if c&types.IsNumeric != 0 { // int/float/complex @@ -113,7 +114,7 @@ func zeroString(t types.Type, from *types.Package) string { } case *types.Pointer, *types.Slice, *types.Interface, *types.Chan, *types.Map, *types.Signature: return "nil" - case *types.Named: + case *types.Named, *aliases.Alias: return zeroString(t.Underlying(), from) case *types.Array, *types.Struct: return relType(t, from) + "{}" diff --git a/go/ssa/coretype.go b/go/ssa/coretype.go index 88136b43842..3a512830b1f 100644 --- a/go/ssa/coretype.go +++ b/go/ssa/coretype.go @@ -7,6 +7,7 @@ package ssa import ( "go/types" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -49,7 +50,7 @@ func typeSetOf(typ types.Type) termList { // This is a adaptation of x/exp/typeparams.NormalTerms which x/tools cannot depend on. var terms []*types.Term var err error - switch typ := typ.(type) { + switch typ := aliases.Unalias(typ).(type) { case *types.TypeParam: terms, err = typeparams.StructuralTerms(typ) case *types.Union: diff --git a/go/ssa/create.go b/go/ssa/create.go index c4da35d0b08..f8f584a1a56 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -245,7 +245,7 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * if len(files) > 0 { // Go source package. for _, file := range files { - goversion := versions.Lang(versions.FileVersions(p.info, file)) + goversion := versions.Lang(versions.FileVersion(p.info, file)) for _, decl := range file.Decls { membersFromDecl(p, decl, goversion) } @@ -259,6 +259,7 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * obj := scope.Lookup(name) memberFromObject(p, obj, nil, "") if obj, ok := obj.(*types.TypeName); ok { + // No Unalias: aliases should not duplicate methods. if named, ok := obj.Type().(*types.Named); ok { for i, n := 0, named.NumMethods(); i < n; i++ { memberFromObject(p, named.Method(i), nil, "") diff --git a/go/ssa/emit.go b/go/ssa/emit.go index d77b4407a80..549c9114d43 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -11,6 +11,9 @@ import ( "go/ast" "go/token" "go/types" + + "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typeparams" ) // emitAlloc emits to f a new Alloc instruction allocating a variable @@ -64,7 +67,7 @@ func emitLocalVar(f *Function, v *types.Var) *Alloc { // new temporary, and returns the value so defined. func emitLoad(f *Function, addr Value) *UnOp { v := &UnOp{Op: token.MUL, X: addr} - v.setType(mustDeref(addr.Type())) + v.setType(typeparams.MustDeref(addr.Type())) f.emit(v) return v } @@ -182,7 +185,7 @@ func emitCompare(f *Function, op token.Token, x, y Value, pos token.Pos) Value { // isValuePreserving returns true if a conversion from ut_src to // ut_dst is value-preserving, i.e. just a change of type. -// Precondition: neither argument is a named type. +// Precondition: neither argument is a named or alias type. func isValuePreserving(ut_src, ut_dst types.Type) bool { // Identical underlying types? if types.IdenticalIgnoreTags(ut_dst, ut_src) { @@ -281,11 +284,11 @@ func emitConv(f *Function, val Value, typ types.Type) Value { } // Conversion from slice to array or slice to array pointer? - if slice, ok := s.(*types.Slice); ok { + if slice, ok := aliases.Unalias(s).(*types.Slice); ok { var arr *types.Array var ptr bool // Conversion from slice to array pointer? - switch d := d.(type) { + switch d := aliases.Unalias(d).(type) { case *types.Array: arr = d case *types.Pointer: @@ -414,7 +417,7 @@ func emitTypeCoercion(f *Function, v Value, typ types.Type) Value { // emitStore emits to f an instruction to store value val at location // addr, applying implicit conversions as required by assignability rules. func emitStore(f *Function, addr, val Value, pos token.Pos) *Store { - typ := mustDeref(addr.Type()) + typ := typeparams.MustDeref(addr.Type()) s := &Store{ Addr: addr, Val: emitConv(f, val, typ), diff --git a/go/ssa/func.go b/go/ssa/func.go index 22f878d4ed4..4d3e39129c5 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -14,6 +14,8 @@ import ( "io" "os" "strings" + + "golang.org/x/tools/internal/typeparams" ) // Like ObjectOf, but panics instead of returning nil. @@ -531,7 +533,7 @@ func WriteFunction(buf *bytes.Buffer, f *Function) { if len(f.Locals) > 0 { buf.WriteString("# Locals:\n") for i, l := range f.Locals { - fmt.Fprintf(buf, "# % 3d:\t%s %s\n", i, l.Name(), relType(mustDeref(l.Type()), from)) + fmt.Fprintf(buf, "# % 3d:\t%s %s\n", i, l.Name(), relType(typeparams.MustDeref(l.Type()), from)) } } writeSignature(buf, from, f.Name(), f.Signature) @@ -586,6 +588,12 @@ func WriteFunction(buf *bytes.Buffer, f *Function) { default: buf.WriteString(instr.String()) } + // -mode=S: show line numbers + if f.Prog.mode&LogSource != 0 { + if pos := instr.Pos(); pos.IsValid() { + fmt.Fprintf(buf, " L%d", f.Prog.Fset.Position(pos).Line) + } + } buf.WriteString("\n") } } diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go index 6f8e21a5536..2cd7ee98502 100644 --- a/go/ssa/interp/interp_test.go +++ b/go/ssa/interp/interp_test.go @@ -2,10 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This test fails at Go 1.18 due to infinite recursion in go/types. - -//go:build go1.19 - package interp_test // This test runs the SSA interpreter over sample Go programs. diff --git a/go/ssa/interp/ops.go b/go/ssa/interp/ops.go index 65d6452b783..62b635c20ac 100644 --- a/go/ssa/interp/ops.go +++ b/go/ssa/interp/ops.go @@ -17,6 +17,7 @@ import ( "unsafe" "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/aliases" ) // If the target program panics, the interpreter panics with this type. @@ -172,7 +173,7 @@ func asUnsigned(x value) (value, bool) { // zero returns a new "zero" value of the specified type. func zero(t types.Type) value { - switch t := t.(type) { + switch t := aliases.Unalias(t).(type) { case *types.Basic: if t.Kind() == types.UntypedNil { panic("untyped nil has no zero value") diff --git a/go/ssa/interp/reflect.go b/go/ssa/interp/reflect.go index 9f2f9e1e457..7df3ea27d8b 100644 --- a/go/ssa/interp/reflect.go +++ b/go/ssa/interp/reflect.go @@ -18,6 +18,7 @@ import ( "unsafe" "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/aliases" ) type opaqueType struct { @@ -119,7 +120,7 @@ func ext۰reflect۰rtype۰NumField(fr *frame, args []value) value { func ext۰reflect۰rtype۰NumIn(fr *frame, args []value) value { // Signature: func (t reflect.rtype) int - return args[0].(rtype).t.(*types.Signature).Params().Len() + return args[0].(rtype).t.Underlying().(*types.Signature).Params().Len() } func ext۰reflect۰rtype۰NumMethod(fr *frame, args []value) value { @@ -129,13 +130,13 @@ func ext۰reflect۰rtype۰NumMethod(fr *frame, args []value) value { func ext۰reflect۰rtype۰NumOut(fr *frame, args []value) value { // Signature: func (t reflect.rtype) int - return args[0].(rtype).t.(*types.Signature).Results().Len() + return args[0].(rtype).t.Underlying().(*types.Signature).Results().Len() } func ext۰reflect۰rtype۰Out(fr *frame, args []value) value { // Signature: func (t reflect.rtype, i int) int i := args[1].(int) - return makeReflectType(rtype{args[0].(rtype).t.(*types.Signature).Results().At(i).Type()}) + return makeReflectType(rtype{args[0].(rtype).t.Underlying().(*types.Signature).Results().At(i).Type()}) } func ext۰reflect۰rtype۰Size(fr *frame, args []value) value { @@ -178,7 +179,7 @@ func ext۰reflect۰Zero(fr *frame, args []value) value { } func reflectKind(t types.Type) reflect.Kind { - switch t := t.(type) { + switch t := aliases.Unalias(t).(type) { case *types.Named: return reflectKind(t.Underlying()) case *types.Basic: diff --git a/go/ssa/interp/value.go b/go/ssa/interp/value.go index 94018b550fc..d35da990ed1 100644 --- a/go/ssa/interp/value.go +++ b/go/ssa/interp/value.go @@ -45,6 +45,7 @@ import ( "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" ) type value interface{} @@ -117,7 +118,7 @@ func usesBuiltinMap(t types.Type) bool { switch t := t.(type) { case *types.Basic, *types.Chan, *types.Pointer: return true - case *types.Named: + case *types.Named, *aliases.Alias: return usesBuiltinMap(t.Underlying()) case *types.Interface, *types.Array, *types.Struct: return false diff --git a/go/ssa/lift.go b/go/ssa/lift.go index da49fe9f177..8bb1949449f 100644 --- a/go/ssa/lift.go +++ b/go/ssa/lift.go @@ -43,6 +43,8 @@ import ( "go/token" "math/big" "os" + + "golang.org/x/tools/internal/typeparams" ) // If true, show diagnostic information at each step of lifting. @@ -465,7 +467,7 @@ func liftAlloc(df domFrontier, alloc *Alloc, newPhis newPhiMap, fresh *int) bool *fresh++ phi.pos = alloc.Pos() - phi.setType(mustDeref(alloc.Type())) + phi.setType(typeparams.MustDeref(alloc.Type())) phi.block = v if debugLifting { fmt.Fprintf(os.Stderr, "\tplace %s = %s at block %s\n", phi.Name(), phi, v) @@ -510,7 +512,7 @@ func replaceAll(x, y Value) { func renamed(renaming []Value, alloc *Alloc) Value { v := renaming[alloc.index] if v == nil { - v = zeroConst(mustDeref(alloc.Type())) + v = zeroConst(typeparams.MustDeref(alloc.Type())) renaming[alloc.index] = v } return v diff --git a/go/ssa/lvalue.go b/go/ssa/lvalue.go index 186cfcae704..eede307eabd 100644 --- a/go/ssa/lvalue.go +++ b/go/ssa/lvalue.go @@ -11,6 +11,8 @@ import ( "go/ast" "go/token" "go/types" + + "golang.org/x/tools/internal/typeparams" ) // An lvalue represents an assignable location that may appear on the @@ -52,7 +54,7 @@ func (a *address) address(fn *Function) Value { } func (a *address) typ() types.Type { - return mustDeref(a.addr.Type()) + return typeparams.MustDeref(a.addr.Type()) } // An element is an lvalue represented by m[k], the location of an diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 4797b39286c..5f46a18484c 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -11,6 +11,7 @@ import ( "go/types" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -209,6 +210,9 @@ func forEachReachable(msets *typeutil.MethodSetCache, T types.Type, f func(types } switch T := T.(type) { + case *aliases.Alias: + visit(aliases.Unalias(T), false) + case *types.Basic: // nop diff --git a/go/ssa/parameterized.go b/go/ssa/parameterized.go index 84db49d392f..74c541107ef 100644 --- a/go/ssa/parameterized.go +++ b/go/ssa/parameterized.go @@ -8,6 +8,7 @@ import ( "go/types" "sync" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -48,6 +49,9 @@ func (w *tpWalker) isParameterizedLocked(typ types.Type) (res bool) { case nil, *types.Basic: // TODO(gri) should nil be handled here? break + case *aliases.Alias: + return w.isParameterizedLocked(aliases.Unalias(t)) + case *types.Array: return w.isParameterizedLocked(t.Elem()) diff --git a/go/ssa/print.go b/go/ssa/print.go index 727a7350265..38d8404fdc4 100644 --- a/go/ssa/print.go +++ b/go/ssa/print.go @@ -17,6 +17,7 @@ import ( "strings" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typeparams" ) // relName returns the name of v relative to i. @@ -94,7 +95,7 @@ func (v *Alloc) String() string { op = "new" } from := v.Parent().relPkg() - return fmt.Sprintf("%s %s (%s)", op, relType(mustDeref(v.Type()), from), v.Comment) + return fmt.Sprintf("%s %s (%s)", op, relType(typeparams.MustDeref(v.Type()), from), v.Comment) } func (v *Phi) String() string { @@ -260,7 +261,7 @@ func (v *MakeChan) String() string { func (v *FieldAddr) String() string { // Be robust against a bad index. name := "?" - if fld := fieldOf(mustDeref(v.X.Type()), v.Field); fld != nil { + if fld := fieldOf(typeparams.MustDeref(v.X.Type()), v.Field); fld != nil { name = fld.Name() } return fmt.Sprintf("&%s.%s [#%d]", relName(v.X, v), name, v.Field) @@ -449,7 +450,7 @@ func WritePackage(buf *bytes.Buffer, p *Package) { case *Global: fmt.Fprintf(buf, " var %-*s %s\n", - maxname, name, relType(mustDeref(mem.Type()), from)) + maxname, name, relType(typeparams.MustDeref(mem.Type()), from)) } } diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index 22a3c6bc3dc..13bd39fe862 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go @@ -349,7 +349,7 @@ func (s *sanity) checkBlock(b *BasicBlock, index int) { // Check that "untyped" types only appear on constant operands. if _, ok := (*op).(*Const); !ok { - if basic, ok := (*op).Type().(*types.Basic); ok { + if basic, ok := (*op).Type().Underlying().(*types.Basic); ok { if basic.Info()&types.IsUntyped != 0 { s.errorf("operand #%d of %s is untyped: %s", i, instr, basic) } diff --git a/go/ssa/subst.go b/go/ssa/subst.go index a9a6d41e813..9f2f2f30008 100644 --- a/go/ssa/subst.go +++ b/go/ssa/subst.go @@ -6,6 +6,8 @@ package ssa import ( "go/types" + + "golang.org/x/tools/internal/aliases" ) // Type substituter for a fixed set of replacement types. @@ -80,6 +82,9 @@ func (subst *subster) typ(t types.Type) (res types.Type) { // fall through if result r will be identical to t, types.Identical(r, t). switch t := t.(type) { + case *aliases.Alias: + return subst.typ(aliases.Unalias(t)) + case *types.TypeParam: r := subst.replacements[t] assert(r != nil, "type param without replacement encountered") @@ -466,7 +471,7 @@ func reaches(t types.Type, c map[types.Type]bool) (res bool) { return true } } - case *types.Named: + case *types.Named, *aliases.Alias: return reaches(t.Underlying(), c) default: panic("unreachable") diff --git a/go/ssa/util.go b/go/ssa/util.go index 6e9f1282b1b..4d65259ed9c 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -17,7 +17,9 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" ) //// Sanity checking utilities @@ -50,16 +52,19 @@ func isNonTypeParamInterface(t types.Type) bool { // isBasic reports whether t is a basic type. func isBasic(t types.Type) bool { - _, ok := t.(*types.Basic) + _, ok := aliases.Unalias(t).(*types.Basic) return ok } // isString reports whether t is exactly a string type. +// t is assumed to be an Underlying type (not Named or Alias). func isString(t types.Type) bool { - return isBasic(t) && t.(*types.Basic).Info()&types.IsString != 0 + basic, ok := t.(*types.Basic) + return ok && basic.Info()&types.IsString != 0 } // isByteSlice reports whether t is of the form []~bytes. +// t is assumed to be an Underlying type (not Named or Alias). func isByteSlice(t types.Type) bool { if b, ok := t.(*types.Slice); ok { e, _ := b.Elem().Underlying().(*types.Basic) @@ -69,6 +74,7 @@ func isByteSlice(t types.Type) bool { } // isRuneSlice reports whether t is of the form []~runes. +// t is assumed to be an Underlying type (not Named or Alias). func isRuneSlice(t types.Type) bool { if b, ok := t.(*types.Slice); ok { e, _ := b.Elem().Underlying().(*types.Basic) @@ -114,15 +120,6 @@ func deref(typ types.Type) (types.Type, bool) { return typ, false } -// mustDeref returns the element type of a type with a pointer core type. -// Panics on failure. -func mustDeref(typ types.Type) types.Type { - if et, ok := deref(typ); ok { - return et - } - panic("cannot dereference type " + typ.String()) -} - // recvType returns the receiver type of method obj. func recvType(obj *types.Func) types.Type { return obj.Type().(*types.Signature).Recv().Type() @@ -139,8 +136,9 @@ func fieldOf(typ types.Type, index int) *types.Var { return nil } -// isUntyped returns true for types that are untyped. +// isUntyped reports whether typ is the type of an untyped constant. func isUntyped(typ types.Type) bool { + // No Underlying/Unalias: untyped constant types cannot be Named or Alias. b, ok := typ.(*types.Basic) return ok && b.Info()&types.IsUntyped != 0 } @@ -180,17 +178,13 @@ func makeLen(T types.Type) *Builtin { } } -// receiverTypeArgs returns the type arguments to a function's receiver. -// Returns an empty list if obj does not have a receiver or its receiver does not have type arguments. -func receiverTypeArgs(obj *types.Func) []types.Type { - rtype := recvType(obj) - if rtype == nil { - return nil - } - rtype, _ = deptr(rtype) - named, ok := rtype.(*types.Named) - if !ok { - return nil +// receiverTypeArgs returns the type arguments to a method's receiver. +// Returns an empty list if the receiver does not have type arguments. +func receiverTypeArgs(method *types.Func) []types.Type { + recv := method.Type().(*types.Signature).Recv() + _, named := typesinternal.ReceiverNamed(recv) + if named == nil { + return nil // recv is anonymous struct/interface } ts := named.TypeArgs() if ts.Len() == 0 { @@ -354,10 +348,10 @@ func (m *typeListMap) hash(ts []types.Type) uint32 { // instantiateMethod instantiates m with targs and returns a canonical representative for this method. func (canon *canonizer) instantiateMethod(m *types.Func, targs []types.Type, ctxt *types.Context) *types.Func { recv := recvType(m) - if p, ok := recv.(*types.Pointer); ok { + if p, ok := aliases.Unalias(recv).(*types.Pointer); ok { recv = p.Elem() } - named := recv.(*types.Named) + named := aliases.Unalias(recv).(*types.Named) inst, err := types.Instantiate(ctxt, named.Origin(), targs, false) if err != nil { panic(err) diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go index 8692d51f0b7..845e26c0529 100644 --- a/go/types/internal/play/play.go +++ b/go/types/internal/play/play.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 - // The play program is a playground for go/types: a simple web-based // text editor into which the user can enter a Go program, select a // region, and see type information about it. @@ -32,6 +30,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -285,7 +284,7 @@ func formatObj(out *strings.Builder, fset *token.FileSet, ref string, obj types. if obj.IsAlias() { kind = "type alias" } - if named, ok := obj.Type().(*types.Named); ok { + if named, ok := aliases.Unalias(obj.Type()).(*types.Named); ok { origin = named.Obj() } } diff --git a/go/types/objectpath/objectpath.go b/go/types/objectpath/objectpath.go index 11d5c8c3adf..6a57ce3b136 100644 --- a/go/types/objectpath/objectpath.go +++ b/go/types/objectpath/objectpath.go @@ -29,9 +29,13 @@ import ( "strconv" "strings" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" ) +// TODO(adonovan): think about generic aliases. + // A Path is an opaque name that identifies a types.Object // relative to its package. Conceptually, the name consists of a // sequence of destructuring operations applied to the package scope @@ -223,7 +227,7 @@ func (enc *Encoder) For(obj types.Object) (Path, error) { // Reject obviously non-viable cases. switch obj := obj.(type) { case *types.TypeName: - if _, ok := obj.Type().(*types.TypeParam); !ok { + if _, ok := aliases.Unalias(obj.Type()).(*types.TypeParam); !ok { // With the exception of type parameters, only package-level type names // have a path. return "", fmt.Errorf("no path for %v", obj) @@ -310,7 +314,7 @@ func (enc *Encoder) For(obj types.Object) (Path, error) { } // Inspect declared methods of defined types. - if T, ok := o.Type().(*types.Named); ok { + if T, ok := aliases.Unalias(o.Type()).(*types.Named); ok { path = append(path, opType) // The method index here is always with respect // to the underlying go/types data structures, @@ -395,13 +399,8 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) { return "", false } - recvT := meth.Type().(*types.Signature).Recv().Type() - if ptr, ok := recvT.(*types.Pointer); ok { - recvT = ptr.Elem() - } - - named, ok := recvT.(*types.Named) - if !ok { + _, named := typesinternal.ReceiverNamed(meth.Type().(*types.Signature).Recv()) + if named == nil { return "", false } @@ -444,6 +443,8 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) { // nil, it will be allocated as necessary. func find(obj types.Object, T types.Type, path []byte, seen map[*types.TypeName]bool) []byte { switch T := T.(type) { + case *aliases.Alias: + return find(obj, aliases.Unalias(T), path, seen) case *types.Basic, *types.Named: // Named types belonging to pkg were handled already, // so T must belong to another package. No path. @@ -616,6 +617,7 @@ func Object(pkg *types.Package, p Path) (types.Object, error) { // Inv: t != nil, obj == nil + t = aliases.Unalias(t) switch code { case opElem: hasElem, ok := t.(hasElem) // Pointer, Slice, Array, Chan, Map diff --git a/go/types/objectpath/objectpath_go118_test.go b/go/types/objectpath/objectpath_go118_test.go index bc156e14d71..f061fd8d695 100644 --- a/go/types/objectpath/objectpath_go118_test.go +++ b/go/types/objectpath/objectpath_go118_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package objectpath_test import ( diff --git a/go/types/typeutil/ui.go b/go/types/typeutil/ui.go index fa55b0a1e65..a0c1a60ac02 100644 --- a/go/types/typeutil/ui.go +++ b/go/types/typeutil/ui.go @@ -6,7 +6,11 @@ package typeutil // This file defines utilities for user interfaces that display types. -import "go/types" +import ( + "go/types" + + "golang.org/x/tools/internal/aliases" +) // IntuitiveMethodSet returns the intuitive method set of a type T, // which is the set of methods you can call on an addressable value of @@ -24,7 +28,7 @@ import "go/types" // The order of the result is as for types.MethodSet(T). func IntuitiveMethodSet(T types.Type, msets *MethodSetCache) []*types.Selection { isPointerToConcrete := func(T types.Type) bool { - ptr, ok := T.(*types.Pointer) + ptr, ok := aliases.Unalias(T).(*types.Pointer) return ok && !types.IsInterface(ptr.Elem()) } diff --git a/godoc/godoc.go b/godoc/godoc.go index dfac2111a67..a9d806f7e8b 100644 --- a/godoc/godoc.go +++ b/godoc/godoc.go @@ -346,16 +346,9 @@ func isDigit(ch rune) bool { } func comment_htmlFunc(info *PageInfo, comment string) string { - var buf bytes.Buffer // TODO(gri) Provide list of words (e.g. function parameters) // to be emphasized by ToHTML. - - // godocToHTML is: - // - buf.Write(info.PDoc.HTML(comment)) on go1.19 - // - go/doc.ToHTML(&buf, comment, nil) on other versions - godocToHTML(&buf, info.PDoc, comment) - - return buf.String() + return string(info.PDoc.HTML(comment)) } // sanitizeFunc sanitizes the argument src by replacing newlines with diff --git a/godoc/tohtml_go119.go b/godoc/tohtml_go119.go deleted file mode 100644 index 6dbf7212b9a..00000000000 --- a/godoc/tohtml_go119.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2022 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. - -//go:build go1.19 -// +build go1.19 - -package godoc - -import ( - "bytes" - "go/doc" -) - -func godocToHTML(buf *bytes.Buffer, pkg *doc.Package, comment string) { - buf.Write(pkg.HTML(comment)) -} diff --git a/godoc/tohtml_other.go b/godoc/tohtml_other.go deleted file mode 100644 index a1dcf2e195b..00000000000 --- a/godoc/tohtml_other.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2022 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. - -//go:build !go1.19 -// +build !go1.19 - -package godoc - -import ( - "bytes" - "go/doc" -) - -func godocToHTML(buf *bytes.Buffer, pkg *doc.Package, comment string) { - doc.ToHTML(buf, comment, nil) -} diff --git a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go index dfa1cb69e55..7194ced9fdf 100644 --- a/gopls/api-diff/api_diff.go +++ b/gopls/api-diff/api_diff.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package main import ( diff --git a/gopls/doc/workspace.md b/gopls/doc/workspace.md index 4ff9994f939..cb26b3dcd43 100644 --- a/gopls/doc/workspace.md +++ b/gopls/doc/workspace.md @@ -1,101 +1,139 @@ # Setting up your workspace -`gopls` supports both Go module and GOPATH modes. However, it needs a defined -scope in which language features like references, rename, and implementation -should operate. - -The following options are available for configuring this scope: - -## Module mode - -### One module - -If you are working with a single module, you can open the module root (the -directory containing the `go.mod` file), a subdirectory within the module, -or a parent directory containing the module. - -**Note**: If you open a parent directory containing a module, it must **only** -contain that single module. Otherwise, you are working with multiple modules. - -### Multiple modules - -Gopls has several alternatives for working on multiple modules simultaneously, -described below. Starting with Go 1.18, Go workspaces are the preferred solution. - -#### Go workspaces (Go 1.18+) - -Starting with Go 1.18, the `go` command has native support for multi-module -workspaces, via [`go.work`](https://go.dev/ref/mod#workspaces) files. These -files are recognized by gopls starting with `gopls@v0.8.0`. - -The easiest way to work on multiple modules in Go 1.18 and later is therefore -to create a `go.work` file containing the modules you wish to work on, and set -your workspace root to the directory containing the `go.work` file. - -For example, suppose this repo is checked out into the `$WORK/tools` directory. -We can work on both `golang.org/x/tools` and `golang.org/x/tools/gopls` -simultaneously by creating a `go.work` file using `go work init`, followed by -`go work use MODULE_DIRECTORIES...` to add directories containing `go.mod` files to the -workspace: +In the language server protocol, a "workspace" consists of a folder along with +per-folder configuration. Some LSP clients such as VS Code allow configuring +workspaces explicitly, while others do so automatically by looking for special +files defining a workspace root (such as a `.git` directory or `go.mod` file). + +In order to function, gopls needs a defined scope in which language features +like references, rename, and implementation should operate. Put differently, +gopls needs to infer from the LSP workspace which `go build` invocations you +would use to build your workspace, including the working directory, +environment, and build flags. + +In the past, it could be tricky to set up your workspace so that gopls would +infer the correct build information. It required opening the correct directory +or using a `go.work` file to tell gopls about the modules you're working on, +and configuring the correct operating system and architecture in advance. +When this didn't work as expected, gopls would often fail in mysterious +ways--the dreaded "No packages found" error. + +Starting with gopls v0.15.0, workspace configuration is much simpler, and gopls +will typically work when you open a Go file anywhere in your workspace. If it +isn't working for you, or if you want to better understand how gopls models +your workspace, please read on. + +## Workspace builds + +Starting with gopls v0.15.0, gopls will guess the builds you are working on +based on the set of open files. When you open a file in a workspace folder, +gopls checks whether the file is contained in a module, `go.work` workspace, or +GOPATH directory, and configures the build accordingly. Additionally, if you +open a file that is constrained to a different operating system or +architecture, for example opening `foo_windows.go` when working on Linux, gopls +will create a scope with `GOOS` and `GOARCH` set to a value that matches the +file. + +For example, suppose we had a repository with three modules: `moda`, `modb`, +and `modc`, and a `go.work` file using modules `moda` and `modb`. If we open +the files `moda/a.go`, `modb/b.go`, `moda/a_windows.go`, and `modc/c.go`, gopls +will automatically create three builds: + +![Zero Config gopls](zeroconfig.png) + +This allows gopls to _just work_ when you open a Go file, but it does come with +several caveats: + +- It causes gopls to do more work, since it is now tracking three builds + instead of one. However, the recent + [scalability redesign](https://go.dev/blog/gopls-scalability) + allows much of this work to be avoided through efficient caching. +- For operations invoked from a given file, such as "References" + or "Implementations", gopls executes the operation in + _the default build for that file_. For example, finding references to + a symbol `S` from `foo_linux.go` will return references from the Linux build, + and finding references to the same symbol `S` from `foo_windows.go` will + return references from the Windows build. Gopls searches the default build + for the file, but it doesn't search all the other possible builds (even + though that would be nice) because it is liable to be too expensive. + Issues [#65757](https://go.dev/issue/65757) and + [#65755](https://go.dev/issue/65755) propose improvements to this behavior. +- When selecting a `GOOS/GOARCH` combination to match a build-constrained file, + gopls will choose the first matching combination from + [this list](https://cs.opensource.google/go/x/tools/+/master:gopls/internal/cache/port.go;l=30;drc=f872b3d6f05822d290bc7bdd29db090fd9d89f5c). + In some cases, that may be surprising. +- When working in a `GOOS/GOARCH` constrained file that does not match your + default toolchain, `CGO_ENABLED=0` is implicitly set, since a C toolchain for + that target is unlikely to be available. This means that gopls will not + work in files including `import "C"`. Issue + [#65758](https://go.dev/issue/65758) may lead to improvements in this + behavior. +- Gopls is currently unable to guess build flags that include arbitrary + user-defined build constraints, such as a file with the build directive + `//go:build mytag`. Issue [#65089](https://go.dev/issue/65089) proposes + a heuristic by which gopls could handle this automatically. + +Please provide feedback on this behavior by upvoting or commenting the issues +mentioned above, or opening a [new issue](https://go.dev/issue/new) for other +improvements you'd like to see. + +## When to use a `go.work` file for development + +Starting with Go 1.18, the `go` command has built-in support for multi-module +workspaces specified by [`go.work`](https://go.dev/ref/mod#workspaces) files. +Gopls will recognize these files if they are present in your workspace. + +Use a `go.work` file when: + +- you want to work on multiple modules simultaneously in a single logical + build, for example if you want changes to one module to be reflected in + another. +- you want to improve gopls' memory usage or performance by reducing the number + of builds it must track. +- you want gopls to know which modules you are working on in a multi-module + workspace, without opening any files. For example, it may be convenient to use + `workspace/symbol` queries before any files are open. +- you are using gopls v0.14.2 or earlier, and want to work on multiple + modules. + +For example, suppose this repo is checked out into the `$WORK/tools` directory, +and [`x/mod`](https://pkg.go.dev/golang.org/x/mod) is checked out into +`$WORK/mod`, and you are working on a new `x/mod` API for editing `go.mod` +files that you want to simultaneously integrate into gopls. + +You can work on both `golang.org/x/tools/gopls` and `golang.org/x/mod` +simultaneously by creating a `go.work` file: ```sh cd $WORK go work init -go work use ./tools/ ./tools/gopls/ +go work use tools/gopls mod ``` -...followed by opening the `$WORK` directory in our editor. - -#### DEPRECATED: Experimental workspace module (Go 1.17 and earlier) - -**This feature is deprecated and will be removed in future versions of gopls. -Please see [issue #52897](https://go.dev/issue/52897) for additional -information.** - -With earlier versions of Go, `gopls` can simulate multi-module workspaces by -creating a synthetic module requiring the modules in the workspace root. -See [the design document](https://github.com/golang/proposal/blob/master/design/37720-gopls-workspaces.md) -for more information. - -This feature is experimental, and will eventually be removed once `go.work` -files are accepted by all supported Go versions. - -You can enable this feature by configuring the -[experimentalWorkspaceModule](settings.md#experimentalworkspacemodule-bool) -setting. - -#### Multiple workspace folders - -If neither of the above solutions work, and your editor allows configuring the -set of -["workspace folders"](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceFolder) -used during your LSP session, you can still work on multiple modules by adding -a workspace folder at each module root (the locations of `go.mod` files). This -means that each module has its own scope, and features will not work across -modules. - -In VS Code, you can create a workspace folder by setting up a -[multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces). -View the [documentation for your editor plugin](../README.md#editor) to learn how to -configure a workspace folder in your editor. - -### GOPATH mode +then opening the `$WORK` directory in your editor. -When opening a directory within your GOPATH, the workspace scope will be just -that directory. +## When to manually configure `GOOS`, `GOARCH`, or `-tags` -### At your own risk +As described in the first section, gopls v0.15.0 and later will try to +configure a new build scope automatically when you open a file that doesn't +match the system default operating system (`GOOS`) or architecture (`GOARCH`). -Some users or companies may have projects that encompass one `$GOPATH`. If you -open your entire `$GOPATH` or `$GOPATH/src` folder, the workspace scope will be -your entire `GOPATH`. If your GOPATH is large, `gopls` to be very slow to start -because it will try to find all of the Go files in the directory you have -opened. It will then load all of the files it has found. +However, per the caveats listed in that section, this automatic behavior comes +with limitations. Customize your gopls environment by setting `GOOS` or +`GOARCH` in your +[`"build.env"`](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#env-mapstringstring) +or `-tags=...` in your" +["build.buildFlags"](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-string) +when: -To work around this case, you can create a new `$GOPATH` that contains only the -packages you want to work on. +- You want to modify the default build environment. +- Gopls is not guessing the `GOOS/GOARCH` combination you want to use for + cross platform development. +- You need to work on a file that is constrained by a user-defined build tags, + such as the build directive `//go:build mytag`. ---- +## GOPATH mode -If you have additional use cases that are not mentioned above, please -[file a new issue](https://github.com/golang/go/issues/new). +When opening a directory within a `GOPATH` directory, the workspace scope will +be just that directory and all directories contained within it. Note that +opening a large GOPATH directory can make gopls very slow to start. diff --git a/gopls/doc/zeroconfig.png b/gopls/doc/zeroconfig.png new file mode 100644 index 00000000000..49d4f8ead74 Binary files /dev/null and b/gopls/doc/zeroconfig.png differ diff --git a/gopls/go.mod b/gopls/go.mod index f17fdcaa522..cc810dc7ee9 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -1,19 +1,19 @@ module golang.org/x/tools/gopls -go 1.18 +go 1.19 require ( github.com/google/go-cmp v0.6.0 github.com/jba/printsrc v0.2.2 - github.com/jba/templatecheck v0.6.0 - golang.org/x/mod v0.15.0 + github.com/jba/templatecheck v0.7.0 + golang.org/x/mod v0.16.0 golang.org/x/sync v0.6.0 - golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78 + golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 golang.org/x/text v0.14.0 - golang.org/x/tools v0.17.0 + golang.org/x/tools v0.18.0 golang.org/x/vuln v1.0.1 gopkg.in/yaml.v3 v3.0.1 - honnef.co/go/tools v0.4.6 + honnef.co/go/tools v0.4.7 mvdan.cc/gofumpt v0.6.0 mvdan.cc/xurls/v2 v2.5.0 ) @@ -22,7 +22,7 @@ require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/google/safehtml v0.1.0 // indirect golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect - golang.org/x/sys v0.17.0 // indirect + golang.org/x/sys v0.18.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/gopls/go.sum b/gopls/go.sum index 9ab4bd75926..228800182b5 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -3,41 +3,39 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 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/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/jba/printsrc v0.2.2 h1:9OHK51UT+/iMAEBlQIIXW04qvKyF3/vvLuwW/hL8tDU= github.com/jba/printsrc v0.2.2/go.mod h1:1xULjw59sL0dPdWpDoVU06TIEO/Wnfv6AHRpiElTwYM= -github.com/jba/templatecheck v0.6.0 h1:SwM8C4hlK/YNLsdcXStfnHWE2HKkuTVwy5FKQHt5ro8= -github.com/jba/templatecheck v0.6.0/go.mod h1:/1k7EajoSErFI9GLHAsiIJEaNLt3ALKNw2TV7z2SYv4= +github.com/jba/templatecheck v0.7.0 h1:wjTb/VhGgSFeim5zjWVePBdaMo28X74bGLSABZV+zIA= +github.com/jba/templatecheck v0.7.0/go.mod h1:n1Etw+Rrw1mDDD8dDRsEKTwMZsJ98EkktgNJC6wLUGo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y= golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509 h1:Nr7eTQpQZ/ytesxDJpQgaf0t4sdLnnDtAbmtViTrSUo= -golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509/go.mod h1:ZthVHHkOi8rlMEsfFr3Ie42Ym1NonbFNNRKW3ci0UrU= -golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g= -golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= -golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78 h1:vcVnuftN4J4UKLRcgetjzfU9FjjgXUUYUc3JhFplgV4= -golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -49,8 +47,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= -honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= +honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= +honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= diff --git a/gopls/internal/analysis/deprecated/deprecated_test.go b/gopls/internal/analysis/deprecated/deprecated_test.go index 0242ef1fa09..89bf3bea252 100644 --- a/gopls/internal/analysis/deprecated/deprecated_test.go +++ b/gopls/internal/analysis/deprecated/deprecated_test.go @@ -8,11 +8,9 @@ import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/internal/testenv" ) func Test(t *testing.T) { - testenv.NeedsGo1Point(t, 19) testdata := analysistest.TestData() analysistest.Run(t, testdata, Analyzer, "a") } diff --git a/gopls/internal/analysis/fillswitch/doc.go b/gopls/internal/analysis/fillswitch/doc.go new file mode 100644 index 00000000000..076c3a1323d --- /dev/null +++ b/gopls/internal/analysis/fillswitch/doc.go @@ -0,0 +1,66 @@ +// Copyright 2024 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 fillswitch identifies switches with missing cases. +// +// It reports a diagnostic for each type switch or 'enum' switch that +// has missing cases, and suggests a fix to fill them in. +// +// The possible cases are: for a type switch, each accessible named +// type T or pointer *T that is assignable to the interface type; and +// for an 'enum' switch, each accessible named constant of the same +// type as the switch value. +// +// For an 'enum' switch, it will suggest cases for all possible values of the +// type. +// +// type Suit int8 +// const ( +// Spades Suit = iota +// Hearts +// Diamonds +// Clubs +// ) +// +// var s Suit +// switch s { +// case Spades: +// } +// +// It will report a diagnostic with a suggested fix to fill in the remaining +// cases: +// +// var s Suit +// switch s { +// case Spades: +// case Hearts: +// case Diamonds: +// case Clubs: +// default: +// panic(fmt.Sprintf("unexpected Suit: %v", s)) +// } +// +// For a type switch, it will suggest cases for all types that implement the +// interface. +// +// var stmt ast.Stmt +// switch stmt.(type) { +// case *ast.IfStmt: +// } +// +// It will report a diagnostic with a suggested fix to fill in the remaining +// cases: +// +// var stmt ast.Stmt +// switch stmt.(type) { +// case *ast.IfStmt: +// case *ast.ForStmt: +// case *ast.RangeStmt: +// case *ast.AssignStmt: +// case *ast.GoStmt: +// ... +// default: +// panic(fmt.Sprintf("unexpected ast.Stmt: %T", stmt)) +// } +package fillswitch diff --git a/gopls/internal/analysis/fillswitch/fillswitch.go b/gopls/internal/analysis/fillswitch/fillswitch.go new file mode 100644 index 00000000000..b93ade01065 --- /dev/null +++ b/gopls/internal/analysis/fillswitch/fillswitch.go @@ -0,0 +1,301 @@ +// Copyright 2024 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 fillswitch + +import ( + "bytes" + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/inspector" +) + +// Diagnose computes diagnostics for switch statements with missing cases +// overlapping with the provided start and end position. +// +// If either start or end is invalid, the entire package is inspected. +func Diagnose(inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { + var diags []analysis.Diagnostic + nodeFilter := []ast.Node{(*ast.SwitchStmt)(nil), (*ast.TypeSwitchStmt)(nil)} + inspect.Preorder(nodeFilter, func(n ast.Node) { + if start.IsValid() && n.End() < start || + end.IsValid() && n.Pos() > end { + return // non-overlapping + } + + var fix *analysis.SuggestedFix + switch n := n.(type) { + case *ast.SwitchStmt: + fix = suggestedFixSwitch(n, pkg, info) + case *ast.TypeSwitchStmt: + fix = suggestedFixTypeSwitch(n, pkg, info) + } + + if fix == nil { + return + } + + diags = append(diags, analysis.Diagnostic{ + Message: fix.Message, + Pos: n.Pos(), + End: n.Pos() + token.Pos(len("switch")), + SuggestedFixes: []analysis.SuggestedFix{*fix}, + }) + }) + + return diags +} + +func suggestedFixTypeSwitch(stmt *ast.TypeSwitchStmt, pkg *types.Package, info *types.Info) *analysis.SuggestedFix { + if hasDefaultCase(stmt.Body) { + return nil + } + + namedType := namedTypeFromTypeSwitch(stmt, info) + if namedType == nil { + return nil + } + + existingCases := caseTypes(stmt.Body, info) + // Gather accessible package-level concrete types + // that implement the switch interface type. + scope := namedType.Obj().Pkg().Scope() + var buf bytes.Buffer + for _, name := range scope.Names() { + obj := scope.Lookup(name) + if tname, ok := obj.(*types.TypeName); !ok || tname.IsAlias() { + continue // not a defined type + } + + if types.IsInterface(obj.Type()) { + continue + } + + samePkg := obj.Pkg() == pkg + if !samePkg && !obj.Exported() { + continue // inaccessible + } + + var key caseType + if types.AssignableTo(obj.Type(), namedType.Obj().Type()) { + key.named = obj.Type().(*types.Named) + } else if ptr := types.NewPointer(obj.Type()); types.AssignableTo(ptr, namedType.Obj().Type()) { + key.named = obj.Type().(*types.Named) + key.ptr = true + } + + if key.named != nil { + if existingCases[key] { + continue + } + + if buf.Len() > 0 { + buf.WriteString("\t") + } + + buf.WriteString("case ") + if key.ptr { + buf.WriteByte('*') + } + + if p := key.named.Obj().Pkg(); p != pkg { + // TODO: use the correct package name when the import is renamed + buf.WriteString(p.Name()) + buf.WriteByte('.') + } + buf.WriteString(key.named.Obj().Name()) + buf.WriteString(":\n") + } + } + + if buf.Len() == 0 { + return nil + } + + switch assign := stmt.Assign.(type) { + case *ast.AssignStmt: + addDefaultCase(&buf, namedType, assign.Lhs[0]) + case *ast.ExprStmt: + if assert, ok := assign.X.(*ast.TypeAssertExpr); ok { + addDefaultCase(&buf, namedType, assert.X) + } + } + + return &analysis.SuggestedFix{ + Message: fmt.Sprintf("Add cases for %s", namedType.Obj().Name()), + TextEdits: []analysis.TextEdit{{ + Pos: stmt.End() - token.Pos(len("}")), + End: stmt.End() - token.Pos(len("}")), + NewText: buf.Bytes(), + }}, + } +} + +func suggestedFixSwitch(stmt *ast.SwitchStmt, pkg *types.Package, info *types.Info) *analysis.SuggestedFix { + if hasDefaultCase(stmt.Body) { + return nil + } + + namedType, ok := info.TypeOf(stmt.Tag).(*types.Named) + if !ok { + return nil + } + + existingCases := caseConsts(stmt.Body, info) + // Gather accessible named constants of the same type as the switch value. + scope := namedType.Obj().Pkg().Scope() + var buf bytes.Buffer + for _, name := range scope.Names() { + obj := scope.Lookup(name) + if c, ok := obj.(*types.Const); ok && + (obj.Pkg() == pkg || obj.Exported()) && // accessible + types.Identical(obj.Type(), namedType.Obj().Type()) && + !existingCases[c] { + + if buf.Len() > 0 { + buf.WriteString("\t") + } + + buf.WriteString("case ") + if c.Pkg() != pkg { + buf.WriteString(c.Pkg().Name()) + buf.WriteByte('.') + } + buf.WriteString(c.Name()) + buf.WriteString(":\n") + } + } + + if buf.Len() == 0 { + return nil + } + + addDefaultCase(&buf, namedType, stmt.Tag) + + return &analysis.SuggestedFix{ + Message: fmt.Sprintf("Add cases for %s", namedType.Obj().Name()), + TextEdits: []analysis.TextEdit{{ + Pos: stmt.End() - token.Pos(len("}")), + End: stmt.End() - token.Pos(len("}")), + NewText: buf.Bytes(), + }}, + } +} + +func addDefaultCase(buf *bytes.Buffer, named *types.Named, expr ast.Expr) { + var dottedBuf bytes.Buffer + // writeDotted emits a dotted path a.b.c. + var writeDotted func(e ast.Expr) bool + writeDotted = func(e ast.Expr) bool { + switch e := e.(type) { + case *ast.SelectorExpr: + if !writeDotted(e.X) { + return false + } + dottedBuf.WriteByte('.') + dottedBuf.WriteString(e.Sel.Name) + return true + case *ast.Ident: + dottedBuf.WriteString(e.Name) + return true + } + return false + } + + buf.WriteString("\tdefault:\n") + typeName := fmt.Sprintf("%s.%s", named.Obj().Pkg().Name(), named.Obj().Name()) + if writeDotted(expr) { + // Switch tag expression is a dotted path. + // It is safe to re-evaluate it in the default case. + format := fmt.Sprintf("unexpected %s: %%#v", typeName) + fmt.Fprintf(buf, "\t\tpanic(fmt.Sprintf(%q, %s))\n\t", format, dottedBuf.String()) + } else { + // Emit simpler message, without re-evaluating tag expression. + fmt.Fprintf(buf, "\t\tpanic(%q)\n\t", "unexpected "+typeName) + } +} + +func namedTypeFromTypeSwitch(stmt *ast.TypeSwitchStmt, info *types.Info) *types.Named { + switch assign := stmt.Assign.(type) { + case *ast.ExprStmt: + if typ, ok := assign.X.(*ast.TypeAssertExpr); ok { + if named, ok := info.TypeOf(typ.X).(*types.Named); ok { + return named + } + } + + case *ast.AssignStmt: + if typ, ok := assign.Rhs[0].(*ast.TypeAssertExpr); ok { + if named, ok := info.TypeOf(typ.X).(*types.Named); ok { + return named + } + } + } + + return nil +} + +func hasDefaultCase(body *ast.BlockStmt) bool { + for _, clause := range body.List { + if len(clause.(*ast.CaseClause).List) == 0 { + return true + } + } + + return false +} + +func caseConsts(body *ast.BlockStmt, info *types.Info) map[*types.Const]bool { + out := map[*types.Const]bool{} + for _, stmt := range body.List { + for _, e := range stmt.(*ast.CaseClause).List { + if info.Types[e].Value == nil { + continue // not a constant + } + + if sel, ok := e.(*ast.SelectorExpr); ok { + e = sel.Sel // replace pkg.C with C + } + + if e, ok := e.(*ast.Ident); ok { + if c, ok := info.Uses[e].(*types.Const); ok { + out[c] = true + } + } + } + } + + return out +} + +type caseType struct { + named *types.Named + ptr bool +} + +func caseTypes(body *ast.BlockStmt, info *types.Info) map[caseType]bool { + out := map[caseType]bool{} + for _, stmt := range body.List { + for _, e := range stmt.(*ast.CaseClause).List { + if tv, ok := info.Types[e]; ok && tv.IsType() { + t := tv.Type + ptr := false + if p, ok := t.(*types.Pointer); ok { + t = p.Elem() + ptr = true + } + + if named, ok := t.(*types.Named); ok { + out[caseType{named, ptr}] = true + } + } + } + } + + return out +} diff --git a/gopls/internal/analysis/fillswitch/fillswitch_test.go b/gopls/internal/analysis/fillswitch/fillswitch_test.go new file mode 100644 index 00000000000..15d3ef1dd70 --- /dev/null +++ b/gopls/internal/analysis/fillswitch/fillswitch_test.go @@ -0,0 +1,38 @@ +// Copyright 2020 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 fillswitch_test + +import ( + "go/token" + "testing" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/analysis/fillswitch" +) + +// analyzer allows us to test the fillswitch code action using the analysistest +// harness. +var analyzer = &analysis.Analyzer{ + Name: "fillswitch", + Doc: "test only", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: func(pass *analysis.Pass) (any, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + for _, d := range fillswitch.Diagnose(inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { + pass.Report(d) + } + return nil, nil + }, + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillswitch", + RunDespiteErrors: true, +} + +func Test(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, analyzer, "a") +} diff --git a/gopls/internal/analysis/fillswitch/testdata/src/a/a.go b/gopls/internal/analysis/fillswitch/testdata/src/a/a.go new file mode 100644 index 00000000000..06d01da5f1e --- /dev/null +++ b/gopls/internal/analysis/fillswitch/testdata/src/a/a.go @@ -0,0 +1,78 @@ +// Copyright 2020 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 fillswitch + +import ( + data "b" +) + +type typeA int + +const ( + typeAOne typeA = iota + typeATwo + typeAThree +) + +func doSwitch() { + var a typeA + switch a { // want `Add cases for typeA` + } + + switch a { // want `Add cases for typeA` + case typeAOne: + } + + switch a { + case typeAOne: + default: + } + + switch a { + case typeAOne: + case typeATwo: + case typeAThree: + } + + var b data.TypeB + switch b { // want `Add cases for TypeB` + case data.TypeBOne: + } +} + +type notification interface { + isNotification() +} + +type notificationOne struct{} + +func (notificationOne) isNotification() {} + +type notificationTwo struct{} + +func (notificationTwo) isNotification() {} + +func doTypeSwitch() { + var not notification + switch not.(type) { // want `Add cases for notification` + } + + switch not.(type) { // want `Add cases for notification` + case notificationOne: + } + + switch not.(type) { + case notificationOne: + case notificationTwo: + } + + switch not.(type) { + default: + } + + var t data.ExportedInterface + switch t { + } +} diff --git a/gopls/internal/analysis/fillswitch/testdata/src/b/b.go b/gopls/internal/analysis/fillswitch/testdata/src/b/b.go new file mode 100644 index 00000000000..f65f3a7e6f2 --- /dev/null +++ b/gopls/internal/analysis/fillswitch/testdata/src/b/b.go @@ -0,0 +1,21 @@ +// Copyright 2020 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 fillswitch + +type TypeB int + +const ( + TypeBOne TypeB = iota + TypeBTwo + TypeBThree +) + +type ExportedInterface interface { + isExportedInterface() +} + +type notExportedType struct{} + +func (notExportedType) isExportedInterface() {} diff --git a/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go b/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go index 69db3100a90..a1a29d42deb 100644 --- a/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go +++ b/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go @@ -1,9 +1,6 @@ // Copyright 2021 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. -// -//go:build go1.18 -// +build go1.18 package testdata diff --git a/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden b/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden index 99ca9e4474b..ce425b72276 100644 --- a/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden +++ b/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden @@ -1,9 +1,6 @@ // Copyright 2021 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. -// -//go:build go1.18 -// +build go1.18 package testdata diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go index 0b7bc08d378..4fe5f1103cc 100644 --- a/gopls/internal/cache/analysis.go +++ b/gopls/internal/cache/analysis.go @@ -32,11 +32,12 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/analysis" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/filecache" - "golang.org/x/tools/gopls/internal/cache/metadata" - "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/progress" + "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" @@ -785,7 +786,7 @@ func (an *analysisNode) cacheKey() [sha256.Size]byte { func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) { // Parse only the "compiled" Go files. // Do the computation in parallel. - parsed := make([]*ParsedGoFile, len(an.files)) + parsed := make([]*parsego.File, len(an.files)) { var group errgroup.Group group.SetLimit(4) // not too much: run itself is already called in parallel @@ -796,7 +797,7 @@ func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) { // as cached ASTs require the global FileSet. // ast.Object resolution is unfortunately an implied part of the // go/analysis contract. - pgf, err := parseGoImpl(ctx, an.fset, fh, ParseFull&^parser.SkipObjectResolution, false) + pgf, err := parseGoImpl(ctx, an.fset, fh, parsego.Full&^parser.SkipObjectResolution, false) parsed[i] = pgf return err }) @@ -910,7 +911,7 @@ func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) { } // Postcondition: analysisPackage.types and an.exportDeps are populated. -func (an *analysisNode) typeCheck(parsed []*ParsedGoFile) *analysisPackage { +func (an *analysisNode) typeCheck(parsed []*parsego.File) *analysisPackage { mp := an.mp if false { // debugging @@ -1101,7 +1102,7 @@ func readShallowManifest(export []byte) ([]PackagePath, error) { type analysisPackage struct { mp *metadata.Package fset *token.FileSet // local to this package - parsed []*ParsedGoFile + parsed []*parsego.File files []*ast.File // same as parsed[i].File types *types.Package compiles bool // package is transitively free of list/parse/type errors diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index 0af59655ab1..50596e130ad 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -23,6 +23,7 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/cache/typerefs" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/filecache" @@ -675,7 +676,7 @@ func (b *typeCheckBatch) checkPackageForImport(ctx context.Context, ph *packageH // Parse the compiled go files, bypassing the parse cache as packages checked // for import are unlikely to get cache hits. Additionally, we can optimize // parsing slightly by not passing parser.ParseComments. - pgfs := make([]*ParsedGoFile, len(ph.localInputs.compiledGoFiles)) + pgfs := make([]*parsego.File, len(ph.localInputs.compiledGoFiles)) { var group errgroup.Group // Set an arbitrary concurrency limit; we want some parallelism but don't @@ -1271,7 +1272,7 @@ func (s *Snapshot) typerefData(ctx context.Context, id PackageID, imports map[Im bug.Reportf("internal error reading typerefs data: %v", err) } - pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), ParseFull&^parser.ParseComments, true, cgfs...) + pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Full&^parser.ParseComments, true, cgfs...) if err != nil { return nil, err } @@ -1471,11 +1472,11 @@ func (b *typeCheckBatch) checkPackage(ctx context.Context, ph *packageHandle) (* // Collect parsed files from the type check pass, capturing parse errors from // compiled files. var err error - pkg.goFiles, err = b.parseCache.parseFiles(ctx, b.fset, ParseFull, false, inputs.goFiles...) + pkg.goFiles, err = b.parseCache.parseFiles(ctx, b.fset, parsego.Full, false, inputs.goFiles...) if err != nil { return nil, err } - pkg.compiledGoFiles, err = b.parseCache.parseFiles(ctx, b.fset, ParseFull, false, inputs.compiledGoFiles...) + pkg.compiledGoFiles, err = b.parseCache.parseFiles(ctx, b.fset, parsego.Full, false, inputs.compiledGoFiles...) if err != nil { return nil, err } @@ -1670,12 +1671,12 @@ func depsErrors(ctx context.Context, snapshot *Snapshot, mp *metadata.Package) ( // Build an index of all imports in the package. type fileImport struct { - cgf *ParsedGoFile + cgf *parsego.File imp *ast.ImportSpec } allImports := map[string][]fileImport{} for _, uri := range mp.CompiledGoFiles { - pgf, err := parseGoURI(ctx, snapshot, uri, ParseHeader) + pgf, err := parseGoURI(ctx, snapshot, uri, parsego.Header) if err != nil { return nil, err } @@ -1822,6 +1823,12 @@ func typeErrorsToDiagnostics(pkg *syntaxPackage, errs []types.Error, linkTarget // report. continue } + + // Invariant: both start and end are IsValid. + if !end.IsValid() { + panic("end is invalid") + } + posn := safetoken.StartPosition(e.Fset, start) if !posn.IsValid() { // All valid positions produced by the type checker should described by @@ -1847,10 +1854,34 @@ func typeErrorsToDiagnostics(pkg *syntaxPackage, errs []types.Error, linkTarget } continue } - if !end.IsValid() || end == start { + + // debugging #65960 + // + // At this point, we know 'start' IsValid, and + // StartPosition(start) worked (with e.Fset). + // + // If the asserted condition is true, 'start' + // is also in range for pgf.Tok, which means + // the PosRange failure must be caused by 'end'. + if pgf.Tok != e.Fset.File(start) { + bug.Reportf("internal error: inconsistent token.Files for pos") + } + + if end == start { // Expand the end position to a more meaningful span. end = analysisinternal.TypeErrorEndPos(e.Fset, pgf.Src, start) + + // debugging #65960 + if _, err := safetoken.Offset(pgf.Tok, end); err != nil { + bug.Reportf("TypeErrorEndPos returned invalid end: %v", err) + } + } else { + // debugging #65960 + if _, err := safetoken.Offset(pgf.Tok, end); err != nil { + bug.Reportf("ReadGo116ErrorData returned invalid end: %v", err) + } } + rng, err := pgf.Mapper.PosRange(pgf.Tok, start, end) if err != nil { bug.Reportf("internal error: could not compute pos to range for %v: %v", e, err) diff --git a/gopls/internal/cache/errors.go b/gopls/internal/cache/errors.go index 83382b0aad2..6c95526d1ea 100644 --- a/gopls/internal/cache/errors.go +++ b/gopls/internal/cache/errors.go @@ -22,6 +22,7 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" @@ -464,7 +465,7 @@ func parseGoListImportCycleError(ctx context.Context, e packages.Error, mp *meta // Imports have quotation marks around them. circImp := strconv.Quote(importList[1]) for _, uri := range mp.CompiledGoFiles { - pgf, err := parseGoURI(ctx, fs, uri, ParseHeader) + pgf, err := parseGoURI(ctx, fs, uri, parsego.Header) if err != nil { return nil, err } @@ -497,7 +498,7 @@ func parseGoListImportCycleError(ctx context.Context, e packages.Error, mp *meta // It returns an error if the file could not be read. // // TODO(rfindley): eliminate this helper. -func parseGoURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI, mode parser.Mode) (*ParsedGoFile, error) { +func parseGoURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI, mode parser.Mode) (*parsego.File, error) { fh, err := fs.ReadFile(ctx, uri) if err != nil { return nil, err diff --git a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go index 4bbeb2d160a..74c90cd66f7 100644 --- a/gopls/internal/cache/load.go +++ b/gopls/internal/cache/load.go @@ -27,6 +27,7 @@ import ( "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/packagesinternal" + "golang.org/x/tools/internal/xcontext" ) var loadID uint64 // atomic identifier for loads @@ -283,7 +284,7 @@ func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc event.Log(ctx, fmt.Sprintf("%s: updating metadata for %d packages", eventName, len(updates))) meta := s.meta.Update(updates) - workspacePackages := computeWorkspacePackagesLocked(s, meta) + workspacePackages := computeWorkspacePackagesLocked(ctx, s, meta) s.meta = meta s.workspacePackages = workspacePackages s.resetActivePackagesLocked() @@ -563,7 +564,7 @@ func computeLoadDiagnostics(ctx context.Context, snapshot *Snapshot, mp *metadat // heuristics. // // s.mu must be held while calling this function. -func isWorkspacePackageLocked(s *Snapshot, meta *metadata.Graph, pkg *metadata.Package) bool { +func isWorkspacePackageLocked(ctx context.Context, s *Snapshot, meta *metadata.Graph, pkg *metadata.Package) bool { if metadata.IsCommandLineArguments(pkg.ID) { // Ad-hoc command-line-arguments packages aren't workspace packages. // With zero-config gopls (golang/go#57979) they should be very rare, as @@ -587,6 +588,16 @@ func isWorkspacePackageLocked(s *Snapshot, meta *metadata.Graph, pkg *metadata.P return containsOpenFileLocked(s, pkg) } + // golang/go#65801: any (non command-line-arguments) open package is a + // workspace package. + // + // Otherwise, we'd display diagnostics for changes in an open package (due to + // the logic of diagnoseChangedFiles), but then erase those diagnostics when + // we do the final diagnostics pass. Diagnostics should be stable. + if containsOpenFileLocked(s, pkg) { + return true + } + // Apply filtering logic. // // Workspace packages must contain at least one non-filtered file. @@ -624,10 +635,24 @@ func isWorkspacePackageLocked(s *Snapshot, meta *metadata.Graph, pkg *metadata.P // In module mode, a workspace package must be contained in a workspace // module. if s.view.moduleMode() { - if pkg.Module == nil { - return false + var modURI protocol.DocumentURI + if pkg.Module != nil { + modURI = protocol.URIFromPath(pkg.Module.GoMod) + } else { + // golang/go#65816: for std and cmd, Module is nil. + // Fall back to an inferior heuristic. + if len(pkg.CompiledGoFiles) == 0 { + return false // need at least one file to guess the go.mod file + } + dir := pkg.CompiledGoFiles[0].Dir() + var err error + modURI, err = findRootPattern(ctx, dir, "go.mod", lockedSnapshot{s}) + if err != nil || modURI == "" { + // err != nil implies context cancellation, in which case the result of + // this query does not matter. + return false + } } - modURI := protocol.URIFromPath(pkg.Module.GoMod) _, ok := s.view.workspaceModFiles[modURI] return ok } @@ -662,10 +687,14 @@ func containsOpenFileLocked(s *Snapshot, mp *metadata.Package) bool { // contain intermediate test variants. // // s.mu must be held while calling this function. -func computeWorkspacePackagesLocked(s *Snapshot, meta *metadata.Graph) immutable.Map[PackageID, PackagePath] { +func computeWorkspacePackagesLocked(ctx context.Context, s *Snapshot, meta *metadata.Graph) immutable.Map[PackageID, PackagePath] { + // The provided context is used for reading snapshot files, which can only + // fail due to context cancellation. Don't let this happen as it could lead + // to inconsistent results. + ctx = xcontext.Detach(ctx) workspacePackages := make(map[PackageID]PackagePath) for _, mp := range meta.Packages { - if !isWorkspacePackageLocked(s, meta, mp) { + if !isWorkspacePackageLocked(ctx, s, meta, mp) { continue } diff --git a/gopls/internal/cache/mod_tidy.go b/gopls/internal/cache/mod_tidy.go index 6dbe9820182..79867855e0c 100644 --- a/gopls/internal/cache/mod_tidy.go +++ b/gopls/internal/cache/mod_tidy.go @@ -16,9 +16,10 @@ import ( "strings" "golang.org/x/mod/modfile" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" @@ -281,7 +282,7 @@ func missingModuleDiagnostics(ctx context.Context, snapshot *Snapshot, pm *Parse continue } for _, goFile := range compiledGoFiles { - pgf, err := snapshot.ParseGo(ctx, goFile, ParseHeader) + pgf, err := snapshot.ParseGo(ctx, goFile, parsego.Header) if err != nil { continue } @@ -461,7 +462,7 @@ func switchDirectness(req *modfile.Require, m *protocol.Mapper) ([]protocol.Text // missingModuleForImport creates an error for a given import path that comes // from a missing module. -func missingModuleForImport(pgf *ParsedGoFile, imp *ast.ImportSpec, req *modfile.Require, fixes []SuggestedFix) (*Diagnostic, error) { +func missingModuleForImport(pgf *parsego.File, imp *ast.ImportSpec, req *modfile.Require, fixes []SuggestedFix) (*Diagnostic, error) { if req.Syntax == nil { return nil, fmt.Errorf("no syntax for %v", req) } @@ -488,7 +489,7 @@ func missingModuleForImport(pgf *ParsedGoFile, imp *ast.ImportSpec, req *modfile // // TODO(rfindley): this should key off ImportPath. func parseImports(ctx context.Context, s *Snapshot, files []file.Handle) (map[string]bool, error) { - pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), ParseHeader, false, files...) + pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Header, false, files...) if err != nil { // e.g. context cancellation return nil, err } diff --git a/gopls/internal/cache/parse.go b/gopls/internal/cache/parse.go index c8da20eed13..56130c6e1fb 100644 --- a/gopls/internal/cache/parse.go +++ b/gopls/internal/cache/parse.go @@ -11,14 +11,14 @@ import ( "go/token" "path/filepath" - "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/file" ) // ParseGo parses the file whose contents are provided by fh. // The resulting tree may have been fixed up. // If the file is not available, returns nil and an error. -func (s *Snapshot) ParseGo(ctx context.Context, fh file.Handle, mode parser.Mode) (*ParsedGoFile, error) { +func (s *Snapshot) ParseGo(ctx context.Context, fh file.Handle, mode parser.Mode) (*parsego.File, error) { pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), mode, false, fh) if err != nil { return nil, err @@ -27,7 +27,7 @@ func (s *Snapshot) ParseGo(ctx context.Context, fh file.Handle, mode parser.Mode } // parseGoImpl parses the Go source file whose content is provided by fh. -func parseGoImpl(ctx context.Context, fset *token.FileSet, fh file.Handle, mode parser.Mode, purgeFuncBodies bool) (*ParsedGoFile, error) { +func parseGoImpl(ctx context.Context, fset *token.FileSet, fh file.Handle, mode parser.Mode, purgeFuncBodies bool) (*parsego.File, error) { ext := filepath.Ext(fh.URI().Path()) if ext != ".go" && ext != "" { // files generated by cgo have no extension return nil, fmt.Errorf("cannot parse non-Go file %s", fh.URI()) diff --git a/gopls/internal/cache/parse_cache.go b/gopls/internal/cache/parse_cache.go index 55eced51403..8586f655d28 100644 --- a/gopls/internal/cache/parse_cache.go +++ b/gopls/internal/cache/parse_cache.go @@ -17,8 +17,8 @@ import ( "time" "golang.org/x/sync/errgroup" - "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/tokeninternal" @@ -135,7 +135,7 @@ type parseKey struct { type parseCacheEntry struct { key parseKey hash file.Hash - promise *memoize.Promise // memoize.Promise[*ParsedGoFile] + promise *memoize.Promise // memoize.Promise[*parsego.File] atime uint64 // clock time of last access, for use in LRU sorting walltime time.Time // actual time of last access, for use in time-based eviction; too coarse for LRU on some systems lruIndex int // owned by the queue implementation @@ -303,7 +303,7 @@ func (c *parseCache) allocateSpace(size int) (int, int) { return base, c.nextBase } -// parseFiles returns a ParsedGoFile for each file handle in fhs, in the +// parseFiles returns a parsego.File for each file handle in fhs, in the // requested parse mode. // // For parsed files that already exists in the cache, access time will be @@ -317,8 +317,8 @@ func (c *parseCache) allocateSpace(size int) (int, int) { // // If parseFiles returns an error, it still returns a slice, // but with a nil entry for each file that could not be parsed. -func (c *parseCache) parseFiles(ctx context.Context, fset *token.FileSet, mode parser.Mode, purgeFuncBodies bool, fhs ...file.Handle) ([]*ParsedGoFile, error) { - pgfs := make([]*ParsedGoFile, len(fhs)) +func (c *parseCache) parseFiles(ctx context.Context, fset *token.FileSet, mode parser.Mode, purgeFuncBodies bool, fhs ...file.Handle) ([]*parsego.File, error) { + pgfs := make([]*parsego.File, len(fhs)) // Temporary fall-back for 32-bit systems, where reservedForParsing is too // small to be viable. We don't actually support 32-bit systems, so this @@ -351,7 +351,7 @@ func (c *parseCache) parseFiles(ctx context.Context, fset *token.FileSet, mode p if err != nil { return err } - pgfs[i] = result.(*ParsedGoFile) + pgfs[i] = result.(*parsego.File) return nil }) } diff --git a/gopls/internal/cache/parse_cache_test.go b/gopls/internal/cache/parse_cache_test.go index eee7ded39af..7aefac77c38 100644 --- a/gopls/internal/cache/parse_cache_test.go +++ b/gopls/internal/cache/parse_cache_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" ) @@ -31,12 +32,12 @@ func TestParseCache(t *testing.T) { fset := token.NewFileSet() cache := newParseCache(0) - pgfs1, err := cache.parseFiles(ctx, fset, ParseFull, false, fh) + pgfs1, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh) if err != nil { t.Fatal(err) } pgf1 := pgfs1[0] - pgfs2, err := cache.parseFiles(ctx, fset, ParseFull, false, fh) + pgfs2, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh) pgf2 := pgfs2[0] if err != nil { t.Fatal(err) @@ -50,7 +51,7 @@ func TestParseCache(t *testing.T) { files := []file.Handle{fh} files = append(files, dummyFileHandles(parseCacheMinFiles-1)...) - pgfs3, err := cache.parseFiles(ctx, fset, ParseFull, false, files...) + pgfs3, err := cache.parseFiles(ctx, fset, parsego.Full, false, files...) if err != nil { t.Fatal(err) } @@ -68,13 +69,13 @@ func TestParseCache(t *testing.T) { // Now overwrite the cache, after which we should get new results. cache.gcOnce() files = dummyFileHandles(parseCacheMinFiles) - _, err = cache.parseFiles(ctx, fset, ParseFull, false, files...) + _, err = cache.parseFiles(ctx, fset, parsego.Full, false, files...) if err != nil { t.Fatal(err) } // force a GC, which should collect the recently parsed files cache.gcOnce() - pgfs4, err := cache.parseFiles(ctx, fset, ParseFull, false, fh) + pgfs4, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh) if err != nil { t.Fatal(err) } @@ -98,7 +99,7 @@ func TestParseCache_Reparsing(t *testing.T) { // Parsing should succeed even though we overflow the padding. cache := newParseCache(0) - _, err := cache.parseFiles(context.Background(), token.NewFileSet(), ParseFull, false, files...) + _, err := cache.parseFiles(context.Background(), token.NewFileSet(), parsego.Full, false, files...) if err != nil { t.Fatal(err) } @@ -118,7 +119,7 @@ func TestParseCache_Issue59097(t *testing.T) { // Parsing should succeed even though we overflow the padding. cache := newParseCache(0) - _, err := cache.parseFiles(context.Background(), token.NewFileSet(), ParseFull, false, files...) + _, err := cache.parseFiles(context.Background(), token.NewFileSet(), parsego.Full, false, files...) if err != nil { t.Fatal(err) } @@ -136,19 +137,19 @@ func TestParseCache_TimeEviction(t *testing.T) { cache := newParseCache(gcDuration) cache.stop() // we'll manage GC manually, for testing. - pgfs0, err := cache.parseFiles(ctx, fset, ParseFull, false, fh, fh) + pgfs0, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh, fh) if err != nil { t.Fatal(err) } files := dummyFileHandles(parseCacheMinFiles) - _, err = cache.parseFiles(ctx, fset, ParseFull, false, files...) + _, err = cache.parseFiles(ctx, fset, parsego.Full, false, files...) if err != nil { t.Fatal(err) } // Even after filling up the 'min' files, we get a cache hit for our original file. - pgfs1, err := cache.parseFiles(ctx, fset, ParseFull, false, fh, fh) + pgfs1, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh, fh) if err != nil { t.Fatal(err) } @@ -158,14 +159,14 @@ func TestParseCache_TimeEviction(t *testing.T) { } // But after GC, we get a cache miss. - _, err = cache.parseFiles(ctx, fset, ParseFull, false, files...) // mark dummy files as newer + _, err = cache.parseFiles(ctx, fset, parsego.Full, false, files...) // mark dummy files as newer if err != nil { t.Fatal(err) } time.Sleep(gcDuration) cache.gcOnce() - pgfs2, err := cache.parseFiles(ctx, fset, ParseFull, false, fh, fh) + pgfs2, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh, fh) if err != nil { t.Fatal(err) } @@ -183,7 +184,7 @@ func TestParseCache_Duplicates(t *testing.T) { fh := makeFakeFileHandle(uri, []byte("package p\n\nconst _ = \"foo\"")) cache := newParseCache(0) - pgfs, err := cache.parseFiles(ctx, token.NewFileSet(), ParseFull, false, fh, fh) + pgfs, err := cache.parseFiles(ctx, token.NewFileSet(), parsego.Full, false, fh, fh) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go index 89fc15fb279..739ee1386bd 100644 --- a/gopls/internal/cache/parsego/parse.go +++ b/gopls/internal/cache/parsego/parse.go @@ -25,14 +25,14 @@ import ( // Common parse modes; these should be reused wherever possible to increase // cache hits. const ( - // ParseHeader specifies that the main package declaration and imports are needed. + // Header specifies that the main package declaration and imports are needed. // This is the mode used when attempting to examine the package graph structure. - ParseHeader = parser.AllErrors | parser.ParseComments | parser.ImportsOnly | parser.SkipObjectResolution + Header = parser.AllErrors | parser.ParseComments | parser.ImportsOnly | parser.SkipObjectResolution - // ParseFull specifies the full AST is needed. + // Full specifies the full AST is needed. // This is used for files of direct interest where the entire contents must // be considered. - ParseFull = parser.AllErrors | parser.ParseComments | parser.SkipObjectResolution + Full = parser.AllErrors | parser.ParseComments | parser.SkipObjectResolution ) // Parse parses a buffer of Go source, repairing the tree if necessary. diff --git a/gopls/internal/cache/parsego/parse_test.go b/gopls/internal/cache/parsego/parse_test.go index 4018e9ed886..c64125427b1 100644 --- a/gopls/internal/cache/parsego/parse_test.go +++ b/gopls/internal/cache/parsego/parse_test.go @@ -32,7 +32,7 @@ func _() { } ` - pgf, _ := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", []byte(src), parsego.ParseFull, false) + 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 { diff --git a/gopls/internal/cache/pkg.go b/gopls/internal/cache/pkg.go index 821b1cc48e8..c0fa37fc225 100644 --- a/gopls/internal/cache/pkg.go +++ b/gopls/internal/cache/pkg.go @@ -21,16 +21,10 @@ import ( // Convenient aliases for very heavily used types. type ( - PackageID = metadata.PackageID - PackagePath = metadata.PackagePath - PackageName = metadata.PackageName - ImportPath = metadata.ImportPath - ParsedGoFile = parsego.File -) - -const ( - ParseHeader = parsego.ParseHeader - ParseFull = parsego.ParseFull + PackageID = metadata.PackageID + PackagePath = metadata.PackagePath + PackageName = metadata.PackageName + ImportPath = metadata.ImportPath ) // A Package is the union of package metadata and type checking results. @@ -51,8 +45,8 @@ type syntaxPackage struct { // -- outputs -- fset *token.FileSet // for now, same as the snapshot's FileSet - goFiles []*ParsedGoFile - compiledGoFiles []*ParsedGoFile + goFiles []*parsego.File + compiledGoFiles []*parsego.File diagnostics []*Diagnostic parseErrors []scanner.ErrorList typeErrors []types.Error @@ -108,15 +102,15 @@ func (packageLoadScope) aScope() {} func (moduleLoadScope) aScope() {} func (viewLoadScope) aScope() {} -func (p *Package) CompiledGoFiles() []*ParsedGoFile { +func (p *Package) CompiledGoFiles() []*parsego.File { return p.pkg.compiledGoFiles } -func (p *Package) File(uri protocol.DocumentURI) (*ParsedGoFile, error) { +func (p *Package) File(uri protocol.DocumentURI) (*parsego.File, error) { return p.pkg.File(uri) } -func (pkg *syntaxPackage) File(uri protocol.DocumentURI) (*ParsedGoFile, error) { +func (pkg *syntaxPackage) File(uri protocol.DocumentURI) (*parsego.File, error) { for _, cgf := range pkg.compiledGoFiles { if cgf.URI == uri { return cgf, nil diff --git a/gopls/internal/cache/port_test.go b/gopls/internal/cache/port_test.go index 3c38a1184f0..a92056a9c22 100644 --- a/gopls/internal/cache/port_test.go +++ b/gopls/internal/cache/port_test.go @@ -94,10 +94,8 @@ func BenchmarkMatchingPreferredPorts(b *testing.B) { // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !windows && !plan9 -// +build !windows,!plan9 - -// TODO(adonovan): use 'unix' tag when go1.19 can be assumed. +//go:build unix +// +build unix package robustio diff --git a/gopls/internal/cache/session.go b/gopls/internal/cache/session.go index 7d2bf43773f..3ad78336c5c 100644 --- a/gopls/internal/cache/session.go +++ b/gopls/internal/cache/session.go @@ -470,7 +470,17 @@ func selectViewDefs(ctx context.Context, fs file.Source, folders []*Folder, open folderForFile := func(uri protocol.DocumentURI) *Folder { var longest *Folder for _, folder := range folders { - if (longest == nil || len(folder.Dir) > len(longest.Dir)) && folder.Dir.Encloses(uri) { + // Check that this is a better match than longest, but not through a + // vendor directory. Count occurrences of "/vendor/" as a quick check + // that the vendor directory is between the folder and the file. Note the + // addition of a trailing "/" to handle the odd case where the folder is named + // vendor (which I hope is exceedingly rare in any case). + // + // Vendored packages are, by definition, part of an existing view. + if (longest == nil || len(folder.Dir) > len(longest.Dir)) && + folder.Dir.Encloses(uri) && + strings.Count(string(uri), "/vendor/") == strings.Count(string(folder.Dir)+"/", "/vendor/") { + longest = folder } } diff --git a/gopls/internal/cache/session_test.go b/gopls/internal/cache/session_test.go index a3bd8ce5800..913c3bd1f27 100644 --- a/gopls/internal/cache/session_test.go +++ b/gopls/internal/cache/session_test.go @@ -20,6 +20,7 @@ import ( func TestZeroConfigAlgorithm(t *testing.T) { testenv.NeedsExec(t) // executes the Go command + t.Setenv("GOPACKAGESDRIVER", "off") type viewSummary struct { // fields exported for cmp.Diff @@ -33,6 +34,12 @@ func TestZeroConfigAlgorithm(t *testing.T) { options func(dir string) map[string]any // options may refer to the temp dir } + includeReplaceInWorkspace := func(string) map[string]any { + return map[string]any{ + "includeReplaceInWorkspace": true, + } + } + type test struct { name string files map[string]string // use a map rather than txtar as file content is tiny @@ -235,7 +242,7 @@ func TestZeroConfigAlgorithm(t *testing.T) { "b/go.mod": "module golang.org/b\ngo 1.18\n", "b/b.go": "package b", }, - []folderSummary{{dir: "."}}, + []folderSummary{{dir: ".", options: includeReplaceInWorkspace}}, []string{"a/a.go", "b/b.go"}, []viewSummary{{GoModView, ".", nil}}, }, @@ -247,7 +254,7 @@ func TestZeroConfigAlgorithm(t *testing.T) { "b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../", "b/b.go": "package b", }, - []folderSummary{{dir: "."}}, + []folderSummary{{dir: ".", options: includeReplaceInWorkspace}}, []string{"a/a.go", "b/b.go"}, []viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}}, }, @@ -277,12 +284,12 @@ replace ( "d/go.mod": "module golang.org/d\ngo 1.18", "d/d.go": "package d", }, - []folderSummary{{dir: "."}}, + []folderSummary{{dir: ".", options: includeReplaceInWorkspace}}, []string{"b/b.go", "c/c.go", "d/d.go"}, []viewSummary{{GoModView, ".", nil}, {GoModView, "d", nil}}, }, { - "go.mod with many replace", + "go.mod with replace outside the workspace", map[string]string{ "go.mod": "module golang.org/a\ngo 1.18", "a.go": "package a", @@ -290,7 +297,7 @@ replace ( "b/b.go": "package b", }, []folderSummary{{dir: "b"}}, - []string{"a/a.go", "b/b.go"}, + []string{"a.go", "b/b.go"}, []viewSummary{{GoModView, "b", nil}}, }, { diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index 3d97ed47ccb..d81fd208c06 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -30,6 +30,7 @@ import ( "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/methodsets" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/cache/typerefs" "golang.org/x/tools/gopls/internal/cache/xrefs" "golang.org/x/tools/gopls/internal/file" @@ -116,7 +117,7 @@ type Snapshot struct { // builtin is the location of builtin.go in GOROOT. // // TODO(rfindley): would it make more sense to eagerly parse builtin, and - // instead store a *ParsedGoFile here? + // instead store a *parsego.File here? builtin protocol.DocumentURI // meta holds loaded metadata. @@ -1273,14 +1274,26 @@ func (s *Snapshot) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file s.mu.Lock() defer s.mu.Unlock() - fh, ok := s.files.get(uri) + return lockedSnapshot{s}.ReadFile(ctx, uri) +} + +// lockedSnapshot implements the file.Source interface, while holding s.mu. +// +// TODO(rfindley): This unfortunate type had been eliminated, but it had to be +// restored to fix golang/go#65801. We should endeavor to remove it again. +type lockedSnapshot struct { + s *Snapshot +} + +func (s lockedSnapshot) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error) { + fh, ok := s.s.files.get(uri) if !ok { var err error - fh, err = s.view.fs.ReadFile(ctx, uri) + fh, err = s.s.view.fs.ReadFile(ctx, uri) if err != nil { return nil, err } - s.files.set(uri, fh) + s.s.files.set(uri, fh) } return fh, nil } @@ -1615,8 +1628,8 @@ https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-str // orphanedFileDiagnosticRange returns the position to use for orphaned file diagnostics. // We only warn about an orphaned file if it is well-formed enough to actually // be part of a package. Otherwise, we need more information. -func orphanedFileDiagnosticRange(ctx context.Context, cache *parseCache, fh file.Handle) (*ParsedGoFile, protocol.Range, bool) { - pgfs, err := cache.parseFiles(ctx, token.NewFileSet(), ParseHeader, false, fh) +func orphanedFileDiagnosticRange(ctx context.Context, cache *parseCache, fh file.Handle) (*parsego.File, protocol.Range, bool) { + pgfs, err := cache.parseFiles(ctx, token.NewFileSet(), parsego.Header, false, fh) if err != nil { return nil, protocol.Range{}, false } @@ -2016,7 +2029,7 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f // Update workspace and active packages, if necessary. if result.meta != s.meta || anyFileOpenedOrClosed { needsDiagnosis = true - result.workspacePackages = computeWorkspacePackagesLocked(result, result.meta) + result.workspacePackages = computeWorkspacePackagesLocked(ctx, result, result.meta) result.resetActivePackagesLocked() } else { result.workspacePackages = s.workspacePackages @@ -2176,8 +2189,8 @@ func metadataChanges(ctx context.Context, lockedSnapshot *Snapshot, oldFH, newFH fset := token.NewFileSet() // Parse headers to compare package names and imports. - oldHeads, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, ParseHeader, false, oldFH) - newHeads, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, ParseHeader, false, newFH) + oldHeads, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Header, false, oldFH) + newHeads, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Header, false, newFH) if oldErr != nil || newErr != nil { errChanged := (oldErr == nil) != (newErr == nil) @@ -2223,12 +2236,12 @@ func metadataChanges(ctx context.Context, lockedSnapshot *Snapshot, oldFH, newFH // Note: if this affects performance we can probably avoid parsing in the // common case by first scanning the source for potential comments. if !invalidate { - origFulls, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, ParseFull, false, oldFH) - newFulls, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, ParseFull, false, newFH) + origFulls, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Full, false, oldFH) + newFulls, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Full, false, newFH) if oldErr == nil && newErr == nil { invalidate = magicCommentsChanged(origFulls[0].File, newFulls[0].File) } else { - // At this point, we shouldn't ever fail to produce a ParsedGoFile, as + // At this point, we shouldn't ever fail to produce a parsego.File, as // we're already past header parsing. bug.Reportf("metadataChanges: unparseable file %v (old error: %v, new error: %v)", oldFH.URI(), oldErr, newErr) } @@ -2292,7 +2305,7 @@ func extractMagicComments(f *ast.File) []string { } // BuiltinFile returns information about the special builtin package. -func (s *Snapshot) BuiltinFile(ctx context.Context) (*ParsedGoFile, error) { +func (s *Snapshot) BuiltinFile(ctx context.Context) (*parsego.File, error) { s.AwaitInitialized(ctx) s.mu.Lock() @@ -2309,7 +2322,7 @@ func (s *Snapshot) BuiltinFile(ctx context.Context) (*ParsedGoFile, error) { } // For the builtin file only, we need syntactic object resolution // (since we can't type check). - mode := ParseFull &^ parser.SkipObjectResolution + mode := parsego.Full &^ parser.SkipObjectResolution pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), mode, false, fh) if err != nil { return nil, err diff --git a/gopls/internal/cache/symbols.go b/gopls/internal/cache/symbols.go index 5dce87df223..9954c747798 100644 --- a/gopls/internal/cache/symbols.go +++ b/gopls/internal/cache/symbols.go @@ -11,6 +11,7 @@ import ( "go/types" "strings" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/astutil" @@ -68,7 +69,7 @@ func (s *Snapshot) symbolize(ctx context.Context, uri protocol.DocumentURI) ([]S // symbolizeImpl reads and parses a file and extracts symbols from it. func symbolizeImpl(ctx context.Context, snapshot *Snapshot, fh file.Handle) ([]Symbol, error) { - pgfs, err := snapshot.view.parseCache.parseFiles(ctx, token.NewFileSet(), ParseFull, false, fh) + pgfs, err := snapshot.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Full, false, fh) if err != nil { return nil, err } diff --git a/gopls/internal/cache/typerefs/pkgrefs_test.go b/gopls/internal/cache/typerefs/pkgrefs_test.go index da14b90cba3..9d4b5c011d3 100644 --- a/gopls/internal/cache/typerefs/pkgrefs_test.go +++ b/gopls/internal/cache/typerefs/pkgrefs_test.go @@ -42,7 +42,6 @@ type ( PackagePath = metadata.PackagePath Metadata = metadata.Package MetadataSource = metadata.Source - ParsedGoFile = parsego.File ) // TestBuildPackageGraph tests the BuildPackageGraph constructor, which uses @@ -277,7 +276,7 @@ type memoizedParser struct { type futureParse struct { done chan struct{} - pgf *ParsedGoFile + pgf *parsego.File err error } @@ -287,15 +286,15 @@ func newParser() *memoizedParser { } } -func (p *memoizedParser) parse(ctx context.Context, uri protocol.DocumentURI) (*ParsedGoFile, error) { - doParse := func(ctx context.Context, uri protocol.DocumentURI) (*ParsedGoFile, error) { +func (p *memoizedParser) parse(ctx context.Context, uri protocol.DocumentURI) (*parsego.File, error) { + doParse := func(ctx context.Context, uri protocol.DocumentURI) (*parsego.File, error) { // TODO(adonovan): hoist this operation outside the benchmark critsec. content, err := os.ReadFile(uri.Path()) if err != nil { return nil, err } content = astutil.PurgeFuncBodies(content) - pgf, _ := parsego.Parse(ctx, token.NewFileSet(), uri, content, parsego.ParseFull, false) + pgf, _ := parsego.Parse(ctx, token.NewFileSet(), uri, content, parsego.Full, false) return pgf, nil } diff --git a/gopls/internal/cache/typerefs/refs_test.go b/gopls/internal/cache/typerefs/refs_test.go index 9bb9ec5bdfa..1e98fb585ed 100644 --- a/gopls/internal/cache/typerefs/refs_test.go +++ b/gopls/internal/cache/typerefs/refs_test.go @@ -507,7 +507,7 @@ type Z map[ext.A]ext.B var pgfs []*parsego.File for i, src := range test.srcs { uri := protocol.DocumentURI(fmt.Sprintf("file:///%d.go", i)) - pgf, _ := parsego.Parse(ctx, token.NewFileSet(), uri, []byte(src), parsego.ParseFull, false) + pgf, _ := parsego.Parse(ctx, token.NewFileSet(), uri, []byte(src), parsego.Full, false) if !test.allowErrs && pgf.ParseErr != nil { t.Fatalf("ParseGoSrc(...) returned parse errors: %v", pgf.ParseErr) } diff --git a/gopls/internal/cmd/serve.go b/gopls/internal/cmd/serve.go index a2f9be9f7e2..3b79ccb6a8c 100644 --- a/gopls/internal/cmd/serve.go +++ b/gopls/internal/cmd/serve.go @@ -14,11 +14,11 @@ import ( "os" "time" + "golang.org/x/telemetry/upload" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/debug" "golang.org/x/tools/gopls/internal/lsprpc" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/telemetry" "golang.org/x/tools/internal/fakenet" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/tool" @@ -78,7 +78,8 @@ func (s *Serve) remoteArgs(network, address string) []string { // Run configures a server based on the flags, and then runs it. // It blocks until the server shuts down. func (s *Serve) Run(ctx context.Context, args ...string) error { - telemetry.Upload() + // TODO(adonovan): eliminate this once telemetry.Start has this effect. + go upload.Run(nil) // start telemetry uploader if len(args) > 0 { return tool.CommandLineErrorf("server does not take arguments, got %v", args) diff --git a/gopls/internal/golang/add_import.go b/gopls/internal/golang/add_import.go index 201edce5306..a43256a6a08 100644 --- a/gopls/internal/golang/add_import.go +++ b/gopls/internal/golang/add_import.go @@ -8,6 +8,7 @@ import ( "context" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/imports" @@ -15,7 +16,7 @@ import ( // AddImport adds a single import statement to the given file func AddImport(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, importPath string) ([]protocol.TextEdit, error) { - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } diff --git a/gopls/internal/golang/call_hierarchy.go b/gopls/internal/golang/call_hierarchy.go index 87a6a54e458..7e88df1a1cf 100644 --- a/gopls/internal/golang/call_hierarchy.go +++ b/gopls/internal/golang/call_hierarchy.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" @@ -116,7 +117,7 @@ func enclosingNodeCallItem(ctx context.Context, snapshot *cache.Snapshot, pkgPat // that don't contain the reference, using either a scanner-based // implementation such as https://go.dev/play/p/KUrObH1YkX8 // (~31% speedup), or a byte-oriented implementation (2x speedup). - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return protocol.CallHierarchyItem{}, err } diff --git a/gopls/internal/golang/change_quote.go b/gopls/internal/golang/change_quote.go index 98a60e11ae0..919b935e79c 100644 --- a/gopls/internal/golang/change_quote.go +++ b/gopls/internal/golang/change_quote.go @@ -11,6 +11,7 @@ import ( "strings" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" @@ -24,7 +25,7 @@ import ( // Only the following conditions are true, the action in result is valid // - [start, end) is enclosed by a string literal // - if the string is interpreted string, need check whether the convert is allowed -func ConvertStringLiteral(pgf *ParsedGoFile, fh file.Handle, rng protocol.Range) (protocol.CodeAction, bool) { +func ConvertStringLiteral(pgf *parsego.File, fh file.Handle, rng protocol.Range) (protocol.CodeAction, bool) { startPos, endPos, err := pgf.RangePos(rng) if err != nil { return protocol.CodeAction{}, false // e.g. invalid range diff --git a/gopls/internal/golang/change_signature.go b/gopls/internal/golang/change_signature.go index e0a829e128e..81d2b78b039 100644 --- a/gopls/internal/golang/change_signature.go +++ b/gopls/internal/golang/change_signature.go @@ -246,7 +246,7 @@ type ParamInfo struct { } // FindParam finds the parameter information spanned by the given range. -func FindParam(pgf *ParsedGoFile, rng protocol.Range) (*ParamInfo, error) { +func FindParam(pgf *parsego.File, rng protocol.Range) (*ParamInfo, error) { start, end, err := pgf.RangePos(rng) if err != nil { return nil, err @@ -542,7 +542,7 @@ func remove[T any](s []T, i int) []T { // replaceFileDecl replaces old with new in the file described by pgf. // // TODO(rfindley): generalize, and combine with rewriteSignature. -func replaceFileDecl(pgf *ParsedGoFile, old, new ast.Decl) ([]byte, error) { +func replaceFileDecl(pgf *parsego.File, old, new ast.Decl) ([]byte, error) { i := findDecl(pgf.File, old) if i == -1 { return nil, bug.Errorf("didn't find old declaration") diff --git a/gopls/internal/golang/code_lens.go b/gopls/internal/golang/code_lens.go index c4a7e5f82c8..ee33bf26c36 100644 --- a/gopls/internal/golang/code_lens.go +++ b/gopls/internal/golang/code_lens.go @@ -13,9 +13,11 @@ import ( "strings" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" + "golang.org/x/tools/internal/typesinternal" ) type LensFunc func(context.Context, *cache.Snapshot, file.Handle) ([]protocol.CodeLens, error) @@ -66,7 +68,7 @@ func runTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand } if len(fns.Benchmarks) > 0 { - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -98,7 +100,7 @@ type TestFns struct { Benchmarks []TestFn } -func TestsAndBenchmarks(pkg *cache.Package, pgf *ParsedGoFile) (TestFns, error) { +func TestsAndBenchmarks(pkg *cache.Package, pgf *parsego.File) (TestFns, error) { var out TestFns if !strings.HasSuffix(pgf.URI.Path(), "_test.go") { @@ -151,12 +153,8 @@ func matchTestFunc(fn *ast.FuncDecl, pkg *cache.Package, nameRe *regexp.Regexp, } // Check the type of the only parameter - paramTyp, ok := sig.Params().At(0).Type().(*types.Pointer) - if !ok { - return false - } - named, ok := paramTyp.Elem().(*types.Named) - if !ok { + isptr, named := typesinternal.ReceiverNamed(sig.Params().At(0)) + if !isptr || named == nil { return false } namedObj := named.Obj() @@ -167,7 +165,7 @@ func matchTestFunc(fn *ast.FuncDecl, pkg *cache.Package, nameRe *regexp.Regexp, } func goGenerateCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -201,7 +199,7 @@ func goGenerateCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.H } func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -227,7 +225,7 @@ func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Ha } func toggleDetailsCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 5c6163cef12..fa876ac474c 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -13,6 +13,7 @@ import ( "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/analysis/fillstruct" + "golang.org/x/tools/gopls/internal/analysis/fillswitch" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" @@ -34,7 +35,7 @@ func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, // Code actions requiring syntax information alone. if wantQuickFixes || want[protocol.SourceOrganizeImports] || want[protocol.RefactorExtract] { - pgf, err := snapshot.ParseGo(ctx, fh, parsego.ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -98,7 +99,7 @@ func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, return nil, err } if want[protocol.RefactorRewrite] { - rewrites, err := getRewriteCodeActions(pkg, pgf, fh, rng, snapshot.Options()) + rewrites, err := getRewriteCodeActions(ctx, pkg, snapshot, pgf, fh, rng, snapshot.Options()) if err != nil { return nil, err } @@ -179,7 +180,7 @@ func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) } // getExtractCodeActions returns any refactor.extract code actions for the selection. -func getExtractCodeActions(pgf *ParsedGoFile, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { +func getExtractCodeActions(pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { if rng.Start == rng.End { return nil, nil } @@ -252,8 +253,7 @@ func newCodeAction(title string, kind protocol.CodeActionKind, cmd *protocol.Com return action } -// getRewriteCodeActions returns refactor.rewrite code actions available at the specified range. -func getRewriteCodeActions(pkg *cache.Package, pgf *ParsedGoFile, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) { +func getRewriteCodeActions(ctx context.Context, pkg *cache.Package, snapshot *cache.Snapshot, pgf *parsego.File, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) { // golang/go#61693: code actions were refactored to run outside of the // analysis framework, but as a result they lost their panic recovery. // @@ -304,6 +304,32 @@ func getRewriteCodeActions(pkg *cache.Package, pgf *ParsedGoFile, fh file.Handle commands = append(commands, cmd) } + if msg, ok, _ := CanSplitLines(pgf.File, pkg.FileSet(), start, end); ok { + cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ + Fix: fixSplitLines, + URI: pgf.URI, + Range: rng, + ResolveEdits: supportsResolveEdits(options), + }) + if err != nil { + return nil, err + } + commands = append(commands, cmd) + } + + if msg, ok, _ := CanJoinLines(pgf.File, pkg.FileSet(), start, end); ok { + cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ + Fix: fixJoinLines, + URI: pgf.URI, + Range: rng, + ResolveEdits: supportsResolveEdits(options), + }) + if err != nil { + return nil, err + } + commands = append(commands, cmd) + } + // N.B.: an inspector only pays for itself after ~5 passes, which means we're // currently not getting a good deal on this inspection. // @@ -328,6 +354,28 @@ func getRewriteCodeActions(pkg *cache.Package, pgf *ParsedGoFile, fh file.Handle } } + for _, diag := range fillswitch.Diagnose(inspect, start, end, pkg.GetTypes(), pkg.GetTypesInfo()) { + edits, err := suggestedFixToEdits(ctx, snapshot, pkg.FileSet(), &diag.SuggestedFixes[0]) + if err != nil { + return nil, err + } + + changes := []protocol.DocumentChanges{} // must be a slice + for _, edit := range edits { + edit := edit + changes = append(changes, protocol.DocumentChanges{ + TextDocumentEdit: &edit, + }) + } + + actions = append(actions, protocol.CodeAction{ + Title: diag.Message, + Kind: protocol.RefactorRewrite, + Edit: &protocol.WorkspaceEdit{ + DocumentChanges: changes, + }, + }) + } for i := range commands { actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorRewrite, &commands[i], nil, options)) } @@ -344,7 +392,7 @@ func getRewriteCodeActions(pkg *cache.Package, pgf *ParsedGoFile, fh file.Handle // // (Note that the unusedparam analyzer also computes this property, but // much more precisely, allowing it to report its findings as diagnostics.) -func canRemoveParameter(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range) bool { +func canRemoveParameter(pkg *cache.Package, pgf *parsego.File, rng protocol.Range) bool { info, err := FindParam(pgf, rng) if err != nil { return false // e.g. invalid range @@ -381,7 +429,7 @@ func canRemoveParameter(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Rang } // getInlineCodeActions returns refactor.inline actions available at the specified range. -func getInlineCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { +func getInlineCodeActions(pkg *cache.Package, pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { start, end, err := pgf.RangePos(rng) if err != nil { return nil, err @@ -411,7 +459,7 @@ func getInlineCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Ra } // getGoTestCodeActions returns any "run this test/benchmark" code actions for the selection. -func getGoTestCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range) ([]protocol.CodeAction, error) { +func getGoTestCodeActions(pkg *cache.Package, pgf *parsego.File, rng protocol.Range) ([]protocol.CodeAction, error) { fns, err := TestsAndBenchmarks(pkg, pgf) if err != nil { return nil, err diff --git a/gopls/internal/golang/comment.go b/gopls/internal/golang/comment.go index ece123801ff..95f0df98293 100644 --- a/gopls/internal/golang/comment.go +++ b/gopls/internal/golang/comment.go @@ -1,19 +1,12 @@ -// Copyright 2019 The Go Authors. All rights reserved. +// Copyright 2022 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. -//go:build !go1.19 -// +build !go1.19 - package golang import ( - "bytes" - "io" - "regexp" - "strings" - "unicode" - "unicode/utf8" + "fmt" + "go/doc/comment" "golang.org/x/tools/gopls/internal/settings" ) @@ -23,364 +16,26 @@ import ( // so it is known not to have leading, trailing blank lines // nor to have trailing spaces at the end of lines. // The comment markers have already been removed. -// -// Each line is converted into a markdown line and empty lines are just converted to -// newlines. Heading are prefixed with `### ` to make it a markdown heading. -// -// A span of indented lines retains a 4 space prefix block, with the common indent -// prefix removed unless empty, in which case it will be converted to a newline. -// -// URLs in the comment text are converted into links. -func CommentToMarkdown(text string, _ *settings.Options) string { - buf := &bytes.Buffer{} - commentToMarkdown(buf, text) - return buf.String() -} - -var ( - mdNewline = []byte("\n") - mdHeader = []byte("### ") - mdIndent = []byte(" ") - mdLinkStart = []byte("[") - mdLinkDiv = []byte("](") - mdLinkEnd = []byte(")") -) - -func commentToMarkdown(w io.Writer, text string) { - blocks := blocks(text) - for i, b := range blocks { - switch b.op { - case opPara: - for _, line := range b.lines { - emphasize(w, line, true) - } - case opHead: - // The header block can consist of only one line. - // However, check the number of lines, just in case. - if len(b.lines) == 0 { - // Skip this block. - continue - } - header := b.lines[0] - - w.Write(mdHeader) - commentEscape(w, header, true) - // Header doesn't end with \n unlike the lines of other blocks. - w.Write(mdNewline) - case opPre: - for _, line := range b.lines { - if isBlank(line) { - w.Write(mdNewline) - continue - } - w.Write(mdIndent) - w.Write([]byte(line)) - } - } - - if i < len(blocks)-1 { - w.Write(mdNewline) - } - } -} - -const ( - ulquo = "“" - urquo = "”" -) - -var ( - markdownEscape = regexp.MustCompile(`([\\\x60*{}[\]()#+\-.!_>~|"$%&'\/:;<=?@^])`) - - unicodeQuoteReplacer = strings.NewReplacer("``", ulquo, "''", urquo) -) - -// commentEscape escapes comment text for markdown. If nice is set, -// also turn double ` and ' into “ and ”. -func commentEscape(w io.Writer, text string, nice bool) { - if nice { - text = convertQuotes(text) - } - text = escapeRegex(text) - w.Write([]byte(text)) -} - -func convertQuotes(text string) string { - return unicodeQuoteReplacer.Replace(text) -} - -func escapeRegex(text string) string { - return markdownEscape.ReplaceAllString(text, `\$1`) -} - -func emphasize(w io.Writer, line string, nice bool) { - for { - m := matchRx.FindStringSubmatchIndex(line) - if m == nil { - break - } - // m >= 6 (two parenthesized sub-regexps in matchRx, 1st one is urlRx) - - // write text before match - commentEscape(w, line[0:m[0]], nice) - - // adjust match for URLs - match := line[m[0]:m[1]] - if strings.Contains(match, "://") { - m0, m1 := m[0], m[1] - for _, s := range []string{"()", "{}", "[]"} { - open, close := s[:1], s[1:] // E.g., "(" and ")" - // require opening parentheses before closing parentheses (#22285) - if i := strings.Index(match, close); i >= 0 && i < strings.Index(match, open) { - m1 = m0 + i - match = line[m0:m1] - } - // require balanced pairs of parentheses (#5043) - for i := 0; strings.Count(match, open) != strings.Count(match, close) && i < 10; i++ { - m1 = strings.LastIndexAny(line[:m1], s) - match = line[m0:m1] - } - } - if m1 != m[1] { - // redo matching with shortened line for correct indices - m = matchRx.FindStringSubmatchIndex(line[:m[0]+len(match)]) - } - } - - // Following code has been modified from go/doc since words is always - // nil. All html formatting has also been transformed into markdown formatting - - // analyze match - url := "" - if m[2] >= 0 { - url = match - } - - // write match - if len(url) > 0 { - w.Write(mdLinkStart) - } - - commentEscape(w, match, nice) - - if len(url) > 0 { - w.Write(mdLinkDiv) - w.Write([]byte(urlReplacer.Replace(url))) - w.Write(mdLinkEnd) - } - - // advance - line = line[m[1]:] - } - commentEscape(w, line, nice) -} - -// Everything from here on is a copy of go/doc/comment.go - -const ( - // Regexp for Go identifiers - identRx = `[\pL_][\pL_0-9]*` - - // Regexp for URLs - // Match parens, and check later for balance - see #5043, #22285 - // Match .,:;?! within path, but not at end - see #18139, #16565 - // This excludes some rare yet valid urls ending in common punctuation - // in order to allow sentences ending in URLs. - - // protocol (required) e.g. http - protoPart = `(https?|ftp|file|gopher|mailto|nntp)` - // host (required) e.g. www.example.com or [::1]:8080 - hostPart = `([a-zA-Z0-9_@\-.\[\]:]+)` - // path+query+fragment (optional) e.g. /path/index.html?q=foo#bar - pathPart = `([.,:;?!]*[a-zA-Z0-9$'()*+&#=@~_/\-\[\]%])*` - - urlRx = protoPart + `://` + hostPart + pathPart -) - -var ( - matchRx = regexp.MustCompile(`(` + urlRx + `)|(` + identRx + `)`) - urlReplacer = strings.NewReplacer(`(`, `\(`, `)`, `\)`) -) - -func indentLen(s string) int { - i := 0 - for i < len(s) && (s[i] == ' ' || s[i] == '\t') { - i++ - } - return i -} - -func isBlank(s string) bool { - return len(s) == 0 || (len(s) == 1 && s[0] == '\n') -} - -func commonPrefix(a, b string) string { - i := 0 - for i < len(a) && i < len(b) && a[i] == b[i] { - i++ - } - return a[0:i] -} - -func unindent(block []string) { - if len(block) == 0 { - return - } - - // compute maximum common white prefix - prefix := block[0][0:indentLen(block[0])] - for _, line := range block { - if !isBlank(line) { - prefix = commonPrefix(prefix, line) - } - } - n := len(prefix) - - // remove - for i, line := range block { - if !isBlank(line) { - block[i] = line[n:] - } - } -} - -// heading returns the trimmed line if it passes as a section heading; -// otherwise it returns the empty string. -func heading(line string) string { - line = strings.TrimSpace(line) - if len(line) == 0 { - return "" - } - - // a heading must start with an uppercase letter - r, _ := utf8.DecodeRuneInString(line) - if !unicode.IsLetter(r) || !unicode.IsUpper(r) { - return "" - } - - // it must end in a letter or digit: - r, _ = utf8.DecodeLastRuneInString(line) - if !unicode.IsLetter(r) && !unicode.IsDigit(r) { - return "" - } - - // exclude lines with illegal characters. we allow "()," - if strings.ContainsAny(line, ";:!?+*/=[]{}_^°&§~%#@<\">\\") { - return "" - } - - // allow "'" for possessive "'s" only - for b := line; ; { - i := strings.IndexRune(b, '\'') - if i < 0 { - break - } - if i+1 >= len(b) || b[i+1] != 's' || (i+2 < len(b) && b[i+2] != ' ') { - return "" // not followed by "s " - } - b = b[i+2:] - } - - // allow "." when followed by non-space - for b := line; ; { - i := strings.IndexRune(b, '.') - if i < 0 { - break - } - if i+1 >= len(b) || b[i+1] == ' ' { - return "" // not followed by non-space - } - b = b[i+1:] - } - - return line -} - -type op int - -const ( - opPara op = iota - opHead - opPre -) - -type block struct { - op op - lines []string -} - -func blocks(text string) []block { - var ( - out []block - para []string - - lastWasBlank = false - lastWasHeading = false - ) - - close := func() { - if para != nil { - out = append(out, block{opPara, para}) - para = nil - } - } - - lines := strings.SplitAfter(text, "\n") - unindent(lines) - for i := 0; i < len(lines); { - line := lines[i] - if isBlank(line) { - // close paragraph - close() - i++ - lastWasBlank = true - continue - } - if indentLen(line) > 0 { - // close paragraph - close() - - // count indented or blank lines - j := i + 1 - for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) { - j++ - } - // but not trailing blank lines - for j > i && isBlank(lines[j-1]) { - j-- - } - pre := lines[i:j] - i = j - - unindent(pre) - - // put those lines in a pre block - out = append(out, block{opPre, pre}) - lastWasHeading = false - continue - } - - if lastWasBlank && !lastWasHeading && i+2 < len(lines) && - isBlank(lines[i+1]) && !isBlank(lines[i+2]) && indentLen(lines[i+2]) == 0 { - // current line is non-blank, surrounded by blank lines - // and the next non-blank line is not indented: this - // might be a heading. - if head := heading(line); head != "" { - close() - out = append(out, block{opHead, []string{head}}) - i += 2 - lastWasHeading = true - continue - } - } - - // open paragraph - lastWasBlank = false - lastWasHeading = false - para = append(para, lines[i]) - i++ - } - close() - - return out +func CommentToMarkdown(text string, options *settings.Options) string { + var p comment.Parser + doc := p.Parse(text) + var pr comment.Printer + // The default produces {#Hdr-...} tags for headings. + // vscode displays thems, which is undesirable. + // The godoc for comment.Printer says the tags + // avoid a security problem. + pr.HeadingID = func(*comment.Heading) string { return "" } + pr.DocLinkURL = func(link *comment.DocLink) string { + msg := fmt.Sprintf("https://%s/%s", options.LinkTarget, link.ImportPath) + if link.Name != "" { + msg += "#" + if link.Recv != "" { + msg += link.Recv + "." + } + msg += link.Name + } + return msg + } + easy := pr.Markdown(doc) + return string(easy) } diff --git a/gopls/internal/golang/comment_go118_test.go b/gopls/internal/golang/comment_go118_test.go deleted file mode 100644 index c38898a28e6..00000000000 --- a/gopls/internal/golang/comment_go118_test.go +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright 2019 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. - -//go:build !go1.19 -// +build !go1.19 - -package golang - -import ( - "bytes" - "reflect" - "strings" - "testing" -) - -// This file is a copy of go/doc/comment_test.go with the exception for -// the test cases for TestEmphasize and TestCommentEscape - -var headingTests = []struct { - line string - ok bool -}{ - {"Section", true}, - {"A typical usage", true}, - {"ΔΛΞ is Greek", true}, - {"Foo 42", true}, - {"", false}, - {"section", false}, - {"A typical usage:", false}, - {"This code:", false}, - {"δ is Greek", false}, - {"Foo §", false}, - {"Fermat's Last Sentence", true}, - {"Fermat's", true}, - {"'sX", false}, - {"Ted 'Too' Bar", false}, - {"Use n+m", false}, - {"Scanning:", false}, - {"N:M", false}, -} - -func TestIsHeading(t *testing.T) { - for _, tt := range headingTests { - if h := heading(tt.line); (len(h) > 0) != tt.ok { - t.Errorf("isHeading(%q) = %v, want %v", tt.line, h, tt.ok) - } - } -} - -var blocksTests = []struct { - in string - out []block - text string -}{ - { - in: `Para 1. -Para 1 line 2. - -Para 2. - -Section - -Para 3. - - pre - pre1 - -Para 4. - - pre - pre1 - - pre2 - -Para 5. - - - pre - - - pre1 - pre2 - -Para 6. - pre - pre2 -`, - out: []block{ - {opPara, []string{"Para 1.\n", "Para 1 line 2.\n"}}, - {opPara, []string{"Para 2.\n"}}, - {opHead, []string{"Section"}}, - {opPara, []string{"Para 3.\n"}}, - {opPre, []string{"pre\n", "pre1\n"}}, - {opPara, []string{"Para 4.\n"}}, - {opPre, []string{"pre\n", "pre1\n", "\n", "pre2\n"}}, - {opPara, []string{"Para 5.\n"}}, - {opPre, []string{"pre\n", "\n", "\n", "pre1\n", "pre2\n"}}, - {opPara, []string{"Para 6.\n"}}, - {opPre, []string{"pre\n", "pre2\n"}}, - }, - text: `. Para 1. Para 1 line 2. - -. Para 2. - - -. Section - -. Para 3. - -$ pre -$ pre1 - -. Para 4. - -$ pre -$ pre1 - -$ pre2 - -. Para 5. - -$ pre - - -$ pre1 -$ pre2 - -. Para 6. - -$ pre -$ pre2 -`, - }, - { - in: "Para.\n\tshould not be ``escaped''", - out: []block{ - {opPara, []string{"Para.\n"}}, - {opPre, []string{"should not be ``escaped''"}}, - }, - text: ". Para.\n\n$ should not be ``escaped''", - }, - { - in: "// A very long line of 46 char for line wrapping.", - out: []block{ - {opPara, []string{"// A very long line of 46 char for line wrapping."}}, - }, - text: `. // A very long line of 46 char for line -. // wrapping. -`, - }, - { - in: `/* A very long line of 46 char for line wrapping. -A very long line of 46 char for line wrapping. */`, - out: []block{ - {opPara, []string{"/* A very long line of 46 char for line wrapping.\n", "A very long line of 46 char for line wrapping. */"}}, - }, - text: `. /* A very long line of 46 char for line -. wrapping. A very long line of 46 char -. for line wrapping. */ -`, - }, -} - -func TestBlocks(t *testing.T) { - for i, tt := range blocksTests { - b := blocks(tt.in) - if !reflect.DeepEqual(b, tt.out) { - t.Errorf("#%d: mismatch\nhave: %v\nwant: %v", i, b, tt.out) - } - } -} - -// This has been modified from go/doc to use markdown links instead of html ones -// and use markdown escaping instead oh html -var emphasizeTests = []struct { - in, out string -}{ - {"", ""}, - {"http://[::1]:8080/foo.txt", `[http\:\/\/\[\:\:1\]\:8080\/foo\.txt](http://[::1]:8080/foo.txt)`}, - {"before (https://www.google.com) after", `before \([https\:\/\/www\.google\.com](https://www.google.com)\) after`}, - {"before https://www.google.com:30/x/y/z:b::c. After", `before [https\:\/\/www\.google\.com\:30\/x\/y\/z\:b\:\:c](https://www.google.com:30/x/y/z:b::c)\. After`}, - {"http://www.google.com/path/:;!-/?query=%34b#093124", `[http\:\/\/www\.google\.com\/path\/\:\;\!\-\/\?query\=\%34b\#093124](http://www.google.com/path/:;!-/?query=%34b#093124)`}, - {"http://www.google.com/path/:;!-/?query=%34bar#093124", `[http\:\/\/www\.google\.com\/path\/\:\;\!\-\/\?query\=\%34bar\#093124](http://www.google.com/path/:;!-/?query=%34bar#093124)`}, - {"http://www.google.com/index.html! After", `[http\:\/\/www\.google\.com\/index\.html](http://www.google.com/index.html)\! After`}, - {"http://www.google.com/", `[http\:\/\/www\.google\.com\/](http://www.google.com/)`}, - {"https://www.google.com/", `[https\:\/\/www\.google\.com\/](https://www.google.com/)`}, - {"http://www.google.com/path.", `[http\:\/\/www\.google\.com\/path](http://www.google.com/path)\.`}, - {"http://en.wikipedia.org/wiki/Camellia_(cipher)", `[http\:\/\/en\.wikipedia\.org\/wiki\/Camellia\_\(cipher\)](http://en.wikipedia.org/wiki/Camellia_\(cipher\))`}, - {"(http://www.google.com/)", `\([http\:\/\/www\.google\.com\/](http://www.google.com/)\)`}, - {"http://gmail.com)", `[http\:\/\/gmail\.com](http://gmail.com)\)`}, - {"((http://gmail.com))", `\(\([http\:\/\/gmail\.com](http://gmail.com)\)\)`}, - {"http://gmail.com ((http://gmail.com)) ()", `[http\:\/\/gmail\.com](http://gmail.com) \(\([http\:\/\/gmail\.com](http://gmail.com)\)\) \(\)`}, - {"Foo bar http://example.com/ quux!", `Foo bar [http\:\/\/example\.com\/](http://example.com/) quux\!`}, - {"Hello http://example.com/%2f/ /world.", `Hello [http\:\/\/example\.com\/\%2f\/](http://example.com/%2f/) \/world\.`}, - {"Lorem http: ipsum //host/path", `Lorem http\: ipsum \/\/host\/path`}, - {"javascript://is/not/linked", `javascript\:\/\/is\/not\/linked`}, - {"http://foo", `[http\:\/\/foo](http://foo)`}, - {"art by [[https://www.example.com/person/][Person Name]]", `art by \[\[[https\:\/\/www\.example\.com\/person\/](https://www.example.com/person/)\]\[Person Name\]\]`}, - {"please visit (http://golang.org/)", `please visit \([http\:\/\/golang\.org\/](http://golang.org/)\)`}, - {"please visit http://golang.org/hello())", `please visit [http\:\/\/golang\.org\/hello\(\)](http://golang.org/hello\(\))\)`}, - {"http://git.qemu.org/?p=qemu.git;a=blob;f=qapi-schema.json;hb=HEAD", `[http\:\/\/git\.qemu\.org\/\?p\=qemu\.git\;a\=blob\;f\=qapi\-schema\.json\;hb\=HEAD](http://git.qemu.org/?p=qemu.git;a=blob;f=qapi-schema.json;hb=HEAD)`}, - {"https://foo.bar/bal/x(])", `[https\:\/\/foo\.bar\/bal\/x\(](https://foo.bar/bal/x\()\]\)`}, - {"foo [ http://bar(])", `foo \[ [http\:\/\/bar\(](http://bar\()\]\)`}, -} - -func TestEmphasize(t *testing.T) { - for i, tt := range emphasizeTests { - var buf bytes.Buffer - emphasize(&buf, tt.in, true) - out := buf.String() - if out != tt.out { - t.Errorf("#%d: mismatch\nhave: %v\nwant: %v", i, out, tt.out) - } - } -} - -func TestCommentEscape(t *testing.T) { - //ldquo -> ulquo and rdquo -> urquo - commentTests := []struct { - in, out string - }{ - {"typically invoked as ``go tool asm'',", "typically invoked as " + ulquo + "go tool asm" + urquo + ","}, - {"For more detail, run ``go help test'' and ``go help testflag''", "For more detail, run " + ulquo + "go help test" + urquo + " and " + ulquo + "go help testflag" + urquo}} - for i, tt := range commentTests { - var buf strings.Builder - commentEscape(&buf, tt.in, true) - out := buf.String() - if out != tt.out { - t.Errorf("#%d: mismatch\nhave: %q\nwant: %q", i, out, tt.out) - } - } -} - -func TestCommentToMarkdown(t *testing.T) { - tests := []struct { - in, out string - }{ - { - in: "F declaration.\n", - out: "F declaration\\.\n", - }, - { - in: ` -F declaration. Lorem ipsum dolor sit amet. -Etiam mattis eros at orci mollis molestie. -`, - out: ` -F declaration\. Lorem ipsum dolor sit amet\. -Etiam mattis eros at orci mollis molestie\. -`, - }, - { - in: ` -F declaration. - -Lorem ipsum dolor sit amet. -Sed id dui turpis. - - - - -Aenean tempus velit non auctor eleifend. -Aenean efficitur a sem id ultricies. - - -Phasellus efficitur mauris et viverra bibendum. -`, - out: ` -F declaration\. - -Lorem ipsum dolor sit amet\. -Sed id dui turpis\. - -Aenean tempus velit non auctor eleifend\. -Aenean efficitur a sem id ultricies\. - -Phasellus efficitur mauris et viverra bibendum\. -`, - }, - { - in: ` -F declaration. - -Aenean tempus velit non auctor eleifend. - -Section - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. - - func foo() {} - - - func bar() {} - -Fusce lorem lacus. - - func foo() {} - - func bar() {} - -Maecenas in lobortis lectus. - - func foo() {} - - func bar() {} - -Phasellus efficitur mauris et viverra bibendum. -`, - out: ` -F declaration\. - -Aenean tempus velit non auctor eleifend\. - -### Section - -Lorem ipsum dolor sit amet, consectetur adipiscing elit\. - - func foo() {} - - - func bar() {} - -Fusce lorem lacus\. - - func foo() {} - - func bar() {} - -Maecenas in lobortis lectus\. - - func foo() {} - - func bar() {} - -Phasellus efficitur mauris et viverra bibendum\. -`, - }, - { - in: ` -F declaration. - - func foo() { - fmt.Println("foo") - } - func bar() { - fmt.Println("bar") - } -`, - out: ` -F declaration\. - - func foo() { - fmt.Println("foo") - } - func bar() { - fmt.Println("bar") - } -`, - }, - } - for i, tt := range tests { - // Comments start with new lines for better readability. So, we should trim them. - tt.in = strings.TrimPrefix(tt.in, "\n") - tt.out = strings.TrimPrefix(tt.out, "\n") - - if out := CommentToMarkdown(tt.in, nil); out != tt.out { - t.Errorf("#%d: mismatch\nhave: %q\nwant: %q", i, out, tt.out) - } - } -} diff --git a/gopls/internal/golang/comment_go119.go b/gopls/internal/golang/comment_go119.go deleted file mode 100644 index eec338d54fa..00000000000 --- a/gopls/internal/golang/comment_go119.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2022 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. - -//go:build go1.19 -// +build go1.19 - -package golang - -// Starting with go1.19, the formatting of comments has changed, and there -// is a new package (go/doc/comment) for processing them. -// As long as gopls has to compile under earlier versions, tests -// have to pass with both the old and new code, which produce -// slightly different results. - -// When gopls no longer needs to compile with go1.18, the old comment.go should -// be replaced by this file, the golden test files should be updated. -// (and checkSameMarkdown() could be replaced by a simple comparison.) - -import ( - "fmt" - "go/doc/comment" - - "golang.org/x/tools/gopls/internal/settings" -) - -// CommentToMarkdown converts comment text to formatted markdown. -// The comment was prepared by DocReader, -// so it is known not to have leading, trailing blank lines -// nor to have trailing spaces at the end of lines. -// The comment markers have already been removed. -func CommentToMarkdown(text string, options *settings.Options) string { - var p comment.Parser - doc := p.Parse(text) - var pr comment.Printer - // The default produces {#Hdr-...} tags for headings. - // vscode displays thems, which is undesirable. - // The godoc for comment.Printer says the tags - // avoid a security problem. - pr.HeadingID = func(*comment.Heading) string { return "" } - pr.DocLinkURL = func(link *comment.DocLink) string { - msg := fmt.Sprintf("https://%s/%s", options.LinkTarget, link.ImportPath) - if link.Name != "" { - msg += "#" - if link.Recv != "" { - msg += link.Recv + "." - } - msg += link.Name - } - return msg - } - easy := pr.Markdown(doc) - return string(easy) -} diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index b796de4932d..8ff7f0b9cf2 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go @@ -37,10 +37,12 @@ import ( goplsastutil "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/gopls/internal/util/typesutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" ) // A CompletionItem represents a possible completion suggested by the algorithm. @@ -1585,7 +1587,7 @@ func (c *completer) lexical(ctx context.Context) error { } if c.inference.objType != nil { - if named, _ := golang.Deref(c.inference.objType).(*types.Named); named != nil { + if named, ok := aliases.Unalias(typesinternal.Unpointer(c.inference.objType)).(*types.Named); ok { // If we expected a named type, check the type's package for // completion items. This is useful when the current file hasn't // imported the type's package yet. @@ -1651,14 +1653,14 @@ func (c *completer) injectType(ctx context.Context, t types.Type) { return } - t = golang.Deref(t) + t = typesinternal.Unpointer(t) // If we have an expected type and it is _not_ a named type, handle // it specially. Non-named types like "[]int" will never be // considered via a lexical search, so we need to directly inject // them. Also allow generic types since lexical search does not // infer instantiated versions of them. - if named, _ := t.(*types.Named); named == nil || named.TypeParams().Len() > 0 { + if named, ok := aliases.Unalias(t).(*types.Named); !ok || named.TypeParams().Len() > 0 { // If our expected type is "[]int", this will add a literal // candidate of "[]int{}". c.literal(ctx, t, nil) @@ -1896,7 +1898,7 @@ func enclosingCompositeLiteral(path []ast.Node, pos token.Pos, info *types.Info) clInfo := compLitInfo{ cl: n, - clType: golang.Deref(tv.Type).Underlying(), + clType: typesinternal.Unpointer(tv.Type).Underlying(), } var ( diff --git a/gopls/internal/golang/completion/definition.go b/gopls/internal/golang/completion/definition.go index 1e3852bffdb..fc8b0ae5c69 100644 --- a/gopls/internal/golang/completion/definition.go +++ b/gopls/internal/golang/completion/definition.go @@ -11,7 +11,7 @@ import ( "unicode" "unicode/utf8" - "golang.org/x/tools/gopls/internal/golang" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/golang/completion/snippet" "golang.org/x/tools/gopls/internal/protocol" ) @@ -21,7 +21,7 @@ import ( // BenchmarkFoo(b *testing.B), FuzzFoo(f *testing.F) // path[0] is known to be *ast.Ident -func definition(path []ast.Node, obj types.Object, pgf *golang.ParsedGoFile) ([]CompletionItem, *Selection) { +func definition(path []ast.Node, obj types.Object, pgf *parsego.File) ([]CompletionItem, *Selection) { if _, ok := obj.(*types.Func); !ok { return nil, nil // not a function at all } diff --git a/gopls/internal/golang/completion/literal.go b/gopls/internal/golang/completion/literal.go index 45f772d789f..dc4fc0dcd60 100644 --- a/gopls/internal/golang/completion/literal.go +++ b/gopls/internal/golang/completion/literal.go @@ -14,7 +14,9 @@ import ( "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/golang/completion/snippet" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/typesinternal" ) // literal generates composite literal, function literal, and make() @@ -49,10 +51,11 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im // // don't offer "mySlice{}" since we have already added a candidate // of "[]int{}". - if _, named := literalType.(*types.Named); named && expType != nil { - if _, named := golang.Deref(expType).(*types.Named); !named { - return - } + if is[*types.Named](aliases.Unalias(literalType)) && + expType != nil && + !is[*types.Named](aliases.Unalias(typesinternal.Unpointer(expType))) { + + return } // Check if an object of type literalType would match our expected type. @@ -589,3 +592,8 @@ func (c *completer) typeParamInScope(tp *types.TypeParam) bool { _, foundObj := scope.LookupParent(obj.Name(), c.pos) return obj == foundObj } + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +} diff --git a/gopls/internal/golang/completion/package.go b/gopls/internal/golang/completion/package.go index 709d65ce3a7..12d4ff0be36 100644 --- a/gopls/internal/golang/completion/package.go +++ b/gopls/internal/golang/completion/package.go @@ -19,6 +19,7 @@ import ( "unicode" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/protocol" @@ -32,7 +33,7 @@ func packageClauseCompletions(ctx context.Context, snapshot *cache.Snapshot, fh // We know that the AST for this file will be empty due to the missing // package declaration, but parse it anyway to get a mapper. // TODO(adonovan): opt: there's no need to parse just to get a mapper. - pgf, err := snapshot.ParseGo(ctx, fh, golang.ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, nil, err } @@ -68,7 +69,7 @@ func packageClauseCompletions(ctx context.Context, snapshot *cache.Snapshot, fh // packageCompletionSurrounding returns surrounding for package completion if a // package completions can be suggested at a given cursor offset. A valid location // for package completion is above any declarations or import statements. -func packageCompletionSurrounding(pgf *golang.ParsedGoFile, offset int) (*Selection, error) { +func packageCompletionSurrounding(pgf *parsego.File, offset int) (*Selection, error) { m := pgf.Mapper // If the file lacks a package declaration, the parser will return an empty // AST. As a work-around, try to parse an expression from the file contents. diff --git a/gopls/internal/golang/completion/postfix_snippets.go b/gopls/internal/golang/completion/postfix_snippets.go index 252d2e77a90..fad8f784713 100644 --- a/gopls/internal/golang/completion/postfix_snippets.go +++ b/gopls/internal/golang/completion/postfix_snippets.go @@ -21,8 +21,10 @@ import ( "golang.org/x/tools/gopls/internal/golang/completion/snippet" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" + "golang.org/x/tools/internal/typesinternal" ) // Postfix snippets are artificial methods that allow the user to @@ -465,7 +467,7 @@ func (a *postfixTmplArgs) VarName(t types.Type, nonNamedDefault string) string { // go/types predicates are undefined on types.Typ[types.Invalid]. if !types.Identical(t, types.Typ[types.Invalid]) && types.Implements(t, errorIntf) { name = "err" - } else if _, isNamed := golang.Deref(t).(*types.Named); !isNamed { + } else if !is[*types.Named](aliases.Unalias(typesinternal.Unpointer(t))) { name = nonNamedDefault } diff --git a/gopls/internal/golang/completion/util.go b/gopls/internal/golang/completion/util.go index 65d1b4d96dc..8ac7f161e72 100644 --- a/gopls/internal/golang/completion/util.go +++ b/gopls/internal/golang/completion/util.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/typesinternal" ) // exprAtPos returns the index of the expression containing pos. @@ -38,7 +39,12 @@ func eachField(T types.Type, fn func(*types.Var)) { var visit func(T types.Type) visit = func(T types.Type) { - if T, ok := golang.Deref(T).Underlying().(*types.Struct); ok { + // T may be a Struct, optionally Named, with an optional + // Pointer (with optional Aliases at every step!): + // Consider: type T *struct{ f int }; _ = T(nil).f + // + // TODO(adonovan): use typeparams.Deref in next CL. + if T, ok := typesinternal.Unpointer(T.Underlying()).Underlying().(*types.Struct); ok { if seen.At(T) != nil { return } diff --git a/gopls/internal/golang/definition.go b/gopls/internal/golang/definition.go index 4cc522978e6..c3d791c9443 100644 --- a/gopls/internal/golang/definition.go +++ b/gopls/internal/golang/definition.go @@ -130,7 +130,7 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object } var ( - pgf *ParsedGoFile + pgf *parsego.File decl ast.Node err error ) @@ -148,7 +148,7 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object if err != nil { return nil, nil, err } - pgf, err = snapshot.ParseGo(ctx, fh, ParseFull&^parser.SkipObjectResolution) + pgf, err = snapshot.ParseGo(ctx, fh, parsego.Full&^parser.SkipObjectResolution) if err != nil { return nil, nil, err } @@ -202,7 +202,7 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object // TODO(rfindley): this function exists to preserve the pre-existing behavior // of golang.Identifier. Eliminate this helper in favor of sharing // functionality with objectsAt, after choosing suitable primitives. -func referencedObject(pkg *cache.Package, pgf *ParsedGoFile, pos token.Pos) (*ast.Ident, types.Object, types.Type) { +func referencedObject(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (*ast.Ident, types.Object, types.Type) { path := pathEnclosingObjNode(pgf.File, pos) if len(path) == 0 { return nil, nil, nil @@ -242,7 +242,7 @@ func referencedObject(pkg *cache.Package, pgf *ParsedGoFile, pos token.Pos) (*as // import spec containing pos. // // If pos is not inside an import spec, it returns nil, nil. -func importDefinition(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *ParsedGoFile, pos token.Pos) ([]protocol.Location, error) { +func importDefinition(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, pos token.Pos) ([]protocol.Location, error) { var imp *ast.ImportSpec for _, spec := range pgf.File.Imports { // We use "<= End" to accept a query immediately after an ImportSpec. @@ -273,7 +273,7 @@ func importDefinition(ctx context.Context, s *cache.Snapshot, pkg *cache.Package } continue } - pgf, err := s.ParseGo(ctx, fh, ParseHeader) + pgf, err := s.ParseGo(ctx, fh, parsego.Header) if err != nil { if ctx.Err() != nil { return nil, ctx.Err() diff --git a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go index 6f07cb869c5..4b196383175 100644 --- a/gopls/internal/golang/fix.go +++ b/gopls/internal/golang/fix.go @@ -64,6 +64,8 @@ const ( fixExtractMethod = "extract_method" fixInlineCall = "inline_call" fixInvertIfCondition = "invert_if_condition" + fixSplitLines = "split_lines" + fixJoinLines = "join_lines" ) // ApplyFix applies the specified kind of suggested fix to the given @@ -115,6 +117,8 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file fixExtractVariable: singleFile(extractVariable), fixInlineCall: inlineCall, fixInvertIfCondition: singleFile(invertIfCondition), + fixSplitLines: singleFile(splitLines), + fixJoinLines: singleFile(joinLines), } fixer, ok := fixers[fix] if !ok { diff --git a/gopls/internal/golang/folding_range.go b/gopls/internal/golang/folding_range.go index c856f0a1184..85faea5e31a 100644 --- a/gopls/internal/golang/folding_range.go +++ b/gopls/internal/golang/folding_range.go @@ -12,6 +12,7 @@ import ( "strings" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" @@ -28,7 +29,7 @@ type FoldingRangeInfo struct { func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, lineFoldingOnly bool) (ranges []*FoldingRangeInfo, err error) { // TODO(suzmue): consider limiting the number of folding ranges returned, and // implement a way to prioritize folding ranges in that case. - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -68,7 +69,7 @@ func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, } // foldingRangeFunc calculates the line folding range for ast.Node n -func foldingRangeFunc(pgf *ParsedGoFile, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { +func foldingRangeFunc(pgf *parsego.File, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { // TODO(suzmue): include trailing empty lines before the closing // parenthesis/brace. var kind protocol.FoldingRangeKind @@ -165,7 +166,7 @@ func validLineFoldingRange(tokFile *token.File, open, close, start, end token.Po // commentsFoldingRange returns the folding ranges for all comment blocks in file. // The folding range starts at the end of the first line of the comment block, and ends at the end of the // comment block and has kind protocol.Comment. -func commentsFoldingRange(pgf *ParsedGoFile) (comments []*FoldingRangeInfo) { +func commentsFoldingRange(pgf *parsego.File) (comments []*FoldingRangeInfo) { tokFile := pgf.Tok for _, commentGrp := range pgf.File.Comments { startGrpLine, endGrpLine := safetoken.Line(tokFile, commentGrp.Pos()), safetoken.Line(tokFile, commentGrp.End()) diff --git a/gopls/internal/golang/format.go b/gopls/internal/golang/format.go index 2eb2b8b01e4..3e5668b32fe 100644 --- a/gopls/internal/golang/format.go +++ b/gopls/internal/golang/format.go @@ -18,6 +18,7 @@ import ( "text/scanner" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" @@ -37,7 +38,7 @@ func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]pr return nil, fmt.Errorf("can't format %q: file is generated", fh.URI().Path()) } - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -111,7 +112,7 @@ type importFix struct { // In addition to returning the result of applying all edits, // it returns a list of fixes that could be applied to the file, with the // corresponding TextEdits that would be needed to apply that fix. -func allImportsFixes(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedGoFile) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { +func allImportsFixes(ctx context.Context, snapshot *cache.Snapshot, pgf *parsego.File) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { ctx, done := event.Start(ctx, "golang.AllImportsFixes") defer done() @@ -126,7 +127,7 @@ func allImportsFixes(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedG // computeImportEdits computes a set of edits that perform one or all of the // necessary import fixes. -func computeImportEdits(ctx context.Context, pgf *ParsedGoFile, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { +func computeImportEdits(ctx context.Context, pgf *parsego.File, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { filename := pgf.URI.Path() // Build up basic information about the original file. @@ -156,7 +157,7 @@ func computeImportEdits(ctx context.Context, pgf *ParsedGoFile, options *imports } // ComputeOneImportFixEdits returns text edits for a single import fix. -func ComputeOneImportFixEdits(snapshot *cache.Snapshot, pgf *ParsedGoFile, fix *imports.ImportFix) ([]protocol.TextEdit, error) { +func ComputeOneImportFixEdits(snapshot *cache.Snapshot, pgf *parsego.File, fix *imports.ImportFix) ([]protocol.TextEdit, error) { options := &imports.Options{ LocalPrefix: snapshot.Options().Local, // Defaults. @@ -170,7 +171,7 @@ func ComputeOneImportFixEdits(snapshot *cache.Snapshot, pgf *ParsedGoFile, fix * return computeFixEdits(pgf, options, []*imports.ImportFix{fix}) } -func computeFixEdits(pgf *ParsedGoFile, options *imports.Options, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) { +func computeFixEdits(pgf *parsego.File, options *imports.Options, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) { // trim the original data to match fixedData left, err := importPrefix(pgf.Src) if err != nil { @@ -303,7 +304,7 @@ func scanForCommentEnd(src []byte) int { return 0 } -func computeTextEdits(ctx context.Context, pgf *ParsedGoFile, formatted string) ([]protocol.TextEdit, error) { +func computeTextEdits(ctx context.Context, pgf *parsego.File, formatted string) ([]protocol.TextEdit, error) { _, done := event.Start(ctx, "golang.computeTextEdits") defer done() diff --git a/gopls/internal/golang/highlight.go b/gopls/internal/golang/highlight.go index f11426319cf..6cc8f6cb812 100644 --- a/gopls/internal/golang/highlight.go +++ b/gopls/internal/golang/highlight.go @@ -103,7 +103,7 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRa highlightIdentifier(node, file, info, result) case *ast.ForStmt, *ast.RangeStmt: highlightLoopControlFlow(path, info, result) - case *ast.SwitchStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt: highlightSwitchFlow(path, info, result) case *ast.BranchStmt: // BREAK can exit a loop, switch or select, while CONTINUE exit a loop so @@ -232,45 +232,47 @@ findEnclosingFunc: } } - // Scan fields, either adding highlights according to the highlightIndexes - // computed above, or accounting for the cursor position within the result - // list. - // (We do both at once to avoid repeating the cumbersome field traversal.) - i := 0 - findField: - for _, field := range funcType.Results.List { - for j, name := range field.Names { - if inNode(name) || highlightIndexes[i+j] { - result[posRange{name.Pos(), name.End()}] = unit{} - highlightIndexes[i+j] = true - break findField // found/highlighted the specific name - } - } - // If the cursor is in a field but not in a name (e.g. in the space, or - // the type), highlight the whole field. - // - // Note that this may not be ideal if we're at e.g. - // - // (x,‸y int, z int8) - // - // ...where it would make more sense to highlight only y. But we don't - // reach this function if not in a func, return, ident, or basiclit. - if inNode(field) || highlightIndexes[i] { - result[posRange{field.Pos(), field.End()}] = unit{} - highlightIndexes[i] = true - if inNode(field) { - for j := range field.Names { + if funcType.Results != nil { + // Scan fields, either adding highlights according to the highlightIndexes + // computed above, or accounting for the cursor position within the result + // list. + // (We do both at once to avoid repeating the cumbersome field traversal.) + i := 0 + findField: + for _, field := range funcType.Results.List { + for j, name := range field.Names { + if inNode(name) || highlightIndexes[i+j] { + result[posRange{name.Pos(), name.End()}] = unit{} highlightIndexes[i+j] = true + break findField // found/highlighted the specific name } } - break findField // found/highlighted the field - } + // If the cursor is in a field but not in a name (e.g. in the space, or + // the type), highlight the whole field. + // + // Note that this may not be ideal if we're at e.g. + // + // (x,‸y int, z int8) + // + // ...where it would make more sense to highlight only y. But we don't + // reach this function if not in a func, return, ident, or basiclit. + if inNode(field) || highlightIndexes[i] { + result[posRange{field.Pos(), field.End()}] = unit{} + highlightIndexes[i] = true + if inNode(field) { + for j := range field.Names { + highlightIndexes[i+j] = true + } + } + break findField // found/highlighted the field + } - n := len(field.Names) - if n == 0 { - n = 1 + n := len(field.Names) + if n == 0 { + n = 1 + } + i += n } - i += n } } } @@ -309,7 +311,7 @@ func highlightUnlabeledBreakFlow(path []ast.Node, info *types.Info, result map[p case *ast.ForStmt, *ast.RangeStmt: highlightLoopControlFlow(path, info, result) return // only highlight the innermost statement - case *ast.SwitchStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt: highlightSwitchFlow(path, info, result) return case *ast.SelectStmt: @@ -331,7 +333,7 @@ func highlightLabeledFlow(path []ast.Node, info *types.Info, stmt *ast.BranchStm switch label.Stmt.(type) { case *ast.ForStmt, *ast.RangeStmt: highlightLoopControlFlow([]ast.Node{label.Stmt, label}, info, result) - case *ast.SwitchStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt: highlightSwitchFlow([]ast.Node{label.Stmt, label}, info, result) } return @@ -381,7 +383,7 @@ Outer: switch n.(type) { case *ast.ForStmt, *ast.RangeStmt: return loop == n - case *ast.SwitchStmt, *ast.SelectStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt: return false } b, ok := n.(*ast.BranchStmt) @@ -434,7 +436,7 @@ Outer: // Reverse walk the path till we get to the switch statement. for i := range path { switch n := path[i].(type) { - case *ast.SwitchStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt: switchNodeLabel = labelFor(path[i:]) if stmtLabel == nil || switchNodeLabel == stmtLabel { switchNode = n @@ -457,7 +459,7 @@ Outer: // Traverse AST to find break statements within the same switch. ast.Inspect(switchNode, func(n ast.Node) bool { switch n.(type) { - case *ast.SwitchStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt: return switchNode == n case *ast.ForStmt, *ast.RangeStmt, *ast.SelectStmt: return false diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index ef486018786..8e842739276 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -38,6 +38,7 @@ import ( "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/tokeninternal" + "golang.org/x/tools/internal/typesinternal" ) // hoverJSON contains the structured result of a hover query. It is @@ -530,7 +531,7 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec // imp in the file pgf of pkg. // // If we do not have metadata for the hovered import, it returns _ -func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *ParsedGoFile, imp *ast.ImportSpec) (protocol.Range, *hoverJSON, error) { +func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, imp *ast.ImportSpec) (protocol.Range, *hoverJSON, error) { rng, err := pgf.NodeRange(imp.Path) if err != nil { return protocol.Range{}, nil, err @@ -559,7 +560,7 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa } continue } - pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { if ctx.Err() != nil { return protocol.Range{}, nil, ctx.Err() @@ -581,7 +582,7 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa // hoverPackageName computes hover information for the package name of the file // pgf in pkg. -func hoverPackageName(pkg *cache.Package, pgf *ParsedGoFile) (protocol.Range, *hoverJSON, error) { +func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *hoverJSON, error) { var comment *ast.CommentGroup for _, pgf := range pkg.CompiledGoFiles() { if pgf.File.Doc != nil { @@ -609,7 +610,7 @@ func hoverPackageName(pkg *cache.Package, pgf *ParsedGoFile) (protocol.Range, *h // For example, hovering over "\u2211" in "foo \u2211 bar" yields: // // '∑', U+2211, N-ARY SUMMATION -func hoverLit(pgf *ParsedGoFile, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *hoverJSON, error) { +func hoverLit(pgf *parsego.File, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *hoverJSON, error) { var ( value string // if non-empty, a constant value to format in hover r rune // if non-zero, format a description of this rune in hover @@ -872,7 +873,7 @@ func objectString(obj types.Object, qf types.Qualifier, declPos token.Pos, file case *types.Named: // Try to add a formatted duration as an inline comment. pkg := typ.Obj().Pkg() - if pkg.Path() == "time" && typ.Obj().Name() == "Duration" { + if pkg.Path() == "time" && typ.Obj().Name() == "Duration" && obj.Val().Kind() == constant.Int { if d, ok := constant.Int64Val(obj.Val()); ok { comment = time.Duration(d).String() } @@ -968,7 +969,7 @@ func parseFull(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSe return nil, 0, err } - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, 0, err } @@ -1096,22 +1097,16 @@ func formatDoc(h *hoverJSON, options *settings.Options) string { // TODO(rfindley): this function has tricky semantics, and may be worth unit // testing and/or refactoring. func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spec, field *ast.Field) { - // panic(found{}) breaks off the traversal and - // causes the function to return normally. - type found struct{} - defer func() { - switch x := recover().(type) { - case nil: - case found: - default: - panic(x) - } - }() + found := false // Visit the files in search of the node at pos. stack := make([]ast.Node, 0, 20) + // Allocate the closure once, outside the loop. f := func(n ast.Node) bool { + if found { + return false + } if n != nil { stack = append(stack, n) // push } else { @@ -1144,7 +1139,8 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe if id.Pos() == pos { field = n findEnclosingDeclAndSpec() - panic(found{}) + found = true + return false } } @@ -1153,7 +1149,8 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe if n.Pos() == pos { field = n findEnclosingDeclAndSpec() - panic(found{}) + found = true + return false } // Also check "X" in "...X". This makes it easy to format variadic @@ -1164,13 +1161,15 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil && ell.Elt.Pos() == pos { field = n findEnclosingDeclAndSpec() - panic(found{}) + found = true + return false } case *ast.FuncDecl: if n.Name.Pos() == pos { decl = n - panic(found{}) + found = true + return false } case *ast.GenDecl: @@ -1180,14 +1179,16 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe if s.Name.Pos() == pos { decl = n spec = s - panic(found{}) + found = true + return false } case *ast.ValueSpec: for _, id := range s.Names { if id.Pos() == pos { decl = n spec = s - panic(found{}) + found = true + return false } } } @@ -1197,6 +1198,9 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe } for _, file := range files { ast.Inspect(file, f) + if found { + return decl, spec, field + } } return nil, nil, nil @@ -1222,7 +1226,7 @@ func promotedFields(t types.Type, from *types.Package) []promotedField { var fields []promotedField var visit func(t types.Type, stack []*types.Named) visit = func(t types.Type, stack []*types.Named) { - tStruct, ok := Deref(t).Underlying().(*types.Struct) + tStruct, ok := typesinternal.Unpointer(t).Underlying().(*types.Struct) if !ok { return } @@ -1232,11 +1236,7 @@ func promotedFields(t types.Type, from *types.Package) []promotedField { // Handle recursion through anonymous fields. if f.Anonymous() { - tf := f.Type() - if ptr, ok := tf.(*types.Pointer); ok { - tf = ptr.Elem() - } - if named, ok := tf.(*types.Named); ok { // (be defensive) + if _, named := typesinternal.ReceiverNamed(f); named != nil { // If we've already visited this named type // on this path, break the cycle. for _, x := range stack { diff --git a/gopls/internal/golang/identifier.go b/gopls/internal/golang/identifier.go index 28f89757057..994010fd74b 100644 --- a/gopls/internal/golang/identifier.go +++ b/gopls/internal/golang/identifier.go @@ -8,6 +8,9 @@ import ( "errors" "go/ast" "go/types" + + "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) // ErrNoIdentFound is error returned when no identifier is found at a particular position @@ -23,24 +26,30 @@ func inferredSignature(info *types.Info, id *ast.Ident) *types.Signature { return sig } +// searchForEnclosing returns, given the AST path to a SelectorExpr, +// the exported named type of the innermost implicit field selection. +// +// For example, given "new(A).d" where this is (due to embedding) a +// shorthand for "new(A).b.c.d", it returns the named type of c, +// if it is exported, otherwise the type of b, or A. func searchForEnclosing(info *types.Info, path []ast.Node) *types.TypeName { for _, n := range path { switch n := n.(type) { case *ast.SelectorExpr: if sel, ok := info.Selections[n]; ok { - recv := Deref(sel.Recv()) + recv := typesinternal.Unpointer(sel.Recv()) // Keep track of the last exported type seen. var exported *types.TypeName - if named, ok := recv.(*types.Named); ok && named.Obj().Exported() { + if named, ok := aliases.Unalias(recv).(*types.Named); ok && named.Obj().Exported() { exported = named.Obj() } // We don't want the last element, as that's the field or // method itself. for _, index := range sel.Index()[:len(sel.Index())-1] { if r, ok := recv.Underlying().(*types.Struct); ok { - recv = Deref(r.Field(index).Type()) - if named, ok := recv.(*types.Named); ok && named.Obj().Exported() { + recv = typesinternal.Unpointer(r.Field(index).Type()) + if named, ok := aliases.Unalias(recv).(*types.Named); ok && named.Obj().Exported() { exported = named.Obj() } } diff --git a/gopls/internal/golang/inline.go b/gopls/internal/golang/inline.go index de2a6ef75ae..f731e6b2120 100644 --- a/gopls/internal/golang/inline.go +++ b/gopls/internal/golang/inline.go @@ -27,7 +27,7 @@ import ( // EnclosingStaticCall returns the innermost function call enclosing // the selected range, along with the callee. -func EnclosingStaticCall(pkg *cache.Package, pgf *ParsedGoFile, start, end token.Pos) (*ast.CallExpr, *types.Func, error) { +func EnclosingStaticCall(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*ast.CallExpr, *types.Func, error) { path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) var call *ast.CallExpr diff --git a/gopls/internal/golang/inline_all.go b/gopls/internal/golang/inline_all.go index f2bab9d6d12..cbb50c3a2ff 100644 --- a/gopls/internal/golang/inline_all.go +++ b/gopls/internal/golang/inline_all.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/internal/refactor/inline" @@ -43,7 +44,7 @@ import ( // // The code below notes where are assumptions are made that only hold true in // the case of parameter removal (annotated with 'Assumption:') -func inlineAllCalls(ctx context.Context, logf func(string, ...any), snapshot *cache.Snapshot, pkg *cache.Package, pgf *ParsedGoFile, origDecl *ast.FuncDecl, callee *inline.Callee, post func([]byte) []byte) (map[protocol.DocumentURI][]byte, error) { +func inlineAllCalls(ctx context.Context, logf func(string, ...any), snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, origDecl *ast.FuncDecl, callee *inline.Callee, post func([]byte) []byte) (map[protocol.DocumentURI][]byte, error) { // Collect references. var refs []protocol.Location { @@ -100,7 +101,7 @@ func inlineAllCalls(ctx context.Context, logf func(string, ...any), snapshot *ca type fileCalls struct { pkg *cache.Package - pgf *ParsedGoFile + pgf *parsego.File calls []*ast.CallExpr } diff --git a/gopls/internal/golang/lines.go b/gopls/internal/golang/lines.go new file mode 100644 index 00000000000..1c4b562280d --- /dev/null +++ b/gopls/internal/golang/lines.go @@ -0,0 +1,261 @@ +// Copyright 2024 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 golang + +// This file defines refactorings for splitting lists of elements +// (arguments, literals, etc) across multiple lines, and joining +// them into a single line. + +import ( + "bytes" + "go/ast" + "go/token" + "go/types" + "sort" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/gopls/internal/util/slices" +) + +// CanSplitLines checks whether we can split lists of elements inside an enclosing curly bracket/parens into separate +// lines. +func CanSplitLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) { + itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end) + if itemType == "" { + return "", false, nil + } + + if !canSplitJoinLines(items, comments) { + return "", false, nil + } + + for i := 1; i < len(items); i++ { + prevLine := safetoken.EndPosition(fset, items[i-1].End()).Line + curLine := safetoken.StartPosition(fset, items[i].Pos()).Line + if prevLine == curLine { + return "Split " + itemType + " into separate lines", true, nil + } + } + + return "", false, nil +} + +// CanJoinLines checks whether we can join lists of elements inside an enclosing curly bracket/parens into a single line. +func CanJoinLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) { + itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end) + if itemType == "" { + return "", false, nil + } + + if !canSplitJoinLines(items, comments) { + return "", false, nil + } + + for i := 1; i < len(items); i++ { + prevLine := safetoken.EndPosition(fset, items[i-1].End()).Line + curLine := safetoken.StartPosition(fset, items[i].Pos()).Line + if prevLine != curLine { + return "Join " + itemType + " into one line", true, nil + } + } + + return "", false, nil +} + +// canSplitJoinLines determines whether we should split/join the lines or not. +func canSplitJoinLines(items []ast.Node, comments []*ast.CommentGroup) bool { + if len(items) <= 1 { + return false + } + + for _, cg := range comments { + if !strings.HasPrefix(cg.List[0].Text, "/*") { + return false // can't split/join lists containing "//" comments + } + } + + return true +} + +// splitLines is a singleFile fixer. +func splitLines(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + itemType, items, comments, indent, braceOpen, braceClose := findSplitJoinTarget(fset, file, src, start, end) + if itemType == "" { + return nil, nil, nil // no fix available + } + + return fset, processLines(fset, items, comments, src, braceOpen, braceClose, ",\n", "\n", ",\n"+indent, indent+"\t"), nil +} + +// joinLines is a singleFile fixer. +func joinLines(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + itemType, items, comments, _, braceOpen, braceClose := findSplitJoinTarget(fset, file, src, start, end) + if itemType == "" { + return nil, nil, nil // no fix available + } + + return fset, processLines(fset, items, comments, src, braceOpen, braceClose, ", ", "", "", ""), nil +} + +// processLines is the common operation for both split and join lines because this split/join operation is +// essentially a transformation of the separating whitespace. +func processLines(fset *token.FileSet, items []ast.Node, comments []*ast.CommentGroup, src []byte, braceOpen, braceClose token.Pos, sep, prefix, suffix, indent string) *analysis.SuggestedFix { + nodes := slices.Clone(items) + + // box *ast.CommentGroup to ast.Node for easier processing later. + for _, cg := range comments { + nodes = append(nodes, cg) + } + + // Sort to interleave comments and nodes. + sort.Slice(nodes, func(i, j int) bool { + return nodes[i].Pos() < nodes[j].Pos() + }) + + edits := []analysis.TextEdit{ + { + Pos: token.Pos(int(braceOpen) + len("{")), + End: nodes[0].Pos(), + NewText: []byte(prefix + indent), + }, + { + Pos: nodes[len(nodes)-1].End(), + End: braceClose, + NewText: []byte(suffix), + }, + } + + for i := 1; i < len(nodes); i++ { + pos, end := nodes[i-1].End(), nodes[i].Pos() + if pos > end { + // this will happen if we have a /*-style comment inside of a Field + // e.g. `a /*comment here */ int` + // + // we will ignore as we only care about finding the field delimiter. + continue + } + + // at this point, the `,` token in between 2 nodes here must be the field delimiter. + posOffset := safetoken.EndPosition(fset, pos).Offset + endOffset := safetoken.StartPosition(fset, end).Offset + if bytes.IndexByte(src[posOffset:endOffset], ',') == -1 { + // nodes[i] or nodes[i-1] is a comment hence no delimiter in between + // in such case, do nothing. + continue + } + + edits = append(edits, analysis.TextEdit{Pos: pos, End: end, NewText: []byte(sep + indent)}) + } + + return &analysis.SuggestedFix{TextEdits: edits} +} + +// findSplitJoinTarget returns the first curly bracket/parens that encloses the current cursor. +func findSplitJoinTarget(fset *token.FileSet, file *ast.File, src []byte, start, end token.Pos) (itemType string, items []ast.Node, comments []*ast.CommentGroup, indent string, open, close token.Pos) { + isCursorInside := func(nodePos, nodeEnd token.Pos) bool { + return nodePos < start && end < nodeEnd + } + + findTarget := func() (targetType string, target ast.Node, open, close token.Pos) { + path, _ := astutil.PathEnclosingInterval(file, start, end) + for _, node := range path { + switch node := node.(type) { + case *ast.FuncDecl: + // target struct method declarations. + // function (...) someMethod(a int, b int, c int) (d int, e, int) {} + params := node.Type.Params + if isCursorInside(params.Opening, params.Closing) { + return "parameters", params, params.Opening, params.Closing + } + + results := node.Type.Results + if results != nil && isCursorInside(results.Opening, results.Closing) { + return "return values", results, results.Opening, results.Closing + } + case *ast.FuncType: + // target function signature args and result. + // type someFunc func (a int, b int, c int) (d int, e int) + params := node.Params + if isCursorInside(params.Opening, params.Closing) { + return "parameters", params, params.Opening, params.Closing + } + + results := node.Results + if results != nil && isCursorInside(results.Opening, results.Closing) { + return "return values", results, results.Opening, results.Closing + } + case *ast.CallExpr: + // target function calls. + // someFunction(a, b, c) + if isCursorInside(node.Lparen, node.Rparen) { + return "parameters", node, node.Lparen, node.Rparen + } + case *ast.CompositeLit: + // target composite lit instantiation (structs, maps, arrays). + // A{b: 1, c: 2, d: 3} + if isCursorInside(node.Lbrace, node.Rbrace) { + return "elements", node, node.Lbrace, node.Rbrace + } + } + } + + return "", nil, 0, 0 + } + + targetType, targetNode, open, close := findTarget() + if targetType == "" { + return "", nil, nil, "", 0, 0 + } + + switch node := targetNode.(type) { + case *ast.FieldList: + for _, field := range node.List { + items = append(items, field) + } + case *ast.CallExpr: + for _, arg := range node.Args { + items = append(items, arg) + } + case *ast.CompositeLit: + for _, arg := range node.Elts { + items = append(items, arg) + } + } + + // preserve comments separately as it's not part of the targetNode AST. + for _, cg := range file.Comments { + if open <= cg.Pos() && cg.Pos() < close { + comments = append(comments, cg) + } + } + + // indent is the leading whitespace before the opening curly bracket/paren. + // + // in case where we don't have access to src yet i.e. src == nil + // it's fine to return incorrect indent because we don't need it yet. + indent = "" + if len(src) > 0 { + var pos token.Pos + switch node := targetNode.(type) { + case *ast.FieldList: + pos = node.Opening + case *ast.CallExpr: + pos = node.Lparen + case *ast.CompositeLit: + pos = node.Lbrace + } + + split := bytes.Split(src, []byte("\n")) + targetLineNumber := safetoken.StartPosition(fset, pos).Line + firstLine := string(split[targetLineNumber-1]) + trimmed := strings.TrimSpace(string(firstLine)) + indent = firstLine[:strings.Index(firstLine, trimmed)] + } + + return targetType, items, comments, indent, open, close +} diff --git a/gopls/internal/golang/linkname.go b/gopls/internal/golang/linkname.go index 2578f8d5485..c340484de57 100644 --- a/gopls/internal/golang/linkname.go +++ b/gopls/internal/golang/linkname.go @@ -13,6 +13,7 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" ) @@ -102,7 +103,7 @@ func parseLinkname(m *protocol.Mapper, pos protocol.Position) (pkgPath, name str // findLinkname searches dependencies of packages containing fh for an object // with linker name matching the given package path and name. -func findLinkname(ctx context.Context, snapshot *cache.Snapshot, pkgPath PackagePath, name string) (*cache.Package, *ParsedGoFile, token.Pos, error) { +func findLinkname(ctx context.Context, snapshot *cache.Snapshot, pkgPath PackagePath, name string) (*cache.Package, *parsego.File, token.Pos, error) { // Typically the linkname refers to a forward dependency // or a reverse dependency, but in general it may refer // to any package that is linked with this one. diff --git a/gopls/internal/golang/origin.go b/gopls/internal/golang/origin.go index c5e84db0ceb..aa77a9b3aa4 100644 --- a/gopls/internal/golang/origin.go +++ b/gopls/internal/golang/origin.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !go1.19 -// +build !go1.19 - package golang import "go/types" @@ -13,14 +10,21 @@ import "go/types" // with the same origin as the provided obj (which may be a synthetic object // created during instantiation). func containsOrigin(objSet map[types.Object]bool, obj types.Object) bool { - if obj == nil { - return objSet[obj] - } - // In Go 1.18, we can't use the types.Var.Origin and types.Func.Origin methods. + objOrigin := origin(obj) for target := range objSet { - if target.Pkg() == obj.Pkg() && target.Pos() == obj.Pos() && target.Name() == obj.Name() { + if origin(target) == objOrigin { return true } } return false } + +func origin(obj types.Object) types.Object { + switch obj := obj.(type) { + case *types.Var: + return obj.Origin() + case *types.Func: + return obj.Origin() + } + return obj +} diff --git a/gopls/internal/golang/origin_119.go b/gopls/internal/golang/origin_119.go deleted file mode 100644 index 16f6ca7c065..00000000000 --- a/gopls/internal/golang/origin_119.go +++ /dev/null @@ -1,33 +0,0 @@ -// 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. - -//go:build go1.19 -// +build go1.19 - -package golang - -import "go/types" - -// containsOrigin reports whether the provided object set contains an object -// with the same origin as the provided obj (which may be a synthetic object -// created during instantiation). -func containsOrigin(objSet map[types.Object]bool, obj types.Object) bool { - objOrigin := origin(obj) - for target := range objSet { - if origin(target) == objOrigin { - return true - } - } - return false -} - -func origin(obj types.Object) types.Object { - switch obj := obj.(type) { - case *types.Var: - return obj.Origin() - case *types.Func: - return obj.Origin() - } - return obj -} diff --git a/gopls/internal/golang/references.go b/gopls/internal/golang/references.go index c448830a1d0..53b8174ed43 100644 --- a/gopls/internal/golang/references.go +++ b/gopls/internal/golang/references.go @@ -28,6 +28,7 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/methodsets" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" @@ -164,7 +165,7 @@ func packageReferences(ctx context.Context, snapshot *cache.Snapshot, uri protoc if err != nil { return nil, err } - f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + f, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return nil, err } @@ -193,7 +194,7 @@ func packageReferences(ctx context.Context, snapshot *cache.Snapshot, uri protoc if err != nil { return nil, err } - f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + f, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return nil, err } @@ -685,7 +686,7 @@ func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Objec // which must belong to m.File. // // Safe for use only by references and implementations. -func mustLocation(pgf *ParsedGoFile, n ast.Node) protocol.Location { +func mustLocation(pgf *parsego.File, n ast.Node) protocol.Location { loc, err := pgf.NodeLocation(n) if err != nil { panic(err) // can't happen in references or implementations diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go index 29485413865..8c40079b2f3 100644 --- a/gopls/internal/golang/rename.go +++ b/gopls/internal/golang/rename.go @@ -68,7 +68,7 @@ import ( "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/refactor/satisfy" ) @@ -151,7 +151,7 @@ func PrepareRename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, }, nil, nil } -func prepareRenamePackageName(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedGoFile) (*PrepareItem, error) { +func prepareRenamePackageName(ctx context.Context, snapshot *cache.Snapshot, pgf *parsego.File) (*PrepareItem, error) { // Does the client support file renaming? fileRenameSupported := false for _, op := range snapshot.Options().SupportedResourceOperations { @@ -346,8 +346,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle var declObjPath objectpath.Path if obj.Exported() { // objectpath.For requires the origin of a generic function or type, not an - // instantiation (a bug?). Unfortunately we can't call Func.Origin as this - // is not available in go/types@go1.18. So we take a scenic route. + // instantiation (a bug?). // // Note that unlike Funcs, TypeNames are always canonical (they are "left" // of the type parameters, unlike methods). @@ -359,7 +358,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle goto skipObjectPath } case *types.Func: - obj = funcOrigin(obj.(*types.Func)) + obj = obj.(*types.Func).Origin() case *types.Var: // TODO(adonovan): do vars need the origin treatment too? (issue #58462) @@ -450,23 +449,6 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle return renameExported(pkgs, declPkgPath, declObjPath, newName) } -// funcOrigin is a go1.18-portable implementation of (*types.Func).Origin. -func funcOrigin(fn *types.Func) *types.Func { - // Method? - if fn.Type().(*types.Signature).Recv() != nil { - return typeparams.OriginMethod(fn) - } - - // Package-level function? - // (Assume the origin has the same position.) - gen := fn.Pkg().Scope().Lookup(fn.Name()) - if gen != nil && gen.Pos() == fn.Pos() { - return gen.(*types.Func) - } - - return fn -} - // typeCheckReverseDependencies returns the type-checked packages for // the reverse dependencies of all packages variants containing // file declURI. The packages are in some topological order. @@ -812,7 +794,7 @@ func renamePackageClause(ctx context.Context, mp *metadata.Package, snapshot *ca if err != nil { return err } - f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + f, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return err } @@ -853,7 +835,7 @@ func renameImports(ctx context.Context, snapshot *cache.Snapshot, mp *metadata.P if err != nil { return err } - f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + f, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return err } @@ -1189,12 +1171,8 @@ func (r *renamer) updateCommentDocLinks() (map[protocol.DocumentURI][]diff.Edit, if recv == nil { continue } - recvT := recv.Type() - if ptr, ok := recvT.(*types.Pointer); ok { - recvT = ptr.Elem() - } - named, isNamed := recvT.(*types.Named) - if !isNamed { + _, named := typesinternal.ReceiverNamed(recv) + if named == nil { continue } // Doc links can't reference interface methods. @@ -1350,7 +1328,7 @@ func (r *docLinkRenamer) update(pgf *parsego.File) (result []diff.Edit, err erro } // docComment returns the doc for an identifier within the specified file. -func docComment(pgf *ParsedGoFile, id *ast.Ident) *ast.CommentGroup { +func docComment(pgf *parsego.File, id *ast.Ident) *ast.CommentGroup { nodes, _ := astutil.PathEnclosingInterval(pgf.File, id.Pos(), id.End()) for _, node := range nodes { switch decl := node.(type) { @@ -1401,7 +1379,7 @@ func docComment(pgf *ParsedGoFile, id *ast.Ident) *ast.CommentGroup { // updatePkgName returns the updates to rename a pkgName in the import spec by // only modifying the package name portion of the import declaration. -func (r *renamer) updatePkgName(pgf *ParsedGoFile, pkgName *types.PkgName) (diff.Edit, error) { +func (r *renamer) updatePkgName(pgf *parsego.File, pkgName *types.PkgName) (diff.Edit, error) { // Modify ImportSpec syntax to add or remove the Name as needed. path, _ := astutil.PathEnclosingInterval(pgf.File, pkgName.Pos(), pkgName.Pos()) if len(path) < 2 { @@ -1428,19 +1406,19 @@ func (r *renamer) updatePkgName(pgf *ParsedGoFile, pkgName *types.PkgName) (diff // whether the position ppos lies within it. // // Note: also used by references. -func parsePackageNameDecl(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, ppos protocol.Position) (*ParsedGoFile, bool, error) { - pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) +func parsePackageNameDecl(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, ppos protocol.Position) (*parsego.File, bool, error) { + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return nil, false, err } - // Careful: because we used ParseHeader, + // Careful: because we used parsego.Header, // pgf.Pos(ppos) may be beyond EOF => (0, err). pos, _ := pgf.PositionPos(ppos) return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil } // enclosingFile returns the CompiledGoFile of pkg that contains the specified position. -func enclosingFile(pkg *cache.Package, pos token.Pos) (*ParsedGoFile, bool) { +func enclosingFile(pkg *cache.Package, pos token.Pos) (*parsego.File, bool) { for _, pgf := range pkg.CompiledGoFiles() { if pgf.File.Pos() <= pos && pos <= pgf.File.End() { return pgf, true diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go index 11b154c4e18..ffaff303afa 100644 --- a/gopls/internal/golang/rename_check.go +++ b/gopls/internal/golang/rename_check.go @@ -45,6 +45,8 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/refactor/satisfy" ) @@ -371,7 +373,7 @@ func forEachLexicalRef(pkg *cache.Package, obj types.Object, fn func(id *ast.Ide return visit(nil) // pop stack, don't descend } // TODO(adonovan): fix: for generics, should be T.core not T.underlying. - if _, ok := Deref(tv.Type).Underlying().(*types.Struct); ok { + if _, ok := typesinternal.Unpointer(tv.Type).Underlying().(*types.Struct); ok { if n.Type != nil { ast.Inspect(n.Type, visit) } @@ -501,7 +503,7 @@ func (r *renamer) checkStructField(from *types.Var) { if from.Anonymous() { if named, ok := from.Type().(*types.Named); ok { r.check(named.Obj()) - } else if named, ok := Deref(from.Type()).(*types.Named); ok { + } else if named, ok := aliases.Unalias(typesinternal.Unpointer(from.Type())).(*types.Named); ok { r.check(named.Obj()) } } diff --git a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go index 181b8ce33ab..7a58065acd6 100644 --- a/gopls/internal/golang/semtok.go +++ b/gopls/internal/golang/semtok.go @@ -21,24 +21,20 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/semtok" + "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/event" ) -// to control comprehensive logging of decisions (gopls semtok foo.go > /dev/null shows log output) -// semDebug should NEVER be true in checked-in code +// semDebug enables comprehensive logging of decisions +// (gopls semtok foo.go > /dev/null shows log output). +// It should never be true in checked-in code. const semDebug = false -// The LSP says that errors for the semantic token requests should only be returned -// for exceptions (a word not otherwise defined). This code treats a too-large file -// as an exception. On parse errors, the code does what it can. - -// reject full semantic token requests for large files -const maxFullFileSize int = 100000 - func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng *protocol.Range) (*protocol.SemanticTokens, error) { pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) if err != nil { @@ -57,64 +53,69 @@ func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handl tok := pgf.Tok start, end = tok.Pos(0), tok.Pos(tok.Size()) // entire file } + + // Reject full semantic token requests for large files. + // + // The LSP says that errors for the semantic token requests + // should only be returned for exceptions (a word not + // otherwise defined). This code treats a too-large file as an + // exception. On parse errors, the code does what it can. + const maxFullFileSize = 100000 if int(end-start) > maxFullFileSize { - err := fmt.Errorf("semantic tokens: range %s too large (%d > %d)", + return nil, fmt.Errorf("semantic tokens: range %s too large (%d > %d)", fh.URI().Path(), end-start, maxFullFileSize) - return nil, err } - tv := &tokenVisitor{ + tv := tokenVisitor{ ctx: ctx, metadataSource: snapshot, + metadata: pkg.Metadata(), + info: pkg.GetTypesInfo(), + fset: pkg.FileSet(), pgf: pgf, start: start, end: end, - ti: pkg.GetTypesInfo(), - pkg: pkg, - fset: pkg.FileSet(), } tv.visit() return &protocol.SemanticTokens{ Data: semtok.Encode( - tv.items, + tv.tokens, snapshot.Options().NoSemanticString, snapshot.Options().NoSemanticNumber, snapshot.Options().SemanticTypes, snapshot.Options().SemanticMods), - // For delta requests, but we've never seen any. - ResultID: time.Now().String(), + ResultID: time.Now().String(), // for delta requests, but we've never seen any }, nil } type tokenVisitor struct { - items []semtok.Token - ctx context.Context // for event logging - // metadataSource is used to resolve imports - metadataSource metadata.Source - pgf *ParsedGoFile - start, end token.Pos // range of interest - ti *types.Info - pkg *cache.Package + // inputs + ctx context.Context // for event logging + metadataSource metadata.Source // used to resolve imports + metadata *metadata.Package + info *types.Info fset *token.FileSet - // path from the root of the parse tree, used for debugging - stack []ast.Node + pgf *parsego.File + start, end token.Pos // range of interest + + // working state + stack []ast.Node // path from root of the syntax tree + tokens []semtok.Token // computed sequence of semantic tokens } func (tv *tokenVisitor) visit() { f := tv.pgf.File // may not be in range, but harmless tv.token(f.Package, len("package"), semtok.TokKeyword, nil) - tv.token(f.Name.NamePos, len(f.Name.Name), semtok.TokNamespace, nil) - inspect := func(n ast.Node) bool { - return tv.inspector(n) - } - for _, d := range f.Decls { - // only look at the decls that overlap the range - start, end := d.Pos(), d.End() - if end <= tv.start || start >= tv.end { + if f.Name != nil { + tv.token(f.Name.NamePos, len(f.Name.Name), semtok.TokNamespace, nil) + } + for _, decl := range f.Decls { + // Only look at the decls that overlap the range. + if decl.End() <= tv.start || decl.Pos() >= tv.end { continue } - ast.Inspect(d, inspect) + ast.Inspect(decl, tv.inspect) } for _, cg := range f.Comments { for _, c := range cg.List { @@ -124,68 +125,70 @@ func (tv *tokenVisitor) visit() { } if !strings.Contains(c.Text, "\n") { tv.token(c.Pos(), len(c.Text), semtok.TokComment, nil) - continue + } else { + tv.multiline(c.Pos(), c.End(), semtok.TokComment) } - tv.multiline(c.Pos(), c.End(), c.Text, semtok.TokComment) } } } -func (tv *tokenVisitor) token(start token.Pos, leng int, typ semtok.TokenType, mods []string) { - if leng <= 0 { +// token emits a token of the specified extent and semantics. +func (tv *tokenVisitor) token(start token.Pos, length int, typ semtok.TokenType, modifiers []string) { + if length <= 0 { return // vscode doesn't like 0-length Tokens } if !start.IsValid() { // This is not worth reporting. TODO(pjw): does it still happen? return } - if start >= tv.end || start+token.Pos(leng) <= tv.start { + end := start + token.Pos(length) + if start >= tv.end || end <= tv.start { return } // want a line and column from start (in LSP coordinates). Ignore line directives. - lspRange, err := tv.pgf.PosRange(start, start+token.Pos(leng)) + rng, err := tv.pgf.PosRange(start, end) if err != nil { event.Error(tv.ctx, "failed to convert to range", err) return } - if lspRange.End.Line != lspRange.Start.Line { + if rng.End.Line != rng.Start.Line { // this happens if users are typing at the end of the file, but report nothing return } - tv.items = append(tv.items, semtok.Token{ - Line: lspRange.Start.Line, - Start: lspRange.Start.Character, - Len: lspRange.End.Character - lspRange.Start.Character, // all on one line + tv.tokens = append(tv.tokens, semtok.Token{ + Line: rng.Start.Line, + Start: rng.Start.Character, + Len: rng.End.Character - rng.Start.Character, // (on same line) Type: typ, - Modifiers: mods, + Modifiers: modifiers, }) } -// convert the stack to a string, for debugging +// strStack converts the stack to a string, for debugging and error messages. func (tv *tokenVisitor) strStack() string { msg := []string{"["} for i := len(tv.stack) - 1; i >= 0; i-- { - s := tv.stack[i] - msg = append(msg, fmt.Sprintf("%T", s)[5:]) + n := tv.stack[i] + msg = append(msg, strings.TrimPrefix(fmt.Sprintf("%T", n), "*ast.")) } if len(tv.stack) > 0 { - loc := tv.stack[len(tv.stack)-1].Pos() - if _, err := safetoken.Offset(tv.pgf.Tok, loc); err != nil { - msg = append(msg, fmt.Sprintf("invalid position %v for %s", loc, tv.pgf.URI)) + pos := tv.stack[len(tv.stack)-1].Pos() + if _, err := safetoken.Offset(tv.pgf.Tok, pos); err != nil { + msg = append(msg, fmt.Sprintf("invalid position %v for %s", pos, tv.pgf.URI)) } else { - add := safetoken.Position(tv.pgf.Tok, loc) - nm := filepath.Base(add.Filename) - msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", nm, add.Line, add.Column)) + posn := safetoken.Position(tv.pgf.Tok, pos) + msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", + filepath.Base(posn.Filename), posn.Line, posn.Column)) } } msg = append(msg, "]") return strings.Join(msg, " ") } -// find the line in the source -func (tv *tokenVisitor) srcLine(x ast.Node) string { +// srcLine returns the source text for n (truncated at first newline). +func (tv *tokenVisitor) srcLine(n ast.Node) string { file := tv.pgf.Tok - line := safetoken.Line(file, x.Pos()) + line := safetoken.Line(file, n.Pos()) start, err := safetoken.Offset(file, file.LineStart(line)) if err != nil { return "" @@ -194,274 +197,246 @@ func (tv *tokenVisitor) srcLine(x ast.Node) string { for ; end < len(tv.pgf.Src) && tv.pgf.Src[end] != '\n'; end++ { } - ans := tv.pgf.Src[start:end] - return string(ans) + return string(tv.pgf.Src[start:end]) } -func (tv *tokenVisitor) inspector(n ast.Node) bool { - pop := func() { - tv.stack = tv.stack[:len(tv.stack)-1] - } +func (tv *tokenVisitor) inspect(n ast.Node) (descend bool) { if n == nil { - pop() + tv.stack = tv.stack[:len(tv.stack)-1] // pop return true } - tv.stack = append(tv.stack, n) - switch x := n.(type) { + tv.stack = append(tv.stack, n) // push + defer func() { + if !descend { + tv.stack = tv.stack[:len(tv.stack)-1] // pop + } + }() + + switch n := n.(type) { case *ast.ArrayType: case *ast.AssignStmt: - tv.token(x.TokPos, len(x.Tok.String()), semtok.TokOperator, nil) + tv.token(n.TokPos, len(n.Tok.String()), semtok.TokOperator, nil) case *ast.BasicLit: - if strings.Contains(x.Value, "\n") { + if strings.Contains(n.Value, "\n") { // has to be a string. - tv.multiline(x.Pos(), x.End(), x.Value, semtok.TokString) + tv.multiline(n.Pos(), n.End(), semtok.TokString) break } - ln := len(x.Value) what := semtok.TokNumber - if x.Kind == token.STRING { + if n.Kind == token.STRING { what = semtok.TokString } - tv.token(x.Pos(), ln, what, nil) + tv.token(n.Pos(), len(n.Value), what, nil) case *ast.BinaryExpr: - tv.token(x.OpPos, len(x.Op.String()), semtok.TokOperator, nil) + tv.token(n.OpPos, len(n.Op.String()), semtok.TokOperator, nil) case *ast.BlockStmt: case *ast.BranchStmt: - tv.token(x.TokPos, len(x.Tok.String()), semtok.TokKeyword, nil) - // There's no semantic encoding for labels + tv.token(n.TokPos, len(n.Tok.String()), semtok.TokKeyword, nil) + if n.Label != nil { + tv.token(n.Label.Pos(), len(n.Label.Name), semtok.TokLabel, nil) + } case *ast.CallExpr: - if x.Ellipsis != token.NoPos { - tv.token(x.Ellipsis, len("..."), semtok.TokOperator, nil) + if n.Ellipsis.IsValid() { + tv.token(n.Ellipsis, len("..."), semtok.TokOperator, nil) } case *ast.CaseClause: iam := "case" - if x.List == nil { + if n.List == nil { iam = "default" } - tv.token(x.Case, len(iam), semtok.TokKeyword, nil) + tv.token(n.Case, len(iam), semtok.TokKeyword, nil) case *ast.ChanType: // chan | chan <- | <- chan switch { - case x.Arrow == token.NoPos: - tv.token(x.Begin, len("chan"), semtok.TokKeyword, nil) - case x.Arrow == x.Begin: - tv.token(x.Arrow, 2, semtok.TokOperator, nil) - pos := tv.findKeyword("chan", x.Begin+2, x.Value.Pos()) + case n.Arrow == token.NoPos: + tv.token(n.Begin, len("chan"), semtok.TokKeyword, nil) + case n.Arrow == n.Begin: + tv.token(n.Arrow, 2, semtok.TokOperator, nil) + pos := tv.findKeyword("chan", n.Begin+2, n.Value.Pos()) tv.token(pos, len("chan"), semtok.TokKeyword, nil) - case x.Arrow != x.Begin: - tv.token(x.Begin, len("chan"), semtok.TokKeyword, nil) - tv.token(x.Arrow, 2, semtok.TokOperator, nil) + case n.Arrow != n.Begin: + tv.token(n.Begin, len("chan"), semtok.TokKeyword, nil) + tv.token(n.Arrow, 2, semtok.TokOperator, nil) } case *ast.CommClause: - iam := len("case") - if x.Comm == nil { - iam = len("default") + length := len("case") + if n.Comm == nil { + length = len("default") } - tv.token(x.Case, iam, semtok.TokKeyword, nil) + tv.token(n.Case, length, semtok.TokKeyword, nil) case *ast.CompositeLit: case *ast.DeclStmt: case *ast.DeferStmt: - tv.token(x.Defer, len("defer"), semtok.TokKeyword, nil) + tv.token(n.Defer, len("defer"), semtok.TokKeyword, nil) case *ast.Ellipsis: - tv.token(x.Ellipsis, len("..."), semtok.TokOperator, nil) + tv.token(n.Ellipsis, len("..."), semtok.TokOperator, nil) case *ast.EmptyStmt: case *ast.ExprStmt: case *ast.Field: case *ast.FieldList: case *ast.ForStmt: - tv.token(x.For, len("for"), semtok.TokKeyword, nil) + tv.token(n.For, len("for"), semtok.TokKeyword, nil) case *ast.FuncDecl: case *ast.FuncLit: case *ast.FuncType: - if x.Func != token.NoPos { - tv.token(x.Func, len("func"), semtok.TokKeyword, nil) + if n.Func != token.NoPos { + tv.token(n.Func, len("func"), semtok.TokKeyword, nil) } case *ast.GenDecl: - tv.token(x.TokPos, len(x.Tok.String()), semtok.TokKeyword, nil) + tv.token(n.TokPos, len(n.Tok.String()), semtok.TokKeyword, nil) case *ast.GoStmt: - tv.token(x.Go, len("go"), semtok.TokKeyword, nil) + tv.token(n.Go, len("go"), semtok.TokKeyword, nil) case *ast.Ident: - tv.ident(x) + tv.ident(n) case *ast.IfStmt: - tv.token(x.If, len("if"), semtok.TokKeyword, nil) - if x.Else != nil { + tv.token(n.If, len("if"), semtok.TokKeyword, nil) + if n.Else != nil { // x.Body.End() or x.Body.End()+1, not that it matters - pos := tv.findKeyword("else", x.Body.End(), x.Else.Pos()) + pos := tv.findKeyword("else", n.Body.End(), n.Else.Pos()) tv.token(pos, len("else"), semtok.TokKeyword, nil) } case *ast.ImportSpec: - tv.importSpec(x) - pop() + tv.importSpec(n) return false case *ast.IncDecStmt: - tv.token(x.TokPos, len(x.Tok.String()), semtok.TokOperator, nil) + tv.token(n.TokPos, len(n.Tok.String()), semtok.TokOperator, nil) case *ast.IndexExpr: case *ast.IndexListExpr: case *ast.InterfaceType: - tv.token(x.Interface, len("interface"), semtok.TokKeyword, nil) + tv.token(n.Interface, len("interface"), semtok.TokKeyword, nil) case *ast.KeyValueExpr: case *ast.LabeledStmt: + tv.token(n.Label.Pos(), len(n.Label.Name), semtok.TokLabel, []string{"definition"}) case *ast.MapType: - tv.token(x.Map, len("map"), semtok.TokKeyword, nil) + tv.token(n.Map, len("map"), semtok.TokKeyword, nil) case *ast.ParenExpr: case *ast.RangeStmt: - tv.token(x.For, len("for"), semtok.TokKeyword, nil) + tv.token(n.For, len("for"), semtok.TokKeyword, nil) // x.TokPos == token.NoPos is legal (for range foo {}) - offset := x.TokPos + offset := n.TokPos if offset == token.NoPos { - offset = x.For + offset = n.For } - pos := tv.findKeyword("range", offset, x.X.Pos()) + pos := tv.findKeyword("range", offset, n.X.Pos()) tv.token(pos, len("range"), semtok.TokKeyword, nil) case *ast.ReturnStmt: - tv.token(x.Return, len("return"), semtok.TokKeyword, nil) + tv.token(n.Return, len("return"), semtok.TokKeyword, nil) case *ast.SelectStmt: - tv.token(x.Select, len("select"), semtok.TokKeyword, nil) + tv.token(n.Select, len("select"), semtok.TokKeyword, nil) case *ast.SelectorExpr: case *ast.SendStmt: - tv.token(x.Arrow, len("<-"), semtok.TokOperator, nil) + tv.token(n.Arrow, len("<-"), semtok.TokOperator, nil) case *ast.SliceExpr: case *ast.StarExpr: - tv.token(x.Star, len("*"), semtok.TokOperator, nil) + tv.token(n.Star, len("*"), semtok.TokOperator, nil) case *ast.StructType: - tv.token(x.Struct, len("struct"), semtok.TokKeyword, nil) + tv.token(n.Struct, len("struct"), semtok.TokKeyword, nil) case *ast.SwitchStmt: - tv.token(x.Switch, len("switch"), semtok.TokKeyword, nil) + tv.token(n.Switch, len("switch"), semtok.TokKeyword, nil) case *ast.TypeAssertExpr: - if x.Type == nil { - pos := tv.findKeyword("type", x.Lparen, x.Rparen) + if n.Type == nil { + pos := tv.findKeyword("type", n.Lparen, n.Rparen) tv.token(pos, len("type"), semtok.TokKeyword, nil) } case *ast.TypeSpec: case *ast.TypeSwitchStmt: - tv.token(x.Switch, len("switch"), semtok.TokKeyword, nil) + tv.token(n.Switch, len("switch"), semtok.TokKeyword, nil) case *ast.UnaryExpr: - tv.token(x.OpPos, len(x.Op.String()), semtok.TokOperator, nil) + tv.token(n.OpPos, len(n.Op.String()), semtok.TokOperator, nil) case *ast.ValueSpec: // things only seen with parsing or type errors, so ignore them case *ast.BadDecl, *ast.BadExpr, *ast.BadStmt: - return true + return false // not going to see these case *ast.File, *ast.Package: - tv.unexpected(fmt.Sprintf("implement %T %s", x, safetoken.Position(tv.pgf.Tok, x.Pos()))) + tv.errorf("implement %T %s", n, safetoken.Position(tv.pgf.Tok, n.Pos())) // other things we knowingly ignore case *ast.Comment, *ast.CommentGroup: - pop() return false default: - tv.unexpected(fmt.Sprintf("failed to implement %T", x)) + tv.errorf("failed to implement %T", n) } return true } -func (tv *tokenVisitor) ident(x *ast.Ident) { - if tv.ti == nil { - what, mods := tv.unkIdent(x) - if what != "" { - tv.token(x.Pos(), len(x.String()), what, mods) - } - if semDebug { - log.Printf(" nil %s/nil/nil %q %v %s", x.String(), what, mods, tv.strStack()) - } - return - } - def := tv.ti.Defs[x] - if def != nil { - what, mods := tv.definitionFor(x, def) - if what != "" { - tv.token(x.Pos(), len(x.String()), what, mods) - } - if semDebug { - log.Printf(" for %s/%T/%T got %s %v (%s)", x.String(), def, def.Type(), what, mods, tv.strStack()) - } - return - } - use := tv.ti.Uses[x] - tok := func(pos token.Pos, lng int, tok semtok.TokenType, mods []string) { - tv.token(pos, lng, tok, mods) - q := "nil" - if use != nil { - q = fmt.Sprintf("%T", use.Type()) - } +func (tv *tokenVisitor) ident(id *ast.Ident) { + var obj types.Object + + // emit emits a token for the identifier's extent. + emit := func(tok semtok.TokenType, modifiers ...string) { + tv.token(id.Pos(), len(id.Name), tok, modifiers) if semDebug { - log.Printf(" use %s/%T/%s got %s %v (%s)", x.String(), use, q, tok, mods, tv.strStack()) + q := "nil" + if obj != nil { + q = fmt.Sprintf("%T", obj.Type()) // e.g. "*types.Map" + } + log.Printf(" use %s/%T/%s got %s %v (%s)", + id.Name, obj, q, tok, modifiers, tv.strStack()) } } - switch y := use.(type) { - case nil: - what, mods := tv.unkIdent(x) - if what != "" { - tok(x.Pos(), len(x.String()), what, mods) + // definition? + obj = tv.info.Defs[id] + if obj != nil { + if tok, modifiers := tv.definitionFor(id, obj); tok != "" { + emit(tok, modifiers...) } else if semDebug { - // tok() wasn't called, so didn't log - log.Printf(" nil %s/%T/nil %q %v (%s)", x.String(), use, what, mods, tv.strStack()) + log.Printf(" for %s/%T/%T got '' %v (%s)", + id.Name, obj, obj.Type(), modifiers, tv.strStack()) } return + } + + // use? + obj = tv.info.Uses[id] + switch obj := obj.(type) { case *types.Builtin: - tok(x.NamePos, len(x.Name), semtok.TokFunction, []string{"defaultLibrary"}) + emit(semtok.TokFunction, "defaultLibrary") case *types.Const: - mods := []string{"readonly"} - tt := y.Type() - if _, ok := tt.(*types.Basic); ok { - tok(x.Pos(), len(x.String()), semtok.TokVariable, mods) - break - } - if ttx, ok := tt.(*types.Named); ok { - if x.String() == "iota" { - tv.unexpected(fmt.Sprintf("iota:%T", ttx)) - } - if _, ok := ttx.Underlying().(*types.Basic); ok { - tok(x.Pos(), len(x.String()), semtok.TokVariable, mods) - break - } - tv.unexpected(fmt.Sprintf("%q/%T", x.String(), tt)) + if is[*types.Named](obj.Type()) && + (id.Name == "iota" || id.Name == "true" || id.Name == "false") { + emit(semtok.TokVariable, "readonly", "defaultLibrary") + } else { + emit(semtok.TokVariable, "readonly") } - // can this happen? Don't think so - tv.unexpected(fmt.Sprintf("%s %T %#v", x.String(), tt, tt)) case *types.Func: - tok(x.Pos(), len(x.Name), semtok.TokFunction, nil) + emit(semtok.TokFunction) case *types.Label: - // nothing to map it to + // Labels are reliably covered by the syntax traversal. case *types.Nil: // nil is a predeclared identifier - tok(x.Pos(), len("nil"), semtok.TokVariable, []string{"readonly", "defaultLibrary"}) + emit(semtok.TokVariable, "readonly", "defaultLibrary") case *types.PkgName: - tok(x.Pos(), len(x.Name), semtok.TokNamespace, nil) - case *types.TypeName: // could be a TokTypeParam - var mods []string - if _, ok := y.Type().(*types.Basic); ok { - mods = []string{"defaultLibrary"} - } else if _, ok := y.Type().(*types.TypeParam); ok { - tok(x.Pos(), len(x.String()), semtok.TokTypeParam, mods) - break + emit(semtok.TokNamespace) + case *types.TypeName: // could be a TypeParam + if is[*types.TypeParam](obj.Type()) { + emit(semtok.TokTypeParam) + } else if is[*types.Basic](obj.Type()) { + emit(semtok.TokType, "defaultLibrary") + } else { + emit(semtok.TokType) } - tok(x.Pos(), len(x.String()), semtok.TokType, mods) case *types.Var: - if isSignature(y) { - tok(x.Pos(), len(x.Name), semtok.TokFunction, nil) - } else if tv.isParam(use.Pos()) { + if is[*types.Signature](obj.Type()) { + emit(semtok.TokFunction) + } else if tv.isParam(obj.Pos()) { // variable, unless use.pos is the pos of a Field in an ancestor FuncDecl // or FuncLit and then it's a parameter - tok(x.Pos(), len(x.Name), semtok.TokParameter, nil) + emit(semtok.TokParameter) } else { - tok(x.Pos(), len(x.Name), semtok.TokVariable, nil) + emit(semtok.TokVariable) } - - default: - // can't happen - if use == nil { - msg := fmt.Sprintf("%#v %#v %#v", x, tv.ti.Defs[x], tv.ti.Uses[x]) - tv.unexpected(msg) - } - if use.Type() != nil { - tv.unexpected(fmt.Sprintf("%s %T/%T,%#v", x.String(), use, use.Type(), use)) - } else { - tv.unexpected(fmt.Sprintf("%s %T", x.String(), use)) + case nil: + if tok, modifiers := tv.unkIdent(id); tok != "" { + emit(tok, modifiers...) } + default: + panic(obj) } } +// isParam reports whether the position is that of a parameter name of +// an enclosing function. func (tv *tokenVisitor) isParam(pos token.Pos) bool { for i := len(tv.stack) - 1; i >= 0; i-- { switch n := tv.stack[i].(type) { @@ -486,31 +461,18 @@ func (tv *tokenVisitor) isParam(pos token.Pos) bool { return false } -func isSignature(use types.Object) bool { - if _, ok := use.(*types.Var); !ok { - return false - } - v := use.Type() - if v == nil { - return false - } - if _, ok := v.(*types.Signature); ok { - return true - } - return false -} - -// both tv.ti.Defs and tv.ti.Uses are nil. use the parse stack. -// a lot of these only happen when the package doesn't compile -// but in that case it is all best-effort from the parse tree -func (tv *tokenVisitor) unkIdent(x *ast.Ident) (semtok.TokenType, []string) { +// unkIdent handles identifiers with no types.Object (neither use nor +// def), use the parse stack. +// A lot of these only happen when the package doesn't compile, +// but in that case it is all best-effort from the parse tree. +func (tv *tokenVisitor) unkIdent(id *ast.Ident) (semtok.TokenType, []string) { def := []string{"definition"} - n := len(tv.stack) - 2 // parent of Ident + n := len(tv.stack) - 2 // parent of Ident; stack is [File ... Ident] if n < 0 { - tv.unexpected("no stack?") + tv.errorf("no stack") // can't happen return "", nil } - switch nd := tv.stack[n].(type) { + switch parent := tv.stack[n].(type) { case *ast.BinaryExpr, *ast.UnaryExpr, *ast.ParenExpr, *ast.StarExpr, *ast.IncDecStmt, *ast.SliceExpr, *ast.ExprStmt, *ast.IndexExpr, *ast.ReturnStmt, *ast.ChanType, *ast.SendStmt, @@ -522,14 +484,12 @@ func (tv *tokenVisitor) unkIdent(x *ast.Ident) (semtok.TokenType, []string) { case *ast.Ellipsis: return semtok.TokType, nil case *ast.CaseClause: - if n-2 >= 0 { - if _, ok := tv.stack[n-2].(*ast.TypeSwitchStmt); ok { - return semtok.TokType, nil - } + if n-2 >= 0 && is[ast.TypeSwitchStmt](tv.stack[n-2]) { + return semtok.TokType, nil } return semtok.TokVariable, nil case *ast.ArrayType: - if x == nd.Len { + if id == parent.Len { // or maybe a Type Param, but we can't just from the parse tree return semtok.TokVariable, nil } else { @@ -538,26 +498,26 @@ func (tv *tokenVisitor) unkIdent(x *ast.Ident) (semtok.TokenType, []string) { case *ast.MapType: return semtok.TokType, nil case *ast.CallExpr: - if x == nd.Fun { + if id == parent.Fun { return semtok.TokFunction, nil } return semtok.TokVariable, nil case *ast.SwitchStmt: return semtok.TokVariable, nil case *ast.TypeAssertExpr: - if x == nd.X { + if id == parent.X { return semtok.TokVariable, nil - } else if x == nd.Type { + } else if id == parent.Type { return semtok.TokType, nil } case *ast.ValueSpec: - for _, p := range nd.Names { - if p == x { + for _, p := range parent.Names { + if p == id { return semtok.TokVariable, def } } - for _, p := range nd.Values { - if p == x { + for _, p := range parent.Values { + if p == id { return semtok.TokVariable, nil } } @@ -566,17 +526,17 @@ func (tv *tokenVisitor) unkIdent(x *ast.Ident) (semtok.TokenType, []string) { if n-1 >= 0 { if ce, ok := tv.stack[n-1].(*ast.CallExpr); ok { // ... CallExpr SelectorExpr Ident (_.x()) - if ce.Fun == nd && nd.Sel == x { + if ce.Fun == parent && parent.Sel == id { return semtok.TokFunction, nil } } } return semtok.TokVariable, nil case *ast.AssignStmt: - for _, p := range nd.Lhs { + for _, p := range parent.Lhs { // x := ..., or x = ... - if p == x { - if nd.Tok != token.DEFINE { + if p == id { + if parent.Tok != token.DEFINE { def = nil } return semtok.TokVariable, def // '_' in _ = ... @@ -585,104 +545,117 @@ func (tv *tokenVisitor) unkIdent(x *ast.Ident) (semtok.TokenType, []string) { // RHS, = x return semtok.TokVariable, nil case *ast.TypeSpec: // it's a type if it is either the Name or the Type - if x == nd.Type { + if id == parent.Type { def = nil } return semtok.TokType, def case *ast.Field: // ident could be type in a field, or a method in an interface type, or a variable - if x == nd.Type { + if id == parent.Type { return semtok.TokType, nil } - if n-2 >= 0 { - _, okit := tv.stack[n-2].(*ast.InterfaceType) - _, okfl := tv.stack[n-1].(*ast.FieldList) - if okit && okfl { - return semtok.TokMethod, def - } + if n > 2 && + is[*ast.InterfaceType](tv.stack[n-2]) && + is[*ast.FieldList](tv.stack[n-1]) { + + return semtok.TokMethod, def } return semtok.TokVariable, nil - case *ast.LabeledStmt, *ast.BranchStmt: - // nothing to report + case *ast.LabeledStmt: + if id == parent.Label { + return semtok.TokLabel, def + } + case *ast.BranchStmt: + if id == parent.Label { + return semtok.TokLabel, nil + } case *ast.CompositeLit: - if nd.Type == x { + if parent.Type == id { return semtok.TokType, nil } return semtok.TokVariable, nil case *ast.RangeStmt: - if nd.Tok != token.DEFINE { + if parent.Tok != token.DEFINE { def = nil } return semtok.TokVariable, def case *ast.FuncDecl: return semtok.TokFunction, def default: - msg := fmt.Sprintf("%T undexpected: %s %s%q", nd, x.Name, tv.strStack(), tv.srcLine(x)) - tv.unexpected(msg) + tv.errorf("%T unexpected: %s %s%q", parent, id.Name, tv.strStack(), tv.srcLine(id)) } return "", nil } func isDeprecated(n *ast.CommentGroup) bool { - if n == nil { - return false - } - for _, c := range n.List { - if strings.HasPrefix(c.Text, "// Deprecated") { - return true + if n != nil { + for _, c := range n.List { + if strings.HasPrefix(c.Text, "// Deprecated") { + return true + } } } return false } -func (tv *tokenVisitor) definitionFor(x *ast.Ident, def types.Object) (semtok.TokenType, []string) { - // PJW: def == types.Label? probably a nothing +// definitionFor handles a defining identifier. +func (tv *tokenVisitor) definitionFor(id *ast.Ident, obj types.Object) (semtok.TokenType, []string) { + // The definition of a types.Label cannot be found by + // ascending the syntax tree, and doing so will reach the + // FuncDecl, causing us to misinterpret the label as a + // parameter (#65494). + // + // However, labels are reliably covered by the syntax + // traversal, so we don't need to use type information. + if is[*types.Label](obj) { + return "", nil + } + // PJW: look into replacing these syntactic tests with types more generally - mods := []string{"definition"} + modifiers := []string{"definition"} for i := len(tv.stack) - 1; i >= 0; i-- { - s := tv.stack[i] - switch y := s.(type) { + switch ancestor := tv.stack[i].(type) { case *ast.AssignStmt, *ast.RangeStmt: - if x.Name == "_" { + if id.Name == "_" { return "", nil // not really a variable } - return semtok.TokVariable, mods + return semtok.TokVariable, modifiers case *ast.GenDecl: - if isDeprecated(y.Doc) { - mods = append(mods, "deprecated") + if isDeprecated(ancestor.Doc) { + modifiers = append(modifiers, "deprecated") } - if y.Tok == token.CONST { - mods = append(mods, "readonly") + if ancestor.Tok == token.CONST { + modifiers = append(modifiers, "readonly") } - return semtok.TokVariable, mods + return semtok.TokVariable, modifiers case *ast.FuncDecl: // If x is immediately under a FuncDecl, it is a function or method if i == len(tv.stack)-2 { - if isDeprecated(y.Doc) { - mods = append(mods, "deprecated") + if isDeprecated(ancestor.Doc) { + modifiers = append(modifiers, "deprecated") } - if y.Recv != nil { - return semtok.TokMethod, mods + if ancestor.Recv != nil { + return semtok.TokMethod, modifiers } - return semtok.TokFunction, mods + return semtok.TokFunction, modifiers } // if x < ... < FieldList < FuncDecl, this is the receiver, a variable // PJW: maybe not. it might be a typeparameter in the type of the receiver - if _, ok := tv.stack[i+1].(*ast.FieldList); ok { - if _, ok := def.(*types.TypeName); ok { - return semtok.TokTypeParam, mods + if is[*ast.FieldList](tv.stack[i+1]) { + if is[*types.TypeName](obj) { + return semtok.TokTypeParam, modifiers } return semtok.TokVariable, nil } // if x < ... < FieldList < FuncType < FuncDecl, this is a param - return semtok.TokParameter, mods - case *ast.FuncType: // is it in the TypeParams? - if isTypeParam(x, y) { - return semtok.TokTypeParam, mods + return semtok.TokParameter, modifiers + case *ast.FuncType: + if isTypeParam(id, ancestor) { + return semtok.TokTypeParam, modifiers } - return semtok.TokParameter, mods + return semtok.TokParameter, modifiers case *ast.InterfaceType: - return semtok.TokMethod, mods + return semtok.TokMethod, modifiers case *ast.TypeSpec: // GenDecl/Typespec/FuncType/FieldList/Field/Ident // (type A func(b uint64)) (err error) @@ -691,8 +664,8 @@ func (tv *tokenVisitor) definitionFor(x *ast.Ident, def types.Object) (semtok.To // (type A struct{b uint64} // but on type B struct{C}), C is a type, but is not being defined. // GenDecl/TypeSpec/FieldList/Field/Ident is a typeParam - if _, ok := tv.stack[i+1].(*ast.FieldList); ok { - return semtok.TokTypeParam, mods + if is[*ast.FieldList](tv.stack[i+1]) { + return semtok.TokTypeParam, modifiers } fldm := tv.stack[len(tv.stack)-2] if fld, ok := fldm.(*ast.Field); ok { @@ -700,36 +673,36 @@ func (tv *tokenVisitor) definitionFor(x *ast.Ident, def types.Object) (semtok.To if len(fld.Names) == 0 { return semtok.TokType, nil } - return semtok.TokVariable, mods + return semtok.TokVariable, modifiers } - return semtok.TokType, mods + return semtok.TokType, modifiers } } // can't happen - msg := fmt.Sprintf("failed to find the decl for %s", safetoken.Position(tv.pgf.Tok, x.Pos())) - tv.unexpected(msg) - return "", []string{""} + tv.errorf("failed to find the decl for %s", safetoken.Position(tv.pgf.Tok, id.Pos())) + return "", nil } -func isTypeParam(x *ast.Ident, y *ast.FuncType) bool { - tp := y.TypeParams - if tp == nil { - return false - } - for _, p := range tp.List { - for _, n := range p.Names { - if x == n { - return true +func isTypeParam(id *ast.Ident, t *ast.FuncType) bool { + if tp := t.TypeParams; tp != nil { + for _, p := range tp.List { + for _, n := range p.Names { + if id == n { + return true + } } } } return false } -func (tv *tokenVisitor) multiline(start, end token.Pos, val string, tok semtok.TokenType) { +// multiline emits a multiline token (`string` or /*comment*/). +func (tv *tokenVisitor) multiline(start, end token.Pos, tok semtok.TokenType) { + // TODO(adonovan): test with non-ASCII. + f := tv.fset.File(start) // the hard part is finding the lengths of lines. include the \n - leng := func(line int) int { + length := func(line int) int { n := f.LineStart(line) if line >= f.LineCount() { return f.Size() - int(n) @@ -741,17 +714,19 @@ func (tv *tokenVisitor) multiline(start, end token.Pos, val string, tok semtok.T sline := spos.Line eline := epos.Line // first line is from spos.Column to end - tv.token(start, leng(sline)-spos.Column, tok, nil) // leng(sline)-1 - (spos.Column-1) + tv.token(start, length(sline)-spos.Column, tok, nil) // leng(sline)-1 - (spos.Column-1) for i := sline + 1; i < eline; i++ { // intermediate lines are from 1 to end - tv.token(f.LineStart(i), leng(i)-1, tok, nil) // avoid the newline + tv.token(f.LineStart(i), length(i)-1, tok, nil) // avoid the newline } // last line is from 1 to epos.Column tv.token(f.LineStart(eline), epos.Column-1, tok, nil) // columns are 1-based } -// findKeyword finds a keyword rather than guessing its location +// findKeyword returns the position of a keyword by searching within +// the specified range, for when its cannot be exactly known from the AST. func (tv *tokenVisitor) findKeyword(keyword string, start, end token.Pos) token.Pos { + // TODO(adonovan): use safetoken.Offset. offset := int(start) - tv.pgf.Tok.Base() last := int(end) - tv.pgf.Tok.Base() buf := tv.pgf.Src @@ -760,25 +735,25 @@ func (tv *tokenVisitor) findKeyword(keyword string, start, end token.Pos) token. return start + token.Pos(idx) } //(in unparsable programs: type _ <-<-chan int) - tv.unexpected(fmt.Sprintf("not found:%s %v", keyword, safetoken.StartPosition(tv.fset, start))) + tv.errorf("not found:%s %v", keyword, safetoken.StartPosition(tv.fset, start)) return token.NoPos } -func (tv *tokenVisitor) importSpec(d *ast.ImportSpec) { +func (tv *tokenVisitor) importSpec(spec *ast.ImportSpec) { // a local package name or the last component of the Path - if d.Name != nil { - nm := d.Name.String() - if nm != "_" && nm != "." { - tv.token(d.Name.Pos(), len(nm), semtok.TokNamespace, nil) + if spec.Name != nil { + name := spec.Name.String() + if name != "_" && name != "." { + tv.token(spec.Name.Pos(), len(name), semtok.TokNamespace, nil) } return // don't mark anything for . or _ } - importPath := metadata.UnquoteImportPath(d) + importPath := metadata.UnquoteImportPath(spec) if importPath == "" { return } // Import strings are implementation defined. Try to match with parse information. - depID := tv.pkg.Metadata().DepsByImpPath[importPath] + depID := tv.metadata.DepsByImpPath[importPath] if depID == "" { return } @@ -788,21 +763,20 @@ func (tv *tokenVisitor) importSpec(d *ast.ImportSpec) { return } // Check whether the original literal contains the package's declared name. - j := strings.LastIndex(d.Path.Value, string(depMD.Name)) - if j == -1 { + j := strings.LastIndex(spec.Path.Value, string(depMD.Name)) + if j < 0 { // Package name does not match import path, so there is nothing to report. return } // Report virtual declaration at the position of the substring. - start := d.Path.Pos() + token.Pos(j) + start := spec.Path.Pos() + token.Pos(j) tv.token(start, len(depMD.Name), semtok.TokNamespace, nil) } -// log unexpected state -func (tv *tokenVisitor) unexpected(msg string) { - if semDebug { - panic(msg) - } +// errorf logs an error and reports a bug. +func (tv *tokenVisitor) errorf(format string, args ...any) { + msg := fmt.Sprintf(format, args...) + bug.Report(msg) event.Error(tv.ctx, tv.strStack(), errors.New(msg)) } @@ -830,7 +804,7 @@ func (tv *tokenVisitor) godirective(c *ast.Comment) { directive, args, _ := strings.Cut(c.Text, " ") kind, _ := stringsCutPrefix(directive, "//go:") if _, ok := godirectives[kind]; !ok { - // Unknown go: directive. + // Unknown 'go:' directive. tv.token(c.Pos(), len(c.Text), semtok.TokComment, nil) return } @@ -854,3 +828,8 @@ func stringsCutPrefix(s, prefix string) (after string, found bool) { } return s[len(prefix):], true } + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +} diff --git a/gopls/internal/golang/snapshot.go b/gopls/internal/golang/snapshot.go index adcbfb9a811..c381c962d08 100644 --- a/gopls/internal/golang/snapshot.go +++ b/gopls/internal/golang/snapshot.go @@ -44,7 +44,7 @@ func NarrowestMetadataForFile(ctx context.Context, snapshot *cache.Snapshot, uri // // Type-checking is expensive. Call snapshot.ParseGo if all you need is a parse // tree, or snapshot.MetadataForFile if you only need metadata. -func NarrowestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*cache.Package, *ParsedGoFile, error) { +func NarrowestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*cache.Package, *parsego.File, error) { return selectPackageForFile(ctx, snapshot, uri, func(metas []*metadata.Package) *metadata.Package { return metas[0] }) } @@ -62,11 +62,11 @@ func NarrowestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri // // Type-checking is expensive. Call snapshot.ParseGo if all you need is a parse // tree, or snapshot.MetadataForFile if you only need metadata. -func WidestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*cache.Package, *ParsedGoFile, error) { +func WidestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*cache.Package, *parsego.File, error) { return selectPackageForFile(ctx, snapshot, uri, func(metas []*metadata.Package) *metadata.Package { return metas[len(metas)-1] }) } -func selectPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, selector func([]*metadata.Package) *metadata.Package) (*cache.Package, *ParsedGoFile, error) { +func selectPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, selector func([]*metadata.Package) *metadata.Package) (*cache.Package, *parsego.File, error) { mps, err := snapshot.MetadataForFile(ctx, uri) if err != nil { return nil, nil, err @@ -88,13 +88,6 @@ func selectPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri pro return pkg, pgf, err } -type ParsedGoFile = parsego.File - -const ( - ParseHeader = parsego.ParseHeader - ParseFull = parsego.ParseFull -) - type ( PackageID = metadata.PackageID PackagePath = metadata.PackagePath diff --git a/gopls/internal/golang/symbols.go b/gopls/internal/golang/symbols.go index 390b8275183..35959c2de7a 100644 --- a/gopls/internal/golang/symbols.go +++ b/gopls/internal/golang/symbols.go @@ -12,6 +12,7 @@ import ( "go/types" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/event" @@ -21,7 +22,7 @@ func DocumentSymbols(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand ctx, done := event.Start(ctx, "golang.DocumentSymbols") defer done() - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, fmt.Errorf("getting file for DocumentSymbols: %w", err) } diff --git a/gopls/internal/golang/util.go b/gopls/internal/golang/util.go index d6e71a964bb..c2f5d50d608 100644 --- a/gopls/internal/golang/util.go +++ b/gopls/internal/golang/util.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" @@ -33,7 +34,7 @@ func IsGenerated(ctx context.Context, snapshot *cache.Snapshot, uri protocol.Doc if err != nil { return false } - pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return false } @@ -112,30 +113,6 @@ func FormatNodeFile(file *token.File, n ast.Node) string { return FormatNode(fset, n) } -// Deref returns a pointer's element type, traversing as many levels as needed. -// Otherwise it returns typ. -// -// It can return a pointer type for cyclic types (see golang/go#45510). -func Deref(typ types.Type) types.Type { - var seen map[types.Type]struct{} - for { - p, ok := typ.Underlying().(*types.Pointer) - if !ok { - return typ - } - if _, ok := seen[p.Elem()]; ok { - return typ - } - - typ = p.Elem() - - if seen == nil { - seen = make(map[types.Type]struct{}) - } - seen[typ] = struct{}{} - } -} - // findFileInDeps finds package metadata containing URI in the transitive // dependencies of m. When using the Go command, the answer is unique. func findFileInDeps(s metadata.Source, mp *metadata.Package, uri protocol.DocumentURI) *metadata.Package { diff --git a/gopls/internal/protocol/generate/generate.go b/gopls/internal/protocol/generate/generate.go index 0496b7d060c..7418918f51f 100644 --- a/gopls/internal/protocol/generate/generate.go +++ b/gopls/internal/protocol/generate/generate.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 -// +build go1.19 - package main import ( diff --git a/gopls/internal/protocol/generate/main.go b/gopls/internal/protocol/generate/main.go index f70c5810d6c..bdc2728e2a9 100644 --- a/gopls/internal/protocol/generate/main.go +++ b/gopls/internal/protocol/generate/main.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 -// +build go1.19 - // The generate command generates Go declarations from VSCode's // description of the Language Server Protocol. // diff --git a/gopls/internal/protocol/generate/main_test.go b/gopls/internal/protocol/generate/main_test.go index 5f336690687..73c22048a80 100644 --- a/gopls/internal/protocol/generate/main_test.go +++ b/gopls/internal/protocol/generate/main_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 -// +build go1.19 - package main import ( diff --git a/gopls/internal/protocol/generate/output.go b/gopls/internal/protocol/generate/output.go index fc64677ff8e..47608626b82 100644 --- a/gopls/internal/protocol/generate/output.go +++ b/gopls/internal/protocol/generate/output.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 -// +build go1.19 - package main import ( diff --git a/gopls/internal/protocol/generate/tables.go b/gopls/internal/protocol/generate/tables.go index ac428b58479..a9207bfc9a3 100644 --- a/gopls/internal/protocol/generate/tables.go +++ b/gopls/internal/protocol/generate/tables.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 -// +build go1.19 - package main import "log" diff --git a/gopls/internal/protocol/generate/typenames.go b/gopls/internal/protocol/generate/typenames.go index 8bacdd2a1cf..83f25a010a0 100644 --- a/gopls/internal/protocol/generate/typenames.go +++ b/gopls/internal/protocol/generate/typenames.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 -// +build go1.19 - package main import ( diff --git a/gopls/internal/protocol/generate/types.go b/gopls/internal/protocol/generate/types.go index 0d01ae43cb1..0537748eb5b 100644 --- a/gopls/internal/protocol/generate/types.go +++ b/gopls/internal/protocol/generate/types.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 -// +build go1.19 - package main import ( diff --git a/gopls/internal/protocol/mapper.go b/gopls/internal/protocol/mapper.go index 69547979224..d1bd957a9e5 100644 --- a/gopls/internal/protocol/mapper.go +++ b/gopls/internal/protocol/mapper.go @@ -375,7 +375,7 @@ func (m *Mapper) NodeMappedRange(tf *token.File, node ast.Node) (MappedRange, er // // Construct one by calling Mapper.OffsetMappedRange with start/end offsets. // From the go/token domain, call safetoken.Offsets first, -// or use a helper such as ParsedGoFile.MappedPosRange. +// or use a helper such as parsego.File.MappedPosRange. // // Two MappedRanges produced the same Mapper are equal if and only if they // denote the same range. Two MappedRanges produced by different Mappers diff --git a/gopls/internal/protocol/protocol.go b/gopls/internal/protocol/protocol.go index 09bfaca2e86..7cc5589aa0b 100644 --- a/gopls/internal/protocol/protocol.go +++ b/gopls/internal/protocol/protocol.go @@ -11,7 +11,7 @@ import ( "fmt" "io" - "golang.org/x/tools/gopls/internal/telemetry" + "golang.org/x/telemetry/crashmonitor" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" @@ -302,7 +302,7 @@ func recoverHandlerPanic(method string) { // Report panics in the handler goroutine, // unless we have enabled the monitor, // which reports all crashes. - if !telemetry.CrashMonitorSupported() { + if !crashmonitor.Supported() { defer func() { if x := recover(); x != nil { bug.Reportf("panic in %s request", method) diff --git a/gopls/internal/protocol/semantic.go b/gopls/internal/protocol/semantic.go index 65253d0323c..03407899b57 100644 --- a/gopls/internal/protocol/semantic.go +++ b/gopls/internal/protocol/semantic.go @@ -8,7 +8,7 @@ package protocol import "fmt" -// SemanticTypes to use in case there is no client, as in the command line, or tests +// SemanticTypes to use in case there is no client, as in the command line, or tests. func SemanticTypes() []string { return semanticTypes[:] } diff --git a/gopls/internal/protocol/semtok/semtok.go b/gopls/internal/protocol/semtok/semtok.go index e69ec825f17..850e234a1b0 100644 --- a/gopls/internal/protocol/semtok/semtok.go +++ b/gopls/internal/protocol/semtok/semtok.go @@ -18,6 +18,9 @@ type Token struct { type TokenType string const ( + // These are the tokens defined by LSP 3.17, but a client is + // free to send its own set; any tokens that the server emits + // that are not in this set are simply not encoded in the bitfield. TokNamespace TokenType = "namespace" TokType TokenType = "type" TokInterface TokenType = "interface" @@ -32,6 +35,10 @@ const ( TokNumber TokenType = "number" TokOperator TokenType = "operator" TokMacro TokenType = "macro" // for templates + + // not part of LSP 3.17 (even though JS has labels) + // https://github.com/microsoft/vscode-languageserver-node/issues/1422 + TokLabel TokenType = "label" ) // Encode returns the LSP encoding of a sequence of tokens. diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 8681db7e0ac..3f5d53c53a2 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -21,6 +21,7 @@ import ( "sync" "golang.org/x/mod/modfile" + "golang.org/x/telemetry/counter" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" @@ -32,7 +33,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/telemetry" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/gopls/internal/vulncheck/scan" @@ -80,7 +80,13 @@ func (*commandHandler) AddTelemetryCounters(_ context.Context, args command.AddT return fmt.Errorf("Names and Values must have the same length") } // invalid counter update requests will be silently dropped. (no audience) - telemetry.AddForwardedCounters(args.Names, args.Values) + for i, n := range args.Names { + v := args.Values[i] + if n == "" || v < 0 { + continue + } + counter.Add("fwd/"+n, v) + } return nil } @@ -814,7 +820,7 @@ func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) ( if err != nil { return err } - pgf, err := deps.snapshot.ParseGo(ctx, fh, parsego.ParseHeader) + pgf, err := deps.snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return err } diff --git a/gopls/internal/server/diagnostics.go b/gopls/internal/server/diagnostics.go index 6aa01aba127..7db8d32761d 100644 --- a/gopls/internal/server/diagnostics.go +++ b/gopls/internal/server/diagnostics.go @@ -11,6 +11,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "sort" "strings" "sync" @@ -61,7 +62,9 @@ type ( diagMap = map[protocol.DocumentURI][]*cache.Diagnostic ) -// hashDiagnostics computes a hash to identify a diagnostic. +// hashDiagnostic computes a hash to identify a diagnostic. +// The hash is for deduplicating within a file, +// so it need not incorporate d.URI. func hashDiagnostic(d *cache.Diagnostic) file.Hash { h := sha256.New() for _, t := range d.Tags { @@ -175,7 +178,6 @@ func (s *server) diagnoseSnapshot(snapshot *cache.Snapshot, changedURIs []protoc ctx, done := event.Start(ctx, "Server.diagnoseSnapshot", snapshot.Labels()...) defer done() - allViews := s.session.Views() if delay > 0 { // 2-phase diagnostics. // @@ -204,7 +206,7 @@ func (s *server) diagnoseSnapshot(snapshot *cache.Snapshot, changedURIs []protoc } return } - s.updateDiagnostics(ctx, allViews, snapshot, diagnostics, false) + s.updateDiagnostics(ctx, snapshot, diagnostics, false) } if delay < minDelay { @@ -227,7 +229,7 @@ func (s *server) diagnoseSnapshot(snapshot *cache.Snapshot, changedURIs []protoc } return } - s.updateDiagnostics(ctx, allViews, snapshot, diagnostics, true) + s.updateDiagnostics(ctx, snapshot, diagnostics, true) } func (s *server) diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snapshot, uris []protocol.DocumentURI) (diagMap, error) { @@ -668,10 +670,7 @@ func (s *server) updateCriticalErrorStatus(ctx context.Context, snapshot *cache. // updateDiagnostics records the result of diagnosing a snapshot, and publishes // any diagnostics that need to be updated on the client. -// -// The allViews argument should be the current set of views present in the -// session, for the purposes of trimming diagnostics produced by deleted views. -func (s *server) updateDiagnostics(ctx context.Context, allViews []*cache.View, snapshot *cache.Snapshot, diagnostics diagMap, final bool) { +func (s *server) updateDiagnostics(ctx context.Context, snapshot *cache.Snapshot, diagnostics diagMap, final bool) { ctx, done := event.Start(ctx, "Server.publishDiagnostics") defer done() @@ -694,8 +693,13 @@ func (s *server) updateDiagnostics(ctx context.Context, allViews []*cache.View, return } + // golang/go#65312: since the set of diagnostics depends on the set of views, + // we get the views *after* locking diagnosticsMu. This ensures that + // updateDiagnostics does not incorrectly delete diagnostics that have been + // set for an existing view that was created between the call to + // s.session.Views() and updateDiagnostics. viewMap := make(viewSet) - for _, v := range allViews { + for _, v := range s.session.Views() { viewMap[v] = unit{} } @@ -822,23 +826,25 @@ func (s *server) updateOrphanedFileDiagnostics(ctx context.Context, modID uint64 // // If the publication succeeds, it updates f.publishedHash and f.mustPublish. func (s *server) publishFileDiagnosticsLocked(ctx context.Context, views viewSet, uri protocol.DocumentURI, version int32, f *fileDiagnostics) error { - // Check that the set of views is up-to-date, and de-dupe diagnostics - // across views. - var ( - diagHashes = make(map[file.Hash]unit) // unique diagnostic hashes - hash file.Hash // XOR of diagnostic hashes - unique []*cache.Diagnostic // unique diagnostics - ) - add := func(diag *cache.Diagnostic) { + + // We add a disambiguating suffix (e.g. " [darwin,arm64]") to + // each diagnostic that doesn't occur in the default view; + // see golang/go#65496. + type diagSuffix struct { + diag *cache.Diagnostic + suffix string // "" for default build (or orphans) + } + + // diagSuffixes records the set of view suffixes for a given diagnostic. + diagSuffixes := make(map[file.Hash][]diagSuffix) + add := func(diag *cache.Diagnostic, suffix string) { h := hashDiagnostic(diag) - if _, ok := diagHashes[h]; !ok { - diagHashes[h] = unit{} - unique = append(unique, diag) - hash.XORWith(h) - } + diagSuffixes[h] = append(diagSuffixes[h], diagSuffix{diag, suffix}) } + + // Construct the inverse mapping, from diagnostic (hash) to its suffixes (views). for _, diag := range f.orphanedFileDiagnostics { - add(diag) + add(diag, "") } for view, viewDiags := range f.byView { if _, ok := views[view]; !ok { @@ -848,10 +854,53 @@ func (s *server) publishFileDiagnosticsLocked(ctx context.Context, views viewSet if viewDiags.version != version { continue // a payload of diagnostics applies to a specific file version } + + // Compute the view's suffix (e.g. " [darwin,arm64]"). + var suffix string + { + var words []string + if view.GOOS() != runtime.GOOS { + words = append(words, view.GOOS()) + } + if view.GOARCH() != runtime.GOARCH { + words = append(words, view.GOARCH()) + } + if len(words) > 0 { + suffix = fmt.Sprintf(" [%s]", strings.Join(words, ",")) + } + } + for _, diag := range viewDiags.diagnostics { - add(diag) + add(diag, suffix) } } + + // De-dup diagnostics across views by hash, and sort. + var ( + hash file.Hash + unique []*cache.Diagnostic + ) + for h, items := range diagSuffixes { + // Sort the items by ascending suffix, so that the + // default view (if present) is first. + // (The others are ordered arbitrarily.) + sort.Slice(items, func(i, j int) bool { + return items[i].suffix < items[j].suffix + }) + + // If the diagnostic was not present in + // the default view, add the view suffix. + first := items[0] + if first.suffix != "" { + diag2 := *first.diag // shallow copy + diag2.Message += first.suffix + first.diag = &diag2 + h = hashDiagnostic(&diag2) // update the hash + } + + hash.XORWith(h) + unique = append(unique, first.diag) + } sortDiagnostics(unique) // Publish, if necessary. diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go index cdaee1b973b..9ead4705cea 100644 --- a/gopls/internal/server/general.go +++ b/gopls/internal/server/general.go @@ -20,12 +20,12 @@ import ( "strings" "sync" + "golang.org/x/telemetry/counter" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/debug" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/telemetry" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/goversion" "golang.org/x/tools/gopls/internal/util/maps" @@ -41,7 +41,7 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ if params != nil && params.ClientInfo != nil { clientName = params.ClientInfo.Name } - telemetry.RecordClientInfo(clientName) + recordClientInfo(clientName) s.stateMu.Lock() if s.state >= serverInitializing { @@ -249,7 +249,9 @@ func (s *server) checkViewGoVersions() { if oldestVersion == -1 || viewVersion < oldestVersion { oldestVersion, fromBuild = viewVersion, false } - telemetry.RecordViewGoVersion(viewVersion) + if viewVersion >= 0 { + counter.Inc(fmt.Sprintf("gopls/goversion:1.%d", viewVersion)) + } } if msg, isError := goversion.Message(oldestVersion, fromBuild); msg != "" { @@ -637,3 +639,42 @@ func (s *server) Exit(ctx context.Context) error { // close naturally if needed after the connection is closed. return nil } + +// recordClientInfo records gopls client info. +func recordClientInfo(clientName string) { + key := "gopls/client:other" + switch clientName { + case "Visual Studio Code": + key = "gopls/client:vscode" + case "Visual Studio Code - Insiders": + key = "gopls/client:vscode-insiders" + case "VSCodium": + key = "gopls/client:vscodium" + case "code-server": + // https://github.com/coder/code-server/blob/3cb92edc76ecc2cfa5809205897d93d4379b16a6/ci/build/build-vscode.sh#L19 + key = "gopls/client:code-server" + case "Eglot": + // https://lists.gnu.org/archive/html/bug-gnu-emacs/2023-03/msg00954.html + key = "gopls/client:eglot" + case "govim": + // https://github.com/govim/govim/pull/1189 + key = "gopls/client:govim" + case "Neovim": + // https://github.com/neovim/neovim/blob/42333ea98dfcd2994ee128a3467dfe68205154cd/runtime/lua/vim/lsp.lua#L1361 + key = "gopls/client:neovim" + case "coc.nvim": + // https://github.com/neoclide/coc.nvim/blob/3dc6153a85ed0f185abec1deb972a66af3fbbfb4/src/language-client/client.ts#L994 + key = "gopls/client:coc.nvim" + case "Sublime Text LSP": + // https://github.com/sublimelsp/LSP/blob/e608f878e7e9dd34aabe4ff0462540fadcd88fcc/plugin/core/sessions.py#L493 + key = "gopls/client:sublimetext" + default: + // Accumulate at least a local counter for an unknown + // client name, but also fall through to count it as + // ":other" for collection. + if clientName != "" { + counter.New(fmt.Sprintf("gopls/client-other:%s", clientName)).Inc() + } + } + counter.Inc(key) +} diff --git a/gopls/internal/server/link.go b/gopls/internal/server/link.go index 6ac397627d9..c0a60f22601 100644 --- a/gopls/internal/server/link.go +++ b/gopls/internal/server/link.go @@ -109,7 +109,7 @@ func modLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([] // goLinks returns the set of hyperlink annotations for the specified Go file. func goLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.DocumentLink, error) { - pgf, err := snapshot.ParseGo(ctx, fh, parsego.ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } diff --git a/gopls/internal/server/prompt.go b/gopls/internal/server/prompt.go index 72c5113dc4e..7dc16b9a324 100644 --- a/gopls/internal/server/prompt.go +++ b/gopls/internal/server/prompt.go @@ -11,8 +11,8 @@ import ( "path/filepath" "time" + "golang.org/x/telemetry" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/telemetry" "golang.org/x/tools/internal/event" ) diff --git a/gopls/internal/server/selection_range.go b/gopls/internal/server/selection_range.go index 89142f42007..042812217f3 100644 --- a/gopls/internal/server/selection_range.go +++ b/gopls/internal/server/selection_range.go @@ -40,7 +40,7 @@ func (s *server) SelectionRange(ctx context.Context, params *protocol.SelectionR return nil, fmt.Errorf("SelectionRange not supported for file of type %s", kind) } - pgf, err := snapshot.ParseGo(ctx, fh, parsego.ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index 752effab3ef..f29b439428b 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go @@ -115,7 +115,7 @@ func DefaultOptions(overrides ...func(*Options)) *Options { ReportAnalysisProgressAfter: 5 * time.Second, TelemetryPrompt: false, LinkifyShowMessage: false, - IncludeReplaceInWorkspace: true, + IncludeReplaceInWorkspace: false, ZeroConfig: true, }, Hooks: Hooks{ diff --git a/gopls/internal/telemetry/telemetry.go b/gopls/internal/telemetry/telemetry.go deleted file mode 100644 index deab5d22eb1..00000000000 --- a/gopls/internal/telemetry/telemetry.go +++ /dev/null @@ -1,112 +0,0 @@ -// 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. - -//go:build go1.19 -// +build go1.19 - -package telemetry - -import ( - "fmt" - - "golang.org/x/telemetry" - "golang.org/x/telemetry/counter" - "golang.org/x/telemetry/crashmonitor" - "golang.org/x/telemetry/upload" -) - -// CounterOpen calls [counter.Open]. -func CounterOpen() { - counter.Open() -} - -// StartCrashMonitor calls [crashmonitor.Start]. -func StartCrashMonitor() { - crashmonitor.Start() -} - -// CrashMonitorSupported calls [crashmonitor.Supported]. -func CrashMonitorSupported() bool { - return crashmonitor.Supported() -} - -// NewStackCounter calls [counter.NewStack]. -func NewStackCounter(name string, depth int) *counter.StackCounter { - return counter.NewStack(name, depth) -} - -// Mode calls x/telemetry.Mode. -func Mode() string { - return telemetry.Mode() -} - -// SetMode calls x/telemetry.SetMode. -func SetMode(mode string) error { - return telemetry.SetMode(mode) -} - -// Upload starts a goroutine for telemetry upload. -func Upload() { - go upload.Run(nil) -} - -// RecordClientInfo records gopls client info. -func RecordClientInfo(clientName string) { - key := "gopls/client:other" - switch clientName { - case "Visual Studio Code": - key = "gopls/client:vscode" - case "Visual Studio Code - Insiders": - key = "gopls/client:vscode-insiders" - case "VSCodium": - key = "gopls/client:vscodium" - case "code-server": - // https://github.com/coder/code-server/blob/3cb92edc76ecc2cfa5809205897d93d4379b16a6/ci/build/build-vscode.sh#L19 - key = "gopls/client:code-server" - case "Eglot": - // https://lists.gnu.org/archive/html/bug-gnu-emacs/2023-03/msg00954.html - key = "gopls/client:eglot" - case "govim": - // https://github.com/govim/govim/pull/1189 - key = "gopls/client:govim" - case "Neovim": - // https://github.com/neovim/neovim/blob/42333ea98dfcd2994ee128a3467dfe68205154cd/runtime/lua/vim/lsp.lua#L1361 - key = "gopls/client:neovim" - case "coc.nvim": - // https://github.com/neoclide/coc.nvim/blob/3dc6153a85ed0f185abec1deb972a66af3fbbfb4/src/language-client/client.ts#L994 - key = "gopls/client:coc.nvim" - case "Sublime Text LSP": - // https://github.com/sublimelsp/LSP/blob/e608f878e7e9dd34aabe4ff0462540fadcd88fcc/plugin/core/sessions.py#L493 - key = "gopls/client:sublimetext" - default: - // Accumulate at least a local counter for an unknown - // client name, but also fall through to count it as - // ":other" for collection. - if clientName != "" { - counter.New(fmt.Sprintf("gopls/client-other:%s", clientName)).Inc() - } - } - counter.Inc(key) -} - -// RecordViewGoVersion records the Go minor version number (1.x) used for a view. -func RecordViewGoVersion(x int) { - if x < 0 { - return - } - name := fmt.Sprintf("gopls/goversion:1.%d", x) - counter.Inc(name) -} - -// AddForwardedCounters adds the given counters on behalf of clients. -// Names and values must have the same length. -func AddForwardedCounters(names []string, values []int64) { - for i, n := range names { - v := values[i] - if n == "" || v < 0 { - continue // Should we report an error? Who is the audience? - } - counter.Add("fwd/"+n, v) - } -} diff --git a/gopls/internal/telemetry/telemetry_go118.go b/gopls/internal/telemetry/telemetry_go118.go deleted file mode 100644 index 12b7803f1a7..00000000000 --- a/gopls/internal/telemetry/telemetry_go118.go +++ /dev/null @@ -1,44 +0,0 @@ -// 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. - -//go:build !go1.19 -// +build !go1.19 - -package telemetry - -// This file defines dummy implementations of telemetry operations to -// permit building with go1.18. Until we drop support for go1.18, -// gopls may not refer to the telemetry module directly, but must go -// through this file. - -func CounterOpen() {} - -func StartCrashMonitor() {} - -func CrashMonitorSupported() bool { return false } - -func NewStackCounter(string, int) dummyCounter { return dummyCounter{} } - -type dummyCounter struct{} - -func (dummyCounter) Inc() {} - -func Mode() string { - return "local" -} - -func SetMode(mode string) error { - return nil -} - -func Upload() { -} - -func RecordClientInfo(string) {} - -func RecordViewGoVersion(x int) { -} - -func AddForwardedCounters(names []string, values []int64) { -} diff --git a/gopls/internal/telemetry/telemetry_test.go b/gopls/internal/telemetry/telemetry_test.go index b52ac7093c7..1abd7ada03c 100644 --- a/gopls/internal/telemetry/telemetry_test.go +++ b/gopls/internal/telemetry/telemetry_test.go @@ -59,7 +59,7 @@ func TestTelemetry(t *testing.T) { for i, c := range sessionCounters { count, err := countertest.ReadCounter(c) if err != nil { - t.Fatalf("ReadCounter(%s): %v", c.Name(), err) + continue // counter db not open, or counter not found } initialCounts[i] = count } @@ -74,7 +74,19 @@ func TestTelemetry(t *testing.T) { goversion = strconv.Itoa(env.GoVersion()) addForwardedCounters(env, []string{"vscode/linter:a"}, []int64{1}) const desc = "got a bug" + + // This will increment a counter named something like: + // + // `gopls/bug + // golang.org/x/tools/gopls/internal/util/bug.report:+35 + // golang.org/x/tools/gopls/internal/util/bug.Report:=68 + // golang.org/x/tools/gopls/internal/telemetry_test.TestTelemetry.func2:+4 + // golang.org/x/tools/gopls/internal/test/integration.(*Runner).Run.func1:+87 + // testing.tRunner:+150 + // runtime.goexit:+0` + // bug.Report(desc) // want a stack counter with the trace starting from here. + env.Await(ShownMessage(desc)) }) diff --git a/gopls/internal/test/integration/completion/completion18_test.go b/gopls/internal/test/integration/completion/completion18_test.go index 0ca83778664..a35061d693b 100644 --- a/gopls/internal/test/integration/completion/completion18_test.go +++ b/gopls/internal/test/integration/completion/completion18_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package completion import ( diff --git a/gopls/internal/test/integration/diagnostics/diagnostics_test.go b/gopls/internal/test/integration/diagnostics/diagnostics_test.go index dba9532dd6d..89c8a14bd37 100644 --- a/gopls/internal/test/integration/diagnostics/diagnostics_test.go +++ b/gopls/internal/test/integration/diagnostics/diagnostics_test.go @@ -557,7 +557,7 @@ func f() { // AdHoc views are not critical errors, but their missing import // diagnostics should specifically mention GOROOT or GOPATH (and not // modules). - NoOutstandingWork(nil), + NoOutstandingWork(IgnoreTelemetryPromptWork), Diagnostics( env.AtRegexp("a.go", `"mod.com`), WithMessage("GOROOT or GOPATH"), diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index 7c15106603f..e93776408b6 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go @@ -82,6 +82,8 @@ type EditorConfig struct { // // Since this can only be set during initialization, changing this field via // Editor.ChangeConfiguration has no effect. + // + // If empty, "fake.Editor" is used. ClientName string // Env holds environment variables to apply on top of the default editor @@ -282,12 +284,15 @@ func makeSettings(sandbox *Sandbox, config EditorConfig, scopeURI *protocol.URI) func (e *Editor) initialize(ctx context.Context) error { config := e.Config() + clientName := config.ClientName + if clientName == "" { + clientName = "fake.Editor" + } + params := &protocol.ParamInitialize{} - if e.config.ClientName != "" { - params.ClientInfo = &protocol.ClientInfo{ - Name: e.config.ClientName, - Version: "v1.0.0", - } + params.ClientInfo = &protocol.ClientInfo{ + Name: clientName, + Version: "v1.0.0", } params.InitializationOptions = makeSettings(e.sandbox, config, nil) params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders) @@ -337,6 +342,8 @@ func clientCapabilities(cfg EditorConfig) (protocol.ClientCapabilities, error) { "struct", "typeParameter", "parameter", "variable", "property", "enumMember", "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator", + // Additional types supported by this client: + "label", } capabilities.TextDocument.SemanticTokens.TokenModifiers = []string{ "declaration", "definition", "readonly", "static", diff --git a/gopls/internal/test/integration/misc/configuration_test.go b/gopls/internal/test/integration/misc/configuration_test.go index fff4576bc5b..39980f353df 100644 --- a/gopls/internal/test/integration/misc/configuration_test.go +++ b/gopls/internal/test/integration/misc/configuration_test.go @@ -15,7 +15,7 @@ import ( // Test that enabling and disabling produces the expected results of showing // and hiding staticcheck analysis results. func TestChangeConfiguration(t *testing.T) { - // Staticcheck only supports Go versions >= 1.19. + // Staticcheck only supports Go versions >= 1.20. // Note: keep this in sync with TestStaticcheckWarning. Below this version we // should get an error when setting staticcheck configuration. testenv.NeedsGo1Point(t, 20) @@ -78,13 +78,13 @@ type B struct { WithOptions( WorkspaceFolders("a"), - FolderSettings(map[string]Settings{ + FolderSettings{ "a": { "analyses": map[string]bool{ "composites": false, }, }, - }), + }, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.AfterChange(NoDiagnostics()) @@ -164,19 +164,6 @@ var FooErr = errors.New("foo") }) } -func TestGofumptWarning(t *testing.T) { - testenv.SkipAfterGo1Point(t, 17) - - WithOptions( - Settings{"gofumpt": true}, - ).Run(t, "", func(t *testing.T, env *Env) { - env.OnceMet( - InitialWorkspaceLoad, - ShownMessage("gofumpt is not supported"), - ) - }) -} - func TestDeprecatedSettings(t *testing.T) { WithOptions( Settings{ diff --git a/gopls/internal/test/integration/misc/definition_test.go b/gopls/internal/test/integration/misc/definition_test.go index b7394b8dd67..6b364e2e9d5 100644 --- a/gopls/internal/test/integration/misc/definition_test.go +++ b/gopls/internal/test/integration/misc/definition_test.go @@ -495,9 +495,7 @@ const _ = b.K } // Run 'go mod vendor' outside the editor. - if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, nil, true); err != nil { - t.Fatalf("go mod vendor: %v", err) - } + env.RunGoCommand("mod", "vendor") // Synchronize changes to watched files. env.Await(env.DoneWithChangeWatchedFiles()) diff --git a/gopls/internal/test/integration/misc/references_test.go b/gopls/internal/test/integration/misc/references_test.go index fcd72d85c68..73e4fffe3b8 100644 --- a/gopls/internal/test/integration/misc/references_test.go +++ b/gopls/internal/test/integration/misc/references_test.go @@ -360,8 +360,6 @@ func _() { // implementations in vendored modules were not found. The actual fix // was the same as for #55995; see TestVendoringInvalidatesMetadata. func TestImplementationsInVendor(t *testing.T) { - t.Skip("golang/go#56169: file watching does not capture vendor dirs") - const proxy = ` -- other.com/b@v1.0.0/go.mod -- module other.com/b @@ -415,9 +413,7 @@ var _ b.B checkVendor(env.Implementations(refLoc), false) // Run 'go mod vendor' outside the editor. - if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, nil, true); err != nil { - t.Fatalf("go mod vendor: %v", err) - } + env.RunGoCommand("mod", "vendor") // Synchronize changes to watched files. env.Await(env.DoneWithChangeWatchedFiles()) diff --git a/gopls/internal/test/integration/misc/vuln_test.go b/gopls/internal/test/integration/misc/vuln_test.go index f74c1c38d77..b2bd520759b 100644 --- a/gopls/internal/test/integration/misc/vuln_test.go +++ b/gopls/internal/test/integration/misc/vuln_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package misc import ( @@ -167,7 +164,7 @@ func TestRunGovulncheckStd(t *testing.T) { -- go.mod -- module mod.com -go 1.18 +go 1.19 -- main.go -- package main @@ -192,9 +189,9 @@ func main() { // Let the analyzer read vulnerabilities data from the testdata/vulndb. "GOVULNDB": db.URI(), // When fetchinging stdlib package vulnerability info, - // behave as if our go version is go1.18 for this testing. + // behave as if our go version is go1.19 for this testing. // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). - cache.GoVersionForVulnTest: "go1.18", + cache.GoVersionForVulnTest: "go1.19", "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. }, Settings{ diff --git a/gopls/internal/test/integration/options.go b/gopls/internal/test/integration/options.go index a6c394e3467..f1d5425d807 100644 --- a/gopls/internal/test/integration/options.go +++ b/gopls/internal/test/integration/options.go @@ -115,16 +115,16 @@ func WorkspaceFolders(relFolders ...string) RunOption { // // Use in conjunction with WorkspaceFolders to have different settings for // different folders. -func FolderSettings(folderSettings map[string]Settings) RunOption { +type FolderSettings map[string]Settings + +func (fs FolderSettings) set(opts *runConfig) { // Re-use the Settings type, for symmetry, but translate back into maps for // the editor config. folders := make(map[string]map[string]any) - for k, v := range folderSettings { + for k, v := range fs { folders[k] = v } - return optionSetter(func(opts *runConfig) { - opts.editor.FolderSettings = folders - }) + opts.editor.FolderSettings = folders } // EnvVars sets environment variables for the LSP session. When applying these diff --git a/gopls/internal/test/integration/workspace/std_test.go b/gopls/internal/test/integration/workspace/std_test.go new file mode 100644 index 00000000000..51c0603f014 --- /dev/null +++ b/gopls/internal/test/integration/workspace/std_test.go @@ -0,0 +1,73 @@ +// Copyright 2024 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 workspace + +import ( + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + . "golang.org/x/tools/gopls/internal/test/integration" +) + +func TestStdWorkspace(t *testing.T) { + // This test checks that we actually load workspace packages when opening + // GOROOT. + // + // In golang/go#65801, we failed to do this because go/packages returns nil + // Module for std and cmd. + // + // Because this test loads std as a workspace, it may be slow on smaller + // builders. + if testing.Short() { + t.Skip("skipping with -short: loads GOROOT") + } + + // The test also fails on Windows because an absolute path does not match + // (likely a misspelling due to slashes). + // TODO(rfindley): investigate and fix this on windows. + if runtime.GOOS == "windows" { + t.Skip("skipping on windows: fails to to misspelled paths") + } + + // Query GOROOT. This is slightly more precise than e.g. runtime.GOROOT, as + // it queries the go command in the environment. + goroot, err := exec.Command("go", "env", "GOROOT").Output() + if err != nil { + t.Fatal(err) + } + stdDir := filepath.Join(strings.TrimSpace(string(goroot)), "src") + WithOptions( + Modes(Default), // This test may be slow. No reason to run it multiple times. + WorkspaceFolders(stdDir), + ).Run(t, "", func(t *testing.T, env *Env) { + // Find parser.ParseFile. Query with `'` to get an exact match. + syms := env.Symbol("'go/parser.ParseFile") + if len(syms) != 1 { + t.Fatalf("got %d symbols, want exactly 1. Symbols:\n%v", len(syms), syms) + } + parserPath := syms[0].Location.URI.Path() + env.OpenFile(parserPath) + + // Find the reference to ast.File from the signature of ParseFile. This + // helps guard against matching a comment. + astFile := env.RegexpSearch(parserPath, `func ParseFile\(.*ast\.(File)`) + refs := env.References(astFile) + + // If we've successfully loaded workspace packages for std, we should find + // a reference in go/types. + foundGoTypesReference := false + for _, ref := range refs { + if strings.Contains(string(ref.URI), "go/types") { + foundGoTypesReference = true + } + } + if !foundGoTypesReference { + t.Errorf("references(ast.File) did not return a go/types reference. Refs:\n%v", refs) + } + }) +} diff --git a/gopls/internal/test/integration/workspace/vendor_test.go b/gopls/internal/test/integration/workspace/vendor_test.go new file mode 100644 index 00000000000..f14cf539de0 --- /dev/null +++ b/gopls/internal/test/integration/workspace/vendor_test.go @@ -0,0 +1,67 @@ +// Copyright 2024 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 workspace + +import ( + "testing" + + . "golang.org/x/tools/gopls/internal/test/integration" +) + +func TestWorkspacePackagesExcludesVendor(t *testing.T) { + // This test verifies that packages in the vendor directory are not workspace + // packages. This would be an easy mistake for gopls to make, since mod + // vendoring excludes go.mod files, and therefore the nearest go.mod file for + // vendored packages is often the workspace mod file. + const proxy = ` +-- other.com/b@v1.0.0/go.mod -- +module other.com/b + +go 1.18 + +-- other.com/b@v1.0.0/b.go -- +package b + +type B int + +func _() { + var V int // unused +} +` + const src = ` +-- go.mod -- +module example.com/a +go 1.14 +require other.com/b v1.0.0 + +-- go.sum -- +other.com/b v1.0.0 h1:ct1+0RPozzMvA2rSYnVvIfr/GDHcd7oVnw147okdi3g= +other.com/b v1.0.0/go.mod h1:bfTSZo/4ZtAQJWBYScopwW6n9Ctfsl2mi8nXsqjDXR8= + +-- a.go -- +package a + +import "other.com/b" + +var _ b.B + +` + WithOptions( + ProxyFiles(proxy), + Modes(Default), + ).Run(t, src, func(t *testing.T, env *Env) { + env.RunGoCommand("mod", "vendor") + // Uncomment for updated go.sum contents. + // env.DumpGoSum(".") + env.OpenFile("a.go") + env.AfterChange( + NoDiagnostics(), // as b is not a workspace package + ) + env.GoToDefinition(env.RegexpSearch("a.go", `b\.(B)`)) + env.AfterChange( + Diagnostics(env.AtRegexp("vendor/other.com/b/b.go", "V"), WithMessage("not used")), + ) + }) +} diff --git a/gopls/internal/test/integration/workspace/workspace_test.go b/gopls/internal/test/integration/workspace/workspace_test.go index 28b3978a8cd..e2819404dfa 100644 --- a/gopls/internal/test/integration/workspace/workspace_test.go +++ b/gopls/internal/test/integration/workspace/workspace_test.go @@ -7,9 +7,11 @@ package workspace import ( "context" "fmt" + "sort" "strings" "testing" + "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/hooks" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/test/integration/fake" @@ -1123,16 +1125,139 @@ import ( env.AfterChange( Diagnostics(env.AtRegexp("a/main.go", "V"), WithMessage("not used")), ) + // Here, diagnostics are added because of zero-config gopls. + // In the past, they were added simply due to diagnosing changed files. + // (see TestClearNonWorkspaceDiagnostics_NoView below for a + // reimplementation of that test). + if got, want := len(env.Views()), 2; got != want { + t.Errorf("after opening a/main.go, got %d views, want %d", got, want) + } env.CloseBuffer("a/main.go") + env.AfterChange( + NoDiagnostics(ForFile("a/main.go")), + ) + if got, want := len(env.Views()), 1; got != want { + t.Errorf("after closing a/main.go, got %d views, want %d", got, want) + } + }) +} + +// This test is like TestClearNonWorkspaceDiagnostics, but bypasses the +// zero-config algorithm by opening a nested workspace folder. +// +// We should still compute diagnostics correctly for open packages. +func TestClearNonWorkspaceDiagnostics_NoView(t *testing.T) { + const ws = ` +-- a/go.mod -- +module example.com/a + +go 1.18 + +require example.com/b v1.2.3 + +replace example.com/b => ../b + +-- a/a.go -- +package a + +import "example.com/b" + +func _() { + V := b.B // unused +} + +-- b/go.mod -- +module b + +go 1.18 + +-- b/b.go -- +package b + +const B = 2 + +func _() { + var V int // unused +} + +-- b/b2.go -- +package b + +const B2 = B + +-- c/c.go -- +package main + +func main() { + var V int // unused +} +` + WithOptions( + WorkspaceFolders("a"), + ).Run(t, ws, func(t *testing.T, env *Env) { + env.OpenFile("a/a.go") + env.AfterChange( + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("b/b.go")), + NoDiagnostics(ForFile("c/c.go")), + ) + env.OpenFile("b/b.go") + env.AfterChange( + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + Diagnostics(env.AtRegexp("b/b.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("c/c.go")), + ) - // Make an arbitrary edit because gopls explicitly diagnoses a/main.go - // whenever it is "changed". + // Opening b/b.go should not result in a new view, because b is not + // contained in a workspace folder. // - // TODO(rfindley): it should not be necessary to make another edit here. - // Gopls should be smart enough to avoid diagnosing a. - env.RegexpReplace("b/main.go", "package b", "package b // a package") + // Yet we should get diagnostics for b, because it is open. + if got, want := len(env.Views()), 1; got != want { + t.Errorf("after opening b/b.go, got %d views, want %d", got, want) + } + env.CloseBuffer("b/b.go") env.AfterChange( - NoDiagnostics(ForFile("a/main.go")), + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("b/b.go")), + NoDiagnostics(ForFile("c/c.go")), + ) + + // We should get references in the b package. + bUse := env.RegexpSearch("a/a.go", `b\.(B)`) + refs := env.References(bUse) + wantRefs := []string{"a/a.go", "b/b.go", "b/b2.go"} + var gotRefs []string + for _, ref := range refs { + gotRefs = append(gotRefs, env.Sandbox.Workdir.URIToPath(ref.URI)) + } + sort.Strings(gotRefs) + if diff := cmp.Diff(wantRefs, gotRefs); diff != "" { + t.Errorf("references(b.B) mismatch (-want +got)\n%s", diff) + } + + // Opening c/c.go should also not result in a new view, yet we should get + // orphaned file diagnostics. + env.OpenFile("c/c.go") + env.AfterChange( + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("b/b.go")), + Diagnostics(env.AtRegexp("c/c.go", "V"), WithMessage("not used")), + ) + if got, want := len(env.Views()), 1; got != want { + t.Errorf("after opening b/b.go, got %d views, want %d", got, want) + } + + env.CloseBuffer("c/c.go") + env.AfterChange( + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("b/b.go")), + NoDiagnostics(ForFile("c/c.go")), + ) + env.CloseBuffer("a/a.go") + env.AfterChange( + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("b/b.go")), + NoDiagnostics(ForFile("c/c.go")), ) }) } diff --git a/gopls/internal/test/integration/workspace/zero_config_test.go b/gopls/internal/test/integration/workspace/zero_config_test.go index 93c24886c9e..57498831a7d 100644 --- a/gopls/internal/test/integration/workspace/zero_config_test.go +++ b/gopls/internal/test/integration/workspace/zero_config_test.go @@ -5,6 +5,7 @@ package workspace import ( + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -268,3 +269,58 @@ package b } }) } + +func TestVendorExcluded(t *testing.T) { + // Test that we don't create Views for vendored modules. + // + // We construct the vendor directory manually here, as `go mod vendor` will + // omit the go.mod file. This synthesizes the setup of Kubernetes, where the + // entire module is vendored through a symlinked directory. + const src = ` +-- go.mod -- +module example.com/a + +go 1.18 + +require other.com/b v1.0.0 + +-- a.go -- +package a +import "other.com/b" +var _ b.B + +-- vendor/modules.txt -- +# other.com/b v1.0.0 +## explicit; go 1.14 +other.com/b + +-- vendor/other.com/b/go.mod -- +module other.com/b +go 1.14 + +-- vendor/other.com/b/b.go -- +package b +type B int + +func _() { + var V int // unused +} +` + WithOptions( + Modes(Default), + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("a.go") + env.AfterChange(NoDiagnostics()) + loc := env.GoToDefinition(env.RegexpSearch("a.go", `b\.(B)`)) + if !strings.Contains(string(loc.URI), "/vendor/") { + t.Fatalf("Definition(b.B) = %v, want vendored location", loc.URI) + } + env.AfterChange( + Diagnostics(env.AtRegexp("vendor/other.com/b/b.go", "V"), WithMessage("not used")), + ) + + if views := env.Views(); len(views) != 1 { + t.Errorf("After opening /vendor/, got %d views, want 1. Views:\n%v", len(views), views) + } + }) +} diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index 1652eec7282..c32b81ac6bc 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -230,19 +230,14 @@ func Test(t *testing.T) { if err := os.WriteFile(filename, formatted, 0644); err != nil { t.Error(err) } - } else { - // On go 1.19 and later, verify that the testdata has not changed. - // - // On earlier Go versions, the golden test data varies due to different - // markdown escaping. + } else if !t.Failed() { + // Verify that the testdata has not changed. // // Only check this if the test hasn't already failed, otherwise we'd // report duplicate mismatches of golden data. - if testenv.Go1Point() >= 19 && !t.Failed() { - // Otherwise, verify that formatted content matches. - if diff := compare.NamedText("formatted", "on-disk", string(formatted), string(test.content)); diff != "" { - t.Errorf("formatted test does not match on-disk content:\n%s", diff) - } + // Otherwise, verify that formatted content matches. + if diff := compare.NamedText("formatted", "on-disk", string(formatted), string(test.content)); diff != "" { + t.Errorf("formatted test does not match on-disk content:\n%s", diff) } } }) diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt new file mode 100644 index 00000000000..2c1b19e130c --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt @@ -0,0 +1,105 @@ +This test checks the behavior of the 'fill switch' code action. +See fill_switch_resolve.txt for same test with resolve support. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/fillswitch + +go 1.18 + +-- data/data.go -- +package data + +type TypeB int + +const ( + TypeBOne TypeB = iota + TypeBTwo + TypeBThree +) + +-- a.go -- +package fillswitch + +import ( + "golang.org/lsptests/fillswitch/data" +) + +type typeA int + +const ( + typeAOne typeA = iota + typeATwo + typeAThree +) + +type notification interface { + isNotification() +} + +type notificationOne struct{} + +func (notificationOne) isNotification() {} + +type notificationTwo struct{} + +func (notificationTwo) isNotification() {} + +func doSwitch() { + var b data.TypeB + switch b { + case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite", a1) + } + + var a typeA + switch a { + case typeAThree: //@codeactionedit(":", "refactor.rewrite", a2) + } + + var n notification + switch n.(type) { //@codeactionedit("{", "refactor.rewrite", a3) + } + + switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite", a4) + } + + var s struct { + a typeA + } + + switch s.a { + case typeAThree: //@codeactionedit(":", "refactor.rewrite", a5) + } +} +-- @a1/a.go -- +@@ -31 +31,4 @@ ++ case data.TypeBThree: ++ case data.TypeBTwo: ++ default: ++ panic(fmt.Sprintf("unexpected data.TypeB: %#v", b)) +-- @a2/a.go -- +@@ -36 +36,4 @@ ++ case typeAOne: ++ case typeATwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", a)) +-- @a3/a.go -- +@@ -40 +40,4 @@ ++ case notificationOne: ++ case notificationTwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", n)) +-- @a4/a.go -- +@@ -43 +43,4 @@ ++ case notificationOne: ++ case notificationTwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", nt)) +-- @a5/a.go -- +@@ -51 +51,4 @@ ++ case typeAOne: ++ case typeATwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", s.a)) diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt new file mode 100644 index 00000000000..504acd6043e --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt @@ -0,0 +1,116 @@ +This test checks the behavior of the 'fill switch' code action, with resolve support. +See fill_switch.txt for same test without resolve support. + +-- capabilities.json -- +{ + "textDocument": { + "codeAction": { + "dataSupport": true, + "resolveSupport": { + "properties": ["edit"] + } + } + } +} +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/fillswitch + +go 1.18 + +-- data/data.go -- +package data + +type TypeB int + +const ( + TypeBOne TypeB = iota + TypeBTwo + TypeBThree +) + +-- a.go -- +package fillswitch + +import ( + "golang.org/lsptests/fillswitch/data" +) + +type typeA int + +const ( + typeAOne typeA = iota + typeATwo + typeAThree +) + +type notification interface { + isNotification() +} + +type notificationOne struct{} + +func (notificationOne) isNotification() {} + +type notificationTwo struct{} + +func (notificationTwo) isNotification() {} + +func doSwitch() { + var b data.TypeB + switch b { + case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite", a1) + } + + var a typeA + switch a { + case typeAThree: //@codeactionedit(":", "refactor.rewrite", a2) + } + + var n notification + switch n.(type) { //@codeactionedit("{", "refactor.rewrite", a3) + } + + switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite", a4) + } + + var s struct { + a typeA + } + + switch s.a { + case typeAThree: //@codeactionedit(":", "refactor.rewrite", a5) + } +} +-- @a1/a.go -- +@@ -31 +31,4 @@ ++ case data.TypeBThree: ++ case data.TypeBTwo: ++ default: ++ panic(fmt.Sprintf("unexpected data.TypeB: %#v", b)) +-- @a2/a.go -- +@@ -36 +36,4 @@ ++ case typeAOne: ++ case typeATwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", a)) +-- @a3/a.go -- +@@ -40 +40,4 @@ ++ case notificationOne: ++ case notificationTwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", n)) +-- @a4/a.go -- +@@ -43 +43,4 @@ ++ case notificationOne: ++ case notificationTwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", nt)) +-- @a5/a.go -- +@@ -51 +51,4 @@ ++ case typeAOne: ++ case typeATwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", s.a)) diff --git a/gopls/internal/test/marker/testdata/codeaction/grouplines.txt b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt new file mode 100644 index 00000000000..8d1134c5d6c --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt @@ -0,0 +1,206 @@ +This test exercises the refactoring of putting arguments, return values, and composite literal elements into a +single line. + +-- go.mod -- +module unused.mod + +go 1.18 + +-- func_arg/func_arg.go -- +package func_arg + +func A( + a string, + b, c int64, + x int /*@codeaction("x", "x", "refactor.rewrite", func_arg)*/, + y int, +) (r1 string, r2, r3 int64, r4 int, r5 int) { + return a, b, c, x, y +} + +-- @func_arg/func_arg/func_arg.go -- +package func_arg + +func A(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite", func_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { + return a, b, c, x, y +} + +-- func_ret/func_ret.go -- +package func_ret + +func A(a string, b, c int64, x int, y int) ( + r1 string /*@codeaction("r1", "r1", "refactor.rewrite", func_ret)*/, + r2, r3 int64, + r4 int, + r5 int, +) { + return a, b, c, x, y +} + +-- @func_ret/func_ret/func_ret.go -- +package func_ret + +func A(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite", func_ret)*/, r2, r3 int64, r4 int, r5 int) { + return a, b, c, x, y +} + +-- functype_arg/functype_arg.go -- +package functype_arg + +type A func( + a string, + b, c int64, + x int /*@codeaction("x", "x", "refactor.rewrite", functype_arg)*/, + y int, +) (r1 string, r2, r3 int64, r4 int, r5 int) + +-- @functype_arg/functype_arg/functype_arg.go -- +package functype_arg + +type A func(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite", functype_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) + +-- functype_ret/functype_ret.go -- +package functype_ret + +type A func(a string, b, c int64, x int, y int) ( + r1 string /*@codeaction("r1", "r1", "refactor.rewrite", functype_ret)*/, + r2, r3 int64, + r4 int, + r5 int, +) + +-- @functype_ret/functype_ret/functype_ret.go -- +package functype_ret + +type A func(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite", functype_ret)*/, r2, r3 int64, r4 int, r5 int) + +-- func_call/func_call.go -- +package func_call + +import "fmt" + +func a() { + fmt.Println( + 1 /*@codeaction("1", "1", "refactor.rewrite", func_call)*/, + 2, + 3, + fmt.Sprintf("hello %d", 4), + ) +} + +-- @func_call/func_call/func_call.go -- +package func_call + +import "fmt" + +func a() { + fmt.Println(1 /*@codeaction("1", "1", "refactor.rewrite", func_call)*/, 2, 3, fmt.Sprintf("hello %d", 4)) +} + +-- indent/indent.go -- +package indent + +import "fmt" + +func a() { + fmt.Println( + 1, + 2, + 3, + fmt.Sprintf( + "hello %d" /*@codeaction("hello", "hello", "refactor.rewrite", indent, "Join parameters into one line")*/, + 4, + )) +} + +-- @indent/indent/indent.go -- +package indent + +import "fmt" + +func a() { + fmt.Println( + 1, + 2, + 3, + fmt.Sprintf("hello %d" /*@codeaction("hello", "hello", "refactor.rewrite", indent, "Join parameters into one line")*/, 4)) +} + +-- structelts/structelts.go -- +package structelts + +type A struct{ + a int + b int +} + +func a() { + _ = A{ + a: 1, + b: 2 /*@codeaction("b", "b", "refactor.rewrite", structelts)*/, + } +} + +-- @structelts/structelts/structelts.go -- +package structelts + +type A struct{ + a int + b int +} + +func a() { + _ = A{a: 1, b: 2 /*@codeaction("b", "b", "refactor.rewrite", structelts)*/} +} + +-- sliceelts/sliceelts.go -- +package sliceelts + +func a() { + _ = []int{ + 1 /*@codeaction("1", "1", "refactor.rewrite", sliceelts)*/, + 2, + } +} + +-- @sliceelts/sliceelts/sliceelts.go -- +package sliceelts + +func a() { + _ = []int{1 /*@codeaction("1", "1", "refactor.rewrite", sliceelts)*/, 2} +} + +-- mapelts/mapelts.go -- +package mapelts + +func a() { + _ = map[string]int{ + "a": 1 /*@codeaction("1", "1", "refactor.rewrite", mapelts)*/, + "b": 2, + } +} + +-- @mapelts/mapelts/mapelts.go -- +package mapelts + +func a() { + _ = map[string]int{"a": 1 /*@codeaction("1", "1", "refactor.rewrite", mapelts)*/, "b": 2} +} + +-- starcomment/starcomment.go -- +package starcomment + +func A( + /*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite", starcomment)*/, + /*4*/ y /*5*/ int /*6*/, +) (string, int) { + return x, y +} + +-- @starcomment/starcomment/starcomment.go -- +package starcomment + +func A(/*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite", starcomment)*/, /*4*/ y /*5*/ int /*6*/) (string, int) { + return x, y +} + diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt index 17a058f2b82..25ec6ae1d96 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt @@ -99,7 +99,7 @@ func _() { -- field/field.go -- package field -func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field) +func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") } func _() { @@ -108,7 +108,7 @@ func _() { -- @field/field/field.go -- package field -func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field) +func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") } func _() { @@ -162,7 +162,7 @@ func i() []any -- ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2) +func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") } func _() { @@ -176,7 +176,7 @@ func h() (int, int) -- @ellipsis2/ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2) +func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") } func _() { diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt index 1c04aa047cf..c67e8a5d039 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt @@ -110,7 +110,7 @@ func _() { -- field/field.go -- package field -func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field) +func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") } func _() { @@ -119,7 +119,7 @@ func _() { -- @field/field/field.go -- package field -func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field) +func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") } func _() { @@ -173,7 +173,7 @@ func i() []any -- ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2) +func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") } func _() { @@ -187,7 +187,7 @@ func h() (int, int) -- @ellipsis2/ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2) +func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") } func _() { diff --git a/gopls/internal/test/marker/testdata/codeaction/splitlines.txt b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt new file mode 100644 index 00000000000..a02e39505e5 --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt @@ -0,0 +1,223 @@ +This test exercises the refactoring of putting arguments, return values, and composite literal elements +into separate lines. + +-- go.mod -- +module unused.mod + +go 1.18 + +-- func_arg/func_arg.go -- +package func_arg + +func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite", func_arg) + return a, b, c, x, y +} + +-- @func_arg/func_arg/func_arg.go -- +package func_arg + +func A( + a string, + b, c int64, + x int, + y int, +) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite", func_arg) + return a, b, c, x, y +} + +-- func_ret/func_ret.go -- +package func_ret + +func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("r1", "r1", "refactor.rewrite", func_ret) + return a, b, c, x, y +} + +-- @func_ret/func_ret/func_ret.go -- +package func_ret + +func A(a string, b, c int64, x int, y int) ( + r1 string, + r2, r3 int64, + r4 int, + r5 int, +) { //@codeaction("r1", "r1", "refactor.rewrite", func_ret) + return a, b, c, x, y +} + +-- functype_arg/functype_arg.go -- +package functype_arg + +type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite", functype_arg) + +-- @functype_arg/functype_arg/functype_arg.go -- +package functype_arg + +type A func( + a string, + b, c int64, + x int, + y int, +) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite", functype_arg) + +-- functype_ret/functype_ret.go -- +package functype_ret + +type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("r1", "r1", "refactor.rewrite", functype_ret) + +-- @functype_ret/functype_ret/functype_ret.go -- +package functype_ret + +type A func(a string, b, c int64, x int, y int) ( + r1 string, + r2, r3 int64, + r4 int, + r5 int, +) //@codeaction("r1", "r1", "refactor.rewrite", functype_ret) + +-- func_call/func_call.go -- +package func_call + +import "fmt" + +func a() { + fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite", func_call) +} + +-- @func_call/func_call/func_call.go -- +package func_call + +import "fmt" + +func a() { + fmt.Println( + 1, + 2, + 3, + fmt.Sprintf("hello %d", 4), + ) //@codeaction("1", "1", "refactor.rewrite", func_call) +} + +-- indent/indent.go -- +package indent + +import "fmt" + +func a() { + fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("hello", "hello", "refactor.rewrite", indent, "Split parameters into separate lines") +} + +-- @indent/indent/indent.go -- +package indent + +import "fmt" + +func a() { + fmt.Println(1, 2, 3, fmt.Sprintf( + "hello %d", + 4, + )) //@codeaction("hello", "hello", "refactor.rewrite", indent, "Split parameters into separate lines") +} + +-- indent2/indent2.go -- +package indent2 + +import "fmt" + +func a() { + fmt. + Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite", indent2, "Split parameters into separate lines") +} + +-- @indent2/indent2/indent2.go -- +package indent2 + +import "fmt" + +func a() { + fmt. + Println( + 1, + 2, + 3, + fmt.Sprintf("hello %d", 4), + ) //@codeaction("1", "1", "refactor.rewrite", indent2, "Split parameters into separate lines") +} + +-- structelts/structelts.go -- +package structelts + +type A struct{ + a int + b int +} + +func a() { + _ = A{a: 1, b: 2} //@codeaction("b", "b", "refactor.rewrite", structelts) +} + +-- @structelts/structelts/structelts.go -- +package structelts + +type A struct{ + a int + b int +} + +func a() { + _ = A{ + a: 1, + b: 2, + } //@codeaction("b", "b", "refactor.rewrite", structelts) +} + +-- sliceelts/sliceelts.go -- +package sliceelts + +func a() { + _ = []int{1, 2} //@codeaction("1", "1", "refactor.rewrite", sliceelts) +} + +-- @sliceelts/sliceelts/sliceelts.go -- +package sliceelts + +func a() { + _ = []int{ + 1, + 2, + } //@codeaction("1", "1", "refactor.rewrite", sliceelts) +} + +-- mapelts/mapelts.go -- +package mapelts + +func a() { + _ = map[string]int{"a": 1, "b": 2} //@codeaction("1", "1", "refactor.rewrite", mapelts) +} + +-- @mapelts/mapelts/mapelts.go -- +package mapelts + +func a() { + _ = map[string]int{ + "a": 1, + "b": 2, + } //@codeaction("1", "1", "refactor.rewrite", mapelts) +} + +-- starcomment/starcomment.go -- +package starcomment + +func A(/*1*/ x /*2*/ string /*3*/, /*4*/ y /*5*/ int /*6*/) (string, int) { //@codeaction("x", "x", "refactor.rewrite", starcomment) + return x, y +} + +-- @starcomment/starcomment/starcomment.go -- +package starcomment + +func A( + /*1*/ x /*2*/ string /*3*/, + /*4*/ y /*5*/ int /*6*/, +) (string, int) { //@codeaction("x", "x", "refactor.rewrite", starcomment) + return x, y +} + diff --git a/gopls/internal/test/marker/testdata/definition/embed.txt b/gopls/internal/test/marker/testdata/definition/embed.txt index 9842c37d047..bd503869f8b 100644 --- a/gopls/internal/test/marker/testdata/definition/embed.txt +++ b/gopls/internal/test/marker/testdata/definition/embed.txt @@ -1,10 +1,5 @@ This test checks definition and hover operations over embedded fields and methods. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/definition/import.txt b/gopls/internal/test/marker/testdata/definition/import.txt index c0e5d76c00f..2ae95a8c29b 100644 --- a/gopls/internal/test/marker/testdata/definition/import.txt +++ b/gopls/internal/test/marker/testdata/definition/import.txt @@ -1,10 +1,5 @@ This test checks definition and hover over imports. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/definition/misc.txt b/gopls/internal/test/marker/testdata/definition/misc.txt index c7147a625be..82c15b5d2ba 100644 --- a/gopls/internal/test/marker/testdata/definition/misc.txt +++ b/gopls/internal/test/marker/testdata/definition/misc.txt @@ -1,10 +1,5 @@ This test exercises miscellaneous definition and hover requests. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/diagnostics/initcycle.txt b/gopls/internal/test/marker/testdata/diagnostics/initcycle.txt new file mode 100644 index 00000000000..f306bccf52c --- /dev/null +++ b/gopls/internal/test/marker/testdata/diagnostics/initcycle.txt @@ -0,0 +1,17 @@ +This test verifies that gopls spreads initialization cycle errors across +multiple declarations. + +We set -ignore_extra_diags due to golang/go#65877: gopls produces redundant +diagnostics for initialization cycles. + +-- flags -- +-ignore_extra_diags + +-- p.go -- +package p + +var X = Y //@diag("X", re"initialization cycle") + +var Y = Z //@diag("Y", re"initialization cycle") + +var Z = X //@diag("Z", re"initialization cycle") diff --git a/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt b/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt index b644d453164..6b8d6ce0ad2 100644 --- a/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt +++ b/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt @@ -1,10 +1,6 @@ This test exercises a crash due to treatment of "comparable" in methodset calculation (golang/go#60544). --min_go is 1.19 as the error message changed at this Go version. --- flags -- --min_go=go1.19 - -- main.go -- package main diff --git a/gopls/internal/test/marker/testdata/diagnostics/osarch_suffix.txt b/gopls/internal/test/marker/testdata/diagnostics/osarch_suffix.txt new file mode 100644 index 00000000000..95336085b2f --- /dev/null +++ b/gopls/internal/test/marker/testdata/diagnostics/osarch_suffix.txt @@ -0,0 +1,46 @@ +This test verifies that we add an [os,arch] suffix to each diagnostic +that doesn't appear in the default build (=runtime.{GOOS,GOARCH}). + +See golang/go#65496. + +The two p/*.go files below are written to trigger the same diagnostic +(range, message, source, etc) but varying only by URI. + +In the q test, a single location in the common code q.go has two +diagnostics, one of which is tagged. + +This test would fail on openbsd/mips64 because it will be +the same as the default build, so we skip that platform. + +-- flags -- +-skip_goos=openbsd + +-- go.mod -- +module example.com + +-- p/p.go -- +package p + +var _ fmt.Stringer //@diag("fmt", re"unde.*: fmt$") + +-- p/p_openbsd_mips64.go -- +package p + +var _ fmt.Stringer //@diag("fmt", re"unde.*: fmt \\[openbsd,mips64\\]") + +-- q/q_default.go -- +//+build !openbsd && !mips64 + +package q + +func f(int) int + +-- q/q_openbsd_mips64.go -- +package q + +func f(string) int + +-- q/q.go -- +package q + +var _ = f() //@ diag(")", re`.*want \(string\) \[openbsd,mips64\]`), diag(")", re`.*want \(int\)$`) diff --git a/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt b/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt index a60cdeebeeb..b14f4dfabd0 100644 --- a/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt +++ b/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt @@ -1,9 +1,6 @@ This test verifies that analyzers without RunDespiteErrors are not executed on a package containing type errors (see issue #54762). -We require go1.18 because the range of the `1 + ""` go/types error -changed then, and the new @diag marker is quite particular. - -- go.mod -- module example.com go 1.12 diff --git a/gopls/internal/test/marker/testdata/highlight/controlflow.txt b/gopls/internal/test/marker/testdata/highlight/controlflow.txt new file mode 100644 index 00000000000..25cc9394a47 --- /dev/null +++ b/gopls/internal/test/marker/testdata/highlight/controlflow.txt @@ -0,0 +1,71 @@ +This test verifies document highlighting for control flow. + +-- go.mod -- +module mod.com + +go 1.18 + +-- p.go -- +package p + +-- issue60589.go -- +package p + +// This test verifies that control flow lighlighting correctly +// accounts for multi-name result parameters. +// In golang/go#60589, it did not. + +func _() (foo int, bar, baz string) { //@ loc(func, "func"), loc(foo, "foo"), loc(fooint, "foo int"), loc(int, "int"), loc(bar, "bar"), loc(beforebaz, " baz"), loc(baz, "baz"), loc(barbazstring, "bar, baz string"), loc(beforestring, re`() string`), loc(string, "string") + return 0, "1", "2" //@ loc(return, `return 0, "1", "2"`), loc(l0, "0"), loc(l1, `"1"`), loc(l2, `"2"`) +} + +// Assertions, expressed here to avoid clutter above. +// Note that when the cursor is over the field type, there is some +// (likely harmless) redundancy. + +//@ highlight(func, func, return) +//@ highlight(foo, foo, l0) +//@ highlight(int, fooint, int, l0) +//@ highlight(bar, bar, l1) +//@ highlight(beforebaz) +//@ highlight(baz, baz, l2) +//@ highlight(beforestring, baz, l2) +//@ highlight(string, barbazstring, string, l1, l2) +//@ highlight(l0, foo, l0) +//@ highlight(l1, bar, l1) +//@ highlight(l2, baz, l2) + +// Check that duplicate result names do not cause +// inaccurate highlighting. + +func _() (x, x int32) { //@ loc(x1, re`\((x)`), loc(x2, re`(x) int`), diag(x1, re"redeclared"), diag(x2, re"redeclared") + return 1, 2 //@ loc(one, "1"), loc(two, "2") +} + +//@ highlight(one, one, x1) +//@ highlight(two, two, x2) +//@ highlight(x1, x1, one) +//@ highlight(x2, x2, two) + +-- issue65516.go -- +package p + +// This test checks that gopls doesn't crash while highlighting +// functions with no body (golang/go#65516). + +func Foo() (int, string) //@highlight("int", "int"), highlight("func", "func") + +-- issue65952.go -- +package p + +// This test checks that gopls doesn't crash while highlighting +// return values in functions with no results. + +func _() { + return 0 //@highlight("0", "0"), diag("0", re"too many return") +} + +func _() () { + // TODO(golang/go#65966): fix the triplicate diagnostics here. + return 0 //@highlight("0", "0"), diag("0", re"too many return"), diag("0", re"too many return"), diag("0", re"too many return") +} diff --git a/gopls/internal/test/marker/testdata/highlight/issue60589.txt b/gopls/internal/test/marker/testdata/highlight/issue60589.txt deleted file mode 100644 index afa4335a9c6..00000000000 --- a/gopls/internal/test/marker/testdata/highlight/issue60589.txt +++ /dev/null @@ -1,30 +0,0 @@ -This test verifies that control flow lighlighting correctly accounts for -multi-name result parameters. In golang/go#60589, it did not. - --- go.mod -- -module mod.com - -go 1.18 - --- p.go -- -package p - -func _() (foo int, bar, baz string) { //@ loc(func, "func"), loc(foo, "foo"), loc(fooint, "foo int"), loc(int, "int"), loc(bar, "bar"), loc(beforebaz, " baz"), loc(baz, "baz"), loc(barbazstring, "bar, baz string"), loc(beforestring, re`() string`), loc(string, "string") - return 0, "1", "2" //@ loc(return, `return 0, "1", "2"`), loc(l0, "0"), loc(l1, `"1"`), loc(l2, `"2"`) -} - -// Assertions, expressed here to avoid clutter above. -// Note that when the cursor is over the field type, there is some -// (likely harmless) redundancy. - -//@ highlight(func, func, return) -//@ highlight(foo, foo, l0) -//@ highlight(int, fooint, int, l0) -//@ highlight(bar, bar, l1) -//@ highlight(beforebaz) -//@ highlight(baz, baz, l2) -//@ highlight(beforestring, baz, l2) -//@ highlight(string, barbazstring, string, l1, l2) -//@ highlight(l0, foo, l0) -//@ highlight(l1, bar, l1) -//@ highlight(l2, baz, l2) diff --git a/gopls/internal/test/marker/testdata/highlight/issue65516.txt b/gopls/internal/test/marker/testdata/highlight/issue65516.txt deleted file mode 100644 index 3fc3be27416..00000000000 --- a/gopls/internal/test/marker/testdata/highlight/issue65516.txt +++ /dev/null @@ -1,7 +0,0 @@ -This test checks that gopls doesn't crash while highlighting functions with no -body (golang/go#65516). - --- p.go -- -package p - -func Foo() (int, string) //@highlight("int", "int"), highlight("func", "func") diff --git a/gopls/internal/test/marker/testdata/highlight/switchbreak.txt b/gopls/internal/test/marker/testdata/highlight/switchbreak.txt new file mode 100644 index 00000000000..b486ad1d80d --- /dev/null +++ b/gopls/internal/test/marker/testdata/highlight/switchbreak.txt @@ -0,0 +1,21 @@ +This is a regression test for issue 65752: a break in a switch should +highlight the switch, not the enclosing loop. + +-- a.go -- +package a + +func _(x any) { + for { + // type switch + switch x.(type) { //@loc(tswitch, "switch") + default: + break //@highlight("break", tswitch, "break") + } + + // value switch + switch { //@loc(vswitch, "switch") + default: + break //@highlight("break", vswitch, "break") + } + } +} diff --git a/gopls/internal/test/marker/testdata/hover/basiclit.txt b/gopls/internal/test/marker/testdata/hover/basiclit.txt index 9269c289840..9c26b2a2f07 100644 --- a/gopls/internal/test/marker/testdata/hover/basiclit.txt +++ b/gopls/internal/test/marker/testdata/hover/basiclit.txt @@ -1,10 +1,5 @@ This test checks gopls behavior when hovering over basic literals. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- basiclit.go -- package basiclit diff --git a/gopls/internal/test/marker/testdata/hover/const.txt b/gopls/internal/test/marker/testdata/hover/const.txt index c5f783c1f5d..179ff155357 100644 --- a/gopls/internal/test/marker/testdata/hover/const.txt +++ b/gopls/internal/test/marker/testdata/hover/const.txt @@ -1,10 +1,5 @@ This test checks hovering over constants. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/hover/godef.txt b/gopls/internal/test/marker/testdata/hover/godef.txt index 2d82ab0debe..9b2e7ec2ce3 100644 --- a/gopls/internal/test/marker/testdata/hover/godef.txt +++ b/gopls/internal/test/marker/testdata/hover/godef.txt @@ -3,6 +3,9 @@ It tests various hover and definition requests. Requires go1.19+ for the new go/doc/comment package. +TODO(adonovan): figure out why this test also fails +without -min_go=go1.20. Or just wait... + -- flags -- -min_go=go1.19 diff --git a/gopls/internal/test/marker/testdata/hover/goprivate.txt b/gopls/internal/test/marker/testdata/hover/goprivate.txt index f5b71795392..7269ba19634 100644 --- a/gopls/internal/test/marker/testdata/hover/goprivate.txt +++ b/gopls/internal/test/marker/testdata/hover/goprivate.txt @@ -1,10 +1,5 @@ This test checks that links in hover obey GOPRIVATE. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- env -- GOPRIVATE=mod.com -- go.mod -- diff --git a/gopls/internal/test/marker/testdata/hover/hover.txt b/gopls/internal/test/marker/testdata/hover/hover.txt index 32a4c195663..b5e4a88434b 100644 --- a/gopls/internal/test/marker/testdata/hover/hover.txt +++ b/gopls/internal/test/marker/testdata/hover/hover.txt @@ -1,10 +1,5 @@ This test demonstrates some features of the new marker test runner. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- a.go -- package a diff --git a/gopls/internal/test/marker/testdata/hover/linkable.txt b/gopls/internal/test/marker/testdata/hover/linkable.txt index b77b9e8a4d9..7fdb2470cdf 100644 --- a/gopls/internal/test/marker/testdata/hover/linkable.txt +++ b/gopls/internal/test/marker/testdata/hover/linkable.txt @@ -4,11 +4,6 @@ identifiers. We should only produce links that work, meaning the object is reachable via the package's public API. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/hover/linkable_generics.txt b/gopls/internal/test/marker/testdata/hover/linkable_generics.txt index 1ea009e0318..0b7ade7965e 100644 --- a/gopls/internal/test/marker/testdata/hover/linkable_generics.txt +++ b/gopls/internal/test/marker/testdata/hover/linkable_generics.txt @@ -1,10 +1,5 @@ This file contains tests for documentation links to generic code in hover. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/hover/linkname.txt b/gopls/internal/test/marker/testdata/hover/linkname.txt index 829097ce307..8bb2eeb33cd 100644 --- a/gopls/internal/test/marker/testdata/hover/linkname.txt +++ b/gopls/internal/test/marker/testdata/hover/linkname.txt @@ -1,10 +1,5 @@ This test check hover on the 2nd argument in go:linkname directives. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/hover/std.txt b/gopls/internal/test/marker/testdata/hover/std.txt index af8f89fe65c..c0db135f6b1 100644 --- a/gopls/internal/test/marker/testdata/hover/std.txt +++ b/gopls/internal/test/marker/testdata/hover/std.txt @@ -7,11 +7,6 @@ synopsis does not. In the future we may need to limit this test to the latest Go version to avoid documentation churn. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- settings.json -- { "hoverKind": "SynopsisDocumentation" diff --git a/gopls/internal/test/marker/testdata/symbol/generic.txt b/gopls/internal/test/marker/testdata/symbol/generic.txt index 84cea99c1f7..1254851ad14 100644 --- a/gopls/internal/test/marker/testdata/symbol/generic.txt +++ b/gopls/internal/test/marker/testdata/symbol/generic.txt @@ -3,9 +3,6 @@ Basic tests of textDocument/documentSymbols with generics. -- symbol.go -- //@symbol(want) -//go:build go1.18 -// +build go1.18 - package main type T[P any] struct { diff --git a/gopls/internal/test/marker/testdata/token/range.txt b/gopls/internal/test/marker/testdata/token/range.txt index 23f08403470..2f98c043d8e 100644 --- a/gopls/internal/test/marker/testdata/token/range.txt +++ b/gopls/internal/test/marker/testdata/token/range.txt @@ -17,3 +17,13 @@ func F() { //@token("F", "function", "definition") _ = x //@token("x", "variable", "") _ = F //@token("F", "function", "") } + +func _() { + // A goto's label cannot be found by ascending the syntax tree. + goto loop //@ token("goto", "keyword", ""), token("loop", "label", "") + +loop: //@token("loop", "label", "definition") + for { + continue loop //@ token("continue", "keyword", ""), token("loop", "label", "") + } +} diff --git a/gopls/internal/util/bug/bug.go b/gopls/internal/util/bug/bug.go index e04b753e315..dcd242d4856 100644 --- a/gopls/internal/util/bug/bug.go +++ b/gopls/internal/util/bug/bug.go @@ -19,7 +19,7 @@ import ( "sync" "time" - "golang.org/x/tools/gopls/internal/telemetry" + "golang.org/x/telemetry/counter" ) // PanicOnBugs controls whether to panic when bugs are reported. @@ -69,7 +69,7 @@ func Report(description string) { } // BugReportCount is a telemetry counter that tracks # of bug reports. -var BugReportCount = telemetry.NewStackCounter("gopls/bug", 16) +var BugReportCount = counter.NewStack("gopls/bug", 16) func report(description string) { _, file, line, ok := runtime.Caller(2) // all exported reporting functions call report directly diff --git a/gopls/internal/util/frob/frob.go b/gopls/internal/util/frob/frob.go index 0f3ede26773..cd385a9d692 100644 --- a/gopls/internal/util/frob/frob.go +++ b/gopls/internal/util/frob/frob.go @@ -93,7 +93,7 @@ func frobFor(t reflect.Type) *frob { case reflect.Array, reflect.Slice, - reflect.Ptr: // TODO(adonovan): after go1.18, use Pointer + reflect.Pointer: fr.addElem(fr.t.Elem()) case reflect.Map: @@ -214,7 +214,7 @@ func (fr *frob) encode(out *writer, v reflect.Value) { } } - case reflect.Ptr: // TODO(adonovan): after go1.18, use Pointer + case reflect.Pointer: if v.IsNil() { out.uint8(0) } else { @@ -341,7 +341,7 @@ func (fr *frob) decode(in *reader, addr reflect.Value) { } } - case reflect.Ptr: // TODO(adonovan): after go1.18, use Pointer + case reflect.Pointer: isNil := in.uint8() == 0 if !isNil { ptr := reflect.New(fr.elems[0].t) @@ -402,38 +402,7 @@ func (r *reader) bytes(n int) []byte { type writer struct{ data []byte } func (w *writer) uint8(v uint8) { w.data = append(w.data, v) } -func (w *writer) uint16(v uint16) { w.data = appendUint16(w.data, v) } -func (w *writer) uint32(v uint32) { w.data = appendUint32(w.data, v) } -func (w *writer) uint64(v uint64) { w.data = appendUint64(w.data, v) } +func (w *writer) uint16(v uint16) { w.data = le.AppendUint16(w.data, v) } +func (w *writer) uint32(v uint32) { w.data = le.AppendUint32(w.data, v) } +func (w *writer) uint64(v uint64) { w.data = le.AppendUint64(w.data, v) } func (w *writer) bytes(v []byte) { w.data = append(w.data, v...) } - -// TODO(adonovan): delete these as in go1.19 they are methods on LittleEndian: - -func appendUint16(b []byte, v uint16) []byte { - return append(b, - byte(v), - byte(v>>8), - ) -} - -func appendUint32(b []byte, v uint32) []byte { - return append(b, - byte(v), - byte(v>>8), - byte(v>>16), - byte(v>>24), - ) -} - -func appendUint64(b []byte, v uint64) []byte { - return append(b, - byte(v), - byte(v>>8), - byte(v>>16), - byte(v>>24), - byte(v>>32), - byte(v>>40), - byte(v>>48), - byte(v>>56), - ) -} diff --git a/gopls/internal/util/lru/lru_fuzz_test.go b/gopls/internal/util/lru/lru_fuzz_test.go index 1733511f35e..b82776b25ba 100644 --- a/gopls/internal/util/lru/lru_fuzz_test.go +++ b/gopls/internal/util/lru/lru_fuzz_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package lru_test import ( diff --git a/gopls/internal/util/slices/slices.go b/gopls/internal/util/slices/slices.go index 4c4fa4d0a79..938ae6c34c0 100644 --- a/gopls/internal/util/slices/slices.go +++ b/gopls/internal/util/slices/slices.go @@ -6,14 +6,14 @@ package slices // Clone returns a copy of the slice. // The elements are copied using assignment, so this is a shallow clone. -// TODO(rfindley): use go1.19 slices.Clone. +// TODO(rfindley): use go1.21 slices.Clone. func Clone[S ~[]E, E any](s S) S { // The s[:0:0] preserves nil in case it matters. return append(s[:0:0], s...) } // Contains reports whether x is present in slice. -// TODO(adonovan): use go1.19 slices.Contains. +// TODO(adonovan): use go1.21 slices.Contains. func Contains[S ~[]E, E comparable](slice S, x E) bool { for _, elem := range slice { if elem == x { @@ -25,7 +25,7 @@ func Contains[S ~[]E, E comparable](slice S, x E) bool { // IndexFunc returns the first index i satisfying f(s[i]), // or -1 if none do. -// TODO(adonovan): use go1.19 slices.IndexFunc. +// TODO(adonovan): use go1.21 slices.IndexFunc. func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { for i := range s { if f(s[i]) { @@ -37,7 +37,7 @@ func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { // ContainsFunc reports whether at least one // element e of s satisfies f(e). -// TODO(adonovan): use go1.19 slices.ContainsFunc. +// TODO(adonovan): use go1.21 slices.ContainsFunc. func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool { return IndexFunc(s, f) >= 0 } @@ -63,7 +63,7 @@ func Concat[S ~[]E, E any](slices ...S) S { // another n elements. After Grow(n), at least n elements can be appended // to the slice without another allocation. If n is negative or too large to // allocate the memory, Grow panics. -// TODO(rfindley): use go1.19 slices.Grow. +// TODO(rfindley): use go1.21 slices.Grow. func Grow[S ~[]E, E any](s S, n int) S { if n < 0 { panic("cannot be negative") diff --git a/gopls/internal/vulncheck/scan/command.go b/gopls/internal/vulncheck/scan/command.go index 1f53d8b55f3..4ef005010c9 100644 --- a/gopls/internal/vulncheck/scan/command.go +++ b/gopls/internal/vulncheck/scan/command.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package scan import ( diff --git a/gopls/internal/vulncheck/semver/semver.go b/gopls/internal/vulncheck/semver/semver.go index 67c4fe8a39e..ade710d0573 100644 --- a/gopls/internal/vulncheck/semver/semver.go +++ b/gopls/internal/vulncheck/semver/semver.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - // Package semver provides shared utilities for manipulating // Go semantic versions. package semver diff --git a/gopls/internal/vulncheck/semver/semver_test.go b/gopls/internal/vulncheck/semver/semver_test.go index 6daead6855b..8a462287fa4 100644 --- a/gopls/internal/vulncheck/semver/semver_test.go +++ b/gopls/internal/vulncheck/semver/semver_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package semver import ( diff --git a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/vulntest/db.go index 659d2f1fd10..e661b83bc71 100644 --- a/gopls/internal/vulncheck/vulntest/db.go +++ b/gopls/internal/vulncheck/vulntest/db.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - // Package vulntest provides helpers for vulncheck functionality testing. package vulntest diff --git a/gopls/internal/vulncheck/vulntest/db_test.go b/gopls/internal/vulncheck/vulntest/db_test.go index 22281249502..3c3407105ac 100644 --- a/gopls/internal/vulncheck/vulntest/db_test.go +++ b/gopls/internal/vulncheck/vulntest/db_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package vulntest import ( diff --git a/gopls/internal/vulncheck/vulntest/report.go b/gopls/internal/vulncheck/vulntest/report.go index cbfd0aeb8ff..b67986cf8c2 100644 --- a/gopls/internal/vulncheck/vulntest/report.go +++ b/gopls/internal/vulncheck/vulntest/report.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package vulntest import ( diff --git a/gopls/internal/vulncheck/vulntest/report_test.go b/gopls/internal/vulncheck/vulntest/report_test.go index 31f62aba838..b88633c2f1c 100644 --- a/gopls/internal/vulncheck/vulntest/report_test.go +++ b/gopls/internal/vulncheck/vulntest/report_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package vulntest import ( diff --git a/gopls/internal/vulncheck/vulntest/stdlib.go b/gopls/internal/vulncheck/vulntest/stdlib.go index 9bf4d4ef0d4..57194f71688 100644 --- a/gopls/internal/vulncheck/vulntest/stdlib.go +++ b/gopls/internal/vulncheck/vulntest/stdlib.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package vulntest import ( diff --git a/gopls/internal/vulncheck/vulntest/stdlib_test.go b/gopls/internal/vulncheck/vulntest/stdlib_test.go index 8f893f3ec42..7b212976350 100644 --- a/gopls/internal/vulncheck/vulntest/stdlib_test.go +++ b/gopls/internal/vulncheck/vulntest/stdlib_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package vulntest import "testing" diff --git a/gopls/main.go b/gopls/main.go index e3fb3861f68..9217b278b1a 100644 --- a/gopls/main.go +++ b/gopls/main.go @@ -17,9 +17,9 @@ import ( "context" "os" + "golang.org/x/telemetry" "golang.org/x/tools/gopls/internal/cmd" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/gopls/internal/telemetry" versionpkg "golang.org/x/tools/gopls/internal/version" "golang.org/x/tools/internal/tool" ) @@ -29,8 +29,7 @@ var version = "" // if set by the linker, overrides the gopls version func main() { versionpkg.VersionOverride = version - telemetry.CounterOpen() - telemetry.StartCrashMonitor() + telemetry.Start(telemetry.Config{ReportCrashes: true}) ctx := context.Background() tool.Main(ctx, cmd.New(hooks.Options), os.Args[1:]) } diff --git a/gopls/release/release.go b/gopls/release/release.go index 77b0aaf40d6..26ce5f7870a 100644 --- a/gopls/release/release.go +++ b/gopls/release/release.go @@ -14,17 +14,14 @@ package main import ( "flag" "fmt" - "go/types" "log" "os" "os/exec" "path/filepath" - "strconv" "strings" "golang.org/x/mod/modfile" "golang.org/x/mod/semver" - "golang.org/x/tools/go/packages" ) var versionFlag = flag.String("version", "", "version to tag") @@ -52,10 +49,6 @@ func main() { if filepath.Base(wd) != "gopls" { log.Fatalf("must run from the gopls module") } - // Confirm that they have updated the hardcoded version. - if err := validateHardcodedVersion(*versionFlag); err != nil { - log.Fatal(err) - } // Confirm that the versions in the go.mod file are correct. if err := validateGoModFile(wd); err != nil { log.Fatal(err) @@ -64,47 +57,6 @@ func main() { os.Exit(0) } -// validateHardcodedVersion reports whether the version hardcoded in the gopls -// binary is equivalent to the version being published. It reports an error if -// not. -func validateHardcodedVersion(version string) error { - const debugPkg = "golang.org/x/tools/gopls/internal/debug" - pkgs, err := packages.Load(&packages.Config{ - Mode: packages.NeedName | packages.NeedFiles | - packages.NeedCompiledGoFiles | packages.NeedImports | - packages.NeedTypes | packages.NeedTypesSizes, - }, debugPkg) - if err != nil { - return err - } - if len(pkgs) != 1 { - return fmt.Errorf("expected 1 package, got %v", len(pkgs)) - } - pkg := pkgs[0] - if len(pkg.Errors) > 0 { - return fmt.Errorf("failed to load %q: first error: %w", debugPkg, pkg.Errors[0]) - } - obj := pkg.Types.Scope().Lookup("Version") - c, ok := obj.(*types.Const) - if !ok { - return fmt.Errorf("no constant named Version") - } - hardcodedVersion, err := strconv.Unquote(c.Val().ExactString()) - if err != nil { - return err - } - if semver.Prerelease(hardcodedVersion) != "" { - return fmt.Errorf("unexpected pre-release for hardcoded version: %s", hardcodedVersion) - } - // Don't worry about pre-release tags and expect that there is no build - // suffix. - version = strings.TrimSuffix(version, semver.Prerelease(version)) - if hardcodedVersion != version { - return fmt.Errorf("expected version to be %s, got %s", *versionFlag, hardcodedVersion) - } - return nil -} - func validateGoModFile(goplsDir string) error { filename := filepath.Join(goplsDir, "go.mod") data, err := os.ReadFile(filename) diff --git a/internal/aliases/aliases_test.go b/internal/aliases/aliases_test.go index fc4efb2cb27..d2d3464e19a 100644 --- a/internal/aliases/aliases_test.go +++ b/internal/aliases/aliases_test.go @@ -9,7 +9,6 @@ import ( "go/parser" "go/token" "go/types" - "os" "testing" "golang.org/x/tools/internal/aliases" @@ -49,9 +48,7 @@ func TestNewAlias(t *testing.T) { for _, godebug := range []string{"", "gotypesalias=1"} { t.Run(godebug, func(t *testing.T) { - saved := os.Getenv("GODEBUG") - defer os.Setenv("GODEBUG", saved) - os.Setenv("GODEBUG", godebug) // non parallel. + t.Setenv("GODEBUG", godebug) A := aliases.NewAlias(token.NoPos, pkg, "A", tv.Type) if got, want := A.Name(), "A"; got != want { diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index b24a0fba9e7..c3022a28625 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -13,6 +13,8 @@ import ( "go/token" "go/types" "strconv" + + "golang.org/x/tools/internal/aliases" ) func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos { @@ -28,7 +30,10 @@ func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos } func ZeroValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { - under := typ + // TODO(adonovan): think about generics, and also generic aliases. + under := aliases.Unalias(typ) + // Don't call Underlying unconditionally: although it removed + // Named and Alias, it also removes TypeParam. if n, ok := typ.(*types.Named); ok { under = n.Underlying() } diff --git a/internal/apidiff/compatibility.go b/internal/apidiff/compatibility.go index 0d2d2b34575..64cad5337be 100644 --- a/internal/apidiff/compatibility.go +++ b/internal/apidiff/compatibility.go @@ -10,6 +10,7 @@ import ( "reflect" "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) func (d *differ) checkCompatible(otn *types.TypeName, old, new types.Type) { @@ -305,7 +306,8 @@ func (d *differ) checkMethodSet(otn *types.TypeName, oldt, newt types.Type, addc // T and one for the embedded type U. We want both messages to appear, // but the messageSet dedup logic will allow only one message for a given // object. So use the part string to distinguish them. - if receiverNamedType(oldMethod).Obj() != otn { + recv := oldMethod.Type().(*types.Signature).Recv() + if _, named := typesinternal.ReceiverNamed(recv); named.Obj() != otn { part = fmt.Sprintf(", method set of %s", msname) } d.incompatible(oldMethod, part, "removed") @@ -336,11 +338,11 @@ func (d *differ) checkMethodSet(otn *types.TypeName, oldt, newt types.Type, addc } // exportedMethods collects all the exported methods of type's method set. -func exportedMethods(t types.Type) map[string]types.Object { - m := map[string]types.Object{} +func exportedMethods(t types.Type) map[string]*types.Func { + m := make(map[string]*types.Func) ms := types.NewMethodSet(t) for i := 0; i < ms.Len(); i++ { - obj := ms.At(i).Obj() + obj := ms.At(i).Obj().(*types.Func) if obj.Exported() { m[obj.Name()] = obj } @@ -348,22 +350,7 @@ func exportedMethods(t types.Type) map[string]types.Object { return m } -func receiverType(method types.Object) types.Type { - return method.Type().(*types.Signature).Recv().Type() -} - -func receiverNamedType(method types.Object) *types.Named { - switch t := aliases.Unalias(receiverType(method)).(type) { - case *types.Pointer: - return aliases.Unalias(t.Elem()).(*types.Named) - case *types.Named: - return t - default: - panic("unreachable") - } -} - -func hasPointerReceiver(method types.Object) bool { - _, ok := aliases.Unalias(receiverType(method)).(*types.Pointer) - return ok +func hasPointerReceiver(method *types.Func) bool { + isptr, _ := typesinternal.ReceiverNamed(method.Type().(*types.Signature).Recv()) + return isptr } diff --git a/internal/compat/appendf.go b/internal/compat/appendf.go deleted file mode 100644 index 069d5171704..00000000000 --- a/internal/compat/appendf.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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. - -//go:build go1.19 - -package compat - -import "fmt" - -func Appendf(b []byte, format string, a ...interface{}) []byte { - return fmt.Appendf(b, format, a...) -} diff --git a/internal/compat/appendf_118.go b/internal/compat/appendf_118.go deleted file mode 100644 index 29af353cdaf..00000000000 --- a/internal/compat/appendf_118.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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. - -//go:build !go1.19 - -package compat - -import "fmt" - -func Appendf(b []byte, format string, a ...interface{}) []byte { - return append(b, fmt.Sprintf(format, a...)...) -} diff --git a/internal/compat/doc.go b/internal/compat/doc.go deleted file mode 100644 index 59c667a37a2..00000000000 --- a/internal/compat/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// 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. - -// The compat package implements API shims for backward compatibility at older -// Go versions. -package compat diff --git a/internal/diff/difftest/difftest_test.go b/internal/diff/difftest/difftest_test.go index c64a0fa0c9f..dcd92d7dfeb 100644 --- a/internal/diff/difftest/difftest_test.go +++ b/internal/diff/difftest/difftest_test.go @@ -64,11 +64,17 @@ func getDiffOutput(a, b string) (string, error) { } cmd := exec.Command("diff", "-u", fileA.Name(), fileB.Name()) cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8") - out, err := cmd.CombinedOutput() + out, err := cmd.Output() if err != nil { - if _, ok := err.(*exec.ExitError); !ok { - return "", fmt.Errorf("failed to run diff -u %v %v: %v\n%v", fileA.Name(), fileB.Name(), err, string(out)) + exit, ok := err.(*exec.ExitError) + if !ok { + return "", fmt.Errorf("can't exec %s: %v", cmd, err) } + if len(out) == 0 { + // Nonzero exit with no output: terminated by signal? + return "", fmt.Errorf("%s failed: %v; stderr:\n%s", cmd, err, exit.Stderr) + } + // nonzero exit + output => files differ } diff := string(out) if len(diff) <= 0 { diff --git a/internal/facts/facts_test.go b/internal/facts/facts_test.go index 56eb599cfd9..daebea2ff59 100644 --- a/internal/facts/facts_test.go +++ b/internal/facts/facts_test.go @@ -18,6 +18,7 @@ import ( "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/facts" "golang.org/x/tools/internal/testenv" ) @@ -360,7 +361,7 @@ func find(p *types.Package, expr string) types.Object { if err != nil { return nil } - if n, ok := tv.Type.(*types.Named); ok { + if n, ok := aliases.Unalias(tv.Type).(*types.Named); ok { return n.Obj() } return nil diff --git a/internal/facts/imports.go b/internal/facts/imports.go index 1fe63ca6b51..9f706cd954f 100644 --- a/internal/facts/imports.go +++ b/internal/facts/imports.go @@ -6,6 +6,8 @@ package facts import ( "go/types" + + "golang.org/x/tools/internal/aliases" ) // importMap computes the import map for a package by traversing the @@ -45,6 +47,8 @@ func importMap(imports []*types.Package) map[string]*types.Package { addType = func(T types.Type) { switch T := T.(type) { + case *aliases.Alias: + addType(aliases.Unalias(T)) case *types.Basic: // nop case *types.Named: diff --git a/internal/gcimporter/bexport_test.go b/internal/gcimporter/bexport_test.go index 72fa8a2a31e..1a2c8e8dd0a 100644 --- a/internal/gcimporter/bexport_test.go +++ b/internal/gcimporter/bexport_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/gcimporter" ) @@ -30,6 +31,8 @@ func fileLine(fset *token.FileSet, obj types.Object) string { } func equalType(x, y types.Type) error { + x = aliases.Unalias(x) + y = aliases.Unalias(y) if reflect.TypeOf(x) != reflect.TypeOf(y) { return fmt.Errorf("unequal kinds: %T vs %T", x, y) } diff --git a/internal/gcimporter/gcimporter.go b/internal/gcimporter/gcimporter.go index 2d078ccb19c..39df91124a4 100644 --- a/internal/gcimporter/gcimporter.go +++ b/internal/gcimporter/gcimporter.go @@ -259,13 +259,6 @@ func Import(packages map[string]*types.Package, path, srcDir string, lookup func return } -func deref(typ types.Type) types.Type { - if p, _ := typ.(*types.Pointer); p != nil { - return p.Elem() - } - return typ -} - type byPath []*types.Package func (a byPath) Len() int { return len(a) } diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index 3af088b23d8..95cc36c4d96 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -11,7 +11,6 @@ import ( "bytes" "fmt" "go/ast" - "go/build" "go/constant" goimporter "go/importer" goparser "go/parser" @@ -28,6 +27,7 @@ import ( "testing" "time" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/goroot" "golang.org/x/tools/internal/testenv" ) @@ -84,8 +84,7 @@ func compilePkg(t *testing.T, dirname, filename, outdirname string, packagefiles importreldir := strings.ReplaceAll(outdirname, string(os.PathSeparator), "/") cmd := exec.Command("go", "tool", "compile", "-p", pkg, "-D", importreldir, "-importcfg", importcfgfile, "-o", outname, filename) cmd.Dir = dirname - out, err := cmd.CombinedOutput() - if err != nil { + if out, err := cmd.CombinedOutput(); err != nil { t.Logf("%s", out) t.Fatalf("go tool compile %s failed: %s", filename, err) } @@ -165,8 +164,7 @@ func TestImportTypeparamTests(t *testing.T) { t.Skipf("in short mode, skipping test that requires export data for all of std") } - testenv.NeedsGo1Point(t, 18) // requires generics - testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache // This package only handles gc export data. if runtime.Compiler != "gc" { @@ -406,18 +404,6 @@ var importedObjectTests = []struct { {"go/types.Type", "type Type interface{String() string; Underlying() Type}"}, } -// TODO(rsc): Delete this init func after x/tools no longer needs to test successfully with Go 1.17. -func init() { - if build.Default.ReleaseTags[len(build.Default.ReleaseTags)-1] <= "go1.17" { - for i := range importedObjectTests { - if importedObjectTests[i].name == "context.Context" { - // Expand any to interface{}. - importedObjectTests[i].want = "type Context interface{Deadline() (deadline time.Time, ok bool); Done() <-chan struct{}; Err() error; Value(key interface{}) interface{}}" - } - } - } -} - func TestImportedTypes(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") @@ -439,7 +425,7 @@ func TestImportedTypes(t *testing.T) { t.Errorf("%s: got %q; want %q", test.name, got, test.want) } - if named, _ := obj.Type().(*types.Named); named != nil { + if named, _ := aliases.Unalias(obj.Type()).(*types.Named); named != nil { verifyInterfaceMethodRecvs(t, named, 0) } } @@ -522,7 +508,7 @@ func verifyInterfaceMethodRecvs(t *testing.T, named *types.Named, level int) { // check embedded interfaces (if they are named, too) for i := 0; i < iface.NumEmbeddeds(); i++ { // embedding of interfaces cannot have cycles; recursion will terminate - if etype, _ := iface.EmbeddedType(i).(*types.Named); etype != nil { + if etype, _ := aliases.Unalias(iface.EmbeddedType(i)).(*types.Named); etype != nil { verifyInterfaceMethodRecvs(t, etype, level+1) } } @@ -542,7 +528,7 @@ func TestIssue5815(t *testing.T) { t.Errorf("no pkg for %s", obj) } if tname, _ := obj.(*types.TypeName); tname != nil { - named := tname.Type().(*types.Named) + named := aliases.Unalias(tname.Type()).(*types.Named) for i := 0; i < named.NumMethods(); i++ { m := named.Method(i) if m.Pkg() == nil { @@ -642,7 +628,7 @@ func TestIssue13898(t *testing.T) { // look for go/types.Object type obj := lookupObj(t, goTypesPkg.Scope(), "Object") - typ, ok := obj.Type().(*types.Named) + typ, ok := aliases.Unalias(obj.Type()).(*types.Named) if !ok { t.Fatalf("go/types.Object type is %v; wanted named type", typ) } @@ -739,8 +725,6 @@ func TestIssue25301(t *testing.T) { } func TestIssue51836(t *testing.T) { - testenv.NeedsGo1Point(t, 18) // requires generics - // This package only handles gc export data. needsCompiler(t, "gc") @@ -770,8 +754,6 @@ func TestIssue51836(t *testing.T) { } func TestIssue61561(t *testing.T) { - testenv.NeedsGo1Point(t, 18) // requires generics - const src = `package p type I[P any] interface { @@ -836,8 +818,6 @@ type K = StillBad[string] } func TestIssue57015(t *testing.T) { - testenv.NeedsGo1Point(t, 18) // requires generics - // This package only handles gc export data. needsCompiler(t, "gc") diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index 2ee8c70164f..638fc1d3b86 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -23,6 +23,7 @@ import ( "strings" "golang.org/x/tools/go/types/objectpath" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/tokeninternal" ) @@ -506,13 +507,13 @@ func (p *iexporter) doDecl(obj types.Object) { case *types.TypeName: t := obj.Type() - if tparam, ok := t.(*types.TypeParam); ok { + if tparam, ok := aliases.Unalias(t).(*types.TypeParam); ok { w.tag('P') w.pos(obj.Pos()) constraint := tparam.Constraint() if p.version >= iexportVersionGo1_18 { implicit := false - if iface, _ := constraint.(*types.Interface); iface != nil { + if iface, _ := aliases.Unalias(constraint).(*types.Interface); iface != nil { implicit = iface.IsImplicit() } w.bool(implicit) @@ -738,6 +739,8 @@ func (w *exportWriter) doTyp(t types.Type, pkg *types.Package) { }() } switch t := t.(type) { + // TODO(adonovan): support types.Alias. + case *types.Named: if targs := t.TypeArgs(); targs.Len() > 0 { w.startType(instanceType) @@ -843,7 +846,7 @@ func (w *exportWriter) doTyp(t types.Type, pkg *types.Package) { for i := 0; i < n; i++ { ft := t.EmbeddedType(i) tPkg := pkg - if named, _ := ft.(*types.Named); named != nil { + if named, _ := aliases.Unalias(ft).(*types.Named); named != nil { w.pos(named.Obj().Pos()) } else { w.pos(token.NoPos) diff --git a/internal/gcimporter/iexport_go118_test.go b/internal/gcimporter/iexport_go118_test.go index 134c231f8c1..c748fb36165 100644 --- a/internal/gcimporter/iexport_go118_test.go +++ b/internal/gcimporter/iexport_go118_test.go @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package gcimporter_test +// This file defines test of generics features introduce in go1.18. + import ( "bytes" "fmt" diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go index 9fffa9ad05c..4d50eb8e587 100644 --- a/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go @@ -22,6 +22,8 @@ import ( "strings" "golang.org/x/tools/go/types/objectpath" + "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) type intReader struct { @@ -522,7 +524,7 @@ func canReuse(def *types.Named, rhs types.Type) bool { if def == nil { return true } - iface, _ := rhs.(*types.Interface) + iface, _ := aliases.Unalias(rhs).(*types.Interface) if iface == nil { return true } @@ -587,14 +589,13 @@ func (r *importReader) obj(name string) { // If the receiver has any targs, set those as the // rparams of the method (since those are the // typeparams being used in the method sig/body). - base := baseType(recv.Type()) - assert(base != nil) - targs := base.TypeArgs() + _, recvNamed := typesinternal.ReceiverNamed(recv) + targs := recvNamed.TypeArgs() var rparams []*types.TypeParam if targs.Len() > 0 { rparams = make([]*types.TypeParam, targs.Len()) for i := range rparams { - rparams[i] = targs.At(i).(*types.TypeParam) + rparams[i] = aliases.Unalias(targs.At(i)).(*types.TypeParam) } } msig := r.signature(recv, rparams, nil) @@ -624,7 +625,7 @@ func (r *importReader) obj(name string) { } constraint := r.typ() if implicit { - iface, _ := constraint.(*types.Interface) + iface, _ := aliases.Unalias(constraint).(*types.Interface) if iface == nil { errorf("non-interface constraint marked implicit") } @@ -831,7 +832,7 @@ func (r *importReader) typ() types.Type { } func isInterface(t types.Type) bool { - _, ok := t.(*types.Interface) + _, ok := aliases.Unalias(t).(*types.Interface) return ok } @@ -1030,7 +1031,7 @@ func (r *importReader) tparamList() []*types.TypeParam { for i := range xs { // Note: the standard library importer is tolerant of nil types here, // though would panic in SetTypeParams. - xs[i] = r.typ().(*types.TypeParam) + xs[i] = aliases.Unalias(r.typ()).(*types.TypeParam) } return xs } @@ -1077,13 +1078,3 @@ func (r *importReader) byte() byte { } return x } - -func baseType(typ types.Type) *types.Named { - // pointer receivers are never types.Named types - if p, _ := typ.(*types.Pointer); p != nil { - typ = p.Elem() - } - // receiver base types are always (possibly generic) types.Named types - n, _ := typ.(*types.Named) - return n -} diff --git a/internal/gcimporter/support_go117.go b/internal/gcimporter/support_go117.go deleted file mode 100644 index d892273efb6..00000000000 --- a/internal/gcimporter/support_go117.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2021 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. - -//go:build !go1.18 -// +build !go1.18 - -package gcimporter - -import "go/types" - -const iexportVersion = iexportVersionGo1_11 - -func additionalPredeclared() []types.Type { - return nil -} diff --git a/internal/gcimporter/support_go118.go b/internal/gcimporter/support_go118.go index edbe6ea7041..0cd3b91b65a 100644 --- a/internal/gcimporter/support_go118.go +++ b/internal/gcimporter/support_go118.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package gcimporter import "go/types" diff --git a/internal/gcimporter/unified_no.go b/internal/gcimporter/unified_no.go index 286bf445483..38b624cadab 100644 --- a/internal/gcimporter/unified_no.go +++ b/internal/gcimporter/unified_no.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !(go1.18 && goexperiment.unified) -// +build !go1.18 !goexperiment.unified +//go:build !goexperiment.unified +// +build !goexperiment.unified package gcimporter diff --git a/internal/gcimporter/unified_yes.go b/internal/gcimporter/unified_yes.go index b5d69ffbe68..b5118d0b3a5 100644 --- a/internal/gcimporter/unified_yes.go +++ b/internal/gcimporter/unified_yes.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 && goexperiment.unified -// +build go1.18,goexperiment.unified +//go:build goexperiment.unified +// +build goexperiment.unified package gcimporter diff --git a/internal/gcimporter/ureader_no.go b/internal/gcimporter/ureader_no.go deleted file mode 100644 index 8eb20729c2a..00000000000 --- a/internal/gcimporter/ureader_no.go +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2022 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. - -//go:build !go1.18 -// +build !go1.18 - -package gcimporter - -import ( - "fmt" - "go/token" - "go/types" -) - -func UImportData(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string) (_ int, pkg *types.Package, err error) { - err = fmt.Errorf("go/tools compiled with a Go version earlier than 1.18 cannot read unified IR export data") - return -} diff --git a/internal/gcimporter/ureader_yes.go b/internal/gcimporter/ureader_yes.go index b977435f626..f4edc46ab74 100644 --- a/internal/gcimporter/ureader_yes.go +++ b/internal/gcimporter/ureader_yes.go @@ -4,9 +4,6 @@ // Derived from go/internal/gcimporter/ureader.go -//go:build go1.18 -// +build go1.18 - package gcimporter import ( @@ -16,6 +13,7 @@ import ( "sort" "strings" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/pkgbits" ) @@ -553,7 +551,7 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { // If the underlying type is an interface, we need to // duplicate its methods so we can replace the receiver // parameter's type (#49906). - if iface, ok := underlying.(*types.Interface); ok && iface.NumExplicitMethods() != 0 { + if iface, ok := aliases.Unalias(underlying).(*types.Interface); ok && iface.NumExplicitMethods() != 0 { methods := make([]*types.Func, iface.NumExplicitMethods()) for i := range methods { fn := iface.ExplicitMethod(i) diff --git a/internal/imports/mod_test.go b/internal/imports/mod_test.go index c624463895d..e65104cbf2e 100644 --- a/internal/imports/mod_test.go +++ b/internal/imports/mod_test.go @@ -559,8 +559,6 @@ package v // Tests that go.work files are respected. func TestModWorkspace(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - mt := setup(t, nil, ` -- go.work -- go 1.18 @@ -595,8 +593,6 @@ package b // respected and that a wildcard replace in go.work overrides a versioned replace // in go.mod. func TestModWorkspaceReplace(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - mt := setup(t, nil, ` -- go.work -- use m @@ -654,8 +650,6 @@ func G() { // Tests a case where conflicting replaces are overridden by a replace // in the go.work file. func TestModWorkspaceReplaceOverride(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - mt := setup(t, nil, `-- go.work -- use m use n @@ -719,8 +713,6 @@ func G() { // workspaces with module pruning. This is based on the // cmd/go mod_prune_all script test. func TestModWorkspacePrune(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - mt := setup(t, nil, ` -- go.work -- go 1.18 diff --git a/internal/jsonrpc2_v2/messages.go b/internal/jsonrpc2_v2/messages.go index af145641d6a..f02b879c3f2 100644 --- a/internal/jsonrpc2_v2/messages.go +++ b/internal/jsonrpc2_v2/messages.go @@ -96,17 +96,17 @@ func (msg *Response) marshal(to *wireCombined) { to.Result = msg.Result } -func toWireError(err error) *wireError { +func toWireError(err error) *WireError { if err == nil { // no error, the response is complete return nil } - if err, ok := err.(*wireError); ok { + if err, ok := err.(*WireError); ok { // already a wire error, just use it return err } - result := &wireError{Message: err.Error()} - var wrapped *wireError + result := &WireError{Message: err.Error()} + var wrapped *WireError if errors.As(err, &wrapped) { // if we wrapped a wire error, keep the code from the wrapped error // but the message from the outer error diff --git a/internal/jsonrpc2_v2/wire.go b/internal/jsonrpc2_v2/wire.go index c8dc9ebf1bf..8f60fc62766 100644 --- a/internal/jsonrpc2_v2/wire.go +++ b/internal/jsonrpc2_v2/wire.go @@ -49,11 +49,11 @@ type wireCombined struct { Method string `json:"method,omitempty"` Params json.RawMessage `json:"params,omitempty"` Result json.RawMessage `json:"result,omitempty"` - Error *wireError `json:"error,omitempty"` + Error *WireError `json:"error,omitempty"` } -// wireError represents a structured error in a Response. -type wireError struct { +// WireError represents a structured error in a Response. +type WireError struct { // Code is an error code indicating the type of failure. Code int64 `json:"code"` // Message is a short description of the error. @@ -67,18 +67,18 @@ type wireError struct { // only be used to build errors for application specific codes as allowed by the // specification. func NewError(code int64, message string) error { - return &wireError{ + return &WireError{ Code: code, Message: message, } } -func (err *wireError) Error() string { +func (err *WireError) Error() string { return err.Message } -func (err *wireError) Is(other error) bool { - w, ok := other.(*wireError) +func (err *WireError) Is(other error) bool { + w, ok := other.(*WireError) if !ok { return false } diff --git a/internal/robustio/robustio_posix.go b/internal/robustio/robustio_posix.go index 8aa13d02786..cf74865d0b5 100644 --- a/internal/robustio/robustio_posix.go +++ b/internal/robustio/robustio_posix.go @@ -5,8 +5,6 @@ //go:build !windows && !plan9 // +build !windows,!plan9 -// TODO(adonovan): use 'unix' tag when go1.19 can be assumed. - package robustio import ( diff --git a/internal/testenv/exec.go b/internal/testenv/exec.go index 43aad5899fc..3b0cb6fcfa2 100644 --- a/internal/testenv/exec.go +++ b/internal/testenv/exec.go @@ -92,8 +92,7 @@ func NeedsExec(t testing.TB) { // for an arbitrary grace period before the test's deadline expires, // - if Cmd has the Cancel field, fails the test if the command is canceled // due to the test's deadline, and -// - if supported, sets a Cleanup function that verifies that the test did not -// leak a subprocess. +// - sets a Cleanup function that verifies that the test did not leak a subprocess. func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { t.Helper() NeedsExec(t) @@ -173,21 +172,14 @@ func CommandContext(t testing.TB, ctx context.Context, name string, args ...stri rWaitDelay.Set(reflect.ValueOf(gracePeriod)) } - // t.Cleanup was added in Go 1.14; for earlier Go versions, - // we just let the Context leak. - type Cleanupper interface { - Cleanup(func()) - } - if ct, ok := t.(Cleanupper); ok { - ct.Cleanup(func() { - if cancelCtx != nil { - cancelCtx() - } - if cmd.Process != nil && cmd.ProcessState == nil { - t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) - } - }) - } + t.Cleanup(func() { + if cancelCtx != nil { + cancelCtx() + } + if cmd.Process != nil && cmd.ProcessState == nil { + t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) + } + }) return cmd } diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go index 511da9d7e85..6cea5d7b6a2 100644 --- a/internal/testenv/testenv.go +++ b/internal/testenv/testenv.go @@ -83,8 +83,11 @@ func hasTool(tool string) error { // GOROOT. Otherwise, 'some/path/go test ./...' will test against some // version of the 'go' binary other than 'some/path/go', which is almost // certainly not what the user intended. - out, err := exec.Command(tool, "env", "GOROOT").CombinedOutput() + out, err := exec.Command(tool, "env", "GOROOT").Output() if err != nil { + if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { + err = fmt.Errorf("%w\nstderr:\n%s)", err, exit.Stderr) + } checkGoBuild.err = err return } @@ -141,8 +144,11 @@ func cgoEnabled(bypassEnvironment bool) (bool, error) { if bypassEnvironment { cmd.Env = append(append([]string(nil), os.Environ()...), "CGO_ENABLED=") } - out, err := cmd.CombinedOutput() + out, err := cmd.Output() if err != nil { + if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { + err = fmt.Errorf("%w\nstderr:\n%s", err, exit.Stderr) + } return false, err } enabled := strings.TrimSpace(string(out)) @@ -199,6 +205,12 @@ func NeedsTool(t testing.TB, tool string) { t.Helper() if allowMissingTool(tool) { + // TODO(adonovan): if we skip because of (e.g.) + // mismatched go env GOROOT and runtime.GOROOT, don't + // we risk some users not getting the coverage they expect? + // bcmills notes: this shouldn't be a concern as of CL 404134 (Go 1.19). + // We could probably safely get rid of that GOPATH consistency + // check entirely at this point. t.Skipf("skipping because %s tool not available: %v", tool, err) } else { t.Fatalf("%s tool not available: %v", tool, err) diff --git a/internal/tokeninternal/tokeninternal.go b/internal/tokeninternal/tokeninternal.go index 7e638ec24fc..ff9437a36cd 100644 --- a/internal/tokeninternal/tokeninternal.go +++ b/internal/tokeninternal/tokeninternal.go @@ -34,30 +34,16 @@ func GetLines(file *token.File) []int { lines []int _ []struct{} } - type tokenFile118 struct { - _ *token.FileSet // deleted in go1.19 - tokenFile119 - } - - type uP = unsafe.Pointer - switch unsafe.Sizeof(*file) { - case unsafe.Sizeof(tokenFile118{}): - var ptr *tokenFile118 - *(*uP)(uP(&ptr)) = uP(file) - ptr.mu.Lock() - defer ptr.mu.Unlock() - return ptr.lines - case unsafe.Sizeof(tokenFile119{}): - var ptr *tokenFile119 - *(*uP)(uP(&ptr)) = uP(file) - ptr.mu.Lock() - defer ptr.mu.Unlock() - return ptr.lines - - default: + if unsafe.Sizeof(*file) != unsafe.Sizeof(tokenFile119{}) { panic("unexpected token.File size") } + var ptr *tokenFile119 + type uP = unsafe.Pointer + *(*uP)(uP(&ptr)) = uP(file) + ptr.mu.Lock() + defer ptr.mu.Unlock() + return ptr.lines } // AddExistingFiles adds the specified files to the FileSet if they diff --git a/internal/typeparams/common.go b/internal/typeparams/common.go index cdab9885314..8c3a42dc311 100644 --- a/internal/typeparams/common.go +++ b/internal/typeparams/common.go @@ -2,20 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package typeparams contains common utilities for writing tools that interact -// with generic Go code, as introduced with Go 1.18. -// -// Many of the types and functions in this package are proxies for the new APIs -// introduced in the standard library with Go 1.18. For example, the -// typeparams.Union type is an alias for go/types.Union, and the ForTypeSpec -// function returns the value of the go/ast.TypeSpec.TypeParams field. At Go -// versions older than 1.18 these helpers are implemented as stubs, allowing -// users of this package to write code that handles generic constructs inline, -// even if the Go version being used to compile does not support generics. -// -// Additionally, this package contains common utilities for working with the -// new generic constructs, to supplement the standard library APIs. Notably, -// the StructuralTerms API computes a minimal representation of the structural +// Package typeparams contains common utilities for writing tools that +// interact with generic Go code, as introduced with Go 1.18. It +// supplements the standard library APIs. Notably, the StructuralTerms +// API computes a minimal representation of the structural // restrictions on a type parameter. // // An external version of these APIs is available in the @@ -27,6 +17,9 @@ import ( "go/ast" "go/token" "go/types" + + "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) // UnpackIndexExpr extracts data from AST nodes that represent index @@ -72,9 +65,9 @@ func PackIndexExpr(x ast.Expr, lbrack token.Pos, indices []ast.Expr, rbrack toke } } -// IsTypeParam reports whether t is a type parameter. +// IsTypeParam reports whether t is a type parameter (or an alias of one). func IsTypeParam(t types.Type) bool { - _, ok := t.(*types.TypeParam) + _, ok := aliases.Unalias(t).(*types.TypeParam) return ok } @@ -90,13 +83,8 @@ func OriginMethod(fn *types.Func) *types.Func { if recv == nil { return fn } - base := recv.Type() - p, isPtr := base.(*types.Pointer) - if isPtr { - base = p.Elem() - } - named, isNamed := base.(*types.Named) - if !isNamed { + _, named := typesinternal.ReceiverNamed(recv) + if named == nil { // Receiver is a *types.Interface. return fn } @@ -158,6 +146,9 @@ func OriginMethod(fn *types.Func) *types.Func { // In this case, GenericAssignableTo reports that instantiations of Container // are assignable to the corresponding instantiation of Interface. func GenericAssignableTo(ctxt *types.Context, V, T types.Type) bool { + V = aliases.Unalias(V) + T = aliases.Unalias(T) + // If V and T are not both named, or do not have matching non-empty type // parameter lists, fall back on types.AssignableTo. diff --git a/internal/typeparams/common_test.go b/internal/typeparams/common_test.go index b6f355cc5d7..b095b65e808 100644 --- a/internal/typeparams/common_test.go +++ b/internal/typeparams/common_test.go @@ -11,7 +11,6 @@ import ( "go/types" "testing" - "golang.org/x/tools/internal/testenv" . "golang.org/x/tools/internal/typeparams" ) @@ -41,7 +40,6 @@ func TestGetIndexExprData(t *testing.T) { } func TestOriginMethodRecursive(t *testing.T) { - testenv.NeedsGo1Point(t, 18) src := `package p type N[A any] int @@ -113,7 +111,6 @@ func (r *N[C]) n() { } } func TestOriginMethodUses(t *testing.T) { - testenv.NeedsGo1Point(t, 18) tests := []string{ `type T interface { m() }; func _(t T) { t.m() }`, @@ -209,8 +206,6 @@ func TestOriginMethod60628(t *testing.T) { } func TestGenericAssignableTo(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - tests := []struct { src string want bool diff --git a/internal/typeparams/coretype.go b/internal/typeparams/coretype.go index 7ea8840eab7..e66e9d0f48c 100644 --- a/internal/typeparams/coretype.go +++ b/internal/typeparams/coretype.go @@ -5,7 +5,10 @@ package typeparams import ( + "fmt" "go/types" + + "golang.org/x/tools/internal/aliases" ) // CoreType returns the core type of T or nil if T does not have a core type. @@ -109,7 +112,7 @@ func CoreType(T types.Type) types.Type { // _NormalTerms makes no guarantees about the order of terms, except that it // is deterministic. func _NormalTerms(typ types.Type) ([]*types.Term, error) { - switch typ := typ.(type) { + switch typ := aliases.Unalias(typ).(type) { case *types.TypeParam: return StructuralTerms(typ) case *types.Union: @@ -120,3 +123,15 @@ func _NormalTerms(typ types.Type) ([]*types.Term, error) { return []*types.Term{types.NewTerm(false, typ)}, nil } } + +// MustDeref returns the type of the variable pointed to by t. +// It panics if t's core type is not a pointer. +// +// TODO(adonovan): ideally this would live in typesinternal, but that +// creates an import cycle. Move there when we melt this package down. +func MustDeref(t types.Type) types.Type { + if ptr, ok := CoreType(t).(*types.Pointer); ok { + return ptr.Elem() + } + panic(fmt.Sprintf("%v is not a pointer", t)) +} diff --git a/internal/typeparams/genericfeatures/features.go b/internal/typeparams/genericfeatures/features.go index e307e677758..e7d0e0e6112 100644 --- a/internal/typeparams/genericfeatures/features.go +++ b/internal/typeparams/genericfeatures/features.go @@ -12,6 +12,7 @@ import ( "strings" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/aliases" ) // Features is a set of flags reporting which features of generic Go code a @@ -92,7 +93,7 @@ func ForPackage(inspect *inspector.Inspector, info *types.Info) Features { }) for _, inst := range info.Instances { - switch inst.Type.(type) { + switch aliases.Unalias(inst.Type).(type) { case *types.Named: direct |= TypeInstantiation case *types.Signature: diff --git a/internal/typesinternal/recv.go b/internal/typesinternal/recv.go new file mode 100644 index 00000000000..fea7c8b75e8 --- /dev/null +++ b/internal/typesinternal/recv.go @@ -0,0 +1,43 @@ +// Copyright 2024 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 typesinternal + +import ( + "go/types" + + "golang.org/x/tools/internal/aliases" +) + +// ReceiverNamed returns the named type (if any) associated with the +// type of recv, which may be of the form N or *N, or aliases thereof. +// It also reports whether a Pointer was present. +func ReceiverNamed(recv *types.Var) (isPtr bool, named *types.Named) { + t := recv.Type() + if ptr, ok := aliases.Unalias(t).(*types.Pointer); ok { + isPtr = true + t = ptr.Elem() + } + named, _ = aliases.Unalias(t).(*types.Named) + return +} + +// Unpointer returns T given *T or an alias thereof. +// For all other types it is the identity function. +// It does not look at underlying types. +// The result may be an alias. +// +// Use this function to strip off the optional pointer on a receiver +// in a field or method selection, without losing the named type +// (which is needed to compute the method set). +// +// See also [typeparams.MustDeref], which removes one level of +// indirection from the type, regardless of named types (analogous to +// a LOAD instruction). +func Unpointer(t types.Type) types.Type { + if ptr, ok := aliases.Unalias(t).(*types.Pointer); ok { + return ptr.Elem() + } + return t +} diff --git a/internal/typesinternal/types_118.go b/internal/typesinternal/types_118.go index a42b072a67d..ef7ea290c0b 100644 --- a/internal/typesinternal/types_118.go +++ b/internal/typesinternal/types_118.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package typesinternal import ( diff --git a/internal/versions/features.go b/internal/versions/features.go new file mode 100644 index 00000000000..b53f1786161 --- /dev/null +++ b/internal/versions/features.go @@ -0,0 +1,43 @@ +// 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 versions + +// This file contains predicates for working with file versions to +// decide when a tool should consider a language feature enabled. + +// GoVersions that features in x/tools can be gated to. +const ( + Go1_18 = "go1.18" + Go1_19 = "go1.19" + Go1_20 = "go1.20" + Go1_21 = "go1.21" + Go1_22 = "go1.22" +) + +// Future is an invalid unknown Go version sometime in the future. +// Do not use directly with Compare. +const Future = "" + +// AtLeast reports whether the file version v comes after a Go release. +// +// Use this predicate to enable a behavior once a certain Go release +// has happened (and stays enabled in the future). +func AtLeast(v, release string) bool { + if v == Future { + return true // an unknown future version is always after y. + } + return Compare(Lang(v), Lang(release)) >= 0 +} + +// Before reports whether the file version v is strictly before a Go release. +// +// Use this predicate to disable a behavior once a certain Go release +// has happened (and stays enabled in the future). +func Before(v, release string) bool { + if v == Future { + return false // an unknown future version happens after y. + } + return Compare(Lang(v), Lang(release)) < 0 +} diff --git a/internal/versions/toolchain.go b/internal/versions/toolchain.go new file mode 100644 index 00000000000..377bf7a53b4 --- /dev/null +++ b/internal/versions/toolchain.go @@ -0,0 +1,14 @@ +// Copyright 2024 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 versions + +// toolchain is maximum version (<1.22) that the go toolchain used +// to build the current tool is known to support. +// +// When a tool is built with >=1.22, the value of toolchain is unused. +// +// x/tools does not support building with go <1.18. So we take this +// as the minimum possible maximum. +var toolchain string = Go1_18 diff --git a/internal/versions/toolchain_go119.go b/internal/versions/toolchain_go119.go new file mode 100644 index 00000000000..f65beed9d83 --- /dev/null +++ b/internal/versions/toolchain_go119.go @@ -0,0 +1,14 @@ +// Copyright 2024 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. + +//go:build go1.19 +// +build go1.19 + +package versions + +func init() { + if Compare(toolchain, Go1_19) < 0 { + toolchain = Go1_19 + } +} diff --git a/internal/versions/toolchain_go120.go b/internal/versions/toolchain_go120.go new file mode 100644 index 00000000000..1a9efa126cd --- /dev/null +++ b/internal/versions/toolchain_go120.go @@ -0,0 +1,14 @@ +// Copyright 2024 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. + +//go:build go1.20 +// +build go1.20 + +package versions + +func init() { + if Compare(toolchain, Go1_20) < 0 { + toolchain = Go1_20 + } +} diff --git a/internal/versions/toolchain_go121.go b/internal/versions/toolchain_go121.go new file mode 100644 index 00000000000..b7ef216dfec --- /dev/null +++ b/internal/versions/toolchain_go121.go @@ -0,0 +1,14 @@ +// Copyright 2024 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. + +//go:build go1.21 +// +build go1.21 + +package versions + +func init() { + if Compare(toolchain, Go1_21) < 0 { + toolchain = Go1_21 + } +} diff --git a/internal/versions/types_go121.go b/internal/versions/types_go121.go index a7b79207aee..b4345d3349e 100644 --- a/internal/versions/types_go121.go +++ b/internal/versions/types_go121.go @@ -12,9 +12,19 @@ import ( "go/types" ) -// FileVersions always reports the a file's Go version as the -// zero version at this Go version. -func FileVersions(info *types.Info, file *ast.File) string { return "" } +// FileVersion returns a language version (<=1.21) derived from runtime.Version() +// or an unknown future version. +func FileVersion(info *types.Info, file *ast.File) string { + // In x/tools built with Go <= 1.21, we do not have Info.FileVersions + // available. We use a go version derived from the toolchain used to + // compile the tool by default. + // This will be <= go1.21. We take this as the maximum version that + // this tool can support. + // + // There are no features currently in x/tools that need to tell fine grained + // differences for versions <1.22. + return toolchain +} -// InitFileVersions is a noop at this Go version. +// InitFileVersions is a noop when compiled with this Go version. func InitFileVersions(*types.Info) {} diff --git a/internal/versions/types_go122.go b/internal/versions/types_go122.go index 7b9ba89a822..e8180632a52 100644 --- a/internal/versions/types_go122.go +++ b/internal/versions/types_go122.go @@ -12,10 +12,27 @@ import ( "go/types" ) -// FileVersions maps a file to the file's semantic Go version. -// The reported version is the zero version if a version cannot be determined. -func FileVersions(info *types.Info, file *ast.File) string { - return info.FileVersions[file] +// FileVersions returns a file's Go version. +// The reported version is an unknown Future version if a +// version cannot be determined. +func FileVersion(info *types.Info, file *ast.File) string { + // In tools built with Go >= 1.22, the Go version of a file + // follow a cascades of sources: + // 1) types.Info.FileVersion, which follows the cascade: + // 1.a) file version (ast.File.GoVersion), + // 1.b) the package version (types.Config.GoVersion), or + // 2) is some unknown Future version. + // + // File versions require a valid package version to be provided to types + // in Config.GoVersion. Config.GoVersion is either from the package's module + // or the toolchain (go run). This value should be provided by go/packages + // or unitchecker.Config.GoVersion. + if v := info.FileVersions[file]; IsValid(v) { + return v + } + // Note: we could instead return runtime.Version() [if valid]. + // This would act as a max version on what a tool can support. + return Future } // InitFileVersions initializes info to record Go versions for Go files. diff --git a/internal/versions/types_test.go b/internal/versions/types_test.go index 0ffdd468dcf..59f6d18b45f 100644 --- a/internal/versions/types_test.go +++ b/internal/versions/types_test.go @@ -52,11 +52,11 @@ func Test(t *testing.T) { if got, want := versions.GoVersion(pkg), item.pversion; versions.Compare(got, want) != 0 { t.Errorf("GoVersion()=%q. expected %q", got, want) } - if got := versions.FileVersions(info, nil); got != "" { + if got := versions.FileVersion(info, nil); got != "" { t.Errorf(`FileVersions(nil)=%q. expected ""`, got) } for i, test := range item.tests { - if got, want := versions.FileVersions(info, files[i]), test.want; got != want { + if got, want := versions.FileVersion(info, files[i]), test.want; got != want { t.Errorf("FileVersions(%s)=%q. expected %q", test.fname, got, want) } } diff --git a/internal/versions/versions.go b/internal/versions/versions.go index e16f6c33a52..8d1f7453dbf 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -4,6 +4,10 @@ package versions +import ( + "strings" +) + // Note: If we use build tags to use go/versions when go >=1.22, // we run into go.dev/issue/53737. Under some operations users would see an // import of "go/versions" even if they would not compile the file. @@ -45,6 +49,7 @@ func IsValid(x string) bool { return isValid(stripGo(x)) } // stripGo converts from a "go1.21" version to a "1.21" version. // If v does not start with "go", stripGo returns the empty string (a known invalid version). func stripGo(v string) string { + v, _, _ = strings.Cut(v, "-") // strip -bigcorp suffix. if len(v) < 2 || v[:2] != "go" { return "" } diff --git a/internal/versions/versions_test.go b/internal/versions/versions_test.go index 1222ba7942a..dbc1c555d22 100644 --- a/internal/versions/versions_test.go +++ b/internal/versions/versions_test.go @@ -5,8 +5,13 @@ package versions_test import ( + "go/ast" + "go/parser" + "go/token" + "go/types" "testing" + "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/versions" ) @@ -20,6 +25,7 @@ func TestIsValid(t *testing.T) { "go0.0", // ?? "go1", "go2", + "go1.20.0-bigcorp", } { if !versions.IsValid(x) { t.Errorf("expected versions.IsValid(%q) to hold", x) @@ -40,6 +46,7 @@ func TestIsValid(t *testing.T) { "go1.21.2_2", "go1.21rc_2", "go1.21rc2_", + "go1.600+auto", } { if versions.IsValid(x) { t.Errorf("expected versions.IsValid(%q) to not hold", x) @@ -52,6 +59,7 @@ func TestVersionComparisons(t *testing.T) { x, y string want int }{ + // All comparisons of go2, go1.21.2, go1.21rc2, go1.21rc2, go1, go0.0, "", bad {"go2", "go2", 0}, {"go2", "go1.21.2", +1}, {"go2", "go1.21rc2", +1}, @@ -97,6 +105,11 @@ func TestVersionComparisons(t *testing.T) { {"", "", 0}, {"", "bad", 0}, {"bad", "bad", 0}, + // Other tests. + {"go1.20", "go1.20.0-bigcorp", 0}, + {"go1.21", "go1.21.0-bigcorp", -1}, // Starting in Go 1.21, patch missing is different from explicit .0. + {"go1.21.0", "go1.21.0-bigcorp", 0}, // Starting in Go 1.21, patch missing is different from explicit .0. + {"go1.19rc1", "go1.19", -1}, } { got := versions.Compare(item.x, item.y) if got != item.want { @@ -129,3 +142,115 @@ func TestLang(t *testing.T) { } } + +func TestKnown(t *testing.T) { + for _, v := range [...]string{ + versions.Go1_18, + versions.Go1_19, + versions.Go1_20, + versions.Go1_21, + versions.Go1_22, + } { + if !versions.IsValid(v) { + t.Errorf("Expected known version %q to be valid.", v) + } + if v != versions.Lang(v) { + t.Errorf("Expected known version %q == Lang(%q).", v, versions.Lang(v)) + } + } +} + +func TestAtLeast(t *testing.T) { + for _, item := range [...]struct { + v, release string + want bool + }{ + {versions.Future, versions.Go1_22, true}, + {versions.Go1_22, versions.Go1_22, true}, + {"go1.21", versions.Go1_22, false}, + {"invalid", versions.Go1_22, false}, + } { + if got := versions.AtLeast(item.v, item.release); got != item.want { + t.Errorf("AtLeast(%q, %q)=%v. wanted %v", item.v, item.release, got, item.want) + } + } +} + +func TestBefore(t *testing.T) { + for _, item := range [...]struct { + v, release string + want bool + }{ + {versions.Future, versions.Go1_22, false}, + {versions.Go1_22, versions.Go1_22, false}, + {"go1.21", versions.Go1_22, true}, + {"invalid", versions.Go1_22, true}, // invalid < Go1_22 + } { + if got := versions.Before(item.v, item.release); got != item.want { + t.Errorf("Before(%q, %q)=%v. wanted %v", item.v, item.release, got, item.want) + } + } +} + +func TestFileVersions122(t *testing.T) { + testenv.NeedsGo1Point(t, 22) + + const source = ` + package P + ` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "hello.go", source, 0) + if err != nil { + t.Fatal(err) + } + + for _, conf := range []types.Config{ + {GoVersion: versions.Go1_22}, + {}, // GoVersion is unset. + } { + info := &types.Info{} + versions.InitFileVersions(info) + + _, err = conf.Check("P", fset, []*ast.File{f}, info) + if err != nil { + t.Fatal(err) + } + + v := versions.FileVersion(info, f) + if !versions.AtLeast(v, versions.Go1_22) { + t.Errorf("versions.AtLeast(%q, %q) expected to hold", v, versions.Go1_22) + } + + if versions.Before(v, versions.Go1_22) { + t.Errorf("versions.AtLeast(%q, %q) expected to be false", v, versions.Go1_22) + } + + if conf.GoVersion == "" && v != versions.Future { + t.Error("Expected the FileVersion to be the Future when conf.GoVersion is unset") + } + } +} + +func TestFileVersions121(t *testing.T) { + testenv.SkipAfterGo1Point(t, 21) + + // If <1.22, info and file are ignored. + v := versions.FileVersion(nil, nil) + oneof := map[string]bool{ + versions.Go1_18: true, + versions.Go1_19: true, + versions.Go1_20: true, + versions.Go1_21: true, + } + if !oneof[v] { + t.Errorf("FileVersion(...)=%q expected to be a known go version <1.22", v) + } + + if versions.AtLeast(v, versions.Go1_22) { + t.Errorf("versions.AtLeast(%q, %q) expected to be false", v, versions.Go1_22) + } + + if !versions.Before(v, versions.Go1_22) { + t.Errorf("versions.Before(%q, %q) expected to hold", v, versions.Go1_22) + } +} diff --git a/present/parse_test.go b/present/parse_test.go index 0e59857a3a0..dad57ea77ca 100644 --- a/present/parse_test.go +++ b/present/parse_test.go @@ -6,6 +6,7 @@ package present import ( "bytes" + "fmt" "html/template" "os" "os/exec" @@ -79,11 +80,13 @@ func diff(prefix string, name1 string, b1 []byte, name2 string, b2 []byte) ([]by cmd = "/bin/ape/diff" } - data, err := exec.Command(cmd, "-u", f1, f2).CombinedOutput() + data, err := exec.Command(cmd, "-u", f1, f2).Output() if len(data) > 0 { // diff exits with a non-zero status when the files don't match. // Ignore that failure as long as we get output. err = nil + } else if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { + err = fmt.Errorf("%w\nstderr:\n%s)", err, exit.Stderr) } data = bytes.Replace(data, []byte(f1), []byte(name1), -1) diff --git a/refactor/rename/check.go b/refactor/rename/check.go index 9f29b98a0a4..ac2c5d4206f 100644 --- a/refactor/rename/check.go +++ b/refactor/rename/check.go @@ -13,6 +13,7 @@ import ( "go/types" "golang.org/x/tools/go/loader" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/refactor/satisfy" ) @@ -467,9 +468,10 @@ func (r *renamer) checkStructField(from *types.Var) { // type T int // this and // var s struct {T} // this must change too. if from.Anonymous() { - if named, ok := from.Type().(*types.Named); ok { + // TODO(adonovan): think carefully about aliases. + if named, ok := aliases.Unalias(from.Type()).(*types.Named); ok { r.check(named.Obj()) - } else if named, ok := deref(from.Type()).(*types.Named); ok { + } else if named, ok := aliases.Unalias(deref(from.Type())).(*types.Named); ok { r.check(named.Obj()) } } @@ -777,7 +779,7 @@ func (r *renamer) checkMethod(from *types.Func) { var iface string I := recv(imeth).Type() - if named, ok := I.(*types.Named); ok { + if named, ok := aliases.Unalias(I).(*types.Named); ok { pos = named.Obj().Pos() iface = "interface " + named.Obj().Name() } else { @@ -851,7 +853,7 @@ func someUse(info *loader.PackageInfo, obj types.Object) *ast.Ident { func isInterface(T types.Type) bool { return types.IsInterface(T) } func deref(typ types.Type) types.Type { - if p, _ := typ.(*types.Pointer); p != nil { + if p, _ := aliases.Unalias(typ).(*types.Pointer); p != nil { return p.Elem() } return typ diff --git a/refactor/rename/rename.go b/refactor/rename/rename.go index db925ca57b2..56603cd33a9 100644 --- a/refactor/rename/rename.go +++ b/refactor/rename/rename.go @@ -588,7 +588,7 @@ func diff(filename string, content []byte) error { } defer os.Remove(renamed) - diff, err := exec.Command(DiffCmd, "-u", filename, renamed).CombinedOutput() + diff, err := exec.Command(DiffCmd, "-u", filename, renamed).Output() if len(diff) > 0 { // diff exits with a non-zero status when the files don't match. // Ignore that failure as long as we get output. @@ -596,6 +596,9 @@ func diff(filename string, content []byte) error { return nil } if err != nil { + if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { + err = fmt.Errorf("%w\nstderr:\n%s", err, exit.Stderr) + } return fmt.Errorf("computing diff: %v", err) } return nil diff --git a/refactor/rename/spec.go b/refactor/rename/spec.go index 22a268a7942..69198cb0d96 100644 --- a/refactor/rename/spec.go +++ b/refactor/rename/spec.go @@ -25,6 +25,7 @@ import ( "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/loader" + "golang.org/x/tools/internal/aliases" ) // A spec specifies an entity to rename. @@ -465,9 +466,9 @@ func findObjects(info *loader.PackageInfo, spec *spec) ([]types.Object, error) { if spec.searchFor == "" { // If it is an embedded field, return the type of the field. if v, ok := obj.(*types.Var); ok && v.Anonymous() { - switch t := v.Type().(type) { + switch t := aliases.Unalias(v.Type()).(type) { case *types.Pointer: - return []types.Object{t.Elem().(*types.Named).Obj()}, nil + return []types.Object{aliases.Unalias(t.Elem()).(*types.Named).Obj()}, nil case *types.Named: return []types.Object{t.Obj()}, nil }