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