.gitignore added
[dotfiles/.git] / .config / coc / extensions / coc-go-data / tools / pkg / mod / golang.org / x / tools@v0.1.1-0.20210319172145-bda8f5cee399 / go / analysis / analysistest / analysistest.go
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         "io/ioutil"
15         "log"
16         "os"
17         "path/filepath"
18         "regexp"
19         "sort"
20         "strconv"
21         "strings"
22         "text/scanner"
23
24         "golang.org/x/tools/go/analysis"
25         "golang.org/x/tools/go/analysis/internal/checker"
26         "golang.org/x/tools/go/packages"
27         "golang.org/x/tools/internal/lsp/diff"
28         "golang.org/x/tools/internal/lsp/diff/myers"
29         "golang.org/x/tools/internal/span"
30         "golang.org/x/tools/internal/testenv"
31         "golang.org/x/tools/txtar"
32 )
33
34 // WriteFiles is a helper function that creates a temporary directory
35 // and populates it with a GOPATH-style project using filemap (which
36 // maps file names to contents). On success it returns the name of the
37 // directory and a cleanup function to delete it.
38 func WriteFiles(filemap map[string]string) (dir string, cleanup func(), err error) {
39         gopath, err := ioutil.TempDir("", "analysistest")
40         if err != nil {
41                 return "", nil, err
42         }
43         cleanup = func() { os.RemoveAll(gopath) }
44
45         for name, content := range filemap {
46                 filename := filepath.Join(gopath, "src", name)
47                 os.MkdirAll(filepath.Dir(filename), 0777) // ignore error
48                 if err := ioutil.WriteFile(filename, []byte(content), 0666); err != nil {
49                         cleanup()
50                         return "", nil, err
51                 }
52         }
53         return gopath, cleanup, nil
54 }
55
56 // TestData returns the effective filename of
57 // the program's "testdata" directory.
58 // This function may be overridden by projects using
59 // an alternative build system (such as Blaze) that
60 // does not run a test in its package directory.
61 var TestData = func() string {
62         testdata, err := filepath.Abs("testdata")
63         if err != nil {
64                 log.Fatal(err)
65         }
66         return testdata
67 }
68
69 // Testing is an abstraction of a *testing.T.
70 type Testing interface {
71         Errorf(format string, args ...interface{})
72 }
73
74 // RunWithSuggestedFixes behaves like Run, but additionally verifies suggested fixes.
75 // It uses golden files placed alongside the source code under analysis:
76 // suggested fixes for code in example.go will be compared against example.go.golden.
77 //
78 // Golden files can be formatted in one of two ways: as plain Go source code, or as txtar archives.
79 // In the first case, all suggested fixes will be applied to the original source, which will then be compared against the golden file.
80 // In the second case, suggested fixes will be grouped by their messages, and each set of fixes will be applied and tested separately.
81 // Each section in the archive corresponds to a single message.
82 //
83 // A golden file using txtar may look like this:
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 func RunWithSuggestedFixes(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Result {
102         r := Run(t, dir, a, patterns...)
103
104         // Process each result (package) separately, matching up the suggested
105         // fixes into a diff, which we will compare to the .golden file.  We have
106         // to do this per-result in case a file appears in two packages, such as in
107         // packages with tests, where mypkg/a.go will appear in both mypkg and
108         // mypkg.test.  In that case, the analyzer may suggest the same set of
109         // changes to a.go for each package.  If we merge all the results, those
110         // changes get doubly applied, which will cause conflicts or mismatches.
111         // Validating the results separately means as long as the two analyses
112         // don't produce conflicting suggestions for a single file, everything
113         // should match up.
114         for _, act := range r {
115                 // file -> message -> edits
116                 fileEdits := make(map[*token.File]map[string][]diff.TextEdit)
117                 fileContents := make(map[*token.File][]byte)
118
119                 // Validate edits, prepare the fileEdits map and read the file contents.
120                 for _, diag := range act.Diagnostics {
121                         for _, sf := range diag.SuggestedFixes {
122                                 for _, edit := range sf.TextEdits {
123                                         // Validate the edit.
124                                         if edit.Pos > edit.End {
125                                                 t.Errorf(
126                                                         "diagnostic for analysis %v contains Suggested Fix with malformed edit: pos (%v) > end (%v)",
127                                                         act.Pass.Analyzer.Name, edit.Pos, edit.End)
128                                                 continue
129                                         }
130                                         file, endfile := act.Pass.Fset.File(edit.Pos), act.Pass.Fset.File(edit.End)
131                                         if file == nil || endfile == nil || file != endfile {
132                                                 t.Errorf(
133                                                         "diagnostic for analysis %v contains Suggested Fix with malformed spanning files %v and %v",
134                                                         act.Pass.Analyzer.Name, file.Name(), endfile.Name())
135                                                 continue
136                                         }
137                                         if _, ok := fileContents[file]; !ok {
138                                                 contents, err := ioutil.ReadFile(file.Name())
139                                                 if err != nil {
140                                                         t.Errorf("error reading %s: %v", file.Name(), err)
141                                                 }
142                                                 fileContents[file] = contents
143                                         }
144                                         spn, err := span.NewRange(act.Pass.Fset, edit.Pos, edit.End).Span()
145                                         if err != nil {
146                                                 t.Errorf("error converting edit to span %s: %v", file.Name(), err)
147                                         }
148
149                                         if _, ok := fileEdits[file]; !ok {
150                                                 fileEdits[file] = make(map[string][]diff.TextEdit)
151                                         }
152                                         fileEdits[file][sf.Message] = append(fileEdits[file][sf.Message], diff.TextEdit{
153                                                 Span:    spn,
154                                                 NewText: string(edit.NewText),
155                                         })
156                                 }
157                         }
158                 }
159
160                 for file, fixes := range fileEdits {
161                         // Get the original file contents.
162                         orig, ok := fileContents[file]
163                         if !ok {
164                                 t.Errorf("could not find file contents for %s", file.Name())
165                                 continue
166                         }
167
168                         // Get the golden file and read the contents.
169                         ar, err := txtar.ParseFile(file.Name() + ".golden")
170                         if err != nil {
171                                 t.Errorf("error reading %s.golden: %v", file.Name(), err)
172                                 continue
173                         }
174
175                         if len(ar.Files) > 0 {
176                                 // one virtual file per kind of suggested fix
177
178                                 if len(ar.Comment) != 0 {
179                                         // we allow either just the comment, or just virtual
180                                         // files, not both. it is not clear how "both" should
181                                         // behave.
182                                         t.Errorf("%s.golden has leading comment; we don't know what to do with it", file.Name())
183                                         continue
184                                 }
185
186                                 for sf, edits := range fixes {
187                                         found := false
188                                         for _, vf := range ar.Files {
189                                                 if vf.Name == sf {
190                                                         found = true
191                                                         out := diff.ApplyEdits(string(orig), edits)
192                                                         // the file may contain multiple trailing
193                                                         // newlines if the user places empty lines
194                                                         // between files in the archive. normalize
195                                                         // this to a single newline.
196                                                         want := string(bytes.TrimRight(vf.Data, "\n")) + "\n"
197                                                         formatted, err := format.Source([]byte(out))
198                                                         if err != nil {
199                                                                 continue
200                                                         }
201                                                         if want != string(formatted) {
202                                                                 d, err := myers.ComputeEdits("", want, string(formatted))
203                                                                 if err != nil {
204                                                                         t.Errorf("failed to compute suggested fixes: %v", err)
205                                                                 }
206                                                                 t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.ToUnified(fmt.Sprintf("%s.golden [%s]", file.Name(), sf), "actual", want, d))
207                                                         }
208                                                         break
209                                                 }
210                                         }
211                                         if !found {
212                                                 t.Errorf("no section for suggested fix %q in %s.golden", sf, file.Name())
213                                         }
214                                 }
215                         } else {
216                                 // all suggested fixes are represented by a single file
217
218                                 var catchallEdits []diff.TextEdit
219                                 for _, edits := range fixes {
220                                         catchallEdits = append(catchallEdits, edits...)
221                                 }
222
223                                 out := diff.ApplyEdits(string(orig), catchallEdits)
224                                 want := string(ar.Comment)
225
226                                 formatted, err := format.Source([]byte(out))
227                                 if err != nil {
228                                         continue
229                                 }
230                                 if want != string(formatted) {
231                                         d, err := myers.ComputeEdits("", want, string(formatted))
232                                         if err != nil {
233                                                 t.Errorf("failed to compute edits: %s", err)
234                                         }
235                                         t.Errorf("suggested fixes failed for %s:\n%s", file.Name(), diff.ToUnified(file.Name()+".golden", "actual", want, d))
236                                 }
237                         }
238                 }
239         }
240         return r
241 }
242
243 // Run applies an analysis to the packages denoted by the "go list" patterns.
244 //
245 // It loads the packages from the specified GOPATH-style project
246 // directory using golang.org/x/tools/go/packages, runs the analysis on
247 // them, and checks that each analysis emits the expected diagnostics
248 // and facts specified by the contents of '// want ...' comments in the
249 // package's source files.
250 //
251 // An expectation of a Diagnostic is specified by a string literal
252 // containing a regular expression that must match the diagnostic
253 // message. For example:
254 //
255 //      fmt.Printf("%s", 1) // want `cannot provide int 1 to %s`
256 //
257 // An expectation of a Fact associated with an object is specified by
258 // 'name:"pattern"', where name is the name of the object, which must be
259 // declared on the same line as the comment, and pattern is a regular
260 // expression that must match the string representation of the fact,
261 // fmt.Sprint(fact). For example:
262 //
263 //      func panicf(format string, args interface{}) { // want panicf:"printfWrapper"
264 //
265 // Package facts are specified by the name "package" and appear on
266 // line 1 of the first source file of the package.
267 //
268 // A single 'want' comment may contain a mixture of diagnostic and fact
269 // expectations, including multiple facts about the same object:
270 //
271 //      // want "diag" "diag2" x:"fact1" x:"fact2" y:"fact3"
272 //
273 // Unexpected diagnostics and facts, and unmatched expectations, are
274 // reported as errors to the Testing.
275 //
276 // Run reports an error to the Testing if loading or analysis failed.
277 // Run also returns a Result for each package for which analysis was
278 // attempted, even if unsuccessful. It is safe for a test to ignore all
279 // the results, but a test may use it to perform additional checks.
280 func Run(t Testing, dir string, a *analysis.Analyzer, patterns ...string) []*Result {
281         if t, ok := t.(testenv.Testing); ok {
282                 testenv.NeedsGoPackages(t)
283         }
284
285         pkgs, err := loadPackages(dir, patterns...)
286         if err != nil {
287                 t.Errorf("loading %s: %v", patterns, err)
288                 return nil
289         }
290
291         results := checker.TestAnalyzer(a, pkgs)
292         for _, result := range results {
293                 if result.Err != nil {
294                         t.Errorf("error analyzing %s: %v", result.Pass, result.Err)
295                 } else {
296                         check(t, dir, result.Pass, result.Diagnostics, result.Facts)
297                 }
298         }
299         return results
300 }
301
302 // A Result holds the result of applying an analyzer to a package.
303 type Result = checker.TestAnalyzerResult
304
305 // loadPackages uses go/packages to load a specified packages (from source, with
306 // dependencies) from dir, which is the root of a GOPATH-style project
307 // tree. It returns an error if any package had an error, or the pattern
308 // matched no packages.
309 func loadPackages(dir string, patterns ...string) ([]*packages.Package, error) {
310         // packages.Load loads the real standard library, not a minimal
311         // fake version, which would be more efficient, especially if we
312         // have many small tests that import, say, net/http.
313         // However there is no easy way to make go/packages to consume
314         // a list of packages we generate and then do the parsing and
315         // typechecking, though this feature seems to be a recurring need.
316
317         cfg := &packages.Config{
318                 Mode:  packages.LoadAllSyntax,
319                 Dir:   dir,
320                 Tests: true,
321                 Env:   append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"),
322         }
323         pkgs, err := packages.Load(cfg, patterns...)
324         if err != nil {
325                 return nil, err
326         }
327
328         // Print errors but do not stop:
329         // some Analyzers may be disposed to RunDespiteErrors.
330         packages.PrintErrors(pkgs)
331
332         if len(pkgs) == 0 {
333                 return nil, fmt.Errorf("no packages matched %s", patterns)
334         }
335         return pkgs, nil
336 }
337
338 // check inspects an analysis pass on which the analysis has already
339 // been run, and verifies that all reported diagnostics and facts match
340 // specified by the contents of "// want ..." comments in the package's
341 // source files, which must have been parsed with comments enabled.
342 func check(t Testing, gopath string, pass *analysis.Pass, diagnostics []analysis.Diagnostic, facts map[types.Object][]analysis.Fact) {
343         type key struct {
344                 file string
345                 line int
346         }
347
348         want := make(map[key][]expectation)
349
350         // processComment parses expectations out of comments.
351         processComment := func(filename string, linenum int, text string) {
352                 text = strings.TrimSpace(text)
353
354                 // Any comment starting with "want" is treated
355                 // as an expectation, even without following whitespace.
356                 if rest := strings.TrimPrefix(text, "want"); rest != text {
357                         lineDelta, expects, err := parseExpectations(rest)
358                         if err != nil {
359                                 t.Errorf("%s:%d: in 'want' comment: %s", filename, linenum, err)
360                                 return
361                         }
362                         if expects != nil {
363                                 want[key{filename, linenum + lineDelta}] = expects
364                         }
365                 }
366         }
367
368         // Extract 'want' comments from parsed Go files.
369         for _, f := range pass.Files {
370                 for _, cgroup := range f.Comments {
371                         for _, c := range cgroup.List {
372
373                                 text := strings.TrimPrefix(c.Text, "//")
374                                 if text == c.Text { // not a //-comment.
375                                         text = strings.TrimPrefix(text, "/*")
376                                         text = strings.TrimSuffix(text, "*/")
377                                 }
378
379                                 // Hack: treat a comment of the form "//...// want..."
380                                 // or "/*...// want... */
381                                 // as if it starts at 'want'.
382                                 // This allows us to add comments on comments,
383                                 // as required when testing the buildtag analyzer.
384                                 if i := strings.Index(text, "// want"); i >= 0 {
385                                         text = text[i+len("// "):]
386                                 }
387
388                                 // It's tempting to compute the filename
389                                 // once outside the loop, but it's
390                                 // incorrect because it can change due
391                                 // to //line directives.
392                                 posn := pass.Fset.Position(c.Pos())
393                                 filename := sanitize(gopath, posn.Filename)
394                                 processComment(filename, posn.Line, text)
395                         }
396                 }
397         }
398
399         // Extract 'want' comments from non-Go files.
400         // TODO(adonovan): we may need to handle //line directives.
401         for _, filename := range pass.OtherFiles {
402                 data, err := ioutil.ReadFile(filename)
403                 if err != nil {
404                         t.Errorf("can't read '// want' comments from %s: %v", filename, err)
405                         continue
406                 }
407                 filename := sanitize(gopath, filename)
408                 linenum := 0
409                 for _, line := range strings.Split(string(data), "\n") {
410                         linenum++
411
412                         // Hack: treat a comment of the form "//...// want..."
413                         // or "/*...// want... */
414                         // as if it starts at 'want'.
415                         // This allows us to add comments on comments,
416                         // as required when testing the buildtag analyzer.
417                         if i := strings.Index(line, "// want"); i >= 0 {
418                                 line = line[i:]
419                         }
420
421                         if i := strings.Index(line, "//"); i >= 0 {
422                                 line = line[i+len("//"):]
423                                 processComment(filename, linenum, line)
424                         }
425                 }
426         }
427
428         checkMessage := func(posn token.Position, kind, name, message string) {
429                 posn.Filename = sanitize(gopath, posn.Filename)
430                 k := key{posn.Filename, posn.Line}
431                 expects := want[k]
432                 var unmatched []string
433                 for i, exp := range expects {
434                         if exp.kind == kind && exp.name == name {
435                                 if exp.rx.MatchString(message) {
436                                         // matched: remove the expectation.
437                                         expects[i] = expects[len(expects)-1]
438                                         expects = expects[:len(expects)-1]
439                                         want[k] = expects
440                                         return
441                                 }
442                                 unmatched = append(unmatched, fmt.Sprintf("%q", exp.rx))
443                         }
444                 }
445                 if unmatched == nil {
446                         t.Errorf("%v: unexpected %s: %v", posn, kind, message)
447                 } else {
448                         t.Errorf("%v: %s %q does not match pattern %s",
449                                 posn, kind, message, strings.Join(unmatched, " or "))
450                 }
451         }
452
453         // Check the diagnostics match expectations.
454         for _, f := range diagnostics {
455                 // TODO(matloob): Support ranges in analysistest.
456                 posn := pass.Fset.Position(f.Pos)
457                 checkMessage(posn, "diagnostic", "", f.Message)
458         }
459
460         // Check the facts match expectations.
461         // Report errors in lexical order for determinism.
462         // (It's only deterministic within each file, not across files,
463         // because go/packages does not guarantee file.Pos is ascending
464         // across the files of a single compilation unit.)
465         var objects []types.Object
466         for obj := range facts {
467                 objects = append(objects, obj)
468         }
469         sort.Slice(objects, func(i, j int) bool {
470                 // Package facts compare less than object facts.
471                 ip, jp := objects[i] == nil, objects[j] == nil // whether i, j is a package fact
472                 if ip != jp {
473                         return ip && !jp
474                 }
475                 return objects[i].Pos() < objects[j].Pos()
476         })
477         for _, obj := range objects {
478                 var posn token.Position
479                 var name string
480                 if obj != nil {
481                         // Object facts are reported on the declaring line.
482                         name = obj.Name()
483                         posn = pass.Fset.Position(obj.Pos())
484                 } else {
485                         // Package facts are reported at the start of the file.
486                         name = "package"
487                         posn = pass.Fset.Position(pass.Files[0].Pos())
488                         posn.Line = 1
489                 }
490
491                 for _, fact := range facts[obj] {
492                         checkMessage(posn, "fact", name, fmt.Sprint(fact))
493                 }
494         }
495
496         // Reject surplus expectations.
497         //
498         // Sometimes an Analyzer reports two similar diagnostics on a
499         // line with only one expectation. The reader may be confused by
500         // the error message.
501         // TODO(adonovan): print a better error:
502         // "got 2 diagnostics here; each one needs its own expectation".
503         var surplus []string
504         for key, expects := range want {
505                 for _, exp := range expects {
506                         err := fmt.Sprintf("%s:%d: no %s was reported matching %q", key.file, key.line, exp.kind, exp.rx)
507                         surplus = append(surplus, err)
508                 }
509         }
510         sort.Strings(surplus)
511         for _, err := range surplus {
512                 t.Errorf("%s", err)
513         }
514 }
515
516 type expectation struct {
517         kind string // either "fact" or "diagnostic"
518         name string // name of object to which fact belongs, or "package" ("fact" only)
519         rx   *regexp.Regexp
520 }
521
522 func (ex expectation) String() string {
523         return fmt.Sprintf("%s %s:%q", ex.kind, ex.name, ex.rx) // for debugging
524 }
525
526 // parseExpectations parses the content of a "// want ..." comment
527 // and returns the expectations, a mixture of diagnostics ("rx") and
528 // facts (name:"rx").
529 func parseExpectations(text string) (lineDelta int, expects []expectation, err error) {
530         var scanErr string
531         sc := new(scanner.Scanner).Init(strings.NewReader(text))
532         sc.Error = func(s *scanner.Scanner, msg string) {
533                 scanErr = msg // e.g. bad string escape
534         }
535         sc.Mode = scanner.ScanIdents | scanner.ScanStrings | scanner.ScanRawStrings | scanner.ScanInts
536
537         scanRegexp := func(tok rune) (*regexp.Regexp, error) {
538                 if tok != scanner.String && tok != scanner.RawString {
539                         return nil, fmt.Errorf("got %s, want regular expression",
540                                 scanner.TokenString(tok))
541                 }
542                 pattern, _ := strconv.Unquote(sc.TokenText()) // can't fail
543                 return regexp.Compile(pattern)
544         }
545
546         for {
547                 tok := sc.Scan()
548                 switch tok {
549                 case '+':
550                         tok = sc.Scan()
551                         if tok != scanner.Int {
552                                 return 0, nil, fmt.Errorf("got +%s, want +Int", scanner.TokenString(tok))
553                         }
554                         lineDelta, _ = strconv.Atoi(sc.TokenText())
555                 case scanner.String, scanner.RawString:
556                         rx, err := scanRegexp(tok)
557                         if err != nil {
558                                 return 0, nil, err
559                         }
560                         expects = append(expects, expectation{"diagnostic", "", rx})
561
562                 case scanner.Ident:
563                         name := sc.TokenText()
564                         tok = sc.Scan()
565                         if tok != ':' {
566                                 return 0, nil, fmt.Errorf("got %s after %s, want ':'",
567                                         scanner.TokenString(tok), name)
568                         }
569                         tok = sc.Scan()
570                         rx, err := scanRegexp(tok)
571                         if err != nil {
572                                 return 0, nil, err
573                         }
574                         expects = append(expects, expectation{"fact", name, rx})
575
576                 case scanner.EOF:
577                         if scanErr != "" {
578                                 return 0, nil, fmt.Errorf("%s", scanErr)
579                         }
580                         return lineDelta, expects, nil
581
582                 default:
583                         return 0, nil, fmt.Errorf("unexpected %s", scanner.TokenString(tok))
584                 }
585         }
586 }
587
588 // sanitize removes the GOPATH portion of the filename,
589 // typically a gnarly /tmp directory, and returns the rest.
590 func sanitize(gopath, filename string) string {
591         prefix := gopath + string(os.PathSeparator) + "src" + string(os.PathSeparator)
592         return filepath.ToSlash(strings.TrimPrefix(filename, prefix))
593 }