// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package source import ( "context" "encoding/json" "fmt" "go/ast" "go/printer" "go/token" "go/types" "path/filepath" "regexp" "sort" "strconv" "strings" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/span" errors "golang.org/x/xerrors" ) // MappedRange provides mapped protocol.Range for a span.Range, accounting for // UTF-16 code points. type MappedRange struct { spanRange span.Range m *protocol.ColumnMapper // protocolRange is the result of converting the spanRange using the mapper. // It is computed on-demand. protocolRange *protocol.Range } // NewMappedRange returns a MappedRange for the given start and end token.Pos. func NewMappedRange(fset *token.FileSet, m *protocol.ColumnMapper, start, end token.Pos) MappedRange { return MappedRange{ spanRange: span.Range{ FileSet: fset, Start: start, End: end, Converter: m.Converter, }, m: m, } } func (s MappedRange) Range() (protocol.Range, error) { if s.protocolRange == nil { spn, err := s.spanRange.Span() if err != nil { return protocol.Range{}, err } prng, err := s.m.Range(spn) if err != nil { return protocol.Range{}, err } s.protocolRange = &prng } return *s.protocolRange, nil } func (s MappedRange) Span() (span.Span, error) { return s.spanRange.Span() } func (s MappedRange) SpanRange() span.Range { return s.spanRange } func (s MappedRange) URI() span.URI { return s.m.URI } // GetParsedFile is a convenience function that extracts the Package and // ParsedGoFile for a file in a Snapshot. pkgPolicy is one of NarrowestPackage/ // WidestPackage. func GetParsedFile(ctx context.Context, snapshot Snapshot, fh FileHandle, pkgPolicy PackageFilter) (Package, *ParsedGoFile, error) { pkg, err := snapshot.PackageForFile(ctx, fh.URI(), TypecheckWorkspace, pkgPolicy) if err != nil { return nil, nil, err } pgh, err := pkg.File(fh.URI()) return pkg, pgh, err } func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { fh, err := snapshot.GetFile(ctx, uri) if err != nil { return false } pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) if err != nil { return false } tok := snapshot.FileSet().File(pgf.File.Pos()) if tok == nil { return false } for _, commentGroup := range pgf.File.Comments { for _, comment := range commentGroup.List { if matched := generatedRx.MatchString(comment.Text); matched { // Check if comment is at the beginning of the line in source. if pos := tok.Position(comment.Slash); pos.Column == 1 { return true } } } } return false } func nodeToProtocolRange(snapshot Snapshot, pkg Package, n ast.Node) (protocol.Range, error) { mrng, err := posToMappedRange(snapshot, pkg, n.Pos(), n.End()) if err != nil { return protocol.Range{}, err } return mrng.Range() } func objToMappedRange(snapshot Snapshot, pkg Package, obj types.Object) (MappedRange, error) { if pkgName, ok := obj.(*types.PkgName); ok { // An imported Go package has a package-local, unqualified name. // When the name matches the imported package name, there is no // identifier in the import spec with the local package name. // // For example: // import "go/ast" // name "ast" matches package name // import a "go/ast" // name "a" does not match package name // // When the identifier does not appear in the source, have the range // of the object be the import path, including quotes. if pkgName.Imported().Name() == pkgName.Name() { return posToMappedRange(snapshot, pkg, obj.Pos(), obj.Pos()+token.Pos(len(pkgName.Imported().Path())+2)) } } return nameToMappedRange(snapshot, pkg, obj.Pos(), obj.Name()) } func nameToMappedRange(snapshot Snapshot, pkg Package, pos token.Pos, name string) (MappedRange, error) { return posToMappedRange(snapshot, pkg, pos, pos+token.Pos(len(name))) } func posToMappedRange(snapshot Snapshot, pkg Package, pos, end token.Pos) (MappedRange, error) { logicalFilename := snapshot.FileSet().File(pos).Position(pos).Filename pgf, _, err := findFileInDeps(pkg, span.URIFromPath(logicalFilename)) if err != nil { return MappedRange{}, err } if !pos.IsValid() { return MappedRange{}, errors.Errorf("invalid position for %v", pos) } if !end.IsValid() { return MappedRange{}, errors.Errorf("invalid position for %v", end) } return NewMappedRange(snapshot.FileSet(), pgf.Mapper, pos, end), nil } // Matches cgo generated comment as well as the proposed standard: // https://golang.org/s/generatedcode var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`) func DetectLanguage(langID, filename string) FileKind { switch langID { case "go": return Go case "go.mod": return Mod case "go.sum": return Sum } // Fallback to detecting the language based on the file extension. switch filepath.Ext(filename) { case ".mod": return Mod case ".sum": return Sum default: // fallback to Go return Go } } func (k FileKind) String() string { switch k { case Mod: return "go.mod" case Sum: return "go.sum" default: return "go" } } // nodeAtPos returns the index and the node whose position is contained inside // the node list. func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) { if nodes == nil { return nil, -1 } for i, node := range nodes { if node.Pos() <= pos && pos <= node.End() { return node, i } } return nil, -1 } // IsInterface returns if a types.Type is an interface func IsInterface(T types.Type) bool { return T != nil && types.IsInterface(T) } // FormatNode returns the "pretty-print" output for an ast node. func FormatNode(fset *token.FileSet, n ast.Node) string { var buf strings.Builder if err := printer.Fprint(&buf, fset, n); err != nil { return "" } return buf.String() } // Deref returns a pointer's element type, traversing as many levels as needed. // Otherwise it returns typ. func Deref(typ types.Type) types.Type { for { p, ok := typ.Underlying().(*types.Pointer) if !ok { return typ } typ = p.Elem() } } func SortDiagnostics(d []*Diagnostic) { sort.Slice(d, func(i int, j int) bool { return CompareDiagnostic(d[i], d[j]) < 0 }) } func CompareDiagnostic(a, b *Diagnostic) int { if r := protocol.CompareRange(a.Range, b.Range); r != 0 { return r } if a.Source < b.Source { return -1 } if a.Message < b.Message { return -1 } if a.Message == b.Message { return 0 } return 1 } // FindPosInPackage finds the parsed file for a position in a given search // package. func FindPosInPackage(snapshot Snapshot, searchpkg Package, pos token.Pos) (*ParsedGoFile, Package, error) { tok := snapshot.FileSet().File(pos) if tok == nil { return nil, nil, errors.Errorf("no file for pos in package %s", searchpkg.ID()) } uri := span.URIFromPath(tok.Name()) pgf, pkg, err := findFileInDeps(searchpkg, uri) if err != nil { return nil, nil, err } return pgf, pkg, nil } // findFileInDeps finds uri in pkg or its dependencies. func findFileInDeps(pkg Package, uri span.URI) (*ParsedGoFile, Package, error) { queue := []Package{pkg} seen := make(map[string]bool) for len(queue) > 0 { pkg := queue[0] queue = queue[1:] seen[pkg.ID()] = true if pgf, err := pkg.File(uri); err == nil { return pgf, pkg, nil } for _, dep := range pkg.Imports() { if !seen[dep.ID()] { queue = append(queue, dep) } } } return nil, nil, errors.Errorf("no file for %s in package %s", uri, pkg.ID()) } // MarshalArgs encodes the given arguments to json.RawMessages. This function // is used to construct arguments to a protocol.Command. // // Example usage: // // jsonArgs, err := EncodeArgs(1, "hello", true, StructuredArg{42, 12.6}) // func MarshalArgs(args ...interface{}) ([]json.RawMessage, error) { var out []json.RawMessage for _, arg := range args { argJSON, err := json.Marshal(arg) if err != nil { return nil, err } out = append(out, argJSON) } return out, nil } // UnmarshalArgs decodes the given json.RawMessages to the variables provided // by args. Each element of args should be a pointer. // // Example usage: // // var ( // num int // str string // bul bool // structured StructuredArg // ) // err := UnmarshalArgs(args, &num, &str, &bul, &structured) // func UnmarshalArgs(jsonArgs []json.RawMessage, args ...interface{}) error { if len(args) != len(jsonArgs) { return fmt.Errorf("DecodeArgs: expected %d input arguments, got %d JSON arguments", len(args), len(jsonArgs)) } for i, arg := range args { if err := json.Unmarshal(jsonArgs[i], arg); err != nil { return err } } return nil } // ImportPath returns the unquoted import path of s, // or "" if the path is not properly quoted. func ImportPath(s *ast.ImportSpec) string { t, err := strconv.Unquote(s.Path.Value) if err != nil { return "" } return t } // NodeContains returns true if a node encloses a given position pos. func NodeContains(n ast.Node, pos token.Pos) bool { return n != nil && n.Pos() <= pos && pos <= n.End() } // CollectScopes returns all scopes in an ast path, ordered as innermost scope // first. func CollectScopes(info *types.Info, path []ast.Node, pos token.Pos) []*types.Scope { // scopes[i], where i= len(c) { return false } for i := 0; i <= colon+1; i++ { if i == colon { continue } b := c[i] if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') { return false } } return true } // InDir checks whether path is in the file tree rooted at dir. // If so, InDir returns an equivalent path relative to dir. // If not, InDir returns an empty string. // InDir makes some effort to succeed even in the presence of symbolic links. // // Copied and slightly adjusted from go/src/cmd/go/internal/search/search.go. func InDir(dir, path string) bool { if rel := inDirLex(path, dir); rel != "" { return true } xpath, err := filepath.EvalSymlinks(path) if err != nil || xpath == path { xpath = "" } else { if rel := inDirLex(xpath, dir); rel != "" { return true } } xdir, err := filepath.EvalSymlinks(dir) if err == nil && xdir != dir { if rel := inDirLex(path, xdir); rel != "" { return true } if xpath != "" { if rel := inDirLex(xpath, xdir); rel != "" { return true } } } return false } // Copied from go/src/cmd/go/internal/search/search.go. // // inDirLex is like inDir but only checks the lexical form of the file names. // It does not consider symbolic links. // TODO(rsc): This is a copy of str.HasFilePathPrefix, modified to // return the suffix. Most uses of str.HasFilePathPrefix should probably // be calling InDir instead. func inDirLex(path, dir string) string { pv := strings.ToUpper(filepath.VolumeName(path)) dv := strings.ToUpper(filepath.VolumeName(dir)) path = path[len(pv):] dir = dir[len(dv):] switch { default: return "" case pv != dv: return "" case len(path) == len(dir): if path == dir { return "." } return "" case dir == "": return path case len(path) > len(dir): if dir[len(dir)-1] == filepath.Separator { if path[:len(dir)] == dir { return path[len(dir):] } return "" } if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir { if len(path) == len(dir)+1 { return "." } return path[len(dir)+1:] } return "" } }