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  }