golang.org/x/tools@v0.21.0/go/analysis/analysistest/analysistest.go (about) 1 // Copyright 2018 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 analysistest provides utilities for testing analyzers. 6 package analysistest 7 8 import ( 9 "bytes" 10 "fmt" 11 "go/format" 12 "go/token" 13 "go/types" 14 "log" 15 "os" 16 "path/filepath" 17 "regexp" 18 "runtime" 19 "sort" 20 "strconv" 21 "strings" 22 "testing" 23 "text/scanner" 24 25 "golang.org/x/tools/go/analysis" 26 "golang.org/x/tools/go/analysis/internal/checker" 27 "golang.org/x/tools/go/packages" 28 "golang.org/x/tools/internal/diff" 29 "golang.org/x/tools/internal/testenv" 30 "golang.org/x/tools/txtar" 31 ) 32 33 // WriteFiles is a helper function that creates a temporary directory 34 // and populates it with a GOPATH-style project using filemap (which 35 // maps file names to contents). On success it returns the name of the 36 // directory and a cleanup function to delete it. 37 func WriteFiles(filemap map[string]string) (dir string, cleanup func(), err error) { 38 gopath, err := os.MkdirTemp("", "analysistest") 39 if err != nil { 40 return "", nil, err 41 } 42 cleanup = func() { os.RemoveAll(gopath) } 43 44 for name, content := range filemap { 45 filename := filepath.Join(gopath, "src", name) 46 os.MkdirAll(filepath.Dir(filename), 0777) // ignore error 47 if err := os.WriteFile(filename, []byte(content), 0666); err != nil { 48 cleanup() 49 return "", nil, err 50 } 51 } 52 return gopath, cleanup, nil 53 } 54 55 // TestData returns the effective filename of 56 // the program's "testdata" directory. 57 // This function may be overridden by projects using 58 // an alternative build system (such as Blaze) that 59 // does not run a test in its package directory. 60 var TestData = func() string { 61 testdata, err := filepath.Abs("testdata") 62 if err != nil { 63 log.Fatal(err) 64 } 65 return testdata 66 } 67 68 // Testing is an abstraction of a *testing.T. 69 type Testing interface { 70 Errorf(format string, args ...interface{}) 71 } 72 73 // RunWithSuggestedFixes behaves like Run, but additionally verifies suggested fixes. 74 // It uses golden files placed alongside the source code under analysis: 75 // suggested fixes for code in example.go will be compared against example.go.golden. 76 // 77 // Golden files can be formatted in one of two ways: as plain Go source code, or as txtar archives. 78 // In the first case, all suggested fixes will be applied to the original source, which will then be compared against the golden file. 79 // In the second case, suggested fixes will be grouped by their messages, and each set of fixes will be applied and tested separately. 80 // Each section in the archive corresponds to a single message. 81 // 82 // A golden file using txtar may look like this: 83 // 84 // -- turn into single negation -- 85 // package pkg 86 // 87 // func fn(b1, b2 bool) { 88 // if !b1 { // want `negating a boolean twice` 89 // println() 90 // } 91 // } 92 // 93 // -- remove double negation -- 94 // package pkg 95 // 96 // func fn(b1, b2 bool) { 97 // if b1 { // want `negating a boolean twice` 98 // println() 99 // } 100 // } 101 // 102 // # Conflicts 103 // 104 // A single analysis pass may offer two or more suggested fixes that 105 // (1) conflict but are nonetheless logically composable, (e.g. 106 // because both update the import declaration), or (2) are 107 // fundamentally incompatible (e.g. alternative fixes to the same 108 // statement). 109 // 110 // It is up to the driver to decide how to apply such fixes. A 111 // sophisticated driver could attempt to resolve conflicts of the 112 // first kind, but this test driver simply reports the fact of the 113 // conflict with the expectation that the user will split their tests 114 // into nonconflicting parts. 115 // 116 // Conflicts of the second kind can be avoided by giving the 117 // alternative fixes different names (SuggestedFix.Message) and using 118 // a multi-section .txtar file with a named section for each 119 // alternative fix. 120 // 121 // Analyzers that compute fixes from a textual diff of the 122 // before/after file contents (instead of directly from syntax tree 123 // positions) may produce fixes that, although logically 124 // non-conflicting, nonetheless conflict due to the particulars of the 125 // diff algorithm. In such cases it may suffice to introduce 126 // sufficient separation of the statements in the test input so that 127 // the computed diffs do not overlap. If that fails, break the test 128 // into smaller parts. 129 func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Result { 130 r := Run(t, dir, a, patterns...) 131 132 // If the immediate caller of RunWithSuggestedFixes is in 133 // x/tools, we apply stricter checks as required by gopls. 134 inTools := false 135 { 136 var pcs [1]uintptr 137 n := runtime.Callers(1, pcs[:]) 138 frames := runtime.CallersFrames(pcs[:n]) 139 fr, _ := frames.Next() 140 if fr.Func != nil && strings.HasPrefix(fr.Func.Name(), "golang.org/x/tools/") { 141 inTools = true 142 } 143 } 144 145 // Process each result (package) separately, matching up the suggested 146 // fixes into a diff, which we will compare to the .golden file. We have 147 // to do this per-result in case a file appears in two packages, such as in 148 // packages with tests, where mypkg/a.go will appear in both mypkg and 149 // mypkg.test. In that case, the analyzer may suggest the same set of 150 // changes to a.go for each package. If we merge all the results, those 151 // changes get doubly applied, which will cause conflicts or mismatches. 152 // Validating the results separately means as long as the two analyses 153 // don't produce conflicting suggestions for a single file, everything 154 // should match up. 155 for _, act := range r { 156 // file -> message -> edits 157 fileEdits := make(map[*token.File]map[string][]diff.Edit) 158 fileContents := make(map[*token.File][]byte) 159 160 // Validate edits, prepare the fileEdits map and read the file contents. 161 for _, diag := range act.Diagnostics { 162 for _, fix := range diag.SuggestedFixes { 163 164 // Assert that lazy fixes have a Category (#65578, #65087). 165 if inTools && len(fix.TextEdits) == 0 && diag.Category == "" { 166 t.Errorf("missing Diagnostic.Category for SuggestedFix without TextEdits (gopls requires the category for the name of the fix command") 167 } 168 169 for _, edit := range fix.TextEdits { 170 start, end := edit.Pos, edit.End 171 if !end.IsValid() { 172 end = start 173 } 174 // Validate the edit. 175 if start > end { 176 t.Errorf( 177 "diagnostic for analysis %v contains Suggested Fix with malformed edit: pos (%v) > end (%v)", 178 act.Pass.Analyzer.Name, start, end) 179 continue 180 } 181 file, endfile := act.Pass.Fset.File(start), act.Pass.Fset.File(end) 182 if file == nil || endfile == nil || file != endfile { 183 t.Errorf( 184 "diagnostic for analysis %v contains Suggested Fix with malformed spanning files %v and %v", 185 act.Pass.Analyzer.Name, file.Name(), endfile.Name()) 186 continue 187 } 188 if _, ok := fileContents[file]; !ok { 189 contents, err := os.ReadFile(file.Name()) 190 if err != nil { 191 t.Errorf("error reading %s: %v", file.Name(), err) 192 } 193 fileContents[file] = contents 194 } 195 if _, ok := fileEdits[file]; !ok { 196 fileEdits[file] = make(map[string][]diff.Edit) 197 } 198 fileEdits[file][fix.Message] = append(fileEdits[file][fix.Message], diff.Edit{ 199 Start: file.Offset(start), 200 End: file.Offset(end), 201 New: string(edit.NewText), 202 }) 203 } 204 } 205 } 206 207 for file, fixes := range fileEdits { 208 // Get the original file contents. 209 orig, ok := fileContents[file] 210 if !ok { 211 t.Errorf("could not find file contents for %s", file.Name()) 212 continue 213 } 214 215 // Get the golden file and read the contents. 216 ar, err := txtar.ParseFile(file.Name() + ".golden") 217 if err != nil { 218 t.Errorf("error reading %s.golden: %v", file.Name(), err) 219 continue 220 } 221 222 if len(ar.Files) > 0 { 223 // one virtual file per kind of suggested fix 224 225 if len(ar.Comment) != 0 { 226 // we allow either just the comment, or just virtual 227 // files, not both. it is not clear how "both" should 228 // behave. 229 t.Errorf("%s.golden has leading comment; we don't know what to do with it", file.Name()) 230 continue 231 } 232 233 for sf, edits := range fixes { 234 found := false 235 for _, vf := range ar.Files { 236 if vf.Name == sf { 237 found = true 238 // the file may contain multiple trailing 239 // newlines if the user places empty lines 240 // between files in the archive. normalize 241 // this to a single newline. 242 golden := append(bytes.TrimRight(vf.Data, "\n"), '\n') 243 244 if err := applyDiffsAndCompare(orig, golden, edits, file.Name()); err != nil { 245 t.Errorf("%s", err) 246 } 247 break 248 } 249 } 250 if !found { 251 t.Errorf("no section for suggested fix %q in %s.golden", sf, file.Name()) 252 } 253 } 254 } else { 255 // all suggested fixes are represented by a single file 256 257 var catchallEdits []diff.Edit 258 for _, edits := range fixes { 259 catchallEdits = append(catchallEdits, edits...) 260 } 261 262 if err := applyDiffsAndCompare(orig, ar.Comment, catchallEdits, file.Name()); err != nil { 263 t.Errorf("%s", err) 264 } 265 } 266 } 267 } 268 return r 269 } 270 271 // applyDiffsAndCompare applies edits to src and compares the results against 272 // golden after formatting both. fileName is use solely for error reporting. 273 func applyDiffsAndCompare(src, golden []byte, edits []diff.Edit, fileName string) error { 274 out, err := diff.ApplyBytes(src, edits) 275 if err != nil { 276 return fmt.Errorf("%s: error applying fixes: %v (see possible explanations at RunWithSuggestedFixes)", fileName, err) 277 } 278 wantRaw, err := format.Source(golden) 279 if err != nil { 280 return fmt.Errorf("%s.golden: error formatting golden file: %v\n%s", fileName, err, out) 281 } 282 want := string(wantRaw) 283 284 formatted, err := format.Source(out) 285 if err != nil { 286 return fmt.Errorf("%s: error formatting resulting source: %v\n%s", fileName, err, out) 287 } 288 if got := string(formatted); got != want { 289 unified := diff.Unified(fileName+".golden", "actual", want, got) 290 return fmt.Errorf("suggested fixes failed for %s:\n%s", fileName, unified) 291 } 292 return nil 293 } 294 295 // Run applies an analysis to the packages denoted by the "go list" patterns. 296 // 297 // It loads the packages from the specified 298 // directory using golang.org/x/tools/go/packages, runs the analysis on 299 // them, and checks that each analysis emits the expected diagnostics 300 // and facts specified by the contents of '// want ...' comments in the 301 // package's source files. It treats a comment of the form 302 // "//...// want..." or "/*...// want... */" as if it starts at 'want'. 303 // 304 // If the directory contains a go.mod file, Run treats it as the root of the 305 // Go module in which to work. Otherwise, Run treats it as the root of a 306 // GOPATH-style tree, with package contained in the src subdirectory. 307 // 308 // An expectation of a Diagnostic is specified by a string literal 309 // containing a regular expression that must match the diagnostic 310 // message. For example: 311 // 312 // fmt.Printf("%s", 1) // want `cannot provide int 1 to %s` 313 // 314 // An expectation of a Fact associated with an object is specified by 315 // 'name:"pattern"', where name is the name of the object, which must be 316 // declared on the same line as the comment, and pattern is a regular 317 // expression that must match the string representation of the fact, 318 // fmt.Sprint(fact). For example: 319 // 320 // func panicf(format string, args interface{}) { // want panicf:"printfWrapper" 321 // 322 // Package facts are specified by the name "package" and appear on 323 // line 1 of the first source file of the package. 324 // 325 // A single 'want' comment may contain a mixture of diagnostic and fact 326 // expectations, including multiple facts about the same object: 327 // 328 // // want "diag" "diag2" x:"fact1" x:"fact2" y:"fact3" 329 // 330 // Unexpected diagnostics and facts, and unmatched expectations, are 331 // reported as errors to the Testing. 332 // 333 // Run reports an error to the Testing if loading or analysis failed. 334 // Run also returns a Result for each package for which analysis was 335 // attempted, even if unsuccessful. It is safe for a test to ignore all 336 // the results, but a test may use it to perform additional checks. 337 func Run(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Result { 338 if t, ok := t.(testing.TB); ok { 339 testenv.NeedsGoPackages(t) 340 } 341 342 pkgs, err := loadPackages(a, dir, patterns...) 343 if err != nil { 344 t.Errorf("loading %s: %v", patterns, err) 345 return nil 346 } 347 348 if err := analysis.Validate([]*analysis.Analyzer{a}); err != nil { 349 t.Errorf("Validate: %v", err) 350 return nil 351 } 352 353 results := checker.TestAnalyzer(a, pkgs) 354 for _, result := range results { 355 if result.Err != nil { 356 t.Errorf("error analyzing %s: %v", result.Pass, result.Err) 357 } else { 358 check(t, dir, result.Pass, result.Diagnostics, result.Facts) 359 } 360 } 361 return results 362 } 363 364 // A Result holds the result of applying an analyzer to a package. 365 type Result = checker.TestAnalyzerResult 366 367 // loadPackages uses go/packages to load a specified packages (from source, with 368 // dependencies) from dir, which is the root of a GOPATH-style project tree. 369 // loadPackages returns an error if any package had an error, or the pattern 370 // matched no packages. 371 func loadPackages(a *analysis.Analyzer, dir string, patterns ...string) ([]*packages.Package, error) { 372 env := []string{"GOPATH=" + dir, "GO111MODULE=off", "GOWORK=off"} // GOPATH mode 373 374 // Undocumented module mode. Will be replaced by something better. 375 if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { 376 gowork := filepath.Join(dir, "go.work") 377 if _, err := os.Stat(gowork); err != nil { 378 gowork = "off" 379 } 380 381 env = []string{"GO111MODULE=on", "GOPROXY=off", "GOWORK=" + gowork} // module mode 382 } 383 384 // packages.Load loads the real standard library, not a minimal 385 // fake version, which would be more efficient, especially if we 386 // have many small tests that import, say, net/http. 387 // However there is no easy way to make go/packages to consume 388 // a list of packages we generate and then do the parsing and 389 // typechecking, though this feature seems to be a recurring need. 390 391 mode := packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | 392 packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | 393 packages.NeedDeps | packages.NeedModule 394 cfg := &packages.Config{ 395 Mode: mode, 396 Dir: dir, 397 Tests: true, 398 Env: append(os.Environ(), env...), 399 } 400 pkgs, err := packages.Load(cfg, patterns...) 401 if err != nil { 402 return nil, err 403 } 404 405 // If any named package couldn't be loaded at all 406 // (e.g. the Name field is unset), fail fast. 407 for _, pkg := range pkgs { 408 if pkg.Name == "" { 409 return nil, fmt.Errorf("failed to load %q: Errors=%v", 410 pkg.PkgPath, pkg.Errors) 411 } 412 } 413 414 // Do NOT print errors if the analyzer will continue running. 415 // It is incredibly confusing for tests to be printing to stderr 416 // willy-nilly instead of their test logs, especially when the 417 // errors are expected and are going to be fixed. 418 if !a.RunDespiteErrors { 419 if packages.PrintErrors(pkgs) > 0 { 420 return nil, fmt.Errorf("there were package loading errors (and RunDespiteErrors is false)") 421 } 422 } 423 424 if len(pkgs) == 0 { 425 return nil, fmt.Errorf("no packages matched %s", patterns) 426 } 427 return pkgs, nil 428 } 429 430 // check inspects an analysis pass on which the analysis has already 431 // been run, and verifies that all reported diagnostics and facts match 432 // specified by the contents of "// want ..." comments in the package's 433 // source files, which must have been parsed with comments enabled. 434 func check(t Testing, gopath string, pass *analysis.Pass, diagnostics []analysis.Diagnostic, facts map[types.Object][]analysis.Fact) { 435 type key struct { 436 file string 437 line int 438 } 439 440 want := make(map[key][]expectation) 441 442 // processComment parses expectations out of comments. 443 processComment := func(filename string, linenum int, text string) { 444 text = strings.TrimSpace(text) 445 446 // Any comment starting with "want" is treated 447 // as an expectation, even without following whitespace. 448 if rest := strings.TrimPrefix(text, "want"); rest != text { 449 lineDelta, expects, err := parseExpectations(rest) 450 if err != nil { 451 t.Errorf("%s:%d: in 'want' comment: %s", filename, linenum, err) 452 return 453 } 454 if expects != nil { 455 want[key{filename, linenum + lineDelta}] = expects 456 } 457 } 458 } 459 460 // Extract 'want' comments from parsed Go files. 461 for _, f := range pass.Files { 462 for _, cgroup := range f.Comments { 463 for _, c := range cgroup.List { 464 465 text := strings.TrimPrefix(c.Text, "//") 466 if text == c.Text { // not a //-comment. 467 text = strings.TrimPrefix(text, "/*") 468 text = strings.TrimSuffix(text, "*/") 469 } 470 471 // Hack: treat a comment of the form "//...// want..." 472 // or "/*...// want... */ 473 // as if it starts at 'want'. 474 // This allows us to add comments on comments, 475 // as required when testing the buildtag analyzer. 476 if i := strings.Index(text, "// want"); i >= 0 { 477 text = text[i+len("// "):] 478 } 479 480 // It's tempting to compute the filename 481 // once outside the loop, but it's 482 // incorrect because it can change due 483 // to //line directives. 484 posn := pass.Fset.Position(c.Pos()) 485 filename := sanitize(gopath, posn.Filename) 486 processComment(filename, posn.Line, text) 487 } 488 } 489 } 490 491 // Extract 'want' comments from non-Go files. 492 // TODO(adonovan): we may need to handle //line directives. 493 for _, filename := range pass.OtherFiles { 494 data, err := os.ReadFile(filename) 495 if err != nil { 496 t.Errorf("can't read '// want' comments from %s: %v", filename, err) 497 continue 498 } 499 filename := sanitize(gopath, filename) 500 linenum := 0 501 for _, line := range strings.Split(string(data), "\n") { 502 linenum++ 503 504 // Hack: treat a comment of the form "//...// want..." 505 // or "/*...// want... */ 506 // as if it starts at 'want'. 507 // This allows us to add comments on comments, 508 // as required when testing the buildtag analyzer. 509 if i := strings.Index(line, "// want"); i >= 0 { 510 line = line[i:] 511 } 512 513 if i := strings.Index(line, "//"); i >= 0 { 514 line = line[i+len("//"):] 515 processComment(filename, linenum, line) 516 } 517 } 518 } 519 520 checkMessage := func(posn token.Position, kind, name, message string) { 521 posn.Filename = sanitize(gopath, posn.Filename) 522 k := key{posn.Filename, posn.Line} 523 expects := want[k] 524 var unmatched []string 525 for i, exp := range expects { 526 if exp.kind == kind && exp.name == name { 527 if exp.rx.MatchString(message) { 528 // matched: remove the expectation. 529 expects[i] = expects[len(expects)-1] 530 expects = expects[:len(expects)-1] 531 want[k] = expects 532 return 533 } 534 unmatched = append(unmatched, fmt.Sprintf("%#q", exp.rx)) 535 } 536 } 537 if unmatched == nil { 538 t.Errorf("%v: unexpected %s: %v", posn, kind, message) 539 } else { 540 t.Errorf("%v: %s %q does not match pattern %s", 541 posn, kind, message, strings.Join(unmatched, " or ")) 542 } 543 } 544 545 // Check the diagnostics match expectations. 546 for _, f := range diagnostics { 547 // TODO(matloob): Support ranges in analysistest. 548 posn := pass.Fset.Position(f.Pos) 549 checkMessage(posn, "diagnostic", "", f.Message) 550 } 551 552 // Check the facts match expectations. 553 // Report errors in lexical order for determinism. 554 // (It's only deterministic within each file, not across files, 555 // because go/packages does not guarantee file.Pos is ascending 556 // across the files of a single compilation unit.) 557 var objects []types.Object 558 for obj := range facts { 559 objects = append(objects, obj) 560 } 561 sort.Slice(objects, func(i, j int) bool { 562 // Package facts compare less than object facts. 563 ip, jp := objects[i] == nil, objects[j] == nil // whether i, j is a package fact 564 if ip != jp { 565 return ip && !jp 566 } 567 return objects[i].Pos() < objects[j].Pos() 568 }) 569 for _, obj := range objects { 570 var posn token.Position 571 var name string 572 if obj != nil { 573 // Object facts are reported on the declaring line. 574 name = obj.Name() 575 posn = pass.Fset.Position(obj.Pos()) 576 } else { 577 // Package facts are reported at the start of the file. 578 name = "package" 579 posn = pass.Fset.Position(pass.Files[0].Pos()) 580 posn.Line = 1 581 } 582 583 for _, fact := range facts[obj] { 584 checkMessage(posn, "fact", name, fmt.Sprint(fact)) 585 } 586 } 587 588 // Reject surplus expectations. 589 // 590 // Sometimes an Analyzer reports two similar diagnostics on a 591 // line with only one expectation. The reader may be confused by 592 // the error message. 593 // TODO(adonovan): print a better error: 594 // "got 2 diagnostics here; each one needs its own expectation". 595 var surplus []string 596 for key, expects := range want { 597 for _, exp := range expects { 598 err := fmt.Sprintf("%s:%d: no %s was reported matching %#q", key.file, key.line, exp.kind, exp.rx) 599 surplus = append(surplus, err) 600 } 601 } 602 sort.Strings(surplus) 603 for _, err := range surplus { 604 t.Errorf("%s", err) 605 } 606 } 607 608 type expectation struct { 609 kind string // either "fact" or "diagnostic" 610 name string // name of object to which fact belongs, or "package" ("fact" only) 611 rx *regexp.Regexp 612 } 613 614 func (ex expectation) String() string { 615 return fmt.Sprintf("%s %s:%q", ex.kind, ex.name, ex.rx) // for debugging 616 } 617 618 // parseExpectations parses the content of a "// want ..." comment 619 // and returns the expectations, a mixture of diagnostics ("rx") and 620 // facts (name:"rx"). 621 func parseExpectations(text string) (lineDelta int, expects []expectation, err error) { 622 var scanErr string 623 sc := new(scanner.Scanner).Init(strings.NewReader(text)) 624 sc.Error = func(s *scanner.Scanner, msg string) { 625 scanErr = msg // e.g. bad string escape 626 } 627 sc.Mode = scanner.ScanIdents | scanner.ScanStrings | scanner.ScanRawStrings | scanner.ScanInts 628 629 scanRegexp := func(tok rune) (*regexp.Regexp, error) { 630 if tok != scanner.String && tok != scanner.RawString { 631 return nil, fmt.Errorf("got %s, want regular expression", 632 scanner.TokenString(tok)) 633 } 634 pattern, _ := strconv.Unquote(sc.TokenText()) // can't fail 635 return regexp.Compile(pattern) 636 } 637 638 for { 639 tok := sc.Scan() 640 switch tok { 641 case '+': 642 tok = sc.Scan() 643 if tok != scanner.Int { 644 return 0, nil, fmt.Errorf("got +%s, want +Int", scanner.TokenString(tok)) 645 } 646 lineDelta, _ = strconv.Atoi(sc.TokenText()) 647 case scanner.String, scanner.RawString: 648 rx, err := scanRegexp(tok) 649 if err != nil { 650 return 0, nil, err 651 } 652 expects = append(expects, expectation{"diagnostic", "", rx}) 653 654 case scanner.Ident: 655 name := sc.TokenText() 656 tok = sc.Scan() 657 if tok != ':' { 658 return 0, nil, fmt.Errorf("got %s after %s, want ':'", 659 scanner.TokenString(tok), name) 660 } 661 tok = sc.Scan() 662 rx, err := scanRegexp(tok) 663 if err != nil { 664 return 0, nil, err 665 } 666 expects = append(expects, expectation{"fact", name, rx}) 667 668 case scanner.EOF: 669 if scanErr != "" { 670 return 0, nil, fmt.Errorf("%s", scanErr) 671 } 672 return lineDelta, expects, nil 673 674 default: 675 return 0, nil, fmt.Errorf("unexpected %s", scanner.TokenString(tok)) 676 } 677 } 678 } 679 680 // sanitize removes the GOPATH portion of the filename, 681 // typically a gnarly /tmp directory, and returns the rest. 682 func sanitize(gopath, filename string) string { 683 prefix := gopath + string(os.PathSeparator) + "src" + string(os.PathSeparator) 684 return filepath.ToSlash(strings.TrimPrefix(filename, prefix)) 685 }