Skip to content

Commit

Permalink
Merge pull request #3376 from onflow/sainati/type-removal
Browse files Browse the repository at this point in the history
Allow types to be removed in contract updates
  • Loading branch information
dsainati1 authored May 28, 2024
2 parents c4ba6d0 + f798222 commit 9f7c785
Show file tree
Hide file tree
Showing 5 changed files with 463 additions and 1 deletion.
11 changes: 11 additions & 0 deletions runtime/ast/memberindices.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ type memberIndices struct {
_attachments []*AttachmentDeclaration
// Use `EnumCases()` instead
_enumCases []*EnumCaseDeclaration
// Use `Pragmas()` instead
_pragmas []*PragmaDeclaration
}

func (i *memberIndices) FieldsByIdentifier(declarations []Declaration) map[string]*FieldDeclaration {
Expand Down Expand Up @@ -150,6 +152,11 @@ func (i *memberIndices) EnumCases(declarations []Declaration) []*EnumCaseDeclara
return i._enumCases
}

func (i *memberIndices) Pragmas(declarations []Declaration) []*PragmaDeclaration {
i.once.Do(i.initializer(declarations))
return i._pragmas
}

func (i *memberIndices) initializer(declarations []Declaration) func() {
return func() {
i.init(declarations)
Expand Down Expand Up @@ -184,6 +191,7 @@ func (i *memberIndices) init(declarations []Declaration) {
i._entitlementMappingsByIdentifier = make(map[string]*EntitlementMappingDeclaration)

i._enumCases = make([]*EnumCaseDeclaration, 0)
i._pragmas = make([]*PragmaDeclaration, 0)

for _, declaration := range declarations {
switch declaration := declaration.(type) {
Expand Down Expand Up @@ -225,6 +233,9 @@ func (i *memberIndices) init(declarations []Declaration) {

case *EnumCaseDeclaration:
i._enumCases = append(i._enumCases, declaration)

case *PragmaDeclaration:
i._pragmas = append(i._pragmas, declaration)
}
}
}
4 changes: 4 additions & 0 deletions runtime/ast/members.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ func (m *Members) EnumCases() []*EnumCaseDeclaration {
return m.indices.EnumCases(m.declarations)
}

func (m *Members) Pragmas() []*PragmaDeclaration {
return m.indices.Pragmas(m.declarations)
}

func (m *Members) FieldsByIdentifier() map[string]*FieldDeclaration {
return m.indices.FieldsByIdentifier(m.declarations)
}
Expand Down
320 changes: 320 additions & 0 deletions runtime/contract_update_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3060,3 +3060,323 @@ func TestRuntimeContractUpdateProgramCaching(t *testing.T) {
)
})
}

func TestPragmaUpdates(t *testing.T) {
t.Parallel()

testWithValidators(t, "Remove pragma", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
#foo(bar)
#baz
}
`

const newCode = `
access(all) contract Test {
#baz
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
require.NoError(t, err)
})

testWithValidators(t, "Remove removedType pragma", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
#removedType(bar)
#baz
}
`

const newCode = `
access(all) contract Test {
#baz
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
var expectedErr *stdlib.TypeRemovalPragmaRemovalError
require.ErrorAs(t, err, &expectedErr)
})

testWithValidators(t, "removedType pragma moved into subdeclaration", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
#removedType(bar)
access(all) struct S {
}
}
`

const newCode = `
access(all) contract Test {
access(all) struct S {
#removedType(bar)
}
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
var expectedErr *stdlib.TypeRemovalPragmaRemovalError
require.ErrorAs(t, err, &expectedErr)
})

testWithValidators(t, "reorder removedType pragmas", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
#removedType(bar)
#removedType(foo)
}
`

const newCode = `
access(all) contract Test {
#removedType(foo)
#removedType(bar)
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
require.NoError(t, err)
})

testWithValidators(t, "malformed removedType pragma integer", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
#baz
}
`

const newCode = `
access(all) contract Test {
#removedType(3)
#baz
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
var expectedErr *stdlib.InvalidTypeRemovalPragmaError
require.ErrorAs(t, err, &expectedErr)
})

