golang.org/x/tools@v0.21.1-0.20240520172518-788d39e776b1/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/aliases"
    22  	"golang.org/x/tools/internal/facts"
    23  	"golang.org/x/tools/internal/testenv"
    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  			testEncodeDecode(t, test.files, test.plookups)
   254  		})
   255  	}
   256  }
   257  
   258  type lookup struct {
   259  	objexpr string
   260  	want    string
   261  }
   262  
   263  type pkgLookups struct {
   264  	path    string
   265  	lookups []lookup
   266  }
   267  
   268  // testEncodeDecode tests fact encoding and decoding and simulates how package facts
   269  // are passed during analysis. It operates on a group of Go file contents. Then
   270  // for each <package, []lookup> in tests it does the following:
   271  //  1. loads and type checks the package,
   272  //  2. calls (*facts.Decoder).Decode to load the facts exported by its imports,
   273  //  3. exports a myFact Fact for all of package level objects,
   274  //  4. For each lookup for the current package:
   275  //     4.a) lookup the types.Object for a Go source expression in the current package
   276  //     (or confirms one is not expected want=="no object"),
   277  //     4.b) finds a Fact for the object (or confirms one is not expected want=="no fact"),
   278  //     4.c) compares the content of the Fact to want.
   279  //  5. encodes the Facts of the package.
   280  //
   281  // Note: tests are not independent test cases; order matters (as does a package being
   282  // skipped). It changes what Facts can be imported.
   283  //
   284  // Failures are reported on t.
   285  func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) {
   286  	dir, cleanup, err := analysistest.WriteFiles(files)
   287  	if err != nil {
   288  		t.Fatal(err)
   289  	}
   290  	defer cleanup()
   291  
   292  	// factmap represents the passing of encoded facts from one
   293  	// package to another. In practice one would use the file system.
   294  	factmap := make(map[string][]byte)
   295  	read := func(pkgPath string) ([]byte, error) { return factmap[pkgPath], nil }
   296  
   297  	// Analyze packages in order, look up various objects accessible within
   298  	// each package, and see if they have a fact.  The "analysis" exports a
   299  	// fact for every object at package level.
   300  	//
   301  	// Note: Loop iterations are not independent test cases;
   302  	// order matters, as we populate factmap.
   303  	for _, test := range tests {
   304  		// load package
   305  		pkg, err := load(t, dir, test.path)
   306  		if err != nil {
   307  			t.Fatal(err)
   308  		}
   309  
   310  		// decode
   311  		facts, err := facts.NewDecoder(pkg).Decode(read)
   312  		if err != nil {
   313  			t.Fatalf("Decode failed: %v", err)
   314  		}
   315  		t.Logf("decode %s facts = %v", pkg.Path(), facts) // show all facts
   316  
   317  		// export
   318  		// (one fact for each package-level object)
   319  		for _, name := range pkg.Scope().Names() {
   320  			obj := pkg.Scope().Lookup(name)
   321  			fact := &myFact{obj.Pkg().Name() + "." + obj.Name()}
   322  			facts.ExportObjectFact(obj, fact)
   323  		}
   324  		t.Logf("exported %s facts = %v", pkg.Path(), facts) // show all facts
   325  
   326  		// import
   327  		// (after export, because an analyzer may import its own facts)
   328  		for _, lookup := range test.lookups {
   329  			fact := new(myFact)
   330  			var got string
   331  			if obj := find(pkg, lookup.objexpr); obj == nil {
   332  				got = "no object"
   333  			} else if facts.ImportObjectFact(obj, fact) {
   334  				got = fact.String()
   335  			} else {
   336  				got = "no fact"
   337  			}
   338  			if got != lookup.want {
   339  				t.Errorf("in %s, ImportObjectFact(%s, %T) = %s, want %s",
   340  					pkg.Path(), lookup.objexpr, fact, got, lookup.want)
   341  			}
   342  		}
   343  
   344  		// encode
   345  		factmap[pkg.Path()] = facts.Encode()
   346  	}
   347  }
   348  
   349  func find(p *types.Package, expr string) types.Object {
   350  	// types.Eval only allows us to compute a TypeName object for an expression.
   351  	// TODO(adonovan): support other expressions that denote an object:
   352  	// - an identifier (or qualified ident) for a func, const, or var
   353  	// - new(T).f for a field or method
   354  	// I've added CheckExpr in https://go-review.googlesource.com/c/go/+/144677.
   355  	// If that becomes available, use it.
   356  
   357  	// Choose an arbitrary position within the (single-file) package
   358  	// so that we are within the scope of its import declarations.
   359  	somepos := p.Scope().Lookup(p.Scope().Names()[0]).Pos()
   360  	tv, err := types.Eval(token.NewFileSet(), p, somepos, expr)
   361  	if err != nil {
   362  		return nil
   363  	}
   364  	if n, ok := aliases.Unalias(tv.Type).(*types.Named); ok {
   365  		return n.Obj()
   366  	}
   367  	return nil
   368  }
   369  
   370  func load(t *testing.T, dir string, path string) (*types.Package, error) {
   371  	cfg := &packages.Config{
   372  		Mode: packages.LoadSyntax,
   373  		Dir:  dir,
   374  		Env:  append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"),
   375  	}
   376  	testenv.NeedsGoPackagesEnv(t, cfg.Env)
   377  	pkgs, err := packages.Load(cfg, path)
   378  	if err != nil {
   379  		return nil, err
   380  	}
   381  	if packages.PrintErrors(pkgs) > 0 {
   382  		return nil, fmt.Errorf("packages had errors")
   383  	}
   384  	if len(pkgs) == 0 {
   385  		return nil, fmt.Errorf("no package matched %s", path)
   386  	}
   387  	return pkgs[0].Types, nil
   388  }
   389  
   390  type otherFact struct {
   391  	S string
   392  }
   393  
   394  func (f *otherFact) String() string { return fmt.Sprintf("otherFact(%s)", f.S) }
   395  func (f *otherFact) AFact()         {}
   396  
   397  func TestFactFilter(t *testing.T) {
   398  	files := map[string]string{
   399  		"a/a.go": `package a; type A int`,
   400  	}
   401  	dir, cleanup, err := analysistest.WriteFiles(files)
   402  	if err != nil {
   403  		t.Fatal(err)
   404  	}
   405  	defer cleanup()
   406  
   407  	pkg, err := load(t, dir, "a")
   408  	if err != nil {
   409  		t.Fatal(err)
   410  	}
   411  
   412  	obj := pkg.Scope().Lookup("A")
   413  	s, err := facts.NewDecoder(pkg).Decode(func(pkgPath string) ([]byte, error) { return nil, nil })
   414  	if err != nil {
   415  		t.Fatal(err)
   416  	}
   417  	s.ExportObjectFact(obj, &myFact{"good object fact"})
   418  	s.ExportPackageFact(&myFact{"good package fact"})
   419  	s.ExportObjectFact(obj, &otherFact{"bad object fact"})
   420  	s.ExportPackageFact(&otherFact{"bad package fact"})
   421  
   422  	filter := map[reflect.Type]bool{
   423  		reflect.TypeOf(&myFact{}): true,
   424  	}
   425  
   426  	pkgFacts := s.AllPackageFacts(filter)
   427  	wantPkgFacts := `[{package a ("a") myFact(good package fact)}]`
   428  	if got := fmt.Sprintf("%v", pkgFacts); got != wantPkgFacts {
   429  		t.Errorf("AllPackageFacts: got %v, want %v", got, wantPkgFacts)
   430  	}
   431  
   432  	objFacts := s.AllObjectFacts(filter)
   433  	wantObjFacts := "[{type a.A int myFact(good object fact)}]"
   434  	if got := fmt.Sprintf("%v", objFacts); got != wantObjFacts {
   435  		t.Errorf("AllObjectFacts: got %v, want %v", got, wantObjFacts)
   436  	}
   437  }
   438  
   439  // TestMalformed checks that facts can be encoded and decoded *despite*
   440  // types.Config.Check returning an error. Importing facts is expected to
   441  // happen when Analyzers have RunDespiteErrors set to true. So this
   442  // needs to robust, e.g. no infinite loops.
   443  func TestMalformed(t *testing.T) {
   444  	var findPkg func(*types.Package, string) *types.Package
   445  	findPkg = func(p *types.Package, name string) *types.Package {
   446  		if p.Name() == name {
   447  			return p
   448  		}
   449  		for _, o := range p.Imports() {
   450  			if f := findPkg(o, name); f != nil {
   451  				return f
   452  			}
   453  		}
   454  		return nil
   455  	}
   456  
   457  	type pkgTest struct {
   458  		content string
   459  		err     string            // if non-empty, expected substring of err.Error() from conf.Check().
   460  		wants   map[string]string // package path to expected name
   461  	}
   462  	tests := []struct {
   463  		name string
   464  		pkgs []pkgTest
   465  	}{
   466  		{
   467  			name: "initialization-cycle",
   468  			pkgs: []pkgTest{
   469  				// Notation: myFact(a.[N]) means: package a has members {N}.
   470  				{
   471  					content: `package a; type N[T any] struct { F *N[N[T]] }`,
   472  					err:     "instantiation cycle:",
   473  					wants:   map[string]string{"a": "myFact(a.[N])", "b": "no package", "c": "no package"},
   474  				},
   475  				{
   476  					content: `package b; import "a"; type B a.N[int]`,
   477  					wants:   map[string]string{"a": "myFact(a.[N])", "b": "myFact(b.[B])", "c": "no package"},
   478  				},
   479  				{
   480  					content: `package c; import "b"; var C b.B`,
   481  					wants:   map[string]string{"a": "no fact", "b": "myFact(b.[B])", "c": "myFact(c.[C])"},
   482  					// package fact myFact(a.[N]) not reexported
   483  				},
   484  			},
   485  		},
   486  	}
   487  
   488  	for i := range tests {
   489  		test := tests[i]
   490  		t.Run(test.name, func(t *testing.T) {
   491  			t.Parallel()
   492  
   493  			// setup for test wide variables.
   494  			packages := make(map[string]*types.Package)
   495  			conf := types.Config{
   496  				Importer: closure(packages),
   497  				Error:    func(err error) {}, // do not stop on first type checking error
   498  			}
   499  			fset := token.NewFileSet()
   500  			factmap := make(map[string][]byte)
   501  			read := func(pkgPath string) ([]byte, error) { return factmap[pkgPath], nil }
   502  
   503  			// Processes the pkgs in order. For package, export a package fact,
   504  			// and use this fact to verify which package facts are reachable via Decode.
   505  			// We allow for packages to have type checking errors.
   506  			for i, pkgTest := range test.pkgs {
   507  				// parse
   508  				f, err := parser.ParseFile(fset, fmt.Sprintf("%d.go", i), pkgTest.content, 0)
   509  				if err != nil {
   510  					t.Fatal(err)
   511  				}
   512  
   513  				// typecheck
   514  				pkg, err := conf.Check(f.Name.Name, fset, []*ast.File{f}, nil)
   515  				var got string
   516  				if err != nil {
   517  					got = err.Error()
   518  				}
   519  				if !strings.Contains(got, pkgTest.err) {
   520  					t.Fatalf("%s: type checking error %q did not match pattern %q", pkg.Path(), err.Error(), pkgTest.err)
   521  				}
   522  				packages[pkg.Path()] = pkg
   523  
   524  				// decode facts
   525  				facts, err := facts.NewDecoder(pkg).Decode(read)
   526  				if err != nil {
   527  					t.Fatalf("Decode failed: %v", err)
   528  				}
   529  
   530  				// export facts
   531  				fact := &myFact{fmt.Sprintf("%s.%s", pkg.Name(), pkg.Scope().Names())}
   532  				facts.ExportPackageFact(fact)
   533  
   534  				// import facts
   535  				for other, want := range pkgTest.wants {
   536  					fact := new(myFact)
   537  					var got string
   538  					if found := findPkg(pkg, other); found == nil {
   539  						got = "no package"
   540  					} else if facts.ImportPackageFact(found, fact) {
   541  						got = fact.String()
   542  					} else {
   543  						got = "no fact"
   544  					}
   545  					if got != want {
   546  						t.Errorf("in %s, ImportPackageFact(%s, %T) = %s, want %s",
   547  							pkg.Path(), other, fact, got, want)
   548  					}
   549  				}
   550  
   551  				// encode facts
   552  				factmap[pkg.Path()] = facts.Encode()
   553  			}
   554  		})
   555  	}
   556  }
   557  
   558  type closure map[string]*types.Package
   559  
   560  func (c closure) Import(path string) (*types.Package, error) { return c[path], nil }