github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/mod/diagnostics.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package mod provides core features related to go.mod file 6 // handling for use by Go editors and tools. 7 package mod 8 9 import ( 10 "context" 11 "fmt" 12 "regexp" 13 "strings" 14 "unicode" 15 16 "golang.org/x/mod/modfile" 17 "golang.org/x/mod/module" 18 "github.com/april1989/origin-go-tools/internal/event" 19 "github.com/april1989/origin-go-tools/internal/lsp/debug/tag" 20 "github.com/april1989/origin-go-tools/internal/lsp/protocol" 21 "github.com/april1989/origin-go-tools/internal/lsp/source" 22 "github.com/april1989/origin-go-tools/internal/span" 23 ) 24 25 func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) { 26 uri := snapshot.View().ModFile() 27 if uri == "" { 28 return nil, nil 29 } 30 31 ctx, done := event.Start(ctx, "mod.Diagnostics", tag.URI.Of(uri)) 32 defer done() 33 34 fh, err := snapshot.GetFile(ctx, uri) 35 if err != nil { 36 return nil, err 37 } 38 tidied, err := snapshot.ModTidy(ctx, fh) 39 if err == source.ErrTmpModfileUnsupported { 40 return nil, nil 41 } 42 reports := map[source.VersionedFileIdentity][]*source.Diagnostic{ 43 fh.VersionedFileIdentity(): {}, 44 } 45 if err != nil { 46 return nil, err 47 } 48 for _, e := range tidied.Errors { 49 diag := &source.Diagnostic{ 50 Message: e.Message, 51 Range: e.Range, 52 Source: e.Category, 53 } 54 if e.Category == "syntax" { 55 diag.Severity = protocol.SeverityError 56 } else { 57 diag.Severity = protocol.SeverityWarning 58 } 59 fh, err := snapshot.GetFile(ctx, e.URI) 60 if err != nil { 61 return nil, err 62 } 63 reports[fh.VersionedFileIdentity()] = append(reports[fh.VersionedFileIdentity()], diag) 64 } 65 return reports, nil 66 } 67 68 var moduleAtVersionRe = regexp.MustCompile(`^(?P<module>.*)@(?P<version>.*)$`) 69 70 // ExtractGoCommandError tries to parse errors that come from the go command 71 // and shape them into go.mod diagnostics. 72 func ExtractGoCommandError(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, loadErr error) (*source.Diagnostic, error) { 73 // We try to match module versions in error messages. Some examples: 74 // 75 // example.com@v1.2.2: reading example.com/@v/v1.2.2.mod: no such file or directory 76 // 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 77 // 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 78 // 79 // We split on colons and whitespace, and attempt to match on something 80 // that matches module@version. If we're able to find a match, we try to 81 // find anything that matches it in the go.mod file. 82 var v module.Version 83 fields := strings.FieldsFunc(loadErr.Error(), func(r rune) bool { 84 return unicode.IsSpace(r) || r == ':' 85 }) 86 for _, s := range fields { 87 s = strings.TrimSpace(s) 88 match := moduleAtVersionRe.FindStringSubmatch(s) 89 if match == nil || len(match) < 3 { 90 continue 91 } 92 v.Path = match[1] 93 v.Version = match[2] 94 if err := module.Check(v.Path, v.Version); err == nil { 95 break 96 } 97 } 98 pm, err := snapshot.ParseMod(ctx, fh) 99 if err != nil { 100 return nil, err 101 } 102 toDiagnostic := func(line *modfile.Line) (*source.Diagnostic, error) { 103 rng, err := rangeFromPositions(fh.URI(), pm.Mapper, line.Start, line.End) 104 if err != nil { 105 return nil, err 106 } 107 return &source.Diagnostic{ 108 Message: loadErr.Error(), 109 Range: rng, 110 Severity: protocol.SeverityError, 111 }, nil 112 } 113 // Check if there are any require, exclude, or replace statements that 114 // match this module version. 115 for _, req := range pm.File.Require { 116 if req.Mod != v { 117 continue 118 } 119 return toDiagnostic(req.Syntax) 120 } 121 for _, ex := range pm.File.Exclude { 122 if ex.Mod != v { 123 continue 124 } 125 return toDiagnostic(ex.Syntax) 126 } 127 for _, rep := range pm.File.Replace { 128 if rep.New != v && rep.Old != v { 129 continue 130 } 131 return toDiagnostic(rep.Syntax) 132 } 133 // No match for the module path was found in the go.mod file. 134 // Show the error on the module declaration, if one exists. 135 if pm.File.Module == nil { 136 return nil, fmt.Errorf("no module declaration in %s", fh.URI()) 137 } 138 return toDiagnostic(pm.File.Module.Syntax) 139 } 140 141 func rangeFromPositions(uri span.URI, m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) { 142 toPoint := func(offset int) (span.Point, error) { 143 l, c, err := m.Converter.ToPosition(offset) 144 if err != nil { 145 return span.Point{}, err 146 } 147 return span.NewPoint(l, c, offset), nil 148 } 149 start, err := toPoint(s.Byte) 150 if err != nil { 151 return protocol.Range{}, err 152 } 153 end, err := toPoint(e.Byte) 154 if err != nil { 155 return protocol.Range{}, err 156 } 157 return m.Range(span.New(uri, start, end)) 158 }