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  }