github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/internal/facts/facts_test.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 facts_test
     6  
     7  import (
     8  	"encoding/gob"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/parser"
    12  	"go/token"
    13  	"go/types"
    14  	"os"
    15  	"reflect"
    16  	"strings"
    17  	"testing"
    18  
    19  	"golang.org/x/tools/go/analysis/analysistest"
    20  	"golang.org/x/tools/go/packages"
    21  	"golang.org/x/tools/internal/facts"
    22  	"golang.org/x/tools/internal/testenv"
    23  	"golang.org/x/tools/internal/typeparams"
    24  )
    25  
    26  type myFact struct {
    27  	S string
    28  }
    29  
    30  func (f *myFact) String() string { return fmt.Sprintf("myFact(%s)", f.S) }
    31  func (f *myFact) AFact()         {}
    32  
    33  func init() {
    34  	gob.Register(new(myFact))
    35  }
    36  
    37  func TestEncodeDecode(t *testing.T) {
    38  	tests := []struct {
    39  		name       string
    40  		typeparams bool // requires typeparams to be enabled
    41  		files      map[string]string
    42  		plookups   []pkgLookups // see testEncodeDecode for details
    43  	}{
    44  		{
    45  			name: "loading-order",
    46  			// c -> b -> a, a2
    47  			// c does not directly depend on a, but it indirectly uses a.T.
    48  			//
    49  			// Package a2 is never loaded directly so it is incomplete.
    50  			//
    51  			// We use only types in this example because we rely on
    52  			// types.Eval to resolve the lookup expressions, and it only
    53  			// works for types. This is a definite gap in the typechecker API.
    54  			files: map[string]string{
    55  				"a/a.go":  `package a; type A int; type T int`,
    56  				"a2/a.go": `package a2; type A2 int; type Unneeded int`,
    57  				"b/b.go":  `package b; import ("a"; "a2"); type B chan a2.A2; type F func() a.T`,
    58  				"c/c.go":  `package c; import "b"; type C []b.B`,
    59  			},
    60  			// In the following table, we analyze packages (a, b, c) in order,
    61  			// look up various objects accessible within each package,
    62  			// and see if they have a fact.  The "analysis" exports a fact
    63  			// for every object at package level.
    64  			//
    65  			// Note: Loop iterations are not independent test cases;
    66  			// order matters, as we populate factmap.
    67  			plookups: []pkgLookups{
    68  				{"a", []lookup{
    69  					{"A", "myFact(a.A)"},
    70  				}},
    71  				{"b", []lookup{
    72  					{"a.A", "myFact(a.A)"},
    73  					{"a.T", "myFact(a.T)"},
    74  					{"B", "myFact(b.B)"},
    75  					{"F", "myFact(b.F)"},
    76  					{"F(nil)()", "myFact(a.T)"}, // (result type of b.F)
    77  				}},
    78  				{"c", []lookup{
    79  					{"b.B", "myFact(b.B)"},
    80  					{"b.F", "myFact(b.F)"},
    81  					{"b.F(nil)()", "myFact(a.T)"},
    82  					{"C", "myFact(c.C)"},
    83  					{"C{}[0]", "myFact(b.B)"},
    84  					{"<-(C{}[0])", "no fact"}, // object but no fact (we never "analyze" a2)
    85  				}},
    86  			},
    87  		},
    88  		{
    89  			name: "underlying",
    90  			// c->b->a
    91  			// c does not import a directly or use any of its types, but it does use
    92  			// the types within a indirectly. c.q has the type a.a so package a should
    93  			// be included by importMap.
    94  			files: map[string]string{
    95  				"a/a.go": `package a; type a int; type T *a`,
    96  				"b/b.go": `package b; import "a"; type B a.T`,
    97  				"c/c.go": `package c; import "b"; type C b.B; var q = *C(nil)`,
    98  			},
    99  			plookups: []pkgLookups{
   100  				{"a", []lookup{
   101  					{"a", "myFact(a.a)"},
   102  					{"T", "myFact(a.T)"},
   103  				}},
   104  				{"b", []lookup{
   105  					{"B", "myFact(b.B)"},
   106  					{"B(nil)", "myFact(b.B)"},
   107  					{"*(B(nil))", "myFact(a.a)"},
   108  				}},
   109  				{"c", []lookup{
   110  					{"C", "myFact(c.C)"},
   111  					{"C(nil)", "myFact(c.C)"},
   112  					{"*C(nil)", "myFact(a.a)"},
   113  					{"q", "myFact(a.a)"},
   114  				}},
   115  			},
   116  		},
   117  		{
   118  			name: "methods",
   119  			// c->b->a
   120  			// c does not import a directly or use any of its types, but it does use
   121  			// the types within a indirectly via a method.
   122  			files: map[string]string{
   123  				"a/a.go": `package a; type T int`,
   124  				"b/b.go": `package b; import "a"; type B struct{}; func (_ B) M() a.T { return 0 }`,
   125  				"c/c.go": `package c; import "b"; var C b.B`,
   126  			},
   127  			plookups: []pkgLookups{
   128  				{"a", []lookup{
   129  					{"T", "myFact(a.T)"},
   130  				}},
   131  				{"b", []lookup{
   132  					{"B{}", "myFact(b.B)"},
   133  					{"B{}.M()", "myFact(a.T)"},
   134  				}},
   135  				{"c", []lookup{
   136  					{"C", "myFact(b.B)"},
   137  					{"C.M()", "myFact(a.T)"},
   138  				}},
   139  			},
   140  		},
   141  		{
   142  			name: "globals",
   143  			files: map[string]string{
   144  				"a/a.go": `package a;
   145  				type T1 int
   146  				type T2 int
   147  				type T3 int
   148  				type T4 int
   149  				type T5 int
   150  				type K int; type V string
   151  				`,
   152  				"b/b.go": `package b
   153  				import "a"
   154  				var (
   155  					G1 []a.T1
   156  					G2 [7]a.T2
   157  					G3 chan a.T3
   158  					G4 *a.T4
   159  					G5 struct{ F a.T5 }
   160  					G6 map[a.K]a.V
   161  				)
   162  				`,
   163  				"c/c.go": `package c; import "b";
   164  				var (
   165  					v1 = b.G1
   166  					v2 = b.G2
   167  					v3 = b.G3
   168  					v4 = b.G4
   169  					v5 = b.G5
   170  					v6 = b.G6
   171  				)
   172  				`,
   173  			},
   174  			plookups: []pkgLookups{
   175  				{"a", []lookup{}},
   176  				{"b", []lookup{}},
   177  				{"c", []lookup{
   178  					{"v1[0]", "myFact(a.T1)"},
   179  					{"v2[0]", "myFact(a.T2)"},
   180  					{"<-v3", "myFact(a.T3)"},
   181  					{"*v4", "myFact(a.T4)"},
   182  					{"v5.F", "myFact(a.T5)"},
   183  					{"v6[0]", "myFact(a.V)"},
   184  				}},
   185  			},
   186  		},
   187  		{
   188  			name:       "typeparams",
   189  			typeparams: true,
   190  			files: map[string]string{
   191  				"a/a.go": `package a
   192  				  type T1 int
   193  				  type T2 int
   194  				  type T3 interface{Foo()}
   195  				  type T4 int
   196  				  type T5 int
   197  				  type T6 interface{Foo()}
   198  				`,
   199  				"b/b.go": `package b
   200  				  import "a"
   201  				  type N1[T a.T1|int8] func() T
   202  				  type N2[T any] struct{ F T }
   203  				  type N3[T a.T3] func() T
   204  				  type N4[T a.T4|int8] func() T
   205  				  type N5[T interface{Bar() a.T5} ] func() T
   206  		
   207  				  type t5 struct{}; func (t5) Bar() a.T5 { return 0 }
   208  		
   209  				  var G1 N1[a.T1]
   210  				  var G2 func() N2[a.T2]
   211  				  var G3 N3[a.T3]
   212  				  var G4 N4[a.T4]
   213  				  var G5 N5[t5]
   214  
   215  				  func F6[T a.T6]() T { var x T; return x }
   216  				  `,
   217  				"c/c.go": `package c; import "b";
   218  				  var (
   219  					  v1 = b.G1
   220  					  v2 = b.G2
   221  					  v3 = b.G3
   222  					  v4 = b.G4
   223  					  v5 = b.G5
   224  					  v6 = b.F6[t6]
   225  				  )
   226  		
   227  				  type t6 struct{}; func (t6) Foo() {}
   228  				`,
   229  			},
   230  			plookups: []pkgLookups{
   231  				{"a", []lookup{}},
   232  				{"b", []lookup{}},
   233  				{"c", []lookup{
   234  					{"v1", "myFact(b.N1)"},
   235  					{"v1()", "myFact(a.T1)"},
   236  					{"v2()", "myFact(b.N2)"},
   237  					{"v2().F", "myFact(a.T2)"},
   238  					{"v3", "myFact(b.N3)"},
   239  					{"v4", "myFact(b.N4)"},
   240  					{"v4()", "myFact(a.T4)"},
   241  					{"v5", "myFact(b.N5)"},
   242  					{"v5()", "myFact(b.t5)"},
   243  					{"v6()", "myFact(c.t6)"},
   244  				}},
   245  			},
   246  		},
   247  	}
   248  
   249  	for i := range tests {
   250  		test := tests[i]
   251  		t.Run(test.name, func(t *testing.T) {
   252  			t.Parallel()
   253  			if test.typeparams && !typeparams.Enabled {
   254  				t.Skip("type parameters are not enabled")
   255  			}
   256  			testEncodeDecode(t, test.files, test.plookups)
   257  		})
   258  	}
   259  }
   260  
   261  type lookup struct {
   262  	objexpr string
   263  	want    string
   264  }
   265  
   266  type pkgLookups struct {
   267  	path    string
   268  	lookups []lookup
   269  }
   270  
   271  // testEncodeDecode tests fact encoding and decoding and simulates how package facts
   272  // are passed during analysis. It operates on a group of Go file contents. Then
   273  // for each <package, []lookup> in tests it does the following:
   274  //  1. loads and type checks the package,
   275  //  2. calls (*facts.Decoder).Decode to load the facts exported by its imports,
   276  //  3. exports a myFact Fact for all of package level objects,
   277  //  4. For each lookup for the current package:
   278  //     4.a) lookup the types.Object for an Go source expression in the curent package
   279  //     (or confirms one is not expected want=="no object"),
   280  //     4.b) finds a Fact for the object (or confirms one is not expected want=="no fact"),
   281  //     4.c) compares the content of the Fact to want.
   282  //  5. encodes the Facts of the package.
   283  //
   284  // Note: tests are not independent test cases; order matters (as does a package being
   285  // skipped). It changes what Facts can be imported.
   286  //
   287  // Failures are reported on t.
   288  func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) {
   289  	dir, cleanup, err := analysistest.WriteFiles(files)
   290  	if err != nil {
   291  		t.Fatal(err)
   292  	}
   293  	defer cleanup()
   294  
   295  	// factmap represents the passing of encoded facts from one
   296  	// package to another. In practice one would use the file system.
   297  	factmap := make(map[string][]byte)
   298  	read := func(imp *types.Package) ([]byte, error) { return factmap[imp.Path()], nil }
   299  
   300  	// Analyze packages in order, look up various objects accessible within
   301  	// each package, and see if they have a fact.  The "analysis" exports a
   302  	// fact for every object at package level.
   303  	//
   304  	// Note: Loop iterations are not independent test cases;
   305  	// order matters, as we populate factmap.
   306  	for _, test := range tests {
   307  		// load package
   308  		pkg, err := load(t, dir, test.path)
   309  		if err != nil {
   310  			t.Fatal(err)
   311  		}
   312  
   313  		// decode
   314  		facts, err := facts.NewDecoder(pkg).Decode(read)
   315  		if err != nil {
   316  			t.Fatalf("Decode failed: %v", err)
   317  		}
   318  		t.Logf("decode %s facts = %v", pkg.Path(), facts) // show all facts
   319  
   320  		// export
   321  		// (one fact for each package-level object)
   322  		for _, name := range pkg.Scope().Names() {
   323  			obj := pkg.Scope().Lookup(name)
   324  			fact := &myFact{obj.Pkg().Name() + "." + obj.Name()}
   325  			facts.ExportObjectFact(obj, fact)
   326  		}
   327  		t.Logf("exported %s facts = %v", pkg.Path(), facts) // show all facts
   328  
   329  		// import
   330  		// (after export, because an analyzer may import its own facts)
   331  		for _, lookup := range test.lookups {
   332  			fact := new(myFact)
   333  			var got string
   334  			if obj := find(pkg, lookup.objexpr); obj == nil {
   335  				got = "no object"
   336  			} else if facts.ImportObjectFact(obj, fact) {
   337  				got = fact.String()
   338  			} else {
   339  				got = "no fact"
   340  			}
   341  			if got != lookup.want {
   342  				t.Errorf("in %s, ImportObjectFact(%s, %T) = %s, want %s",
   343  					pkg.Path(), lookup.objexpr, fact, got, lookup.want)
   344  			}
   345  		}
   346  
   347  		// encode
   348  		factmap[pkg.Path()] = facts.Encode()
   349  	}
   350  }
   351  
   352  func find(p *types.Package, expr string) types.Object {
   353  	// types.Eval only allows us to compute a TypeName object for an expression.
   354  	// TODO(adonovan): support other expressions that denote an object:
   355  	// - an identifier (or qualified ident) for a func, const, or var
   356  	// - new(T).f for a field or method
   357  	// I've added CheckExpr in https://go-review.googlesource.com/c/go/+/144677.
   358  	// If that becomes available, use it.
   359  
   360  	// Choose an arbitrary position within the (single-file) package
   361  	// so that we are within the scope of its import declarations.
   362  	somepos := p.Scope().Lookup(p.Scope().Names()[0]).Pos()
   363  	tv, err := types.Eval(token.NewFileSet(), p, somepos, expr)
   364  	if err != nil {
   365  		return nil
   366  	}
   367  	if n, ok := tv.Type.(*types.Named); ok {
   368  		return n.Obj()
   369  	}
   370  	return nil
   371  }
   372  
   373  func load(t *testing.T, dir string, path string) (*types.Package, error) {
   374  	cfg := &packages.Config{
   375  		Mode: packages.LoadSyntax,
   376  		Dir:  dir,
   377  		Env:  append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"),
   378  	}
   379  	testenv.NeedsGoPackagesEnv(t, cfg.Env)
   380  	pkgs, err := packages.Load(cfg, path)
   381  	if err != nil {
   382  		return nil, err
   383  	}
   384  	if packages.PrintErrors(pkgs) > 0 {
   385  		return nil, fmt.Errorf("packages had errors")
   386  	}
   387  	if len(pkgs) == 0 {
   388  		return nil, fmt.Errorf("no package matched %s", path)
   389  	}
   390  	return pkgs[0].Types, nil
   391  }
   392  
   393  type otherFact struct {
   394  	S string
   395  }
   396  
   397  func (f *otherFact) String() string { return fmt.Sprintf("otherFact(%s)", f.S) }
   398  func (f *otherFact) AFact()         {}
   399  
   400  func TestFactFilter(t *testing.T) {
   401  	files := map[string]string{
   402  		"a/a.go": `package a; type A int`,
   403  	}
   404  	dir, cleanup, err := analysistest.WriteFiles(files)
   405  	if err != nil {
   406  		t.Fatal(err)
   407  	}
   408  	defer cleanup()
   409  
   410  	pkg, err := load(t, dir, "a")
   411  	if err != nil {
   412  		t.Fatal(err)
   413  	}
   414  
   415  	obj := pkg.Scope().Lookup("A")
   416  	s, err := facts.NewDecoder(pkg).Decode(func(*types.Package) ([]byte, error) { return nil, nil })
   417  	if err != nil {
   418  		t.Fatal(err)
   419  	}
   420  	s.ExportObjectFact(obj, &myFact{"good object fact"})
   421  	s.ExportPackageFact(&myFact{"good package fact"})
   422  	s.ExportObjectFact(obj, &otherFact{"bad object fact"})
   423  	s.ExportPackageFact(&otherFact{"bad package fact"})
   424  
   425  	filter := map[reflect.Type]bool{
   426  		reflect.TypeOf(&myFact{}): true,
   427  	}
   428  
   429  	pkgFacts := s.AllPackageFacts(filter)
   430  	wantPkgFacts := `[{package a ("a") myFact(good package fact)}]`
   431  	if got := fmt.Sprintf("%v", pkgFacts); got != wantPkgFacts {
   432  		t.Errorf("AllPackageFacts: got %v, want %v", got, wantPkgFacts)
   433  	}
   434  
   435  	objFacts := s.AllObjectFacts(filter)
   436  	wantObjFacts := "[{type a.A int myFact(good object fact)}]"
   437  	if got := fmt.Sprintf("%v", objFacts); got != wantObjFacts {
   438  		t.Errorf("AllObjectFacts: got %v, want %v", got, wantObjFacts)
   439  	}
   440  }
   441  
   442  // TestMalformed checks that facts can be encoded and decoded *despite*
   443  // types.Config.Check returning an error. Importing facts is expected to
   444  // happen when Analyzers have RunDespiteErrors set to true. So this
   445  // needs to robust, e.g. no infinite loops.
   446  func TestMalformed(t *testing.T) {
   447  	if !typeparams.Enabled {
   448  		t.Skip("type parameters are not enabled")
   449  	}
   450  	var findPkg func(*types.Package, string) *types.Package
   451  	findPkg = func(p *types.Package, name string) *types.Package {
   452  		if p.Name() == name {
   453  			return p
   454  		}
   455  		for _, o := range p.Imports() {
   456  			if f := findPkg(o, name); f != nil {
   457  				return f
   458  			}
   459  		}
   460  		return nil
   461  	}
   462  
   463  	type pkgTest struct {
   464  		content string
   465  		err     string            // if non-empty, expected substring of err.Error() from conf.Check().
   466  		wants   map[string]string // package path to expected name
   467  	}
   468  	tests := []struct {
   469  		name string
   470  		pkgs []pkgTest
   471  	}{
   472  		{
   473  			name: "initialization-cycle",
   474  			pkgs: []pkgTest{
   475  				{
   476  					content: `package a; type N[T any] struct { F *N[N[T]] }`,
   477  					err:     "instantiation cycle:",
   478  					wants:   map[string]string{"a": "myFact(a.[N])", "b": "no package", "c": "no package"},
   479  				},
   480  				{
   481  					content: `package b; import "a"; type B a.N[int]`,
   482  					wants:   map[string]string{"a": "myFact(a.[N])", "b": "myFact(b.[B])", "c": "no package"},
   483  				},
   484  				{
   485  					content: `package c; import "b"; var C b.B`,
   486  					wants:   map[string]string{"a": "myFact(a.[N])", "b": "myFact(b.[B])", "c": "myFact(c.[C])"},
   487  				},
   488  			},
   489  		},
   490  	}
   491  
   492  	for i := range tests {
   493  		test := tests[i]
   494  		t.Run(test.name, func(t *testing.T) {
   495  			t.Parallel()
   496  
   497  			// setup for test wide variables.
   498  			packages := make(map[string]*types.Package)
   499  			conf := types.Config{
   500  				Importer: closure(packages),
   501  				Error:    func(err error) {}, // do not stop on first type checking error
   502  			}
   503  			fset := token.NewFileSet()
   504  			factmap := make(map[string][]byte)
   505  			read := func(imp *types.Package) ([]byte, error) { return factmap[imp.Path()], nil }
   506  
   507  			// Processes the pkgs in order. For package, export a package fact,
   508  			// and use this fact to verify which package facts are reachable via Decode.
   509  			// We allow for packages to have type checking errors.
   510  			for i, pkgTest := range test.pkgs {
   511  				// parse
   512  				f, err := parser.ParseFile(fset, fmt.Sprintf("%d.go", i), pkgTest.content, 0)
   513  				if err != nil {
   514  					t.Fatal(err)
   515  				}
   516  
   517  				// typecheck
   518  				pkg, err := conf.Check(f.Name.Name, fset, []*ast.File{f}, nil)
   519  				var got string
   520  				if err != nil {
   521  					got = err.Error()
   522  				}
   523  				if !strings.Contains(got, pkgTest.err) {
   524  					t.Fatalf("%s: type checking error %q did not match pattern %q", pkg.Path(), err.Error(), pkgTest.err)
   525  				}
   526  				packages[pkg.Path()] = pkg
   527  
   528  				// decode facts
   529  				facts, err := facts.NewDecoder(pkg).Decode(read)
   530  				if err != nil {
   531  					t.Fatalf("Decode failed: %v", err)
   532  				}
   533  
   534  				// export facts
   535  				fact := &myFact{fmt.Sprintf("%s.%s", pkg.Name(), pkg.Scope().Names())}
   536  				facts.ExportPackageFact(fact)
   537  
   538  				// import facts
   539  				for other, want := range pkgTest.wants {
   540  					fact := new(myFact)
   541  					var got string
   542  					if found := findPkg(pkg, other); found == nil {
   543  						got = "no package"
   544  					} else if facts.ImportPackageFact(found, fact) {
   545  						got = fact.String()
   546  					} else {
   547  						got = "no fact"
   548  					}
   549  					if got != want {
   550  						t.Errorf("in %s, ImportPackageFact(%s, %T) = %s, want %s",
   551  							pkg.Path(), other, fact, got, want)
   552  					}
   553  				}
   554  
   555  				// encode facts
   556  				factmap[pkg.Path()] = facts.Encode()
   557  			}
   558  		})
   559  	}
   560  }
   561  
   562  type closure map[string]*types.Package
   563  
   564  func (c closure) Import(path string) (*types.Package, error) { return c[path], nil }