|  | // 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" | 
|  | "errors" | 
|  | "fmt" | 
|  | "regexp" | 
|  | "strings" | 
|  |  | 
|  | "golang.org/x/mod/modfile" | 
|  | "golang.org/x/mod/module" | 
|  | "golang.org/x/tools/gopls/internal/file" | 
|  | "golang.org/x/tools/gopls/internal/label" | 
|  | "golang.org/x/tools/gopls/internal/protocol" | 
|  | "golang.org/x/tools/gopls/internal/protocol/command" | 
|  | "golang.org/x/tools/internal/event" | 
|  | "golang.org/x/tools/internal/memoize" | 
|  | ) | 
|  |  | 
|  | // A ParsedModule contains the results of parsing a go.mod file. | 
|  | type ParsedModule struct { | 
|  | URI         protocol.DocumentURI | 
|  | File        *modfile.File | 
|  | Mapper      *protocol.Mapper | 
|  | ParseErrors []*Diagnostic | 
|  | } | 
|  |  | 
|  | // ParseMod parses a go.mod file, using a cache. It may return partial results and an error. | 
|  | func (s *Snapshot) ParseMod(ctx context.Context, fh file.Handle) (*ParsedModule, error) { | 
|  | uri := fh.URI() | 
|  |  | 
|  | s.mu.Lock() | 
|  | entry, hit := s.parseModHandles.Get(uri) | 
|  | s.mu.Unlock() | 
|  |  | 
|  | type parseModKey file.Identity | 
|  | type parseModResult struct { | 
|  | parsed *ParsedModule | 
|  | err    error | 
|  | } | 
|  |  | 
|  | // cache miss? | 
|  | if !hit { | 
|  | promise, release := s.store.Promise(parseModKey(fh.Identity()), func(ctx context.Context, _ any) any { | 
|  | parsed, err := parseModImpl(ctx, fh) | 
|  | return parseModResult{parsed, err} | 
|  | }) | 
|  |  | 
|  | entry = promise | 
|  | s.mu.Lock() | 
|  | s.parseModHandles.Set(uri, entry, func(_, _ any) { release() }) | 
|  | s.mu.Unlock() | 
|  | } | 
|  |  | 
|  | // Await result. | 
|  | v, err := s.awaitPromise(ctx, entry) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | res := v.(parseModResult) | 
|  | return res.parsed, res.err | 
|  | } | 
|  |  | 
|  | // parseModImpl parses the go.mod file whose name and contents are in fh. | 
|  | // It may return partial results and an error. | 
|  | func parseModImpl(ctx context.Context, fh file.Handle) (*ParsedModule, error) { | 
|  | _, done := event.Start(ctx, "cache.ParseMod", label.URI.Of(fh.URI())) | 
|  | defer done() | 
|  |  | 
|  | contents, err := fh.Content() | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | m := protocol.NewMapper(fh.URI(), contents) | 
|  | file, parseErr := modfile.Parse(fh.URI().Path(), contents, nil) | 
|  | // Attempt to convert the error to a standardized parse error. | 
|  | var parseErrors []*Diagnostic | 
|  | if parseErr != nil { | 
|  | mfErrList, ok := parseErr.(modfile.ErrorList) | 
|  | if !ok { | 
|  | return nil, fmt.Errorf("unexpected parse error type %v", parseErr) | 
|  | } | 
|  | for _, mfErr := range mfErrList { | 
|  | rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | parseErrors = append(parseErrors, &Diagnostic{ | 
|  | URI:      fh.URI(), | 
|  | Range:    rng, | 
|  | Severity: protocol.SeverityError, | 
|  | Source:   ParseError, | 
|  | Message:  mfErr.Err.Error(), | 
|  | }) | 
|  | } | 
|  | } | 
|  | return &ParsedModule{ | 
|  | URI:         fh.URI(), | 
|  | Mapper:      m, | 
|  | File:        file, | 
|  | ParseErrors: parseErrors, | 
|  | }, parseErr | 
|  | } | 
|  |  | 
|  | // A ParsedWorkFile contains the results of parsing a go.work file. | 
|  | type ParsedWorkFile struct { | 
|  | URI         protocol.DocumentURI | 
|  | File        *modfile.WorkFile | 
|  | Mapper      *protocol.Mapper | 
|  | ParseErrors []*Diagnostic | 
|  | } | 
|  |  | 
|  | // ParseWork parses a go.work file, using a cache. It may return partial results and an error. | 
|  | // TODO(adonovan): move to new work.go file. | 
|  | func (s *Snapshot) ParseWork(ctx context.Context, fh file.Handle) (*ParsedWorkFile, error) { | 
|  | uri := fh.URI() | 
|  |  | 
|  | s.mu.Lock() | 
|  | entry, hit := s.parseWorkHandles.Get(uri) | 
|  | s.mu.Unlock() | 
|  |  | 
|  | type parseWorkKey file.Identity | 
|  | type parseWorkResult struct { | 
|  | parsed *ParsedWorkFile | 
|  | err    error | 
|  | } | 
|  |  | 
|  | // cache miss? | 
|  | if !hit { | 
|  | handle, release := s.store.Promise(parseWorkKey(fh.Identity()), func(ctx context.Context, _ any) any { | 
|  | parsed, err := parseWorkImpl(ctx, fh) | 
|  | return parseWorkResult{parsed, err} | 
|  | }) | 
|  |  | 
|  | entry = handle | 
|  | s.mu.Lock() | 
|  | s.parseWorkHandles.Set(uri, entry, func(_, _ any) { release() }) | 
|  | s.mu.Unlock() | 
|  | } | 
|  |  | 
|  | // Await result. | 
|  | v, err := s.awaitPromise(ctx, entry) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | res := v.(parseWorkResult) | 
|  | return res.parsed, res.err | 
|  | } | 
|  |  | 
|  | // parseWorkImpl parses a go.work file. It may return partial results and an error. | 
|  | func parseWorkImpl(ctx context.Context, fh file.Handle) (*ParsedWorkFile, error) { | 
|  | _, done := event.Start(ctx, "cache.ParseWork", label.URI.Of(fh.URI())) | 
|  | defer done() | 
|  |  | 
|  | content, err := fh.Content() | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | m := protocol.NewMapper(fh.URI(), content) | 
|  | file, parseErr := modfile.ParseWork(fh.URI().Path(), content, nil) | 
|  | // Attempt to convert the error to a standardized parse error. | 
|  | var parseErrors []*Diagnostic | 
|  | if parseErr != nil { | 
|  | mfErrList, ok := parseErr.(modfile.ErrorList) | 
|  | if !ok { | 
|  | return nil, fmt.Errorf("unexpected parse error type %v", parseErr) | 
|  | } | 
|  | for _, mfErr := range mfErrList { | 
|  | rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | parseErrors = append(parseErrors, &Diagnostic{ | 
|  | URI:      fh.URI(), | 
|  | Range:    rng, | 
|  | Severity: protocol.SeverityError, | 
|  | Source:   ParseError, | 
|  | Message:  mfErr.Err.Error(), | 
|  | }) | 
|  | } | 
|  | } | 
|  | return &ParsedWorkFile{ | 
|  | URI:         fh.URI(), | 
|  | Mapper:      m, | 
|  | File:        file, | 
|  | ParseErrors: parseErrors, | 
|  | }, parseErr | 
|  | } | 
|  |  | 
|  | // ModWhy returns the "go mod why" result for each module named in a | 
|  | // require statement in the go.mod file. | 
|  | // TODO(adonovan): move to new mod_why.go file. | 
|  | func (s *Snapshot) ModWhy(ctx context.Context, fh file.Handle) (map[string]string, error) { | 
|  | uri := fh.URI() | 
|  |  | 
|  | if s.FileKind(fh) != file.Mod { | 
|  | return nil, fmt.Errorf("%s is not a go.mod file", uri) | 
|  | } | 
|  |  | 
|  | s.mu.Lock() | 
|  | entry, hit := s.modWhyHandles.Get(uri) | 
|  | s.mu.Unlock() | 
|  |  | 
|  | type modWhyResult struct { | 
|  | why map[string]string | 
|  | err error | 
|  | } | 
|  |  | 
|  | // cache miss? | 
|  | if !hit { | 
|  | handle := memoize.NewPromise("modWhy", func(ctx context.Context, arg any) any { | 
|  | why, err := modWhyImpl(ctx, arg.(*Snapshot), fh) | 
|  | return modWhyResult{why, err} | 
|  | }) | 
|  |  | 
|  | entry = handle | 
|  | s.mu.Lock() | 
|  | s.modWhyHandles.Set(uri, entry, nil) | 
|  | s.mu.Unlock() | 
|  | } | 
|  |  | 
|  | // Await result. | 
|  | v, err := s.awaitPromise(ctx, entry) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | res := v.(modWhyResult) | 
|  | return res.why, res.err | 
|  | } | 
|  |  | 
|  | // modWhyImpl returns the result of "go mod why -m" on the specified go.mod file. | 
|  | func modWhyImpl(ctx context.Context, snapshot *Snapshot, fh file.Handle) (map[string]string, error) { | 
|  | ctx, done := event.Start(ctx, "cache.ModWhy", label.URI.Of(fh.URI())) | 
|  | defer done() | 
|  |  | 
|  | pm, err := snapshot.ParseMod(ctx, fh) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | // No requires to explain. | 
|  | if len(pm.File.Require) == 0 { | 
|  | return nil, nil // empty result | 
|  | } | 
|  | // Run `go mod why` on all the dependencies. | 
|  | args := []string{"why", "-m"} | 
|  | for _, req := range pm.File.Require { | 
|  | args = append(args, req.Mod.Path) | 
|  | } | 
|  | inv, cleanupInvocation, err := snapshot.GoCommandInvocation(NoNetwork, fh.URI().DirPath(), "mod", args) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | defer cleanupInvocation() | 
|  | stdout, err := snapshot.View().GoCommandRunner().Run(ctx, *inv) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | whyList := strings.Split(stdout.String(), "\n\n") | 
|  | if len(whyList) != len(pm.File.Require) { | 
|  | return nil, fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require)) | 
|  | } | 
|  | why := make(map[string]string, len(pm.File.Require)) | 
|  | for i, req := range pm.File.Require { | 
|  | why[req.Mod.Path] = whyList[i] | 
|  | } | 
|  | return why, nil | 
|  | } | 
|  |  | 
|  | // extractGoCommandErrors tries to parse errors that come from the go command | 
|  | // and shape them into go.mod diagnostics. | 
|  | // TODO: rename this to 'load errors' | 
|  | func (s *Snapshot) extractGoCommandErrors(ctx context.Context, goCmdError error) []*Diagnostic { | 
|  | if goCmdError == nil { | 
|  | return nil | 
|  | } | 
|  |  | 
|  | type locatedErr struct { | 
|  | loc protocol.Location | 
|  | msg string | 
|  | } | 
|  | diagLocations := map[*ParsedModule]locatedErr{} | 
|  | backupDiagLocations := map[*ParsedModule]locatedErr{} | 
|  |  | 
|  | // If moduleErrs is non-nil, go command errors are scoped to specific | 
|  | // modules. | 
|  | var moduleErrs *moduleErrorMap | 
|  | _ = errors.As(goCmdError, &moduleErrs) | 
|  |  | 
|  | // Match the error against all the mod files in the workspace. | 
|  | for _, uri := range s.View().ModFiles() { | 
|  | fh, err := s.ReadFile(ctx, uri) | 
|  | if err != nil { | 
|  | event.Error(ctx, "getting modfile for Go command error", err) | 
|  | continue | 
|  | } | 
|  | pm, err := s.ParseMod(ctx, fh) | 
|  | if err != nil { | 
|  | // Parsing errors are reported elsewhere | 
|  | return nil | 
|  | } | 
|  | var msgs []string // error messages to consider | 
|  | if moduleErrs != nil { | 
|  | if pm.File.Module != nil { | 
|  | for _, mes := range moduleErrs.errs[pm.File.Module.Mod.Path] { | 
|  | msgs = append(msgs, mes.Error()) | 
|  | } | 
|  | } | 
|  | } else { | 
|  | msgs = append(msgs, goCmdError.Error()) | 
|  | } | 
|  | for _, msg := range msgs { | 
|  | if strings.Contains(goCmdError.Error(), "errors parsing go.mod") { | 
|  | // The go command emits parse errors for completely invalid go.mod files. | 
|  | // Those are reported by our own diagnostics and can be ignored here. | 
|  | // As of writing, we are not aware of any other errors that include | 
|  | // file/position information, so don't even try to find it. | 
|  | continue | 
|  | } | 
|  | loc, found, err := s.matchErrorToModule(pm, msg) | 
|  | if err != nil { | 
|  | event.Error(ctx, "matching error to module", err) | 
|  | continue | 
|  | } | 
|  | le := locatedErr{ | 
|  | loc: loc, | 
|  | msg: msg, | 
|  | } | 
|  | if found { | 
|  | diagLocations[pm] = le | 
|  | } else { | 
|  | backupDiagLocations[pm] = le | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // If we didn't find any good matches, assign diagnostics to all go.mod files. | 
|  | if len(diagLocations) == 0 { | 
|  | diagLocations = backupDiagLocations | 
|  | } | 
|  |  | 
|  | var srcErrs []*Diagnostic | 
|  | for pm, le := range diagLocations { | 
|  | diag, err := s.goCommandDiagnostic(pm, le.loc, le.msg) | 
|  | if err != nil { | 
|  | event.Error(ctx, "building go command diagnostic", err) | 
|  | continue | 
|  | } | 
|  | srcErrs = append(srcErrs, diag) | 
|  | } | 
|  | return srcErrs | 
|  | } | 
|  |  | 
|  | var moduleVersionInErrorRe = regexp.MustCompile(`[:\s]([+-._~0-9A-Za-z]+)@([+-._~0-9A-Za-z]+)[:\s]`) | 
|  |  | 
|  | // matchErrorToModule matches a go command error message to a go.mod file. | 
|  | // Some examples: | 
|  | // | 
|  | //	example.com@v1.2.2: reading example.com/@v/v1.2.2.mod: no such file or directory | 
|  | //	go: github.com/cockroachdb/apd/v2@v2.0.72: reading github.com/cockroachdb/apd/go.mod at revision v2.0.72: unknown revision v2.0.72 | 
|  | //	go: example.com@v1.2.3 requires\n\trandom.org@v1.2.3: parsing go.mod:\n\tmodule declares its path as: bob.org\n\tbut was required as: random.org | 
|  | // | 
|  | // It returns the location of a reference to the one of the modules and true | 
|  | // if one exists. If none is found it returns a fallback location and false. | 
|  | func (s *Snapshot) matchErrorToModule(pm *ParsedModule, goCmdError string) (protocol.Location, bool, error) { | 
|  | var reference *modfile.Line | 
|  | matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1) | 
|  |  | 
|  | for i := len(matches) - 1; i >= 0; i-- { | 
|  | ver := module.Version{Path: matches[i][1], Version: matches[i][2]} | 
|  | if err := module.Check(ver.Path, ver.Version); err != nil { | 
|  | continue | 
|  | } | 
|  | reference = findModuleReference(pm.File, ver) | 
|  | if reference != nil { | 
|  | break | 
|  | } | 
|  | } | 
|  |  | 
|  | if reference == nil { | 
|  | // No match for the module path was found in the go.mod file. | 
|  | // Show the error on the module declaration, if one exists, or | 
|  | // just the first line of the file. | 
|  | var start, end int | 
|  | if pm.File.Module != nil && pm.File.Module.Syntax != nil { | 
|  | syntax := pm.File.Module.Syntax | 
|  | start, end = syntax.Start.Byte, syntax.End.Byte | 
|  | } | 
|  | loc, err := pm.Mapper.OffsetLocation(start, end) | 
|  | return loc, false, err | 
|  | } | 
|  |  | 
|  | loc, err := pm.Mapper.OffsetLocation(reference.Start.Byte, reference.End.Byte) | 
|  | return loc, true, err | 
|  | } | 
|  |  | 
|  | // goCommandDiagnostic creates a diagnostic for a given go command error. | 
|  | func (s *Snapshot) goCommandDiagnostic(pm *ParsedModule, loc protocol.Location, goCmdError string) (*Diagnostic, error) { | 
|  | matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1) | 
|  | var innermost *module.Version | 
|  | for i := len(matches) - 1; i >= 0; i-- { | 
|  | ver := module.Version{Path: matches[i][1], Version: matches[i][2]} | 
|  | if err := module.Check(ver.Path, ver.Version); err != nil { | 
|  | continue | 
|  | } | 
|  | innermost = &ver | 
|  | break | 
|  | } | 
|  |  | 
|  | switch { | 
|  | case strings.Contains(goCmdError, "inconsistent vendoring"): | 
|  | cmd := command.NewVendorCommand("Run go mod vendor", command.URIArg{URI: pm.URI}) | 
|  | return &Diagnostic{ | 
|  | URI:      pm.URI, | 
|  | Range:    loc.Range, | 
|  | Severity: protocol.SeverityError, | 
|  | Source:   ListError, | 
|  | Message: `Inconsistent vendoring detected. Please re-run "go mod vendor". | 
|  | See https://github.com/golang/go/issues/39164 for more detail on this issue.`, | 
|  | SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)}, | 
|  | }, nil | 
|  |  | 
|  | case strings.Contains(goCmdError, "updates to go.sum needed"), strings.Contains(goCmdError, "missing go.sum entry"): | 
|  | var args []protocol.DocumentURI | 
|  | args = append(args, s.View().ModFiles()...) | 
|  | tidyCmd := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: args}) | 
|  | updateCmd := command.NewUpdateGoSumCommand("Update go.sum", command.URIArgs{URIs: args}) | 
|  | msg := "go.sum is out of sync with go.mod. Please update it by applying the quick fix." | 
|  | if innermost != nil { | 
|  | msg = fmt.Sprintf("go.sum is out of sync with go.mod: entry for %v is missing. Please updating it by applying the quick fix.", innermost) | 
|  | } | 
|  | return &Diagnostic{ | 
|  | URI:      pm.URI, | 
|  | Range:    loc.Range, | 
|  | Severity: protocol.SeverityError, | 
|  | Source:   ListError, | 
|  | Message:  msg, | 
|  | SuggestedFixes: []SuggestedFix{ | 
|  | SuggestedFixFromCommand(tidyCmd, protocol.QuickFix), | 
|  | SuggestedFixFromCommand(updateCmd, protocol.QuickFix), | 
|  | }, | 
|  | }, nil | 
|  | case strings.Contains(goCmdError, "disabled by GOPROXY=off") && innermost != nil: | 
|  | title := fmt.Sprintf("Download %v@%v", innermost.Path, innermost.Version) | 
|  | cmd := command.NewAddDependencyCommand(title, command.DependencyArgs{ | 
|  | URI:        pm.URI, | 
|  | AddRequire: false, | 
|  | GoCmdArgs:  []string{fmt.Sprintf("%v@%v", innermost.Path, innermost.Version)}, | 
|  | }) | 
|  | return &Diagnostic{ | 
|  | URI:            pm.URI, | 
|  | Range:          loc.Range, | 
|  | Severity:       protocol.SeverityError, | 
|  | Message:        fmt.Sprintf("%v@%v has not been downloaded", innermost.Path, innermost.Version), | 
|  | Source:         ListError, | 
|  | SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)}, | 
|  | }, nil | 
|  | default: | 
|  | return &Diagnostic{ | 
|  | URI:      pm.URI, | 
|  | Range:    loc.Range, | 
|  | Severity: protocol.SeverityError, | 
|  | Source:   ListError, | 
|  | Message:  goCmdError, | 
|  | }, nil | 
|  | } | 
|  | } | 
|  |  | 
|  | func findModuleReference(mf *modfile.File, ver module.Version) *modfile.Line { | 
|  | for _, req := range mf.Require { | 
|  | if req.Mod == ver { | 
|  | return req.Syntax | 
|  | } | 
|  | } | 
|  | for _, ex := range mf.Exclude { | 
|  | if ex.Mod == ver { | 
|  | return ex.Syntax | 
|  | } | 
|  | } | 
|  | for _, rep := range mf.Replace { | 
|  | if rep.New == ver || rep.Old == ver { | 
|  | return rep.Syntax | 
|  | } | 
|  | } | 
|  | return nil | 
|  | } |