testWithValidators(t, "malformed removedType qualified name", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
#baz
}
`

const newCode = `
access(all) contract Test {
#removedType(X.Y)
#baz
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
var expectedErr *stdlib.InvalidTypeRemovalPragmaError
require.ErrorAs(t, err, &expectedErr)
})

testWithValidators(t, "removedType with zero args", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
}
`

const newCode = `
access(all) contract Test {
#removedType()
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
var expectedErr *stdlib.InvalidTypeRemovalPragmaError
require.ErrorAs(t, err, &expectedErr)
})

testWithValidators(t, "removedType with two args", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
}
`

const newCode = `
access(all) contract Test {
#removedType(x, y)
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
var expectedErr *stdlib.InvalidTypeRemovalPragmaError
require.ErrorAs(t, err, &expectedErr)
})

testWithValidators(t, "#removedType allows type removal", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
access(all) resource R {}
}
`

const newCode = `
access(all) contract Test {
#removedType(R)
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
require.NoError(t, err)
})

testWithValidators(t, "#removedType allows two type removals", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
access(all) resource R {}
access(all) struct interface I {}
}
`

const newCode = `
access(all) contract Test {
#removedType(R)
#removedType(I)
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
require.NoError(t, err)
})

testWithValidators(t, "#removedType can be added", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
#removedType(I)
access(all) resource R {}
}
`

const newCode = `
access(all) contract Test {
#removedType(R)
#removedType(I)
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
require.NoError(t, err)
})

testWithValidators(t, "#removedType can be added without removing a type", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
}
`

const newCode = `
access(all) contract Test {
#removedType(X)
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
require.NoError(t, err)
})

testWithValidators(t, "declarations cannot co-exist with removed type of the same name, composite", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
access(all) resource R {}
}
`

const newCode = `
access(all) contract Test {
#removedType(R)
access(all) resource R {}
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
var expectedErr *stdlib.UseOfRemovedTypeError
require.ErrorAs(t, err, &expectedErr)
})

testWithValidators(t, "declarations cannot co-exist with removed type of the same name, interface", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
access(all) resource interface R {}
}
`

const newCode = `
access(all) contract Test {
#removedType(R)
access(all) resource interface R {}
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
var expectedErr *stdlib.UseOfRemovedTypeError
require.ErrorAs(t, err, &expectedErr)
})

testWithValidators(t, "declarations cannot co-exist with removed type of the same name, attachment", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
access(all) attachment R for AnyResource {}
}
`

const newCode = `
access(all) contract Test {
#removedType(R)
access(all) attachment R for AnyResource {}
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
var expectedErr *stdlib.UseOfRemovedTypeError
require.ErrorAs(t, err, &expectedErr)
})

testWithValidators(t, "#removedType is only scoped to the current declaration, inner", func(t *testing.T, withC1Upgrade bool) {

const oldCode = `
access(all) contract Test {
access(all) resource R {}
access(all) struct S {}
}
`

const newCode = `
access(all) contract Test {
access(all) struct S {
#removedType(R)
}
}
`

err := testDeployAndUpdate(t, "Test", oldCode, newCode, withC1Upgrade)
var expectedErr *stdlib.MissingDeclarationError
require.ErrorAs(t, err, &expectedErr)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/onflow/cadence/runtime/ast"
"github.com/onflow/cadence/runtime/common"
"github.com/onflow/cadence/runtime/common/orderedmap"
"github.com/onflow/cadence/runtime/errors"
"github.com/onflow/cadence/runtime/interpreter"
"github.com/onflow/cadence/runtime/sema"
Expand Down Expand Up @@ -741,6 +742,7 @@ func (validator *CadenceV042ToV1ContractUpdateValidator) checkNestedDeclarationR
nestedDeclaration ast.Declaration,
oldContainingDeclaration ast.Declaration,
newContainingDeclaration ast.Declaration,
removedTypes *orderedmap.OrderedMap[string, struct{}],
) {

// enums can be removed from contract interfaces, as they have no interface equivalent and are not
Expand All @@ -755,6 +757,7 @@ func (validator *CadenceV042ToV1ContractUpdateValidator) checkNestedDeclarationR
nestedDeclaration,
oldContainingDeclaration,
newContainingDeclaration,
removedTypes,
)
}

Expand Down
Loading

0 comments on commit 9f7c785

Please sign in to comment.