// 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. package cache import ( "context" "os" "path/filepath" "strings" "golang.org/x/mod/modfile" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/lsp/debug/tag" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/memoize" "golang.org/x/tools/internal/span" errors "golang.org/x/xerrors" ) const ( ModTidyError = "go mod tidy" SyntaxError = "syntax" ) type modKey struct { sessionID string cfg string gomod string view string } type modTidyKey struct { sessionID string cfg string gomod string imports string unsavedOverlays string view string } type modHandle struct { handle *memoize.Handle file source.FileHandle cfg *packages.Config } type modData struct { memoize.NoCopy // parsed contains the parsed contents that are used to diff with // the ideal contents. parsed *modfile.File // m is the column mapper for the original go.mod file. m *protocol.ColumnMapper // upgrades is a map of path->version that contains any upgrades for the go.mod. upgrades map[string]string // why is a map of path->explanation that contains all the "go mod why" contents // for each require statement. why map[string]string // err is any error that occurs while we are calculating the parseErrors. err error } func (mh *modHandle) String() string { return mh.File().URI().Filename() } func (mh *modHandle) File() source.FileHandle { return mh.file } func (mh *modHandle) Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error) { v := mh.handle.Get(ctx) if v == nil { return nil, nil, errors.Errorf("no parsed file for %s", mh.File().URI()) } data := v.(*modData) return data.parsed, data.m, data.err } func (mh *modHandle) Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) { v := mh.handle.Get(ctx) if v == nil { return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().URI()) } data := v.(*modData) return data.parsed, data.m, data.upgrades, data.err } func (mh *modHandle) Why(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) { v := mh.handle.Get(ctx) if v == nil { return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().URI()) } data := v.(*modData) return data.parsed, data.m, data.why, data.err } func (s *snapshot) ModHandle(ctx context.Context, modFH source.FileHandle) (source.ModHandle, error) { if err := s.awaitLoaded(ctx); err != nil { return nil, err } var sumFH source.FileHandle if s.view.sumURI != "" { var err error sumFH, err = s.GetFile(ctx, s.view.sumURI) if err != nil { return nil, err } } var ( cfg = s.config(ctx) modURI = s.view.modURI tmpMod = s.view.tmpMod ) if handle := s.getModHandle(modFH.URI()); handle != nil { return handle, nil } key := modKey{ sessionID: s.view.session.id, cfg: hashConfig(cfg), gomod: modFH.Identity().String(), view: s.view.folder.Filename(), } h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} { ctx, done := event.Start(ctx, "cache.ModHandle", tag.URI.Of(modFH.URI())) defer done() contents, err := modFH.Read() if err != nil { return &modData{ err: err, } } parsedFile, err := modfile.Parse(modFH.URI().Filename(), contents, nil) if err != nil { return &modData{ err: err, } } data := &modData{ parsed: parsedFile, m: &protocol.ColumnMapper{ URI: modFH.URI(), Converter: span.NewContentConverter(modFH.URI().Filename(), contents), Content: contents, }, } // If this go.mod file is not the view's go.mod file, or if the // -modfile flag is not supported, then we just want to parse. if modFH.URI() != modURI { return data } // Only get dependency upgrades if the go.mod file is the same as the view's. if err := dependencyUpgrades(ctx, cfg, modFH, sumFH, tmpMod, data); err != nil { return &modData{err: err} } // Only run "go mod why" if the go.mod file is the same as the view's. if err := goModWhy(ctx, cfg, modFH, sumFH, tmpMod, data); err != nil { return &modData{err: err} } return data }) s.mu.Lock() defer s.mu.Unlock() s.modHandles[modFH.URI()] = &modHandle{ handle: h, file: modFH, cfg: cfg, } return s.modHandles[modFH.URI()], nil } func goModWhy(ctx context.Context, cfg *packages.Config, modFH, sumFH source.FileHandle, tmpMod bool, data *modData) error { if len(data.parsed.Require) == 0 { return nil } // Run "go mod why" on all the dependencies. args := []string{"why", "-m"} for _, req := range data.parsed.Require { args = append(args, req.Mod.Path) } // If the -modfile flag is disabled, don't pass in a go.mod URI. if !tmpMod { modFH = nil } _, stdout, err := runGoCommand(ctx, cfg, modFH, sumFH, "mod", args) if err != nil { return err } whyList := strings.Split(stdout.String(), "\n\n") if len(whyList) != len(data.parsed.Require) { return nil } data.why = make(map[string]string, len(data.parsed.Require)) for i, req := range data.parsed.Require { data.why[req.Mod.Path] = whyList[i] } return nil } func dependencyUpgrades(ctx context.Context, cfg *packages.Config, modFH, sumFH source.FileHandle, tmpMod bool, data *modData) error { if len(data.parsed.Require) == 0 { return nil } // Run "go list -mod readonly -u -m all" to be able to see which deps can be // upgraded without modifying mod file. args := []string{"-u", "-m", "all"} if !tmpMod || containsVendor(modFH.URI()) { // Use -mod=readonly if the module contains a vendor directory // (see golang/go#38711). args = append([]string{"-mod", "readonly"}, args...) } // If the -modfile flag is disabled, don't pass in a go.mod URI. if !tmpMod { modFH = nil } _, stdout, err := runGoCommand(ctx, cfg, modFH, sumFH, "list", args) if err != nil { return err } upgradesList := strings.Split(stdout.String(), "\n") if len(upgradesList) <= 1 { return nil } data.upgrades = make(map[string]string) for _, upgrade := range upgradesList[1:] { // Example: "github.com/x/tools v1.1.0 [v1.2.0]" info := strings.Split(upgrade, " ") if len(info) < 3 { continue } dep, version := info[0], info[2] latest := version[1:] // remove the "[" latest = strings.TrimSuffix(latest, "]") // remove the "]" data.upgrades[dep] = latest } return nil } // containsVendor reports whether the module has a vendor folder. func containsVendor(modURI span.URI) bool { dir := filepath.Dir(modURI.Filename()) f, err := os.Stat(filepath.Join(dir, "vendor")) if err != nil { return false } return f.IsDir() }