compiler: do not emit code for unused imported functions
Our current algorithm marks function as used if it is called at least ones, even if the callee function is itself unused. This commit implements more clever traversal to collect usage information more precisely. Signed-off-by: Evgeniy Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
parent
a340e11b0b
commit
0bc81aecf4
6 changed files with 174 additions and 10 deletions
|
@ -241,34 +241,86 @@ func (c *codegen) fillDocumentInfo() {
|
|||
})
|
||||
}
|
||||
|
||||
// analyzeFuncUsage traverses all code and returns map with functions
|
||||
// which should be present in the emitted code.
|
||||
// This is done using BFS starting from exported functions or
|
||||
// function used in variable declarations (graph edge corresponds to
|
||||
// function being called in declaration).
|
||||
func (c *codegen) analyzeFuncUsage() funcUsage {
|
||||
usage := 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.Pkg
|
||||
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:
|
||||
var pkgPath string
|
||||
if !isMain {
|
||||
pkgPath = pkg.Path()
|
||||
}
|
||||
usage[c.getIdentName(pkgPath, t.Name)] = true
|
||||
name = c.getIdentName(pkgPath, t.Name)
|
||||
case *ast.SelectorExpr:
|
||||
name, _ := c.getFuncNameFromSelector(t)
|
||||
usage[name] = true
|
||||
name, _ = c.getFuncNameFromSelector(t)
|
||||
default:
|
||||
return true
|
||||
}
|
||||
diff[name] = true
|
||||
case *ast.FuncDecl:
|
||||
name := c.getFuncNameFromDecl(pkgPath, n)
|
||||
|
||||
// exported functions are always assumed to be used
|
||||
if isMain && n.Name.IsExported() {
|
||||
usage[c.getFuncNameFromDecl("", n)] = true
|
||||
if isMain && n.Name.IsExported() || isInitFunc(n) || isDeployFunc(n) {
|
||||
diff[name] = true
|
||||
}
|
||||
nodeCache[name] = declPair{n, c.importMap, pkgPath}
|
||||
return false // will be processed in the next stage
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
usage := funcUsage{}
|
||||
for len(diff) != 0 {
|
||||
nextDiff := funcUsage{}
|
||||
for name := range diff {
|
||||
fd, ok := nodeCache[name]
|
||||
if !ok || usage[name] {
|
||||
continue
|
||||
}
|
||||
usage[name] = true
|
||||
|
||||
old := c.importMap
|
||||
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
|
||||
})
|
||||
c.importMap = old
|
||||
}
|
||||
diff = nextDiff
|
||||
}
|
||||
return usage
|
||||
}
|
||||
|
||||
|
|
|
@ -180,6 +180,40 @@ func TestEventWarnings(t *testing.T) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("event in imported package", func(t *testing.T) {
|
||||
t.Run("unused", func(t *testing.T) {
|
||||
src := `package foo
|
||||
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/notify"
|
||||
func Main() int {
|
||||
return notify.Value
|
||||
}`
|
||||
|
||||
_, di, err := compiler.CompileWithDebugInfo("eventTest", strings.NewReader(src))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = compiler.CreateManifest(di, &compiler.Options{NoEventsCheck: true})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("used", func(t *testing.T) {
|
||||
src := `package foo
|
||||
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/notify"
|
||||
func Main() int {
|
||||
notify.EmitEvent()
|
||||
return 42
|
||||
}`
|
||||
|
||||
_, di, err := compiler.CompileWithDebugInfo("eventTest", strings.NewReader(src))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = compiler.CreateManifest(di, &compiler.Options{})
|
||||
require.Error(t, err)
|
||||
|
||||
_, err = compiler.CreateManifest(di, &compiler.Options{
|
||||
ContractEvents: []manifest.Event{{Name: "Event"}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotifyInVerify(t *testing.T) {
|
||||
|
|
|
@ -324,3 +324,30 @@ func TestFunctionUnusedParameters(t *testing.T) {
|
|||
}`
|
||||
eval(t, src, big.NewInt(101))
|
||||
}
|
||||
|
||||
func TestUnusedFunctions(t *testing.T) {
|
||||
t.Run("only variable", func(t *testing.T) {
|
||||
src := `package foo
|
||||
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/nestedcall"
|
||||
func Main() int {
|
||||
return nestedcall.X
|
||||
}`
|
||||
|
||||
b, err := compiler.Compile("foo", strings.NewReader(src))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, len(b)) // PUSHINT8 (42) + RET
|
||||
eval(t, src, big.NewInt(42))
|
||||
})
|
||||
t.Run("imported function", func(t *testing.T) {
|
||||
// Check that import map is set correctly during package traversal.
|
||||
src := `package foo
|
||||
import inner "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/nestedcall"
|
||||
func Main() int {
|
||||
return inner.N()
|
||||
}`
|
||||
|
||||
_, err := compiler.Compile("foo", strings.NewReader(src))
|
||||
require.NoError(t, err)
|
||||
eval(t, src, big.NewInt(65))
|
||||
})
|
||||
}
|
||||
|
|
30
pkg/compiler/testdata/nestedcall/call.go
vendored
Normal file
30
pkg/compiler/testdata/nestedcall/call.go
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
package nestedcall
|
||||
|
||||
import "github.com/nspcc-dev/neo-go/pkg/compiler/testdata/nestedcall/inner"
|
||||
|
||||
// X is what we use.
|
||||
const X = 42
|
||||
|
||||
// N returns inner.A().
|
||||
func N() int {
|
||||
return inner.Return65()
|
||||
}
|
||||
|
||||
// F calls G.
|
||||
func F() int {
|
||||
a := 1
|
||||
return G() + a
|
||||
}
|
||||
|
||||
// G calls x and returns y().
|
||||
func G() int {
|
||||
x()
|
||||
z := 3
|
||||
return y() + z
|
||||
}
|
||||
|
||||
func x() {}
|
||||
func y() int {
|
||||
tmp := 10
|
||||
return tmp
|
||||
}
|
6
pkg/compiler/testdata/nestedcall/inner/call.go
vendored
Normal file
6
pkg/compiler/testdata/nestedcall/inner/call.go
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
package inner
|
||||
|
||||
// Return65 returns 65.
|
||||
func Return65() int {
|
||||
return 65
|
||||
}
|
15
pkg/compiler/testdata/notify/event.go
vendored
Normal file
15
pkg/compiler/testdata/notify/event.go
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
package notify
|
||||
|
||||
import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
|
||||
// Value is the constant we use.
|
||||
const Value = 42
|
||||
|
||||
// EmitEvent emits some event.
|
||||
func EmitEvent() {
|
||||
emitPrivate()
|
||||
}
|
||||
|
||||
func emitPrivate() {
|
||||
runtime.Notify("Event")
|
||||
}
|
Loading…
Reference in a new issue