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