-
Notifications
You must be signed in to change notification settings - Fork 2.3k
/
Copy pathcomment.go
297 lines (272 loc) · 9.04 KB
/
comment.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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
// Copyright 2022 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 golang
import (
"context"
"errors"
"fmt"
"go/ast"
"go/doc/comment"
"go/token"
"go/types"
pathpkg "path"
"slices"
"strings"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/gopls/internal/util/astutil"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/gopls/internal/util/safetoken"
)
var errNoCommentReference = errors.New("no comment reference found")
// DocCommentToMarkdown converts the text of a [doc comment] to Markdown.
//
// TODO(adonovan): provide a package (or file imports) as context for
// proper rendering of doc links; see [newDocCommentParser] and golang/go#61677.
//
// [doc comment]: https://go.dev/doc/comment
func DocCommentToMarkdown(text string, options *settings.Options) string {
var parser comment.Parser
doc := parser.Parse(text)
var printer comment.Printer
// The default produces {#Hdr-...} tags for headings.
// vscode displays thems, which is undesirable.
// The godoc for comment.Printer says the tags
// avoid a security problem.
printer.HeadingID = func(*comment.Heading) string { return "" }
printer.DocLinkURL = func(link *comment.DocLink) string {
msg := fmt.Sprintf("https://%s/%s", options.LinkTarget, link.ImportPath)
if link.Name != "" {
msg += "#"
if link.Recv != "" {
msg += link.Recv + "."
}
msg += link.Name
}
return msg
}
return string(printer.Markdown(doc))
}
// docLinkDefinition finds the definition of the doc link in comments at pos.
// If there is no reference at pos, returns errNoCommentReference.
func docLinkDefinition(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, pos token.Pos) ([]protocol.Location, error) {
obj, _, err := parseDocLink(pkg, pgf, pos)
if err != nil {
return nil, err
}
loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj))
if err != nil {
return nil, err
}
return []protocol.Location{loc}, nil
}
// parseDocLink parses a doc link in a comment such as [fmt.Println]
// and returns the symbol at pos, along with the link's start position.
func parseDocLink(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (types.Object, protocol.Range, error) {
var comment *ast.Comment
for _, cg := range pgf.File.Comments {
for _, c := range cg.List {
if c.Pos() <= pos && pos <= c.End() {
comment = c
break
}
}
if comment != nil {
break
}
}
if comment == nil {
return nil, protocol.Range{}, errNoCommentReference
}
// The canonical parsing algorithm is defined by go/doc/comment, but
// unfortunately its API provides no way to reliably reconstruct the
// position of each doc link from the parsed result.
line := safetoken.Line(pgf.Tok, pos)
var start, end token.Pos
start = max(pgf.Tok.LineStart(line), comment.Pos())
if line < pgf.Tok.LineCount() && pgf.Tok.LineStart(line+1) < comment.End() {
end = pgf.Tok.LineStart(line + 1)
} else {
end = comment.End()
}
offsetStart, offsetEnd, err := safetoken.Offsets(pgf.Tok, start, end)
if err != nil {
return nil, protocol.Range{}, err
}
text := string(pgf.Src[offsetStart:offsetEnd])
lineOffset := int(pos - start)
for _, idx := range docLinkRegex.FindAllStringSubmatchIndex(text, -1) {
// The [idx[2], idx[3]) identifies the first submatch,
// which is the reference name in the doc link (sans '*').
// e.g. The "[fmt.Println]" reference name is "fmt.Println".
if !(idx[2] <= lineOffset && lineOffset < idx[3]) {
continue
}
p := lineOffset - idx[2]
name := text[idx[2]:idx[3]]
i := strings.LastIndexByte(name, '.')
for i != -1 {
if p > i {
break
}
name = name[:i]
i = strings.LastIndexByte(name, '.')
}
obj := lookupDocLinkSymbol(pkg, pgf, name)
if obj == nil {
return nil, protocol.Range{}, errNoCommentReference
}
namePos := start + token.Pos(idx[2]+i+1)
rng, err := pgf.PosRange(namePos, namePos+token.Pos(len(obj.Name())))
if err != nil {
return nil, protocol.Range{}, err
}
return obj, rng, nil
}
return nil, protocol.Range{}, errNoCommentReference
}
// lookupDocLinkSymbol returns the symbol denoted by a doc link such
// as "fmt.Println" or "bytes.Buffer.Write" in the specified file.
func lookupDocLinkSymbol(pkg *cache.Package, pgf *parsego.File, name string) types.Object {
scope := pkg.Types().Scope()
prefix, suffix, _ := strings.Cut(name, ".")
// Try treating the prefix as a package name,
// allowing for non-renaming and renaming imports.
fileScope := pkg.TypesInfo().Scopes[pgf.File]
if fileScope == nil {
// This is theoretically possible if pgf is a GoFile but not a
// CompiledGoFile. However, we do not know how to produce such a package
// without using an external GoPackagesDriver.
// See if this is the source of golang/go#70635
if slices.Contains(pkg.CompiledGoFiles(), pgf) {
bug.Reportf("missing file scope for compiled file")
} else {
bug.Reportf("missing file scope for non-compiled file")
}
return nil
}
pkgname, ok := fileScope.Lookup(prefix).(*types.PkgName) // ok => prefix is imported name
if !ok {
// Handle renaming import, e.g.
// [path.Join] after import pathpkg "path".
// (Should we look at all files of the package?)
for _, imp := range pgf.File.Imports {
pkgname2 := pkg.TypesInfo().PkgNameOf(imp)
if pkgname2 != nil && pkgname2.Imported().Name() == prefix {
pkgname = pkgname2
break
}
}
}
if pkgname != nil {
scope = pkgname.Imported().Scope()
if suffix == "" {
return pkgname // not really a valid doc link
}
name = suffix
}
// TODO(adonovan): try searching the forward closure for packages
// that define the symbol but are not directly imported;
// see https://github.com/golang/go/issues/61677
// Type.Method?
recv, method, ok := strings.Cut(name, ".")
if ok {
obj, ok := scope.Lookup(recv).(*types.TypeName)
if !ok {
return nil
}
t, ok := obj.Type().(*types.Named)
if !ok {
return nil
}
for i := 0; i < t.NumMethods(); i++ {
m := t.Method(i)
if m.Name() == method {
return m
}
}
return nil
}
// package-level symbol
return scope.Lookup(name)
}
// newDocCommentParser returns a function that parses [doc comments],
// with context for Doc Links supplied by the specified package.
//
// Imported symbols are rendered using the import mapping for the file
// that encloses fileNode.
//
// The resulting function is not concurrency safe.
//
// See issue #61677 for how this might be generalized to support
// correct contextual parsing of doc comments in Hover too.
//
// [doc comment]: https://go.dev/doc/comment
func newDocCommentParser(pkg *cache.Package) func(fileNode ast.Node, text string) *comment.Doc {
var currentFilePos token.Pos // pos whose enclosing file's import mapping should be used
parser := &comment.Parser{
LookupPackage: func(name string) (importPath string, ok bool) {
for _, f := range pkg.Syntax() {
// Different files in the same package have
// different import mappings. Use the provided
// syntax node to find the correct file.
if astutil.NodeContains(f, currentFilePos) {
// First try each actual imported package name.
for _, imp := range f.Imports {
pkgName := pkg.TypesInfo().PkgNameOf(imp)
if pkgName != nil && pkgName.Name() == name {
return pkgName.Imported().Path(), true
}
}
// Then try each imported package's declared name,
// as some packages are typically imported under a
// non-default name (e.g. pathpkg "path") but
// may be referred to in doc links using their
// canonical name.
for _, imp := range f.Imports {
pkgName := pkg.TypesInfo().PkgNameOf(imp)
if pkgName != nil && pkgName.Imported().Name() == name {
return pkgName.Imported().Path(), true
}
}
// Finally try matching the last segment of each import
// path imported by any file in the package, as the
// doc comment may appear in a different file from the
// import.
//
// Ideally we would look up the DepsByPkgPath value
// (a PackageID) in the metadata graph and use the
// package's declared name instead of this heuristic,
// but we don't have access to the graph here.
for path := range pkg.Metadata().DepsByPkgPath {
if pathpkg.Base(trimVersionSuffix(string(path))) == name {
return string(path), true
}
}
break
}
}
return "", false
},
LookupSym: func(recv, name string) (ok bool) {
// package-level decl?
if recv == "" {
return pkg.Types().Scope().Lookup(name) != nil
}
// method?
tname, ok := pkg.Types().Scope().Lookup(recv).(*types.TypeName)
if !ok {
return false
}
m, _, _ := types.LookupFieldOrMethod(tname.Type(), true, pkg.Types(), name)
return is[*types.Func](m)
},
}
return func(fileNode ast.Node, text string) *comment.Doc {
currentFilePos = fileNode.Pos()
return parser.Parse(text)
}
}