| // Copyright 2025 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package cache |
| |
| import ( |
| "context" |
| "log" |
| "maps" |
| "slices" |
| "strings" |
| |
| "golang.org/x/tools/gopls/internal/cache/metadata" |
| "golang.org/x/tools/gopls/internal/cache/symbols" |
| "golang.org/x/tools/gopls/internal/protocol" |
| "golang.org/x/tools/gopls/internal/util/moremaps" |
| "golang.org/x/tools/internal/imports" |
| ) |
| |
| // goplsSource is an imports.Source that provides import information using |
| // gopls and the module cache index. |
| type goplsSource struct { |
| snapshot *Snapshot |
| envSource *imports.ProcessEnvSource |
| |
| // set by each invocation of ResolveReferences |
| ctx context.Context |
| } |
| |
| func (s *Snapshot) NewGoplsSource(is *imports.ProcessEnvSource) *goplsSource { |
| return &goplsSource{ |
| snapshot: s, |
| envSource: is, |
| } |
| } |
| |
| func (s *goplsSource) LoadPackageNames(ctx context.Context, srcDir string, paths []imports.ImportPath) (map[imports.ImportPath]imports.PackageName, error) { |
| // TODO: use metadata graph. Aside from debugging, this is the only used of envSource |
| return s.envSource.LoadPackageNames(ctx, srcDir, paths) |
| } |
| |
| type result struct { |
| res *imports.Result |
| deprecated bool |
| } |
| |
| // ResolveReferences tries to find resolving imports in the workspace, and failing |
| // that, in the module cache. It uses heuristics to decide among alternatives. |
| // The heuristics will usually prefer a v2 version, if there is one. |
| // TODO: It does not take advantage of hints provided by the user: |
| // 1. syntactic context: pkg.Name().Foo |
| // 3. already imported files in the same module |
| func (s *goplsSource) ResolveReferences(ctx context.Context, filename string, missing imports.References) ([]*imports.Result, error) { |
| s.ctx = ctx |
| // get results from the workspace. There will at most one for each package name |
| fromWS, err := s.resolveWorkspaceReferences(filename, missing) |
| if err != nil { |
| return nil, err |
| } |
| // collect the ones that are still |
| needed := maps.Clone(missing) |
| for _, a := range fromWS { |
| delete(needed, a.Package.Name) |
| } |
| // when debug (below) is gone, change this to: if len(needed) == 0 {return fromWS, nil} |
| var fromCache []*result |
| if len(needed) != 0 { |
| var err error |
| fromCache, err = s.resolveCacheReferences(needed) |
| if err != nil { |
| return nil, err |
| } |
| // trim cans to one per missing package. |
| byPkgNm := make(map[string][]*result) |
| for _, c := range fromCache { |
| byPkgNm[c.res.Package.Name] = append(byPkgNm[c.res.Package.Name], c) |
| } |
| for k, v := range byPkgNm { |
| fromWS = append(fromWS, s.bestCache(k, v)) |
| } |
| } |
| const debug = false |
| if debug { // debugging. |
| // what does the old one find? |
| old, err := s.envSource.ResolveReferences(ctx, filename, missing) |
| if err != nil { |
| log.Fatal(err) |
| } |
| log.Printf("fromCache:%d %s", len(fromCache), filename) |
| for i, c := range fromCache { |
| log.Printf("cans%d %#v %#v %v", i, c.res.Import, c.res.Package, c.deprecated) |
| } |
| for k, v := range missing { |
| for x := range v { |
| log.Printf("missing %s.%s", k, x) |
| } |
| } |
| for k, v := range needed { |
| for x := range v { |
| log.Printf("needed %s.%s", k, x) |
| } |
| } |
| |
| dbgpr := func(hdr string, v []*imports.Result) { |
| for i := range v { |
| log.Printf("%s%d %+v %+v", hdr, i, v[i].Import, v[i].Package) |
| } |
| } |
| |
| dbgpr("fromWS", fromWS) |
| dbgpr("old", old) |
| for k, v := range s.snapshot.workspacePackages.All() { |
| log.Printf("workspacePackages[%s]=%s", k, v) |
| } |
| // anything in ans with >1 matches? |
| seen := make(map[string]int) |
| for _, a := range fromWS { |
| seen[a.Package.Name]++ |
| } |
| for k, v := range seen { |
| if v > 1 { |
| log.Printf("saw %d %s", v, k) |
| for i, x := range fromWS { |
| if x.Package.Name == k { |
| log.Printf("%d: %+v %+v", i, x.Package, x.Import) |
| } |
| } |
| } |
| } |
| } |
| return fromWS, nil |
| |
| } |
| |
| func (s *goplsSource) resolveCacheReferences(missing imports.References) ([]*result, error) { |
| ix, err := s.snapshot.view.ModcacheIndex() |
| if err != nil { |
| return nil, err |
| } |
| |
| found := make(map[string]*result) |
| for pkgName, nameSet := range missing { |
| names := moremaps.KeySlice(nameSet) |
| for importPath, cands := range ix.LookupAll(pkgName, names...) { |
| res := found[importPath] |
| if res == nil { |
| res = &result{ |
| res: &imports.Result{ |
| Import: &imports.ImportInfo{ |
| ImportPath: importPath, |
| }, |
| Package: &imports.PackageInfo{ |
| Name: pkgName, |
| Exports: make(map[string]bool)}, |
| }, |
| deprecated: false, |
| } |
| found[importPath] = res |
| } |
| for _, c := range cands { |
| res.res.Package.Exports[c.Name] = true |
| // The import path is deprecated if a symbol that would be used is deprecated |
| res.deprecated = res.deprecated || c.Deprecated |
| } |
| } |
| |
| } |
| return moremaps.ValueSlice(found), nil |
| } |
| |
| type found struct { |
| sym *symbols.Package |
| res *imports.Result |
| } |
| |
| func (s *goplsSource) resolveWorkspaceReferences(filename string, missing imports.References) ([]*imports.Result, error) { |
| uri := protocol.URIFromPath(filename) |
| mypkgs, err := s.snapshot.MetadataForFile(s.ctx, uri, false) |
| if err != nil { |
| return nil, err |
| } |
| if len(mypkgs) == 0 { |
| return nil, nil |
| } |
| mypkg := mypkgs[0] // narrowest package |
| // search the metadata graph for package ids correstponding to missing |
| g := s.snapshot.MetadataGraph() |
| var ids []metadata.PackageID |
| var pkgs []*metadata.Package |
| for pid, pkg := range g.Packages { |
| // no test packages, except perhaps for ourselves |
| if pkg.ForTest != "" && pkg != mypkg { |
| continue |
| } |
| if missingWants(missing, pkg.Name) { |
| ids = append(ids, pid) |
| pkgs = append(pkgs, pkg) |
| } |
| } |
| // find the symbols in those packages |
| // the syms occur in the same order as the ids and the pkgs |
| syms, err := s.snapshot.Symbols(s.ctx, ids...) |
| if err != nil { |
| return nil, err |
| } |
| // keep track of used syms and found results by package name |
| // TODO: avoid import cycles (is current package in forward closure) |
| founds := make(map[string][]found) |
| for i := range len(ids) { |
| nm := string(pkgs[i].Name) |
| if satisfies(syms[i], missing[nm]) { |
| got := &imports.Result{ |
| Import: &imports.ImportInfo{ |
| Name: "", |
| ImportPath: string(pkgs[i].PkgPath), |
| }, |
| Package: &imports.PackageInfo{ |
| Name: string(pkgs[i].Name), |
| Exports: missing[imports.PackageName(pkgs[i].Name)], |
| }, |
| } |
| founds[nm] = append(founds[nm], found{syms[i], got}) |
| } |
| } |
| var ans []*imports.Result |
| for _, v := range founds { |
| // make sure the elements of v are unique |
| // (Import.ImportPath or Package.Name must differ) |
| cmp := func(l, r found) int { |
| switch strings.Compare(l.res.Import.ImportPath, r.res.Import.ImportPath) { |
| case -1: |
| return -1 |
| case 1: |
| return 1 |
| } |
| return strings.Compare(l.res.Package.Name, r.res.Package.Name) |
| } |
| slices.SortFunc(v, cmp) |
| newv := make([]found, 0, len(v)) |
| newv = append(newv, v[0]) |
| for i := 1; i < len(v); i++ { |
| if cmp(v[i], v[i-1]) != 0 { |
| newv = append(newv, v[i]) |
| } |
| } |
| ans = append(ans, bestImport(filename, newv)) |
| } |
| return ans, nil |
| } |
| |
| // for each package name, choose one using heuristics |
| func bestImport(filename string, got []found) *imports.Result { |
| if len(got) == 1 { |
| return got[0].res |
| } |
| isTestFile := strings.HasSuffix(filename, "_test.go") |
| var leftovers []found |
| for _, g := range got { |
| // don't use _test packages unless isTestFile |
| testPkg := strings.HasSuffix(string(g.res.Package.Name), "_test") || strings.HasSuffix(string(g.res.Import.Name), "_test") |
| if testPkg && !isTestFile { |
| continue // no test covers this |
| } |
| if imports.CanUse(filename, g.sym.Files[0].DirPath()) { |
| leftovers = append(leftovers, g) |
| } |
| } |
| switch len(leftovers) { |
| case 0: |
| break // use got, they are all bad |
| case 1: |
| return leftovers[0].res // only one left |
| default: |
| got = leftovers // filtered some out |
| } |
| |
| // TODO: if there are versions (like /v2) prefer them |
| |
| // use distance to common ancestor with filename |
| // (TestDirectoryFilters_MultiRootImportScanning) |
| // filename is .../a/main.go, choices are |
| // .../a/hi/hi.go and .../b/hi/hi.go |
| longest := -1 |
| ix := -1 |
| for i := 0; i < len(got); i++ { |
| d := commonpref(filename, got[i].sym.Files[0].Path()) |
| if d > longest { |
| longest = d |
| ix = i |
| } |
| } |
| // it is possible that there were several tied, but we return the first |
| return got[ix].res |
| } |
| |
| // choose the best result for the package named nm from the module cache |
| func (s *goplsSource) bestCache(nm string, got []*result) *imports.Result { |
| if len(got) == 1 { |
| return got[0].res |
| } |
| // does the go.mod file choose one? |
| if ans := s.fromGoMod(got); ans != nil { |
| return ans |
| } |
| got = preferUndeprecated(got) |
| // want the best Import.ImportPath |
| // these are all for the package named nm, |
| // nm (probably) occurs in all the paths; |
| // choose the longest (after nm), so as to get /v2 |
| maxlen, which := -1, -1 |
| for i := 0; i < len(got); i++ { |
| ix := strings.Index(got[i].res.Import.ImportPath, nm) |
| if ix == -1 { |
| continue // now what? |
| } |
| cnt := len(got[i].res.Import.ImportPath) - ix |
| if cnt > maxlen { |
| maxlen = cnt |
| which = i |
| } |
| // what about ties? (e.g., /v2 and /v3) |
| } |
| if which >= 0 { |
| return got[which].res |
| } |
| return got[0].res // arbitrary guess |
| } |
| |
| // if go.mod requires one of the packages, return that |
| func (s *goplsSource) fromGoMod(got []*result) *imports.Result { |
| // should we use s.S.view.worsspaceModFiles, and the union of their requires? |
| // (note that there are no tests where it contains more than one) |
| modURI := s.snapshot.view.gomod |
| modfh, ok := s.snapshot.files.get(modURI) |
| if !ok { |
| return nil |
| } |
| parsed, err := s.snapshot.ParseMod(s.ctx, modfh) |
| if err != nil { |
| return nil |
| } |
| reqs := parsed.File.Require |
| for _, g := range got { |
| for _, req := range reqs { |
| if strings.HasPrefix(g.res.Import.ImportPath, req.Syntax.Token[1]) { |
| return g.res |
| } |
| } |
| } |
| return nil |
| } |
| |
| func commonpref(filename string, path string) int { |
| k := 0 |
| for ; k < len(filename) && k < len(path) && filename[k] == path[k]; k++ { |
| } |
| return k |
| } |
| |
| func satisfies(pkg *symbols.Package, missing map[string]bool) bool { |
| syms := make(map[string]bool) |
| for _, x := range pkg.Symbols { |
| for _, s := range x { |
| syms[s.Name] = true |
| } |
| } |
| for k := range missing { |
| if !syms[k] { |
| return false |
| } |
| } |
| return true |
| } |
| |
| // does pkgPath potentially satisfy a missing reference? |
| func missingWants(missing imports.References, pkgPath metadata.PackageName) bool { |
| for k := range missing { |
| if string(k) == string(pkgPath) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // If there are both deprecated and undprecated ones |
| // then return only the undeprecated one |
| func preferUndeprecated(got []*result) []*result { |
| var ok []*result |
| for _, g := range got { |
| if !g.deprecated { |
| ok = append(ok, g) |
| } |
| } |
| if len(ok) > 0 { |
| return ok |
| } |
| return got |
| } |