github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/go/analysis/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/token"
    11  	"go/types"
    12  	"os"
    13  	"reflect"
    14  	"testing"
    15  
    16  	"github.com/powerman/golang-tools/go/analysis/analysistest"
    17  	"github.com/powerman/golang-tools/go/analysis/internal/facts"
    18  	"github.com/powerman/golang-tools/go/packages"
    19  	"github.com/powerman/golang-tools/internal/testenv"
    20  	"github.com/powerman/golang-tools/internal/typeparams"
    21  )
    22  
    23  type myFact struct {
    24  	S string
    25  }
    26  
    27  func (f *myFact) String() string { return fmt.Sprintf("myFact(%s)", f.S) }
    28  func (f *myFact) AFact()         {}
    29  
    30  func init() {
    31  	gob.Register(new(myFact))
    32  }
    33  
    34  func TestEncodeDecode(t *testing.T) {
    35  	tests := []struct {
    36  		name       string
    37  		typeparams bool // requires typeparams to be enabled
    38  		files      map[string]string
    39  		plookups   []pkgLookups // see testEncodeDecode for details
    40  	}{
    41  		{
    42  			name: "loading-order",
    43  			// c -> b -> a, a2
    44  			// c does not directly depend on a, but it indirectly uses a.T.
    45  			//
    46  			// Package a2 is never loaded directly so it is incomplete.
    47  			//
    48  			// We use only types in this example because we rely on
    49  			// types.Eval to resolve the lookup expressions, and it only
    50  			// works for types. This is a definite gap in the typechecker API.
    51  			files: map[string]string{
    52  				"a/a.go":  `package a; type A int; type T int`,
    53  				"a2/a.go": `package a2; type A2 int; type Unneeded int`,
    54  				"b/b.go":  `package b; import ("a"; "a2"); type B chan a2.A2; type F func() a.T`,
    55  				"c/c.go":  `package c; import "b"; type C []b.B`,
    56  			},
    57  			// In the following table, we analyze packages (a, b, c) in order,
    58  			// look up various objects accessible within each package,
    59  			// and see if they have a fact.  The "analysis" exports a fact
    60  			// for every object at package level.
    61  			//
    62  			// Note: Loop iterations are not independent test cases;
    63  			// order matters, as we populate factmap.
    64  			plookups: []pkgLookups{
    65  				{"a", []lookup{
    66  					{"A", "myFact(a.A)"},
    67  				}},
    68  				{"b", []lookup{
    69  					{"a.A", "myFact(a.A)"},
    70  					{"a.T", "myFact(a.T)"},
    71  					{"B", "myFact(b.B)"},
    72  					{"F", "myFact(b.F)"},
    73  					{"F(nil)()", "myFact(a.T)"}, // (result type of b.F)
    74  				}},
    75  				{"c", []lookup{
    76  					{"b.B", "myFact(b.B)"},
    77  					{"b.F", "myFact(b.F)"},
    78  					//{"b.F(nil)()", "myFact(a.T)"}, // no fact; TODO(adonovan): investigate
    79  					{"C", "myFact(c.C)"},
    80  					{"C{}[0]", "myFact(b.B)"},
    81  					{"<-(C{}[0])", "no fact"}, // object but no fact (we never "analyze" a2)
    82  				}},
    83  			},
    84  		},
    85  		{
    86  			name: "globals",
    87  			files: map[string]string{
    88  				"a/a.go": `package a;
    89  				type T1 int
    90  				type T2 int
    91  				type T3 int
    92  				type T4 int
    93  				type T5 int
    94  				type K int; type V string
    95  				`,
    96  				"b/b.go": `package b
    97  				import "a"
    98  				var (
    99  					G1 []a.T1
   100  					G2 [7]a.T2
   101  					G3 chan a.T3
   102  					G4 *a.T4
   103  					G5 struct{ F a.T5 }
   104  					G6 map[a.K]a.V
   105  				)
   106  				`,
   107  				"c/c.go": `package c; import "b";
   108  				var (
   109  					v1 = b.G1
   110  					v2 = b.G2
   111  					v3 = b.G3
   112  					v4 = b.G4
   113  					v5 = b.G5
   114  					v6 = b.G6
   115  				)
   116  				`,
   117  			},
   118  			plookups: []pkgLookups{
   119  				{"a", []lookup{}},
   120  				{"b", []lookup{}},
   121  				{"c", []lookup{
   122  					{"v1[0]", "myFact(a.T1)"},
   123  					{"v2[0]", "myFact(a.T2)"},
   124  					{"<-v3", "myFact(a.T3)"},
   125  					{"*v4", "myFact(a.T4)"},
   126  					{"v5.F", "myFact(a.T5)"},
   127  					{"v6[0]", "myFact(a.V)"},
   128  				}},
   129  			},
   130  		},
   131  		{
   132  			name:       "typeparams",
   133  			typeparams: true,
   134  			files: map[string]string{
   135  				"a/a.go": `package a
   136  				  type T1 int
   137  				  type T2 int
   138  				  type T3 interface{Foo()}
   139  				  type T4 int
   140  				  type T5 int
   141  				  type T6 interface{Foo()}
   142  				`,
   143  				"b/b.go": `package b
   144  				  import "a"
   145  				  type N1[T a.T1|int8] func() T
   146  				  type N2[T any] struct{ F T }
   147  				  type N3[T a.T3] func() T
   148  				  type N4[T a.T4|int8] func() T
   149  				  type N5[T interface{Bar() a.T5} ] func() T
   150  		
   151  				  type t5 struct{}; func (t5) Bar() a.T5
   152  		
   153  				  var G1 N1[a.T1]
   154  				  var G2 func() N2[a.T2]
   155  				  var G3 N3[a.T3]
   156  				  var G4 N4[a.T4]
   157  				  var G5 N5[t5]
   158  		
   159  				  func F6[T a.T6]() T { var x T; return x }
   160  				  `,
   161  				"c/c.go": `package c; import "b";
   162  				  var (
   163  					  v1 = b.G1
   164  					  v2 = b.G2
   165  					  v3 = b.G3
   166  					  v4 = b.G4
   167  					  v5 = b.G5
   168  					  v6 = b.F6[t6]
   169  				  )
   170  		
   171  				  type t6 struct{}; func (t6) Foo() {}
   172  				`,
   173  			},
   174  			plookups: []pkgLookups{
   175  				{"a", []lookup{}},
   176  				{"b", []lookup{}},
   177  				{"c", []lookup{
   178  					{"v1", "myFact(b.N1)"},
   179  					{"v1()", "myFact(a.T1)"},
   180  					{"v2()", "myFact(b.N2)"},
   181  					{"v2().F", "myFact(a.T2)"},
   182  					{"v3", "myFact(b.N3)"},
   183  					{"v4", "myFact(b.N4)"},
   184  					{"v4()", "myFact(a.T4)"},
   185  					{"v5", "myFact(b.N5)"},
   186  					{"v5()", "myFact(b.t5)"},
   187  					{"v6()", "myFact(c.t6)"},
   188  				}},
   189  			},
   190  		},
   191  	}
   192  
   193  	for i := range tests {
   194  		test := tests[i]
   195  		t.Run(test.name, func(t *testing.T) {
   196  			t.Parallel()
   197  			if test.typeparams && !typeparams.Enabled {
   198  				t.Skip("type parameters are not enabled")
   199  			}
   200  			testEncodeDecode(t, test.files, test.plookups)
   201  		})
   202  	}
   203  }
   204  
   205  type lookup struct {
   206  	objexpr string
   207  	want    string
   208  }
   209  
   210  type pkgLookups struct {
   211  	path    string
   212  	lookups []lookup
   213  }
   214  
   215  // testEncodeDecode tests fact encoding and decoding and simulates how package facts
   216  // are passed during analysis. It operates on a group of Go file contents. Then
   217  // for each <package, []lookup> in tests it does the following:
   218  //  1) loads and type checks the package,
   219  //  2) calls facts.Decode to loads the facts exported by its imports,
   220  //  3) exports a myFact Fact for all of package level objects,
   221  //  4) For each lookup for the current package:
   222  //  4.a) lookup the types.Object for an Go source expression in the curent package
   223  //       (or confirms one is not expected want=="no object"),
   224  //  4.b) finds a Fact for the object (or confirms one is not expected want=="no fact"),
   225  //  4.c) compares the content of the Fact to want.
   226  //  5) encodes the Facts of the package.
   227  //
   228  // Note: tests are not independent test cases; order matters (as does a package being
   229  // skipped). It changes what Facts can be imported.
   230  //
   231  // Failures are reported on t.
   232  func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) {
   233  	dir, cleanup, err := analysistest.WriteFiles(files)
   234  	if err != nil {
   235  		t.Fatal(err)
   236  	}
   237  	defer cleanup()
   238  
   239  	// factmap represents the passing of encoded facts from one
   240  	// package to another. In practice one would use the file system.
   241  	factmap := make(map[string][]byte)
   242  	read := func(path string) ([]byte, error) { return factmap[path], nil }
   243  
   244  	// Analyze packages in order, look up various objects accessible within
   245  	// each package, and see if they have a fact.  The "analysis" exports a
   246  	// fact for every object at package level.
   247  	//
   248  	// Note: Loop iterations are not independent test cases;
   249  	// order matters, as we populate factmap.
   250  	for _, test := range tests {
   251  		// load package
   252  		pkg, err := load(t, dir, test.path)
   253  		if err != nil {
   254  			t.Fatal(err)
   255  		}
   256  
   257  		// decode
   258  		facts, err := facts.Decode(pkg, read)
   259  		if err != nil {
   260  			t.Fatalf("Decode failed: %v", err)
   261  		}
   262  		t.Logf("decode %s facts = %v", pkg.Path(), facts) // show all facts
   263  
   264  		// export
   265  		// (one fact for each package-level object)
   266  		for _, name := range pkg.Scope().Names() {
   267  			obj := pkg.Scope().Lookup(name)
   268  			fact := &myFact{obj.Pkg().Name() + "." + obj.Name()}
   269  			facts.ExportObjectFact(obj, fact)
   270  		}
   271  		t.Logf("exported %s facts = %v", pkg.Path(), facts) // show all facts
   272  
   273  		// import
   274  		// (after export, because an analyzer may import its own facts)
   275  		for _, lookup := range test.lookups {
   276  			fact := new(myFact)
   277  			var got string
   278  			if obj := find(pkg, lookup.objexpr); obj == nil {
   279  				got = "no object"
   280  			} else if facts.ImportObjectFact(obj, fact) {
   281  				got = fact.String()
   282  			} else {
   283  				got = "no fact"
   284  			}
   285  			if got != lookup.want {
   286  				t.Errorf("in %s, ImportObjectFact(%s, %T) = %s, want %s",
   287  					pkg.Path(), lookup.objexpr, fact, got, lookup.want)
   288  			}
   289  		}
   290  
   291  		// encode
   292  		factmap[pkg.Path()] = facts.Encode()
   293  	}
   294  }
   295  
   296  func find(p *types.Package, expr string) types.Object {
   297  	// types.Eval only allows us to compute a TypeName object for an expression.
   298  	// TODO(adonovan): support other expressions that denote an object:
   299  	// - an identifier (or qualified ident) for a func, const, or var
   300  	// - new(T).f for a field or method
   301  	// I've added CheckExpr in https://go-review.googlesource.com/c/go/+/144677.
   302  	// If that becomes available, use it.
   303  
   304  	// Choose an arbitrary position within the (single-file) package
   305  	// so that we are within the scope of its import declarations.
   306  	somepos := p.Scope().Lookup(p.Scope().Names()[0]).Pos()
   307  	tv, err := types.Eval(token.NewFileSet(), p, somepos, expr)
   308  	if err != nil {
   309  		return nil
   310  	}
   311  	if n, ok := tv.Type.(*types.Named); ok {
   312  		return n.Obj()
   313  	}
   314  	return nil
   315  }
   316  
   317  func load(t *testing.T, dir string, path string) (*types.Package, error) {
   318  	cfg := &packages.Config{
   319  		Mode: packages.LoadSyntax,
   320  		Dir:  dir,
   321  		Env:  append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"),
   322  	}
   323  	testenv.NeedsGoPackagesEnv(t, cfg.Env)
   324  	pkgs, err := packages.Load(cfg, path)
   325  	if err != nil {
   326  		return nil, err
   327  	}
   328  	if packages.PrintErrors(pkgs) > 0 {
   329  		return nil, fmt.Errorf("packages had errors")
   330  	}
   331  	if len(pkgs) == 0 {
   332  		return nil, fmt.Errorf("no package matched %s", path)
   333  	}
   334  	return pkgs[0].Types, nil
   335  }
   336  
   337  type otherFact struct {
   338  	S string
   339  }
   340  
   341  func (f *otherFact) String() string { return fmt.Sprintf("otherFact(%s)", f.S) }
   342  func (f *otherFact) AFact()         {}
   343  
   344  func TestFactFilter(t *testing.T) {
   345  	files := map[string]string{
   346  		"a/a.go": `package a; type A int`,
   347  	}
   348  	dir, cleanup, err := analysistest.WriteFiles(files)
   349  	if err != nil {
   350  		t.Fatal(err)
   351  	}
   352  	defer cleanup()
   353  
   354  	pkg, err := load(t, dir, "a")
   355  	if err != nil {
   356  		t.Fatal(err)
   357  	}
   358  
   359  	obj := pkg.Scope().Lookup("A")
   360  	s, err := facts.Decode(pkg, func(string) ([]byte, error) { return nil, nil })
   361  	if err != nil {
   362  		t.Fatal(err)
   363  	}
   364  	s.ExportObjectFact(obj, &myFact{"good object fact"})
   365  	s.ExportPackageFact(&myFact{"good package fact"})
   366  	s.ExportObjectFact(obj, &otherFact{"bad object fact"})
   367  	s.ExportPackageFact(&otherFact{"bad package fact"})
   368  
   369  	filter := map[reflect.Type]bool{
   370  		reflect.TypeOf(&myFact{}): true,
   371  	}
   372  
   373  	pkgFacts := s.AllPackageFacts(filter)
   374  	wantPkgFacts := `[{package a ("a") myFact(good package fact)}]`
   375  	if got := fmt.Sprintf("%v", pkgFacts); got != wantPkgFacts {
   376  		t.Errorf("AllPackageFacts: got %v, want %v", got, wantPkgFacts)
   377  	}
   378  
   379  	objFacts := s.AllObjectFacts(filter)
   380  	wantObjFacts := "[{type a.A int myFact(good object fact)}]"
   381  	if got := fmt.Sprintf("%v", objFacts); got != wantObjFacts {
   382  		t.Errorf("AllObjectFacts: got %v, want %v", got, wantObjFacts)
   383  	}
   384  }