// 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" "fmt" "os" "path/filepath" "regexp" "strconv" "strings" "golang.org/x/mod/modfile" "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 ( SyntaxError = "syntax" ) type parseModHandle struct { handle *memoize.Handle mod, sum source.FileHandle } type parseModData struct { memoize.NoCopy parsed *modfile.File m *protocol.ColumnMapper // parseErrors refers to syntax errors found in the go.mod file. parseErrors []source.Error // err is any error encountered while parsing the file. err error } func (mh *parseModHandle) Mod() source.FileHandle { return mh.mod } func (mh *parseModHandle) Sum() source.FileHandle { return mh.sum } func (mh *parseModHandle) Parse(ctx context.Context, s source.Snapshot) (*modfile.File, *protocol.ColumnMapper, []source.Error, error) { v, err := mh.handle.Get(ctx, s.(*snapshot)) if err != nil { return nil, nil, nil, err } data := v.(*parseModData) return data.parsed, data.m, data.parseErrors, data.err } func (s *snapshot) ParseModHandle(ctx context.Context, modFH source.FileHandle) (source.ParseModHandle, error) { if handle := s.getModHandle(modFH.URI()); handle != nil { return handle, nil } h := s.view.session.cache.store.Bind(modFH.Identity().String(), func(ctx context.Context, _ memoize.Arg) interface{} { _, done := event.Start(ctx, "cache.ParseModHandle", tag.URI.Of(modFH.URI())) defer done() contents, err := modFH.Read() if err != nil { return &parseModData{err: err} } m := &protocol.ColumnMapper{ URI: modFH.URI(), Converter: span.NewContentConverter(modFH.URI().Filename(), contents), Content: contents, } parsed, err := modfile.Parse(modFH.URI().Filename(), contents, nil) if err != nil { parseErr, _ := extractModParseErrors(modFH.URI(), m, err, contents) var parseErrors []source.Error if parseErr != nil { parseErrors = append(parseErrors, *parseErr) } return &parseModData{ parseErrors: parseErrors, err: err, } } return &parseModData{ parsed: parsed, m: m, } }) // Get the go.sum file, either from the snapshot or directly from the // cache. Avoid (*snapshot).GetFile here, as we don't want to add // nonexistent file handles to the snapshot if the file does not exist. sumURI := span.URIFromPath(sumFilename(modFH.URI())) sumFH := s.FindFile(sumURI) if sumFH == nil { fh, err := s.view.session.cache.getFile(ctx, sumURI) if err != nil && !os.IsNotExist(err) { return nil, err } if fh.err != nil && !os.IsNotExist(fh.err) { return nil, fh.err } // If the file doesn't exist, we can just keep the go.sum nil. if err != nil || fh.err != nil { sumFH = nil } else { sumFH = fh } } s.mu.Lock() defer s.mu.Unlock() s.parseModHandles[modFH.URI()] = &parseModHandle{ handle: h, mod: modFH, sum: sumFH, } return s.parseModHandles[modFH.URI()], nil } func sumFilename(modURI span.URI) string { return modURI.Filename()[:len(modURI.Filename())-len("mod")] + "sum" } // extractModParseErrors processes the raw errors returned by modfile.Parse, // extracting the filenames and line numbers that correspond to the errors. func extractModParseErrors(uri span.URI, m *protocol.ColumnMapper, parseErr error, content []byte) (*source.Error, error) { re := regexp.MustCompile(`.*:([\d]+): (.+)`) matches := re.FindStringSubmatch(strings.TrimSpace(parseErr.Error())) if len(matches) < 3 { return nil, errors.Errorf("could not parse go.mod error message: %s", parseErr) } line, err := strconv.Atoi(matches[1]) if err != nil { return nil, err } lines := strings.Split(string(content), "\n") if line > len(lines) { return nil, errors.Errorf("could not parse go.mod error message %q, line number %v out of range", content, line) } // The error returned from the modfile package only returns a line number, // so we assume that the diagnostic should be for the entire line. endOfLine := len(lines[line-1]) sOffset, err := m.Converter.ToOffset(line, 0) if err != nil { return nil, err } eOffset, err := m.Converter.ToOffset(line, endOfLine) if err != nil { return nil, err } spn := span.New(uri, span.NewPoint(line, 0, sOffset), span.NewPoint(line, endOfLine, eOffset)) rng, err := m.Range(spn) if err != nil { return nil, err } return &source.Error{ Category: SyntaxError, Message: matches[2], Range: rng, URI: uri, }, nil } // modKey is uniquely identifies cached data for `go mod why` or dependencies // to upgrade. type modKey struct { sessionID, cfg, mod, view string verb modAction } type modAction int const ( why modAction = iota upgrade ) type modWhyHandle struct { handle *memoize.Handle } type modWhyData struct { // why keeps track of the `go mod why` results for each require statement // in the go.mod file. why map[string]string err error } func (mwh *modWhyHandle) Why(ctx context.Context, s source.Snapshot) (map[string]string, error) { v, err := mwh.handle.Get(ctx, s.(*snapshot)) if err != nil { return nil, err } data := v.(*modWhyData) return data.why, data.err } func (s *snapshot) ModWhyHandle(ctx context.Context) (source.ModWhyHandle, error) { if err := s.awaitLoaded(ctx); err != nil { return nil, err } fh, err := s.GetFile(ctx, s.view.modURI) if err != nil { return nil, err } cfg := s.config(ctx) key := modKey{ sessionID: s.view.session.id, cfg: hashConfig(s.config(ctx)), mod: fh.Identity().String(), view: s.view.root.Filename(), verb: why, } h := s.view.session.cache.store.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { ctx, done := event.Start(ctx, "cache.ModWhyHandle", tag.URI.Of(fh.URI())) defer done() snapshot := arg.(*snapshot) pmh, err := snapshot.ParseModHandle(ctx, fh) if err != nil { return &modWhyData{err: err} } parsed, _, _, err := pmh.Parse(ctx, snapshot) if err != nil { return &modWhyData{err: err} } // No requires to explain. if len(parsed.Require) == 0 { return &modWhyData{} } // Run `go mod why` on all the dependencies. args := []string{"why", "-m"} for _, req := range parsed.Require { args = append(args, req.Mod.Path) } _, stdout, err := runGoCommand(ctx, cfg, pmh, snapshot.view.tmpMod, "mod", args) if err != nil { return &modWhyData{err: err} } whyList := strings.Split(stdout.String(), "\n\n") if len(whyList) != len(parsed.Require) { return &modWhyData{ err: fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(parsed.Require)), } } why := make(map[string]string, len(parsed.Require)) for i, req := range parsed.Require { why[req.Mod.Path] = whyList[i] } return &modWhyData{why: why} }) s.mu.Lock() defer s.mu.Unlock() s.modWhyHandle = &modWhyHandle{ handle: h, } return s.modWhyHandle, nil } type modUpgradeHandle struct { handle *memoize.Handle } type modUpgradeData struct { // upgrades maps modules to their latest versions. upgrades map[string]string err error } func (muh *modUpgradeHandle) Upgrades(ctx context.Context, s source.Snapshot) (map[string]string, error) { v, err := muh.handle.Get(ctx, s.(*snapshot)) if v == nil { return nil, err } data := v.(*modUpgradeData) return data.upgrades, data.err } func (s *snapshot) ModUpgradeHandle(ctx context.Context) (source.ModUpgradeHandle, error) { if err := s.awaitLoaded(ctx); err != nil { return nil, err } fh, err := s.GetFile(ctx, s.view.modURI) if err != nil { return nil, err } cfg := s.config(ctx) key := modKey{ sessionID: s.view.session.id, cfg: hashConfig(cfg), mod: fh.Identity().String(), view: s.view.root.Filename(), verb: upgrade, } h := s.view.session.cache.store.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} { ctx, done := event.Start(ctx, "cache.ModUpgradeHandle", tag.URI.Of(fh.URI())) defer done() snapshot := arg.(*snapshot) pmh, err := s.ParseModHandle(ctx, fh) if err != nil { return &modUpgradeData{err: err} } parsed, _, _, err := pmh.Parse(ctx, snapshot) if err != nil { return &modUpgradeData{err: err} } // No requires to upgrade. if len(parsed.Require) == 0 { return &modUpgradeData{} } // 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 !snapshot.view.tmpMod || containsVendor(pmh.Mod().URI()) { // Use -mod=readonly if the module contains a vendor directory // (see golang/go#38711). args = append([]string{"-mod", "readonly"}, args...) } _, stdout, err := runGoCommand(ctx, cfg, pmh, snapshot.view.tmpMod, "list", args) if err != nil { return &modUpgradeData{err: err} } upgradesList := strings.Split(stdout.String(), "\n") if len(upgradesList) <= 1 { return nil } 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] // Make sure that the format matches our expectation. if len(version) < 2 { continue } if version[0] != '[' || version[len(version)-1] != ']' { continue } latest := version[1 : len(version)-1] // remove the "[" and "]" upgrades[dep] = latest } return &modUpgradeData{ upgrades: upgrades, } }) s.mu.Lock() defer s.mu.Unlock() s.modUpgradeHandle = &modUpgradeHandle{ handle: h, } return s.modUpgradeHandle, 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() }