You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
6.8 KiB
294 lines
6.8 KiB
// Package goparse contains logic for parsing Go files. Specifically it parses |
|
// source and test files into domain model for generating tests. |
|
package goparser |
|
|
|
import ( |
|
"errors" |
|
"fmt" |
|
"go/ast" |
|
"go/parser" |
|
"go/token" |
|
"go/types" |
|
"io/ioutil" |
|
"strings" |
|
|
|
"go-common/app/tool/gorpc/model" |
|
) |
|
|
|
// ErrEmptyFile represents an empty file error. |
|
var ErrEmptyFile = errors.New("file is empty") |
|
|
|
// Result representats a parsed Go file. |
|
type Result struct { |
|
// The package name and imports of a Go file. |
|
Header *model.Header |
|
// All the functions and methods in a Go file. |
|
Funcs []*model.Function |
|
} |
|
|
|
// Parser can parse Go files. |
|
type Parser struct { |
|
// The importer to resolve packages from import paths. |
|
Importer types.Importer |
|
} |
|
|
|
// Parse parses a given Go file at srcPath, along any files that share the same |
|
// package, into a domain model for generating tests. |
|
func (p *Parser) Parse(srcPath string, files []model.Path) (*Result, error) { |
|
b, err := p.readFile(srcPath) |
|
if err != nil { |
|
return nil, err |
|
} |
|
fset := token.NewFileSet() |
|
f, err := p.parseFile(fset, srcPath) |
|
if err != nil { |
|
return nil, err |
|
} |
|
fs, err := p.parseFiles(fset, f, files) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return &Result{ |
|
Header: &model.Header{ |
|
Comments: parseComment(f, f.Package), |
|
Package: f.Name.String(), |
|
Imports: parseImports(f.Imports), |
|
Code: goCode(b, f), |
|
}, |
|
Funcs: p.parseFunctions(fset, f, fs), |
|
}, nil |
|
} |
|
|
|
func (p *Parser) readFile(srcPath string) ([]byte, error) { |
|
b, err := ioutil.ReadFile(srcPath) |
|
if err != nil { |
|
return nil, fmt.Errorf("ioutil.ReadFile: %v", err) |
|
} |
|
if len(b) == 0 { |
|
return nil, ErrEmptyFile |
|
} |
|
return b, nil |
|
} |
|
|
|
func (p *Parser) parseFile(fset *token.FileSet, srcPath string) (*ast.File, error) { |
|
f, err := parser.ParseFile(fset, srcPath, nil, parser.ParseComments) |
|
if err != nil { |
|
return nil, fmt.Errorf("target parser.ParseFile(): %v", err) |
|
} |
|
return f, nil |
|
} |
|
|
|
func (p *Parser) parseFiles(fset *token.FileSet, f *ast.File, files []model.Path) ([]*ast.File, error) { |
|
pkg := f.Name.String() |
|
var fs []*ast.File |
|
for _, file := range files { |
|
ff, err := parser.ParseFile(fset, string(file), nil, 0) |
|
if err != nil { |
|
return nil, fmt.Errorf("other file parser.ParseFile: %v", err) |
|
} |
|
if name := ff.Name.String(); name != pkg { |
|
continue |
|
} |
|
fs = append(fs, ff) |
|
} |
|
return fs, nil |
|
} |
|
|
|
func (p *Parser) parseFunctions(fset *token.FileSet, f *ast.File, fs []*ast.File) []*model.Function { |
|
ul, el := p.parseTypes(fset, fs) |
|
var funcs []*model.Function |
|
for _, d := range f.Decls { |
|
fDecl, ok := d.(*ast.FuncDecl) |
|
if !ok { |
|
continue |
|
} |
|
funcs = append(funcs, parseFunc(fDecl, ul, el)) |
|
} |
|
return funcs |
|
} |
|
|
|
func (p *Parser) parseTypes(fset *token.FileSet, fs []*ast.File) (map[string]types.Type, map[*types.Struct]ast.Expr) { |
|
conf := &types.Config{ |
|
Importer: p.Importer, |
|
// Adding a NO-OP error function ignores errors and performs best-effort |
|
// type checking. https://godoc.org/golang.org/x/tools/go/types#Config |
|
Error: func(error) {}, |
|
} |
|
ti := &types.Info{ |
|
Types: make(map[ast.Expr]types.TypeAndValue), |
|
} |
|
// Note: conf.Check can fail, but since Info is not required data, it's ok. |
|
conf.Check("", fset, fs, ti) |
|
ul := make(map[string]types.Type) |
|
el := make(map[*types.Struct]ast.Expr) |
|
for e, t := range ti.Types { |
|
// Collect the underlying types. |
|
ul[t.Type.String()] = t.Type.Underlying() |
|
// Collect structs to determine the fields of a receiver. |
|
if v, ok := t.Type.(*types.Struct); ok { |
|
el[v] = e |
|
} |
|
} |
|
return ul, el |
|
} |
|
|
|
func parseComment(f *ast.File, pkgPos token.Pos) []string { |
|
var comments []string |
|
var count int |
|
|
|
for _, comment := range f.Comments { |
|
if comment.End() < pkgPos && comment != f.Doc { |
|
for _, c := range comment.List { |
|
count += len(c.Text) + 1 // +1 for '\n' |
|
if count < int(c.End()) { |
|
n := int(c.End()) - count |
|
comments = append(comments, strings.Repeat("\n", n)) |
|
count++ // for last of '\n' |
|
} |
|
comments = append(comments, c.Text) |
|
} |
|
} |
|
} |
|
return comments |
|
} |
|
|
|
// Returns the Go code below the imports block. |
|
func goCode(b []byte, f *ast.File) []byte { |
|
furthestPos := f.Name.End() |
|
for _, node := range f.Imports { |
|
if pos := node.End(); pos > furthestPos { |
|
furthestPos = pos |
|
} |
|
} |
|
if furthestPos < token.Pos(len(b)) { |
|
furthestPos++ |
|
} |
|
return b[furthestPos:] |
|
} |
|
|
|
func parseFunc(fDecl *ast.FuncDecl, ul map[string]types.Type, el map[*types.Struct]ast.Expr) *model.Function { |
|
f := &model.Function{ |
|
Name: fDecl.Name.String(), |
|
IsExported: fDecl.Name.IsExported(), |
|
Receiver: parseReceiver(fDecl.Recv, ul, el), |
|
Parameters: parseFieldList(fDecl.Type.Params, ul), |
|
} |
|
fs := parseFieldList(fDecl.Type.Results, ul) |
|
i := 0 |
|
for _, fi := range fs { |
|
if fi.Type.String() == "error" { |
|
f.ReturnsError = true |
|
continue |
|
} |
|
fi.Index = i |
|
f.Results = append(f.Results, fi) |
|
i++ |
|
} |
|
return f |
|
} |
|
|
|
func parseImports(imps []*ast.ImportSpec) []*model.Import { |
|
var is []*model.Import |
|
for _, imp := range imps { |
|
var n string |
|
if imp.Name != nil { |
|
n = imp.Name.String() |
|
} |
|
is = append(is, &model.Import{ |
|
Name: n, |
|
Path: imp.Path.Value, |
|
}) |
|
} |
|
return is |
|
} |
|
|
|
func parseReceiver(fl *ast.FieldList, ul map[string]types.Type, el map[*types.Struct]ast.Expr) *model.Receiver { |
|
if fl == nil { |
|
return nil |
|
} |
|
r := &model.Receiver{ |
|
Field: parseFieldList(fl, ul)[0], |
|
} |
|
t, ok := ul[r.Type.Value] |
|
if !ok { |
|
return r |
|
} |
|
s, ok := t.(*types.Struct) |
|
if !ok { |
|
return r |
|
} |
|
st := el[s].(*ast.StructType) |
|
r.Fields = append(r.Fields, parseFieldList(st.Fields, ul)...) |
|
for i, f := range r.Fields { |
|
f.Name = s.Field(i).Name() |
|
} |
|
return r |
|
|
|
} |
|
|
|
func parseFieldList(fl *ast.FieldList, ul map[string]types.Type) []*model.Field { |
|
if fl == nil { |
|
return nil |
|
} |
|
i := 0 |
|
var fs []*model.Field |
|
for _, f := range fl.List { |
|
for _, pf := range parseFields(f, ul) { |
|
pf.Index = i |
|
fs = append(fs, pf) |
|
i++ |
|
} |
|
} |
|
return fs |
|
} |
|
|
|
func parseFields(f *ast.Field, ul map[string]types.Type) []*model.Field { |
|
t := parseExpr(f.Type, ul) |
|
if len(f.Names) == 0 { |
|
return []*model.Field{{ |
|
Type: t, |
|
}} |
|
} |
|
var fs []*model.Field |
|
for _, n := range f.Names { |
|
fs = append(fs, &model.Field{ |
|
Name: n.Name, |
|
Type: t, |
|
}) |
|
} |
|
return fs |
|
} |
|
|
|
func parseExpr(e ast.Expr, ul map[string]types.Type) *model.Expression { |
|
switch v := e.(type) { |
|
case *ast.StarExpr: |
|
val := types.ExprString(v.X) |
|
return &model.Expression{ |
|
Value: val, |
|
IsStar: true, |
|
Underlying: underlying(val, ul), |
|
} |
|
case *ast.Ellipsis: |
|
exp := parseExpr(v.Elt, ul) |
|
return &model.Expression{ |
|
Value: exp.Value, |
|
IsStar: exp.IsStar, |
|
IsVariadic: true, |
|
Underlying: underlying(exp.Value, ul), |
|
} |
|
default: |
|
val := types.ExprString(e) |
|
return &model.Expression{ |
|
Value: val, |
|
Underlying: underlying(val, ul), |
|
IsWriter: val == "io.Writer", |
|
} |
|
} |
|
} |
|
|
|
func underlying(val string, ul map[string]types.Type) string { |
|
if ul[val] != nil { |
|
return ul[val].String() |
|
} |
|
return "" |
|
}
|
|
|