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  }