compiler: check emitted event names
Check that all `Notify` invocations in source correspond to some event in manifest. Helpful for typos such as `transfer` instead of `Transfer`.
This commit is contained in:
parent
25f1db6de0
commit
75a9a42403
15 changed files with 172 additions and 7 deletions
|
@ -142,6 +142,24 @@ func TestCompileExamples(t *testing.T) {
|
|||
e.Run(t, opts...)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("invalid events in manifest", func(t *testing.T) {
|
||||
const dir = "./testdata/"
|
||||
for _, name := range []string{"invalid1", "invalid2", "invalid3"} {
|
||||
outPath := path.Join(tmpDir, name+".nef")
|
||||
manifestPath := path.Join(tmpDir, name+".manifest.json")
|
||||
defer func() {
|
||||
os.Remove(outPath)
|
||||
os.Remove(manifestPath)
|
||||
}()
|
||||
e.RunWithError(t, "neo-go", "contract", "compile",
|
||||
"--in", path.Join(dir, name),
|
||||
"--out", outPath,
|
||||
"--manifest", manifestPath,
|
||||
"--config", path.Join(dir, name, "invalid.yml"),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func filterFilename(infos []os.FileInfo, ext string) string {
|
||||
|
|
|
@ -154,6 +154,10 @@ func NewCommands() []cli.Command {
|
|||
Name: "no-standards",
|
||||
Usage: "do not check compliance with supported standards",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-events",
|
||||
Usage: "do not check emitted events with the manifest",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -404,6 +408,7 @@ func contractCompile(ctx *cli.Context) error {
|
|||
ManifestFile: manifestFile,
|
||||
|
||||
NoStandardCheck: ctx.Bool("no-standards"),
|
||||
NoEventsCheck: ctx.Bool("no-events"),
|
||||
}
|
||||
|
||||
if len(confFile) != 0 {
|
||||
|
|
21
cli/testdata/invalid1/invalid.go
vendored
Normal file
21
cli/testdata/invalid1/invalid.go
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
// invalid is an example of contract which doesn't pass event check.
|
||||
package invalid1
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
)
|
||||
|
||||
// Notify1 emits correctly typed event.
|
||||
func Notify1() bool {
|
||||
runtime.Notify("Event", interop.Hash160{1, 2, 3})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Notify2 emits invalid event (ByteString instead of Hash160).
|
||||
func Notify2() bool {
|
||||
runtime.Notify("Event", []byte{1, 2, 3})
|
||||
|
||||
return true
|
||||
}
|
7
cli/testdata/invalid1/invalid.yml
vendored
Normal file
7
cli/testdata/invalid1/invalid.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
name: "Invalid example"
|
||||
supportedstandards: []
|
||||
events:
|
||||
- name: Event
|
||||
parameters:
|
||||
- name: address
|
||||
type: Hash160
|
21
cli/testdata/invalid2/invalid.go
vendored
Normal file
21
cli/testdata/invalid2/invalid.go
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
// invalid is an example of contract which doesn't pass event check.
|
||||
package invalid2
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
)
|
||||
|
||||
// Notify1 emits correctly typed event.
|
||||
func Notify1() bool {
|
||||
runtime.Notify("Event", interop.Hash160{1, 2, 3})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Notify2 emits invalid event (extra parameter).
|
||||
func Notify2() bool {
|
||||
runtime.Notify("Event", interop.Hash160{1, 2, 3}, "extra parameter")
|
||||
|
||||
return true
|
||||
}
|
7
cli/testdata/invalid2/invalid.yml
vendored
Normal file
7
cli/testdata/invalid2/invalid.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
name: "Invalid example"
|
||||
supportedstandards: []
|
||||
events:
|
||||
- name: Event
|
||||
parameters:
|
||||
- name: address
|
||||
type: Hash160
|
21
cli/testdata/invalid3/invalid.go
vendored
Normal file
21
cli/testdata/invalid3/invalid.go
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
// invalid is an example of contract which doesn't pass event check.
|
||||
package invalid3
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
)
|
||||
|
||||
// Notify1 emits correctly typed event.
|
||||
func Notify1() bool {
|
||||
runtime.Notify("Event", interop.Hash160{1, 2, 3})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Notify2 emits invalid event (missing from manifest).
|
||||
func Notify2() bool {
|
||||
runtime.Notify("AnotherEvent", interop.Hash160{1, 2, 3})
|
||||
|
||||
return true
|
||||
}
|
7
cli/testdata/invalid3/invalid.yml
vendored
Normal file
7
cli/testdata/invalid3/invalid.yml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
name: "Invalid example"
|
||||
supportedstandards: []
|
||||
events:
|
||||
- name: Event
|
||||
parameters:
|
||||
- name: address
|
||||
type: Hash160
|
|
@ -4,4 +4,16 @@ events:
|
|||
- name: Tx
|
||||
parameters:
|
||||
- name: txHash
|
||||
type: ByteString
|
||||
type: Hash256
|
||||
- name: Calling
|
||||
parameters:
|
||||
- name: hash
|
||||
type: Hash160
|
||||
- name: Executing
|
||||
parameters:
|
||||
- name: hash
|
||||
type: Hash160
|
||||
- name: Entry
|
||||
parameters:
|
||||
- name: hash
|
||||
type: Hash160
|
|
@ -13,7 +13,9 @@ func NotifyKeysAndValues() bool {
|
|||
keys := iterator.Keys(iter)
|
||||
|
||||
runtime.Notify("found storage values", values)
|
||||
runtime.Notify("found storage keys", keys)
|
||||
// For illustration purposes event is emitted with 'Any' type.
|
||||
var typedKeys interface{} = keys
|
||||
runtime.Notify("found storage keys", typedKeys)
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ events:
|
|||
- name: found storage values
|
||||
parameters:
|
||||
- name: values
|
||||
type: Any
|
||||
type: InteropInterface
|
||||
- name: found storage keys
|
||||
parameters:
|
||||
- name: keys
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package nep17
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/util"
|
||||
|
@ -44,7 +45,7 @@ func (t Token) BalanceOf(ctx storage.Context, holder []byte) int {
|
|||
}
|
||||
|
||||
// Transfer token from one user to another
|
||||
func (t Token) Transfer(ctx storage.Context, from []byte, to []byte, amount int, data interface{}) bool {
|
||||
func (t Token) Transfer(ctx storage.Context, from, to interop.Hash160, amount int, data interface{}) bool {
|
||||
amountFrom := t.CanTransfer(ctx, from, to, amount)
|
||||
if amountFrom == -1 {
|
||||
return false
|
||||
|
@ -62,7 +63,7 @@ func (t Token) Transfer(ctx storage.Context, from []byte, to []byte, amount int,
|
|||
amountTo := getIntFromDB(ctx, to)
|
||||
totalAmountTo := amountTo + amount
|
||||
storage.Put(ctx, to, totalAmountTo)
|
||||
runtime.Notify("transfer", from, to, amount)
|
||||
runtime.Notify("Transfer", from, to, amount)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -105,7 +106,7 @@ func IsUsableAddress(addr []byte) bool {
|
|||
}
|
||||
|
||||
// Mint initial supply of tokens
|
||||
func (t Token) Mint(ctx storage.Context, to []byte) bool {
|
||||
func (t Token) Mint(ctx storage.Context, to interop.Hash160) bool {
|
||||
if !IsUsableAddress(t.Owner) {
|
||||
return false
|
||||
}
|
||||
|
@ -117,6 +118,7 @@ func (t Token) Mint(ctx storage.Context, to []byte) bool {
|
|||
storage.Put(ctx, to, t.TotalSupply)
|
||||
storage.Put(ctx, []byte("minted"), true)
|
||||
storage.Put(ctx, []byte(t.CirculationKey), t.TotalSupply)
|
||||
runtime.Notify("transfer", "", to, t.TotalSupply)
|
||||
var from interop.Hash160
|
||||
runtime.Notify("Transfer", from, to, t.TotalSupply)
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -87,6 +87,9 @@ type codegen struct {
|
|||
// docIndex maps file path to an index in documents array.
|
||||
docIndex map[string]int
|
||||
|
||||
// emittedEvents contains all events emitted by contract.
|
||||
emittedEvents map[string][][]string
|
||||
|
||||
// Label table for recording jump destinations.
|
||||
l []int
|
||||
}
|
||||
|
@ -870,6 +873,15 @@ func (c *codegen) Visit(node ast.Node) ast.Visitor {
|
|||
ast.Walk(c, n.Fun)
|
||||
emit.Opcodes(c.prog.BinWriter, opcode.CALLA)
|
||||
case isSyscall(f):
|
||||
if f.pkg.Name() == "runtime" && f.name == "Notify" {
|
||||
tv := c.typeAndValueOf(n.Args[0])
|
||||
params := make([]string, 0, len(n.Args[1:]))
|
||||
for _, p := range n.Args[1:] {
|
||||
params = append(params, c.scTypeFromExpr(p))
|
||||
}
|
||||
name := constant.StringVal(tv.Value)
|
||||
c.emittedEvents[name] = append(c.emittedEvents[name], params)
|
||||
}
|
||||
c.convertSyscall(n, f.pkg.Name(), f.name)
|
||||
default:
|
||||
emit.Call(c.prog.BinWriter, opcode.CALLL, f.label)
|
||||
|
@ -1883,6 +1895,7 @@ func newCodegen(info *buildInfo, pkg *loader.PackageInfo) *codegen {
|
|||
initEndOffset: -1,
|
||||
deployEndOffset: -1,
|
||||
|
||||
emittedEvents: make(map[string][][]string),
|
||||
sequencePoints: make(map[string][]DebugSeqPoint),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,10 @@ type Options struct {
|
|||
// The name of the output for contract manifest file.
|
||||
ManifestFile string
|
||||
|
||||
// NoEventsCheck specifies if events emitted by contract needs to be present in manifest.
|
||||
// This setting has effect only if manifest is emitted.
|
||||
NoEventsCheck bool
|
||||
|
||||
// NoStandardCheck specifies if supported standards compliance needs to be checked.
|
||||
// This setting has effect only if manifest is emitted.
|
||||
NoStandardCheck bool
|
||||
|
@ -224,6 +228,28 @@ func CompileAndSave(src string, o *Options) ([]byte, error) {
|
|||
return b, err
|
||||
}
|
||||
}
|
||||
if !o.NoEventsCheck {
|
||||
for name := range di.EmittedEvents {
|
||||
ev := m.ABI.GetEvent(name)
|
||||
if ev == nil {
|
||||
return nil, fmt.Errorf("event '%s' is emitted but not specified in manifest", name)
|
||||
}
|
||||
argsList := di.EmittedEvents[name]
|
||||
for i := range argsList {
|
||||
if len(argsList[i]) != len(ev.Parameters) {
|
||||
return nil, fmt.Errorf("event '%s' should have %d parameters but has %d",
|
||||
name, len(ev.Parameters), len(argsList[i]))
|
||||
}
|
||||
for j := range ev.Parameters {
|
||||
expected := ev.Parameters[j].Type.String()
|
||||
if argsList[i][j] != expected {
|
||||
return nil, fmt.Errorf("event '%s' should have '%s' as type of %d parameter, "+
|
||||
"got: %s", name, expected, j+1, argsList[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mData, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return b, fmt.Errorf("failed to marshal manifest to JSON: %w", err)
|
||||
|
|
|
@ -24,6 +24,8 @@ type DebugInfo struct {
|
|||
Documents []string `json:"documents"`
|
||||
Methods []MethodDebugInfo `json:"methods"`
|
||||
Events []EventDebugInfo `json:"events"`
|
||||
// EmittedEvents contains events occuring in code.
|
||||
EmittedEvents map[string][][]string `json:"-"`
|
||||
}
|
||||
|
||||
// MethodDebugInfo represents smart-contract's method debug information.
|
||||
|
@ -162,6 +164,7 @@ func (c *codegen) emitDebugInfo(contract []byte) *DebugInfo {
|
|||
}
|
||||
d.Methods = append(d.Methods, *m)
|
||||
}
|
||||
d.EmittedEvents = c.emittedEvents
|
||||
return d
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue