github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/source/util.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 source
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/printer"
    12  	"go/token"
    13  	"go/types"
    14  	"path/filepath"
    15  	"regexp"
    16  	"sort"
    17  	"strconv"
    18  	"strings"
    19  
    20  	"github.com/jhump/golang-x-tools/internal/lsp/protocol"
    21  	"github.com/jhump/golang-x-tools/internal/span"
    22  	errors "golang.org/x/xerrors"
    23  )
    24  
    25  // MappedRange provides mapped protocol.Range for a span.Range, accounting for
    26  // UTF-16 code points.
    27  type MappedRange struct {
    28  	spanRange span.Range
    29  	m         *protocol.ColumnMapper
    30  
    31  	// protocolRange is the result of converting the spanRange using the mapper.
    32  	// It is computed on-demand.
    33  	protocolRange *protocol.Range
    34  }
    35  
    36  // NewMappedRange returns a MappedRange for the given start and end token.Pos.
    37  func NewMappedRange(fset *token.FileSet, m *protocol.ColumnMapper, start, end token.Pos) MappedRange {
    38  	return MappedRange{
    39  		spanRange: span.Range{
    40  			FileSet:   fset,
    41  			Start:     start,
    42  			End:       end,
    43  			Converter: m.Converter,
    44  		},
    45  		m: m,
    46  	}
    47  }
    48  
    49  func (s MappedRange) Range() (protocol.Range, error) {
    50  	if s.protocolRange == nil {
    51  		spn, err := s.spanRange.Span()
    52  		if err != nil {
    53  			return protocol.Range{}, err
    54  		}
    55  		prng, err := s.m.Range(spn)
    56  		if err != nil {
    57  			return protocol.Range{}, err
    58  		}
    59  		s.protocolRange = &prng
    60  	}
    61  	return *s.protocolRange, nil
    62  }
    63  
    64  func (s MappedRange) Span() (span.Span, error) {
    65  	return s.spanRange.Span()
    66  }
    67  
    68  func (s MappedRange) SpanRange() span.Range {
    69  	return s.spanRange
    70  }
    71  
    72  func (s MappedRange) URI() span.URI {
    73  	return s.m.URI
    74  }
    75  
    76  // GetParsedFile is a convenience function that extracts the Package and
    77  // ParsedGoFile for a file in a Snapshot. pkgPolicy is one of NarrowestPackage/
    78  // WidestPackage.
    79  func GetParsedFile(ctx context.Context, snapshot Snapshot, fh FileHandle, pkgPolicy PackageFilter) (Package, *ParsedGoFile, error) {
    80  	pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckWorkspace, pkgPolicy)
    81  	if err != nil {
    82  		return nil, nil, err
    83  	}
    84  	pgh, err := pkg.File(fh.URI())
    85  	return pkg, pgh, err
    86  }
    87  
    88  func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool {
    89  	fh, err := snapshot.GetFile(ctx, uri)
    90  	if err != nil {
    91  		return false
    92  	}
    93  	pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader)
    94  	if err != nil {
    95  		return false
    96  	}
    97  	tok := snapshot.FileSet().File(pgf.File.Pos())
    98  	if tok == nil {
    99  		return false
   100  	}
   101  	for _, commentGroup := range pgf.File.Comments {
   102  		for _, comment := range commentGroup.List {
   103  			if matched := generatedRx.MatchString(comment.Text); matched {
   104  				// Check if comment is at the beginning of the line in source.
   105  				if pos := tok.Position(comment.Slash); pos.Column == 1 {
   106  					return true
   107  				}
   108  			}
   109  		}
   110  	}
   111  	return false
   112  }
   113  
   114  func nodeToProtocolRange(snapshot Snapshot, pkg Package, n ast.Node) (protocol.Range, error) {
   115  	mrng, err := posToMappedRange(snapshot, pkg, n.Pos(), n.End())
   116  	if err != nil {
   117  		return protocol.Range{}, err
   118  	}
   119  	return mrng.Range()
   120  }
   121  
   122  func objToMappedRange(snapshot Snapshot, pkg Package, obj types.Object) (MappedRange, error) {
   123  	if pkgName, ok := obj.(*types.PkgName); ok {
   124  		// An imported Go package has a package-local, unqualified name.
   125  		// When the name matches the imported package name, there is no
   126  		// identifier in the import spec with the local package name.
   127  		//
   128  		// For example:
   129  		// 		import "go/ast" 	// name "ast" matches package name
   130  		// 		import a "go/ast"  	// name "a" does not match package name
   131  		//
   132  		// When the identifier does not appear in the source, have the range
   133  		// of the object be the import path, including quotes.
   134  		if pkgName.Imported().Name() == pkgName.Name() {
   135  			return posToMappedRange(snapshot, pkg, obj.Pos(), obj.Pos()+token.Pos(len(pkgName.Imported().Path())+2))
   136  		}
   137  	}
   138  	return nameToMappedRange(snapshot, pkg, obj.Pos(), obj.Name())
   139  }
   140  
   141  func nameToMappedRange(snapshot Snapshot, pkg Package, pos token.Pos, name string) (MappedRange, error) {
   142  	return posToMappedRange(snapshot, pkg, pos, pos+token.Pos(len(name)))
   143  }
   144  
   145  func posToMappedRange(snapshot Snapshot, pkg Package, pos, end token.Pos) (MappedRange, error) {
   146  	logicalFilename := snapshot.FileSet().File(pos).Position(pos).Filename
   147  	pgf, _, err := findFileInDeps(pkg, span.URIFromPath(logicalFilename))
   148  	if err != nil {
   149  		return MappedRange{}, err
   150  	}
   151  	if !pos.IsValid() {
   152  		return MappedRange{}, errors.Errorf("invalid position for %v", pos)
   153  	}
   154  	if !end.IsValid() {
   155  		return MappedRange{}, errors.Errorf("invalid position for %v", end)
   156  	}
   157  	return NewMappedRange(snapshot.FileSet(), pgf.Mapper, pos, end), nil
   158  }
   159  
   160  // Matches cgo generated comment as well as the proposed standard:
   161  //	https://golang.org/s/generatedcode
   162  var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
   163  
   164  // FileKindForLang returns the file kind associated with the given language ID,
   165  // or UnknownKind if the language ID is not recognized.
   166  func FileKindForLang(langID string) FileKind {
   167  	switch langID {
   168  	case "go":
   169  		return Go
   170  	case "go.mod":
   171  		return Mod
   172  	case "go.sum":
   173  		return Sum
   174  	case "tmpl", "gotmpl":
   175  		return Tmpl
   176  	case "go.work":
   177  		return Work
   178  	default:
   179  		return UnknownKind
   180  	}
   181  }
   182  
   183  func (k FileKind) String() string {
   184  	switch k {
   185  	case Go:
   186  		return "go"
   187  	case Mod:
   188  		return "go.mod"
   189  	case Sum:
   190  		return "go.sum"
   191  	case Tmpl:
   192  		return "tmpl"
   193  	case Work:
   194  		return "go.work"
   195  	default:
   196  		return fmt.Sprintf("unk%d", k)
   197  	}
   198  }
   199  
   200  // nodeAtPos returns the index and the node whose position is contained inside
   201  // the node list.
   202  func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) {
   203  	if nodes == nil {
   204  		return nil, -1
   205  	}
   206  	for i, node := range nodes {
   207  		if node.Pos() <= pos && pos <= node.End() {
   208  			return node, i
   209  		}
   210  	}
   211  	return nil, -1
   212  }
   213  
   214  // IsInterface returns if a types.Type is an interface
   215  func IsInterface(T types.Type) bool {
   216  	return T != nil && types.IsInterface(T)
   217  }
   218  
   219  // FormatNode returns the "pretty-print" output for an ast node.
   220  func FormatNode(fset *token.FileSet, n ast.Node) string {
   221  	var buf strings.Builder
   222  	if err := printer.Fprint(&buf, fset, n); err != nil {
   223  		return ""
   224  	}
   225  	return buf.String()
   226  }
   227  
   228  // Deref returns a pointer's element type, traversing as many levels as needed.
   229  // Otherwise it returns typ.
   230  //
   231  // It can return a pointer type for cyclic types (see golang/go#45510).
   232  func Deref(typ types.Type) types.Type {
   233  	var seen map[types.Type]struct{}
   234  	for {
   235  		p, ok := typ.Underlying().(*types.Pointer)
   236  		if !ok {
   237  			return typ
   238  		}
   239  		if _, ok := seen[p.Elem()]; ok {
   240  			return typ
   241  		}
   242  
   243  		typ = p.Elem()
   244  
   245  		if seen == nil {
   246  			seen = make(map[types.Type]struct{})
   247  		}
   248  		seen[typ] = struct{}{}
   249  	}
   250  }
   251  
   252  func SortDiagnostics(d []*Diagnostic) {
   253  	sort.Slice(d, func(i int, j int) bool {
   254  		return CompareDiagnostic(d[i], d[j]) < 0
   255  	})
   256  }
   257  
   258  func CompareDiagnostic(a, b *Diagnostic) int {
   259  	if r := protocol.CompareRange(a.Range, b.Range); r != 0 {
   260  		return r
   261  	}
   262  	if a.Source < b.Source {
   263  		return -1
   264  	}
   265  	if a.Message < b.Message {
   266  		return -1
   267  	}
   268  	if a.Message == b.Message {
   269  		return 0
   270  	}
   271  	return 1
   272  }
   273  
   274  // FindPackageFromPos finds the first package containing pos in its
   275  // type-checked AST.
   276  func FindPackageFromPos(ctx context.Context, snapshot Snapshot, pos token.Pos) (Package, error) {
   277  	tok := snapshot.FileSet().File(pos)
   278  	if tok == nil {
   279  		return nil, errors.Errorf("no file for pos %v", pos)
   280  	}
   281  	uri := span.URIFromPath(tok.Name())
   282  	pkgs, err := snapshot.PackagesForFile(ctx, uri, TypecheckAll, true)
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	// Only return the package if it actually type-checked the given position.
   287  	for _, pkg := range pkgs {
   288  		parsed, err := pkg.File(uri)
   289  		if err != nil {
   290  			return nil, err
   291  		}
   292  		if parsed == nil {
   293  			continue
   294  		}
   295  		if parsed.Tok.Base() != tok.Base() {
   296  			continue
   297  		}
   298  		return pkg, nil
   299  	}
   300  	return nil, errors.Errorf("no package for given file position")
   301  }
   302  
   303  // findFileInDeps finds uri in pkg or its dependencies.
   304  func findFileInDeps(pkg Package, uri span.URI) (*ParsedGoFile, Package, error) {
   305  	queue := []Package{pkg}
   306  	seen := make(map[string]bool)
   307  
   308  	for len(queue) > 0 {
   309  		pkg := queue[0]
   310  		queue = queue[1:]
   311  		seen[pkg.ID()] = true
   312  
   313  		if pgf, err := pkg.File(uri); err == nil {
   314  			return pgf, pkg, nil
   315  		}
   316  		for _, dep := range pkg.Imports() {
   317  			if !seen[dep.ID()] {
   318  				queue = append(queue, dep)
   319  			}
   320  		}
   321  	}
   322  	return nil, nil, errors.Errorf("no file for %s in package %s", uri, pkg.ID())
   323  }
   324  
   325  // ImportPath returns the unquoted import path of s,
   326  // or "" if the path is not properly quoted.
   327  func ImportPath(s *ast.ImportSpec) string {
   328  	t, err := strconv.Unquote(s.Path.Value)
   329  	if err != nil {
   330  		return ""
   331  	}
   332  	return t
   333  }
   334  
   335  // NodeContains returns true if a node encloses a given position pos.
   336  func NodeContains(n ast.Node, pos token.Pos) bool {
   337  	return n != nil && n.Pos() <= pos && pos <= n.End()
   338  }
   339  
   340  // CollectScopes returns all scopes in an ast path, ordered as innermost scope
   341  // first.
   342  func CollectScopes(info *types.Info, path []ast.Node, pos token.Pos) []*types.Scope {
   343  	// scopes[i], where i<len(path), is the possibly nil Scope of path[i].
   344  	var scopes []*types.Scope
   345  	for _, n := range path {
   346  		// Include *FuncType scope if pos is inside the function body.
   347  		switch node := n.(type) {
   348  		case *ast.FuncDecl:
   349  			if node.Body != nil && NodeContains(node.Body, pos) {
   350  				n = node.Type
   351  			}
   352  		case *ast.FuncLit:
   353  			if node.Body != nil && NodeContains(node.Body, pos) {
   354  				n = node.Type
   355  			}
   356  		}
   357  		scopes = append(scopes, info.Scopes[n])
   358  	}
   359  	return scopes
   360  }
   361  
   362  // Qualifier returns a function that appropriately formats a types.PkgName
   363  // appearing in a *ast.File.
   364  func Qualifier(f *ast.File, pkg *types.Package, info *types.Info) types.Qualifier {
   365  	// Construct mapping of import paths to their defined or implicit names.
   366  	imports := make(map[*types.Package]string)
   367  	for _, imp := range f.Imports {
   368  		var obj types.Object
   369  		if imp.Name != nil {
   370  			obj = info.Defs[imp.Name]
   371  		} else {
   372  			obj = info.Implicits[imp]
   373  		}
   374  		if pkgname, ok := obj.(*types.PkgName); ok {
   375  			imports[pkgname.Imported()] = pkgname.Name()
   376  		}
   377  	}
   378  	// Define qualifier to replace full package paths with names of the imports.
   379  	return func(p *types.Package) string {
   380  		if p == pkg {
   381  			return ""
   382  		}
   383  		if name, ok := imports[p]; ok {
   384  			if name == "." {
   385  				return ""
   386  			}
   387  			return name
   388  		}
   389  		return p.Name()
   390  	}
   391  }
   392  
   393  // isDirective reports whether c is a comment directive.
   394  //
   395  // Copied and adapted from go/src/go/ast/ast.go.
   396  func isDirective(c string) bool {
   397  	if len(c) < 3 {
   398  		return false
   399  	}
   400  	if c[1] != '/' {
   401  		return false
   402  	}
   403  	//-style comment (no newline at the end)
   404  	c = c[2:]
   405  	if len(c) == 0 {
   406  		// empty line
   407  		return false
   408  	}
   409  	// "//line " is a line directive.
   410  	// (The // has been removed.)
   411  	if strings.HasPrefix(c, "line ") {
   412  		return true
   413  	}
   414  
   415  	// "//[a-z0-9]+:[a-z0-9]"
   416  	// (The // has been removed.)
   417  	colon := strings.Index(c, ":")
   418  	if colon <= 0 || colon+1 >= len(c) {
   419  		return false
   420  	}
   421  	for i := 0; i <= colon+1; i++ {
   422  		if i == colon {
   423  			continue
   424  		}
   425  		b := c[i]
   426  		if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') {
   427  			return false
   428  		}
   429  	}
   430  	return true
   431  }
   432  
   433  // honorSymlinks toggles whether or not we consider symlinks when comparing
   434  // file or directory URIs.
   435  const honorSymlinks = false
   436  
   437  func CompareURI(left, right span.URI) int {
   438  	if honorSymlinks {
   439  		return span.CompareURI(left, right)
   440  	}
   441  	if left == right {
   442  		return 0
   443  	}
   444  	if left < right {
   445  		return -1
   446  	}
   447  	return 1
   448  }
   449  
   450  // InDir checks whether path is in the file tree rooted at dir.
   451  // InDir makes some effort to succeed even in the presence of symbolic links.
   452  //
   453  // Copied and slightly adjusted from go/src/cmd/go/internal/search/search.go.
   454  func InDir(dir, path string) bool {
   455  	if inDirLex(dir, path) {
   456  		return true
   457  	}
   458  	if !honorSymlinks {
   459  		return false
   460  	}
   461  	xpath, err := filepath.EvalSymlinks(path)
   462  	if err != nil || xpath == path {
   463  		xpath = ""
   464  	} else {
   465  		if inDirLex(dir, xpath) {
   466  			return true
   467  		}
   468  	}
   469  
   470  	xdir, err := filepath.EvalSymlinks(dir)
   471  	if err == nil && xdir != dir {
   472  		if inDirLex(xdir, path) {
   473  			return true
   474  		}
   475  		if xpath != "" {
   476  			if inDirLex(xdir, xpath) {
   477  				return true
   478  			}
   479  		}
   480  	}
   481  	return false
   482  }
   483  
   484  // inDirLex is like inDir but only checks the lexical form of the file names.
   485  // It does not consider symbolic links.
   486  //
   487  // Copied from go/src/cmd/go/internal/search/search.go.
   488  func inDirLex(dir, path string) bool {
   489  	pv := strings.ToUpper(filepath.VolumeName(path))
   490  	dv := strings.ToUpper(filepath.VolumeName(dir))
   491  	path = path[len(pv):]
   492  	dir = dir[len(dv):]
   493  	switch {
   494  	default:
   495  		return false
   496  	case pv != dv:
   497  		return false
   498  	case len(path) == len(dir):
   499  		if path == dir {
   500  			return true
   501  		}
   502  		return false
   503  	case dir == "":
   504  		return path != ""
   505  	case len(path) > len(dir):
   506  		if dir[len(dir)-1] == filepath.Separator {
   507  			if path[:len(dir)] == dir {
   508  				return path[len(dir):] != ""
   509  			}
   510  			return false
   511  		}
   512  		if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir {
   513  			if len(path) == len(dir)+1 {
   514  				return true
   515  			}
   516  			return path[len(dir)+1:] != ""
   517  		}
   518  		return false
   519  	}
   520  }
   521  
   522  // IsValidImport returns whether importPkgPath is importable
   523  // by pkgPath
   524  func IsValidImport(pkgPath, importPkgPath string) bool {
   525  	i := strings.LastIndex(string(importPkgPath), "/internal/")
   526  	if i == -1 {
   527  		return true
   528  	}
   529  	if IsCommandLineArguments(string(pkgPath)) {
   530  		return true
   531  	}
   532  	return strings.HasPrefix(string(pkgPath), string(importPkgPath[:i]))
   533  }
   534  
   535  // IsCommandLineArguments reports whether a given value denotes
   536  // "command-line-arguments" package, which is a package with an unknown ID
   537  // created by the go command. It can have a test variant, which is why callers
   538  // should not check that a value equals "command-line-arguments" directly.
   539  func IsCommandLineArguments(s string) bool {
   540  	return strings.Contains(s, "command-line-arguments")
   541  }
   542  
   543  // Offset returns tok.Offset(pos), but it also checks that the pos is in range
   544  // for the given file.
   545  func Offset(tok *token.File, pos token.Pos) (int, error) {
   546  	if !InRange(tok, pos) {
   547  		return -1, fmt.Errorf("pos %v is not in range for file [%v:%v)", pos, tok.Base(), tok.Base()+tok.Size())
   548  	}
   549  	return tok.Offset(pos), nil
   550  }
   551  
   552  // InRange reports whether the given position is in the given token.File.
   553  func InRange(tok *token.File, pos token.Pos) bool {
   554  	size := tok.Pos(tok.Size())
   555  	return int(pos) >= tok.Base() && pos <= size
   556  }