github.com/gopherjs/gopherjs@v1.19.0-beta1.0.20240506212314-27071a8796e4/internal/srctesting/srctesting.go (about) 1 // Package srctesting contains common helpers for unit testing source code 2 // analysis and transformation. 3 package srctesting 4 5 import ( 6 "bytes" 7 "fmt" 8 "go/ast" 9 "go/format" 10 "go/parser" 11 "go/token" 12 "go/types" 13 "strings" 14 "testing" 15 ) 16 17 // Fixture provides utilities for parsing and type checking Go code in tests. 18 type Fixture struct { 19 T *testing.T 20 FileSet *token.FileSet 21 Info *types.Info 22 Packages map[string]*types.Package 23 } 24 25 func newInfo() *types.Info { 26 return &types.Info{ 27 Types: make(map[ast.Expr]types.TypeAndValue), 28 Defs: make(map[*ast.Ident]types.Object), 29 Uses: make(map[*ast.Ident]types.Object), 30 Implicits: make(map[ast.Node]types.Object), 31 Selections: make(map[*ast.SelectorExpr]*types.Selection), 32 Scopes: make(map[ast.Node]*types.Scope), 33 Instances: make(map[*ast.Ident]types.Instance), 34 } 35 } 36 37 // New creates a fresh Fixture. 38 func New(t *testing.T) *Fixture { 39 return &Fixture{ 40 T: t, 41 FileSet: token.NewFileSet(), 42 Info: newInfo(), 43 Packages: map[string]*types.Package{}, 44 } 45 } 46 47 // Parse source from the string and return complete AST. 48 func (f *Fixture) Parse(name, src string) *ast.File { 49 f.T.Helper() 50 file, err := parser.ParseFile(f.FileSet, name, src, parser.ParseComments) 51 if err != nil { 52 f.T.Fatalf("Failed to parse test source: %s", err) 53 } 54 return file 55 } 56 57 // Check type correctness of the provided AST. 58 // 59 // Fails the test if type checking fails. Provided AST is expected not to have 60 // any imports. If f.Info is nil, it will create a new types.Info instance 61 // to store type checking results and return it, otherwise f.Info is used. 62 func (f *Fixture) Check(importPath string, files ...*ast.File) (*types.Info, *types.Package) { 63 f.T.Helper() 64 config := &types.Config{ 65 Sizes: &types.StdSizes{WordSize: 4, MaxAlign: 8}, 66 Importer: f, 67 } 68 info := f.Info 69 if info == nil { 70 info = newInfo() 71 } 72 pkg, err := config.Check(importPath, f.FileSet, files, info) 73 if err != nil { 74 f.T.Fatalf("Filed to type check test source: %s", err) 75 } 76 f.Packages[importPath] = pkg 77 return info, pkg 78 } 79 80 // Import implements types.Importer. 81 func (f *Fixture) Import(path string) (*types.Package, error) { 82 pkg, ok := f.Packages[path] 83 if !ok { 84 return nil, fmt.Errorf("missing type info for package %q", path) 85 } 86 return pkg, nil 87 } 88 89 // ParseFuncDecl parses source with a single function defined and returns the 90 // function AST. 91 // 92 // Fails the test if there isn't exactly one function declared in the source. 93 func ParseFuncDecl(t *testing.T, src string) *ast.FuncDecl { 94 t.Helper() 95 decl := ParseDecl(t, src) 96 fdecl, ok := decl.(*ast.FuncDecl) 97 if !ok { 98 t.Fatalf("Got %T decl, expected *ast.FuncDecl", decl) 99 } 100 return fdecl 101 } 102 103 // ParseDecl parses source with a single declaration and 104 // returns that declaration AST. 105 // 106 // Fails the test if there isn't exactly one declaration in the source. 107 func ParseDecl(t *testing.T, src string) ast.Decl { 108 t.Helper() 109 file := New(t).Parse("test.go", src) 110 if l := len(file.Decls); l != 1 { 111 t.Fatalf(`Got %d decls in the sources, expected exactly 1`, l) 112 } 113 return file.Decls[0] 114 } 115 116 // ParseSpec parses source with a single declaration containing 117 // a single specification and returns that specification AST. 118 // 119 // Fails the test if there isn't exactly one declaration and 120 // one specification in the source. 121 func ParseSpec(t *testing.T, src string) ast.Spec { 122 t.Helper() 123 decl := ParseDecl(t, src) 124 gdecl, ok := decl.(*ast.GenDecl) 125 if !ok { 126 t.Fatalf("Got %T decl, expected *ast.GenDecl", decl) 127 } 128 if l := len(gdecl.Specs); l != 1 { 129 t.Fatalf(`Got %d spec in the sources, expected exactly 1`, l) 130 } 131 return gdecl.Specs[0] 132 } 133 134 // Format AST node into a string. 135 // 136 // The node type must be *ast.File, *printer.CommentedNode, []ast.Decl, 137 // []ast.Stmt, or assignment-compatible to ast.Expr, ast.Decl, ast.Spec, or 138 // ast.Stmt. 139 func Format(t *testing.T, fset *token.FileSet, node any) string { 140 t.Helper() 141 buf := &bytes.Buffer{} 142 if err := format.Node(buf, fset, node); err != nil { 143 t.Fatalf("Failed to format AST node %T: %s", node, err) 144 } 145 return buf.String() 146 } 147 148 // LookupObj returns a top-level object with the given name. 149 // 150 // Methods can be referred to as RecvTypeName.MethodName. 151 func LookupObj(pkg *types.Package, name string) types.Object { 152 path := strings.Split(name, ".") 153 scope := pkg.Scope() 154 var obj types.Object 155 156 for len(path) > 0 { 157 obj = scope.Lookup(path[0]) 158 path = path[1:] 159 160 if fun, ok := obj.(*types.Func); ok { 161 scope = fun.Scope() 162 continue 163 } 164 165 // If we are here, the latest object is a named type. If there are more path 166 // elements left, they must refer to field or method. 167 if len(path) > 0 { 168 obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, obj.Pkg(), path[0]) 169 path = path[1:] 170 } 171 } 172 return obj 173 }