golang.org/x/tools/gopls@v0.15.3/internal/server/link.go (about) 1 // Copyright 2018 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 server 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "go/ast" 12 "go/token" 13 "net/url" 14 "regexp" 15 "strings" 16 "sync" 17 18 "golang.org/x/mod/modfile" 19 "golang.org/x/tools/gopls/internal/cache" 20 "golang.org/x/tools/gopls/internal/cache/metadata" 21 "golang.org/x/tools/gopls/internal/cache/parsego" 22 "golang.org/x/tools/gopls/internal/file" 23 "golang.org/x/tools/gopls/internal/golang" 24 "golang.org/x/tools/gopls/internal/protocol" 25 "golang.org/x/tools/gopls/internal/util/safetoken" 26 "golang.org/x/tools/internal/event" 27 "golang.org/x/tools/internal/event/tag" 28 ) 29 30 func (s *server) DocumentLink(ctx context.Context, params *protocol.DocumentLinkParams) (links []protocol.DocumentLink, err error) { 31 ctx, done := event.Start(ctx, "lsp.Server.documentLink") 32 defer done() 33 34 fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) 35 if err != nil { 36 return nil, err 37 } 38 defer release() 39 40 switch snapshot.FileKind(fh) { 41 case file.Mod: 42 links, err = modLinks(ctx, snapshot, fh) 43 case file.Go: 44 links, err = goLinks(ctx, snapshot, fh) 45 } 46 // Don't return errors for document links. 47 if err != nil { 48 event.Error(ctx, "failed to compute document links", err, tag.URI.Of(fh.URI())) 49 return nil, nil // empty result 50 } 51 return links, nil // may be empty (for other file types) 52 } 53 54 func modLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.DocumentLink, error) { 55 pm, err := snapshot.ParseMod(ctx, fh) 56 if err != nil { 57 return nil, err 58 } 59 60 var links []protocol.DocumentLink 61 for _, req := range pm.File.Require { 62 if req.Syntax == nil { 63 continue 64 } 65 // See golang/go#36998: don't link to modules matching GOPRIVATE. 66 if snapshot.IsGoPrivatePath(req.Mod.Path) { 67 continue 68 } 69 dep := []byte(req.Mod.Path) 70 start, end := req.Syntax.Start.Byte, req.Syntax.End.Byte 71 i := bytes.Index(pm.Mapper.Content[start:end], dep) 72 if i == -1 { 73 continue 74 } 75 // Shift the start position to the location of the 76 // dependency within the require statement. 77 target := cache.BuildLink(snapshot.Options().LinkTarget, "mod/"+req.Mod.String(), "") 78 l, err := toProtocolLink(pm.Mapper, target, start+i, start+i+len(dep)) 79 if err != nil { 80 return nil, err 81 } 82 links = append(links, l) 83 } 84 // TODO(ridersofrohan): handle links for replace and exclude directives. 85 if syntax := pm.File.Syntax; syntax == nil { 86 return links, nil 87 } 88 89 // Get all the links that are contained in the comments of the file. 90 urlRegexp := snapshot.Options().URLRegexp 91 for _, expr := range pm.File.Syntax.Stmt { 92 comments := expr.Comment() 93 if comments == nil { 94 continue 95 } 96 for _, section := range [][]modfile.Comment{comments.Before, comments.Suffix, comments.After} { 97 for _, comment := range section { 98 l, err := findLinksInString(urlRegexp, comment.Token, comment.Start.Byte, pm.Mapper) 99 if err != nil { 100 return nil, err 101 } 102 links = append(links, l...) 103 } 104 } 105 } 106 return links, nil 107 } 108 109 // goLinks returns the set of hyperlink annotations for the specified Go file. 110 func goLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.DocumentLink, error) { 111 112 pgf, err := snapshot.ParseGo(ctx, fh, parsego.ParseFull) 113 if err != nil { 114 return nil, err 115 } 116 117 var links []protocol.DocumentLink 118 119 // Create links for import specs. 120 if snapshot.Options().ImportShortcut.ShowLinks() { 121 122 // If links are to pkg.go.dev, append module version suffixes. 123 // This requires the import map from the package metadata. Ignore errors. 124 var depsByImpPath map[golang.ImportPath]golang.PackageID 125 if strings.ToLower(snapshot.Options().LinkTarget) == "pkg.go.dev" { 126 if meta, err := golang.NarrowestMetadataForFile(ctx, snapshot, fh.URI()); err == nil { 127 depsByImpPath = meta.DepsByImpPath 128 } 129 } 130 131 for _, imp := range pgf.File.Imports { 132 importPath := metadata.UnquoteImportPath(imp) 133 if importPath == "" { 134 continue // bad import 135 } 136 // See golang/go#36998: don't link to modules matching GOPRIVATE. 137 if snapshot.IsGoPrivatePath(string(importPath)) { 138 continue 139 } 140 141 urlPath := string(importPath) 142 143 // For pkg.go.dev, append module version suffix to package import path. 144 if mp := snapshot.Metadata(depsByImpPath[importPath]); mp != nil && mp.Module != nil && mp.Module.Path != "" && mp.Module.Version != "" { 145 urlPath = strings.Replace(urlPath, mp.Module.Path, mp.Module.Path+"@"+mp.Module.Version, 1) 146 } 147 148 start, end, err := safetoken.Offsets(pgf.Tok, imp.Path.Pos(), imp.Path.End()) 149 if err != nil { 150 return nil, err 151 } 152 targetURL := cache.BuildLink(snapshot.Options().LinkTarget, urlPath, "") 153 // Account for the quotation marks in the positions. 154 l, err := toProtocolLink(pgf.Mapper, targetURL, start+len(`"`), end-len(`"`)) 155 if err != nil { 156 return nil, err 157 } 158 links = append(links, l) 159 } 160 } 161 162 urlRegexp := snapshot.Options().URLRegexp 163 164 // Gather links found in string literals. 165 var str []*ast.BasicLit 166 ast.Inspect(pgf.File, func(node ast.Node) bool { 167 switch n := node.(type) { 168 case *ast.ImportSpec: 169 return false // don't process import strings again 170 case *ast.BasicLit: 171 if n.Kind == token.STRING { 172 str = append(str, n) 173 } 174 } 175 return true 176 }) 177 for _, s := range str { 178 strOffset, err := safetoken.Offset(pgf.Tok, s.Pos()) 179 if err != nil { 180 return nil, err 181 } 182 l, err := findLinksInString(urlRegexp, s.Value, strOffset, pgf.Mapper) 183 if err != nil { 184 return nil, err 185 } 186 links = append(links, l...) 187 } 188 189 // Gather links found in comments. 190 for _, commentGroup := range pgf.File.Comments { 191 for _, comment := range commentGroup.List { 192 commentOffset, err := safetoken.Offset(pgf.Tok, comment.Pos()) 193 if err != nil { 194 return nil, err 195 } 196 l, err := findLinksInString(urlRegexp, comment.Text, commentOffset, pgf.Mapper) 197 if err != nil { 198 return nil, err 199 } 200 links = append(links, l...) 201 } 202 } 203 204 return links, nil 205 } 206 207 // acceptedSchemes controls the schemes that URLs must have to be shown to the 208 // user. Other schemes can't be opened by LSP clients, so linkifying them is 209 // distracting. See golang/go#43990. 210 var acceptedSchemes = map[string]bool{ 211 "http": true, 212 "https": true, 213 } 214 215 // urlRegexp is the user-supplied regular expression to match URL. 216 // srcOffset is the start offset of 'src' within m's file. 217 func findLinksInString(urlRegexp *regexp.Regexp, src string, srcOffset int, m *protocol.Mapper) ([]protocol.DocumentLink, error) { 218 var links []protocol.DocumentLink 219 for _, index := range urlRegexp.FindAllIndex([]byte(src), -1) { 220 start, end := index[0], index[1] 221 link := src[start:end] 222 linkURL, err := url.Parse(link) 223 // Fallback: Linkify IP addresses as suggested in golang/go#18824. 224 if err != nil { 225 linkURL, err = url.Parse("//" + link) 226 // Not all potential links will be valid, so don't return this error. 227 if err != nil { 228 continue 229 } 230 } 231 // If the URL has no scheme, use https. 232 if linkURL.Scheme == "" { 233 linkURL.Scheme = "https" 234 } 235 if !acceptedSchemes[linkURL.Scheme] { 236 continue 237 } 238 239 l, err := toProtocolLink(m, linkURL.String(), srcOffset+start, srcOffset+end) 240 if err != nil { 241 return nil, err 242 } 243 links = append(links, l) 244 } 245 // Handle golang/go#1234-style links. 246 r := getIssueRegexp() 247 for _, index := range r.FindAllIndex([]byte(src), -1) { 248 start, end := index[0], index[1] 249 matches := r.FindStringSubmatch(src) 250 if len(matches) < 4 { 251 continue 252 } 253 org, repo, number := matches[1], matches[2], matches[3] 254 targetURL := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number) 255 l, err := toProtocolLink(m, targetURL, srcOffset+start, srcOffset+end) 256 if err != nil { 257 return nil, err 258 } 259 links = append(links, l) 260 } 261 return links, nil 262 } 263 264 func getIssueRegexp() *regexp.Regexp { 265 once.Do(func() { 266 issueRegexp = regexp.MustCompile(`(\w+)/([\w-]+)#([0-9]+)`) 267 }) 268 return issueRegexp 269 } 270 271 var ( 272 once sync.Once 273 issueRegexp *regexp.Regexp 274 ) 275 276 func toProtocolLink(m *protocol.Mapper, targetURL string, start, end int) (protocol.DocumentLink, error) { 277 rng, err := m.OffsetRange(start, end) 278 if err != nil { 279 return protocol.DocumentLink{}, err 280 } 281 return protocol.DocumentLink{ 282 Range: rng, 283 Target: &targetURL, 284 }, nil 285 }