neo-go/pkg/compiler/analysis.go
Anna Shaleva 9b9d72937b compiler: restrict return values count for manifest methods
Exported functions from main package shouldn't have more than one return
value.
2022-08-17 15:42:56 +03:00

425 lines
12 KiB
Go

package compiler
import (
"errors"
"fmt"
"go/ast"
"go/token"
"go/types"
"strings"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"golang.org/x/tools/go/packages"
)
// Various exported functions usage errors.
var (
// ErrMissingExportedParamName is returned when exported contract method has unnamed parameter.
ErrMissingExportedParamName = errors.New("exported method is not allowed to have unnamed parameter")
// ErrInvalidExportedRetCount is returned when exported contract method has invalid return values count.
ErrInvalidExportedRetCount = errors.New("exported method is not allowed to have more than one return value")
)
var (
// Go language builtin functions.
goBuiltins = []string{"len", "append", "panic", "make", "copy", "recover", "delete"}
// Custom builtin utility functions.
customBuiltins = []string{
"FromAddress",
}
)
// newGlobal creates a new global variable.
func (c *codegen) newGlobal(pkg string, name string) {
name = c.getIdentName(pkg, name)
c.globals[name] = len(c.globals)
}
// getIdentName returns a fully-qualified name for a variable.
func (c *codegen) getIdentName(pkg string, name string) string {
if fullName, ok := c.importMap[pkg]; ok {
pkg = fullName
}
return pkg + "." + name
}
// traverseGlobals visits and initializes global variables.
// It returns `true` if contract has `_deploy` function.
func (c *codegen) traverseGlobals() bool {
var hasDefer bool
var n, nConst int
var hasDeploy bool
c.ForEachFile(func(f *ast.File, pkg *types.Package) {
nv, nc := countGlobals(f)
n += nv
nConst += nc
if !hasDeploy || !hasDefer {
ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.FuncDecl:
hasDeploy = hasDeploy || isDeployFunc(n)
case *ast.DeferStmt:
hasDefer = true
return false
}
return true
})
}
})
if hasDefer {
n++
}
if n > 255 {
c.prog.BinWriter.Err = errors.New("too many global variables")
return hasDeploy
}
if n != 0 {
emit.Instruction(c.prog.BinWriter, opcode.INITSSLOT, []byte{byte(n)})
}
initOffset := c.prog.Len()
emit.Instruction(c.prog.BinWriter, opcode.INITSLOT, []byte{0, 0})
lastCnt, maxCnt := -1, -1
c.ForEachPackage(func(pkg *packages.Package) {
if n+nConst > 0 {
for _, f := range pkg.Syntax {
c.fillImportMap(f, pkg)
c.convertGlobals(f, pkg.Types)
}
}
for _, f := range pkg.Syntax {
c.fillImportMap(f, pkg)
var currMax int
lastCnt, currMax = c.convertInitFuncs(f, pkg.Types, lastCnt)
if currMax > maxCnt {
maxCnt = currMax
}
}
// because we reuse `convertFuncDecl` for init funcs,
// we need to clear scope, so that global variables
// encountered after will be recognized as globals.
c.scope = nil
})
if c.globalInlineCount > maxCnt {
maxCnt = c.globalInlineCount
}
// Here we remove `INITSLOT` if no code was emitted for `init` function.
// Note that the `INITSSLOT` must stay in place.
hasNoInit := initOffset+3 == c.prog.Len()
if hasNoInit {
buf := c.prog.Bytes()
c.prog.Reset()
c.prog.WriteBytes(buf[:initOffset])
}
if initOffset != 0 || !hasNoInit { // if there are some globals or `init()`.
c.initEndOffset = c.prog.Len()
emit.Opcodes(c.prog.BinWriter, opcode.RET)
if maxCnt >= 0 {
c.reverseOffsetMap[initOffset] = nameWithLocals{
name: "init",
count: maxCnt,
}
}
}
// store auxiliary variables after all others.
if hasDefer {
c.exceptionIndex = len(c.globals)
c.globals[exceptionVarName] = c.exceptionIndex
}
return hasDeploy
}
// countGlobals counts the global variables in the program to add
// them with the stack size of the function.
// Second returned argument contains the amount of global constants.
func countGlobals(f ast.Node) (int, int) {
var numVar, numConst int
ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) {
// Skip all function declarations if we have already encountered `defer`.
case *ast.FuncDecl:
return false
// After skipping all funcDecls, we are sure that each value spec
// is a global declared variable or constant.
case *ast.GenDecl:
isVar := n.Tok == token.VAR
if isVar || n.Tok == token.CONST {
for _, s := range n.Specs {
for _, id := range s.(*ast.ValueSpec).Names {
if id.Name != "_" {
if isVar {
numVar++
} else {
numConst++
}
}
}
}
}
return false
}
return true
})
return numVar, numConst
}
// isExprNil looks if the given expression is a `nil`.
func isExprNil(e ast.Expr) bool {
v, ok := e.(*ast.Ident)
return ok && v.Name == "nil"
}
// indexOfStruct returns the index of the given field inside that struct.
// If the struct does not contain that field, it will return -1.
func indexOfStruct(strct *types.Struct, fldName string) int {
for i := 0; i < strct.NumFields(); i++ {
if strct.Field(i).Name() == fldName {
return i
}
}
return -1
}
type funcUsage map[string]bool
func (f funcUsage) funcUsed(name string) bool {
_, ok := f[name]
return ok
}
// lastStmtIsReturn checks if the last statement of the declaration was return statement.
func lastStmtIsReturn(body *ast.BlockStmt) (b bool) {
if l := len(body.List); l != 0 {
switch inner := body.List[l-1].(type) {
case *ast.BlockStmt:
return lastStmtIsReturn(inner)
case *ast.ReturnStmt:
return true
default:
return false
}
}
return false
}
// analyzePkgOrder sets the order in which packages should be processed.
// From Go spec:
//
// A package with no imports is initialized by assigning initial values to all its package-level variables
// followed by calling all init functions in the order they appear in the source, possibly in multiple files,
// as presented to the compiler. If a package has imports, the imported packages are initialized before
// initializing the package itself. If multiple packages import a package, the imported package
// will be initialized only once. The importing of packages, by construction, guarantees
// that there can be no cyclic initialization dependencies.
func (c *codegen) analyzePkgOrder() {
seen := make(map[string]bool)
info := c.buildInfo.program[0]
c.visitPkg(info, seen)
}
func (c *codegen) visitPkg(pkg *packages.Package, seen map[string]bool) {
if seen[pkg.PkgPath] {
return
}
for _, imp := range pkg.Types.Imports() {
c.visitPkg(pkg.Imports[imp.Path()], seen)
}
seen[pkg.PkgPath] = true
c.packages = append(c.packages, pkg.PkgPath)
c.packageCache[pkg.PkgPath] = pkg
}
func (c *codegen) fillDocumentInfo() {
fset := c.buildInfo.config.Fset
fset.Iterate(func(f *token.File) bool {
filePath := f.Position(f.Pos(0)).Filename
c.docIndex[filePath] = len(c.documents)
c.documents = append(c.documents, filePath)
return true
})
}
// analyzeFuncUsage traverses all code and returns a map with functions
// which should be present in the emitted code.
// This is done using BFS starting from exported functions or
// the function used in variable declarations (graph edge corresponds to
// the function being called in declaration).
func (c *codegen) analyzeFuncUsage() funcUsage {
type declPair struct {
decl *ast.FuncDecl
importMap map[string]string
path string
}
// nodeCache contains top-level function declarations .
nodeCache := make(map[string]declPair)
diff := funcUsage{}
c.ForEachFile(func(f *ast.File, pkg *types.Package) {
var pkgPath string
isMain := pkg == c.mainPkg.Types
if !isMain {
pkgPath = pkg.Path()
}
ast.Inspect(f, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.CallExpr:
// functions invoked in variable declarations in imported packages
// are marked as used.
var name string
switch t := n.Fun.(type) {
case *ast.Ident:
name = c.getIdentName(pkgPath, t.Name)
case *ast.SelectorExpr:
name, _ = c.getFuncNameFromSelector(t)
default:
return true
}
diff[name] = true
case *ast.FuncDecl:
name := c.getFuncNameFromDecl(pkgPath, n)
// exported functions and methods are always assumed to be used
if isMain && n.Name.IsExported() || isInitFunc(n) || isDeployFunc(n) {
diff[name] = true
}
// exported functions are not allowed to have unnamed parameters or multiple return values
if isMain && n.Name.IsExported() && n.Recv == nil {
if n.Type.Params.List != nil {
for i, param := range n.Type.Params.List {
if param.Names == nil {
c.prog.Err = fmt.Errorf("%w: %s", ErrMissingExportedParamName, n.Name)
return false // Program is invalid.
}
for _, name := range param.Names {
if name == nil || name.Name == "_" {
c.prog.Err = fmt.Errorf("%w: %s/%d", ErrMissingExportedParamName, n.Name, i)
return false // Program is invalid.
}
}
}
}
if retCnt := n.Type.Results.NumFields(); retCnt > 1 {
c.prog.Err = fmt.Errorf("%w: %s/%d return values", ErrInvalidExportedRetCount, n.Name, retCnt)
}
}
nodeCache[name] = declPair{n, c.importMap, pkgPath}
return false // will be processed in the next stage
}
return true
})
})
if c.prog.Err != nil {
return nil
}
usage := funcUsage{}
for len(diff) != 0 {
nextDiff := funcUsage{}
for name := range diff {
fd, ok := nodeCache[name]
if !ok || usage[name] {
continue
}
usage[name] = true
pkg := c.mainPkg
if fd.path != "" {
pkg = c.packageCache[fd.path]
}
c.typeInfo = pkg.TypesInfo
c.currPkg = pkg
c.importMap = fd.importMap
ast.Inspect(fd.decl, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.CallExpr:
switch t := n.Fun.(type) {
case *ast.Ident:
nextDiff[c.getIdentName(fd.path, t.Name)] = true
case *ast.SelectorExpr:
name, _ := c.getFuncNameFromSelector(t)
nextDiff[name] = true
}
}
return true
})
}
diff = nextDiff
}
return usage
}
func isGoBuiltin(name string) bool {
for i := range goBuiltins {
if name == goBuiltins[i] {
return true
}
}
return false
}
func isCustomBuiltin(f *funcScope) bool {
if !isInteropPath(f.pkg.Path()) {
return false
}
for _, n := range customBuiltins {
if f.name == n {
return true
}
}
return false
}
func isSyscall(fun *funcScope) bool {
if fun.selector == nil || fun.pkg == nil || !isInteropPath(fun.pkg.Path()) {
return false
}
return fun.pkg.Name() == "neogointernal" && (strings.HasPrefix(fun.name, "Syscall") ||
strings.HasPrefix(fun.name, "Opcode") || strings.HasPrefix(fun.name, "CallWithToken"))
}
const interopPrefix = "github.com/nspcc-dev/neo-go/pkg/interop"
func isInteropPath(s string) bool {
return strings.HasPrefix(s, interopPrefix)
}
// canConvert returns true if type doesn't need to be converted on type assertion.
func canConvert(s string) bool {
if len(s) != 0 && s[0] == '*' {
s = s[1:]
}
if isInteropPath(s) {
s = s[len(interopPrefix):]
return s != "/iterator.Iterator" && s != "/storage.Context" &&
s != "/native/ledger.Block" && s != "/native/ledger.Transaction" &&
s != "/native/management.Contract" && s != "/native/neo.AccountState" &&
s != "/native/ledger.BlockSR"
}
return true
}
// canInline returns true if the function is to be inlined.
// Currently, there is a static list of functions which are inlined,
// this may change in future.
func canInline(s string, name string) bool {
if strings.HasPrefix(s, "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/inline") {
return true
}
if !isInteropPath(s) {
return false
}
return !strings.HasPrefix(s[len(interopPrefix):], "/neogointernal") &&
!(strings.HasPrefix(s[len(interopPrefix):], "/util") && name == "FromAddress")
}