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 }