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.
19 "golang.org/x/tools/internal/lsp/protocol"
20 "golang.org/x/tools/internal/span"
21 errors "golang.org/x/xerrors"
24 // MappedRange provides mapped protocol.Range for a span.Range, accounting for
25 // UTF-16 code points.
26 type MappedRange struct {
28 m *protocol.ColumnMapper
30 // protocolRange is the result of converting the spanRange using the mapper.
31 // It is computed on-demand.
32 protocolRange *protocol.Range
35 // NewMappedRange returns a MappedRange for the given start and end token.Pos.
36 func NewMappedRange(fset *token.FileSet, m *protocol.ColumnMapper, start, end token.Pos) MappedRange {
38 spanRange: span.Range{
42 Converter: m.Converter,
48 func (s MappedRange) Range() (protocol.Range, error) {
49 if s.protocolRange == nil {
50 spn, err := s.spanRange.Span()
52 return protocol.Range{}, err
54 prng, err := s.m.Range(spn)
56 return protocol.Range{}, err
58 s.protocolRange = &prng
60 return *s.protocolRange, nil
63 func (s MappedRange) Span() (span.Span, error) {
64 return s.spanRange.Span()
67 func (s MappedRange) SpanRange() span.Range {
71 func (s MappedRange) URI() span.URI {
75 // GetParsedFile is a convenience function that extracts the Package and
76 // ParsedGoFile for a file in a Snapshot. pkgPolicy is one of NarrowestPackage/
78 func GetParsedFile(ctx context.Context, snapshot Snapshot, fh FileHandle, pkgPolicy PackageFilter) (Package, *ParsedGoFile, error) {
79 pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckWorkspace, pkgPolicy)
83 pgh, err := pkg.File(fh.URI())
87 func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool {
88 fh, err := snapshot.GetFile(ctx, uri)
92 pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader)
96 tok := snapshot.FileSet().File(pgf.File.Pos())
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 {
113 func nodeToProtocolRange(snapshot Snapshot, pkg Package, n ast.Node) (protocol.Range, error) {
114 mrng, err := posToMappedRange(snapshot, pkg, n.Pos(), n.End())
116 return protocol.Range{}, err
121 func 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.
128 // import "go/ast" // name "ast" matches package name
129 // import a "go/ast" // name "a" does not match package name
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))
137 return nameToMappedRange(snapshot, pkg, obj.Pos(), obj.Name())
140 func nameToMappedRange(snapshot Snapshot, pkg Package, pos token.Pos, name string) (MappedRange, error) {
141 return posToMappedRange(snapshot, pkg, pos, pos+token.Pos(len(name)))
144 func 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))
148 return MappedRange{}, err
151 return MappedRange{}, errors.Errorf("invalid position for %v", pos)
154 return MappedRange{}, errors.Errorf("invalid position for %v", end)
156 return NewMappedRange(snapshot.FileSet(), pgf.Mapper, pos, end), nil
159 // Matches cgo generated comment as well as the proposed standard:
160 // https://golang.org/s/generatedcode
161 var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`)
163 func DetectLanguage(langID, filename string) FileKind {
172 // Fallback to detecting the language based on the file extension.
173 switch filepath.Ext(filename) {
178 default: // fallback to Go
183 func (k FileKind) String() string {
194 // nodeAtPos returns the index and the node whose position is contained inside
196 func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) {
200 for i, node := range nodes {
201 if node.Pos() <= pos && pos <= node.End() {
208 // IsInterface returns if a types.Type is an interface
209 func IsInterface(T types.Type) bool {
210 return T != nil && types.IsInterface(T)
213 // FormatNode returns the "pretty-print" output for an ast node.
214 func FormatNode(fset *token.FileSet, n ast.Node) string {
215 var buf strings.Builder
216 if err := printer.Fprint(&buf, fset, n); err != nil {
222 // Deref returns a pointer's element type, traversing as many levels as needed.
223 // Otherwise it returns typ.
224 func Deref(typ types.Type) types.Type {
226 p, ok := typ.Underlying().(*types.Pointer)
234 func SortDiagnostics(d []*Diagnostic) {
235 sort.Slice(d, func(i int, j int) bool {
236 return CompareDiagnostic(d[i], d[j]) < 0
240 func CompareDiagnostic(a, b *Diagnostic) int {
241 if r := protocol.CompareRange(a.Range, b.Range); r != 0 {
244 if a.Source < b.Source {
247 if a.Message < b.Message {
250 if a.Message == b.Message {
256 // FindPosInPackage finds the parsed file for a position in a given search
258 func FindPosInPackage(snapshot Snapshot, searchpkg Package, pos token.Pos) (*ParsedGoFile, Package, error) {
259 tok := snapshot.FileSet().File(pos)
261 return nil, nil, errors.Errorf("no file for pos in package %s", searchpkg.ID())
263 uri := span.URIFromPath(tok.Name())
265 pgf, pkg, err := findFileInDeps(searchpkg, uri)
272 // findFileInDeps finds uri in pkg or its dependencies.
273 func findFileInDeps(pkg Package, uri span.URI) (*ParsedGoFile, Package, error) {
274 queue := []Package{pkg}
275 seen := make(map[string]bool)
280 seen[pkg.ID()] = true
282 if pgf, err := pkg.File(uri); err == nil {
285 for _, dep := range pkg.Imports() {
287 queue = append(queue, dep)
291 return nil, nil, errors.Errorf("no file for %s in package %s", uri, pkg.ID())
294 // ImportPath returns the unquoted import path of s,
295 // or "" if the path is not properly quoted.
296 func ImportPath(s *ast.ImportSpec) string {
297 t, err := strconv.Unquote(s.Path.Value)
304 // NodeContains returns true if a node encloses a given position pos.
305 func NodeContains(n ast.Node, pos token.Pos) bool {
306 return n != nil && n.Pos() <= pos && pos <= n.End()
309 // CollectScopes returns all scopes in an ast path, ordered as innermost scope
311 func CollectScopes(info *types.Info, path []ast.Node, pos token.Pos) []*types.Scope {
312 // scopes[i], where i<len(path), is the possibly nil Scope of path[i].
313 var scopes []*types.Scope
314 for _, n := range path {
315 // Include *FuncType scope if pos is inside the function body.
316 switch node := n.(type) {
318 if node.Body != nil && NodeContains(node.Body, pos) {
322 if node.Body != nil && NodeContains(node.Body, pos) {
326 scopes = append(scopes, info.Scopes[n])
331 // Qualifier returns a function that appropriately formats a types.PkgName
332 // appearing in a *ast.File.
333 func Qualifier(f *ast.File, pkg *types.Package, info *types.Info) types.Qualifier {
334 // Construct mapping of import paths to their defined or implicit names.
335 imports := make(map[*types.Package]string)
336 for _, imp := range f.Imports {
339 obj = info.Defs[imp.Name]
341 obj = info.Implicits[imp]
343 if pkgname, ok := obj.(*types.PkgName); ok {
344 imports[pkgname.Imported()] = pkgname.Name()
347 // Define qualifier to replace full package paths with names of the imports.
348 return func(p *types.Package) string {
352 if name, ok := imports[p]; ok {
359 // isDirective reports whether c is a comment directive.
361 // Copied and adapted from go/src/go/ast/ast.go.
362 func isDirective(c string) bool {
369 //-style comment (no newline at the end)
375 // "//line " is a line directive.
376 // (The // has been removed.)
377 if strings.HasPrefix(c, "line ") {
381 // "//[a-z0-9]+:[a-z0-9]"
382 // (The // has been removed.)
383 colon := strings.Index(c, ":")
384 if colon <= 0 || colon+1 >= len(c) {
387 for i := 0; i <= colon+1; i++ {
392 if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') {
399 // honorSymlinks toggles whether or not we consider symlinks when comparing
400 // file or directory URIs.
401 const honorSymlinks = false
403 func CompareURI(left, right span.URI) int {
405 return span.CompareURI(left, right)
416 // InDir checks whether path is in the file tree rooted at dir.
417 // InDir makes some effort to succeed even in the presence of symbolic links.
419 // Copied and slightly adjusted from go/src/cmd/go/internal/search/search.go.
420 func InDir(dir, path string) bool {
421 if inDirLex(dir, path) {
427 xpath, err := filepath.EvalSymlinks(path)
428 if err != nil || xpath == path {
431 if inDirLex(dir, xpath) {
436 xdir, err := filepath.EvalSymlinks(dir)
437 if err == nil && xdir != dir {
438 if inDirLex(xdir, path) {
442 if inDirLex(xdir, xpath) {
450 // inDirLex is like inDir but only checks the lexical form of the file names.
451 // It does not consider symbolic links.
453 // Copied from go/src/cmd/go/internal/search/search.go.
454 func inDirLex(dir, path string) bool {
455 pv := strings.ToUpper(filepath.VolumeName(path))
456 dv := strings.ToUpper(filepath.VolumeName(dir))
457 path = path[len(pv):]
464 case len(path) == len(dir):
471 case len(path) > len(dir):
472 if dir[len(dir)-1] == filepath.Separator {
473 if path[:len(dir)] == dir {
474 return path[len(dir):] != ""
478 if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir {
479 if len(path) == len(dir)+1 {
482 return path[len(dir)+1:] != ""