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