diff --git a/docs/language/access-control.md b/docs/language/access-control.md index ab19bc895b..9609bdaf9a 100644 --- a/docs/language/access-control.md +++ b/docs/language/access-control.md @@ -30,7 +30,8 @@ will be covered in a later section. Top-level declarations (variables, constants, functions, structures, resources, interfaces) and fields (in structures, and resources) are always only able to be written -to in the scope where it is defined (self). +to and mutated (modified, such as by indexed assignment or methods like `append`) +in the scope where it is defined (self). There are four levels of access control defined in the code that specify where a declaration can be accessed or called. @@ -74,21 +75,21 @@ a declaration can be accessed or called. **Access level must be specified for each declaration** -The `(set)` suffix can be used to make variables also publicly writable. +The `(set)` suffix can be used to make variables also publicly writable and mutable. To summarize the behavior for variable declarations, constant declarations, and fields: -| Declaration kind | Access modifier | Read scope | Write scope | -|:-----------------|:-------------------------|:-----------------------------------------------------|:------------------| -| `let` | `priv` / `access(self)` | Current and inner | *None* | -| `let` | `access(contract)` | Current, inner, and containing contract | *None* | -| `let` | `access(account)` | Current, inner, and other contracts in same account | *None* | -| `let` | `pub`,`access(all)` | **All** | *None* | -| `var` | `access(self)` | Current and inner | Current and inner | -| `var` | `access(contract)` | Current, inner, and containing contract | Current and inner | -| `var` | `access(account)` | Current, inner, and other contracts in same account | Current and inner | -| `var` | `pub` / `access(all)` | **All** | Current and inner | -| `var` | `pub(set)` | **All** | **All** | +| Declaration kind | Access modifier | Read scope | Write scope | Mutate scope | +|:-----------------|:-------------------------|:-----------------------------------------------------|:------------------|:------------------| +| `let` | `priv` / `access(self)` | Current and inner | *None* | Current and inner | +| `let` | `access(contract)` | Current, inner, and containing contract | *None* | Current and inner | +| `let` | `access(account)` | Current, inner, and other contracts in same account | *None* | Current and inner | +| `let` | `pub`,`access(all)` | **All** | *None* | Current and inner | +| `var` | `access(self)` | Current and inner | Current and inner | Current and inner | +| `var` | `access(contract)` | Current, inner, and containing contract | Current and inner | Current and inner | +| `var` | `access(account)` | Current, inner, and other contracts in same account | Current and inner | Current and inner | +| `var` | `pub` / `access(all)` | **All** | Current and inner | Current and inner | +| `var` | `pub(set)` | **All** | **All** | **All** | To summarize the behavior for functions: @@ -143,6 +144,10 @@ pub struct SomeStruct { // pub(set) var e: Int + // Arrays and dictionaries declared without (set) cannot be + // mutated in external scopes + pub let arr: [Int] + // The initializer is omitted for brevity. // Declare a private function which is only callable @@ -203,4 +208,15 @@ some.e // Valid: can set publicly settable variable field in outer scope. // some.e = 5 + +// Invalid: cannot mutate a public field in outer scope. +// +some.f.append(0) + +// Invalid: cannot mutate a public field in outer scope. +// +some.f[3] = 1 + +// Valid: can call non-mutating methods on a public field in outer scope +some.f.contains(0) ``` diff --git a/docs/language/values-and-types.mdx b/docs/language/values-and-types.mdx index 39f03821e6..be44e78a5b 100644 --- a/docs/language/values-and-types.mdx +++ b/docs/language/values-and-types.mdx @@ -1112,7 +1112,9 @@ It is invalid to use one of these functions on a fixed-sized array. Adds the new element `element` of type `T` to the end of the array. - The new element must be the same type as all the other elements in the array. + The new element must be the same type as all the other elements in the array. + + This function [mutates](../access-control) the array. ```cadence // Declare an array of integers. @@ -1133,6 +1135,8 @@ It is invalid to use one of these functions on a fixed-sized array. Both arrays must be the same type `T`. + This function [mutates](../access-control) the array. + ```cadence // Declare an array of integers. let numbers = [42, 23] @@ -1160,6 +1164,8 @@ It is invalid to use one of these functions on a fixed-sized array. All the elements after the new inserted element are shifted to the right by one. + This function [mutates](../access-control) the array. + ```cadence // Declare an array of integers. let numbers = [42, 23, 31, 12] @@ -1179,6 +1185,8 @@ It is invalid to use one of these functions on a fixed-sized array. The `index` must be within the bounds of the array. If the index is outside the bounds, the program aborts. + This function [mutates](../access-control) the array. + ```cadence // Declare an array of integers. let numbers = [42, 23, 31] @@ -1199,6 +1207,8 @@ It is invalid to use one of these functions on a fixed-sized array. The array must not be empty. If the array is empty, the program aborts. + This function [mutates](../access-control) the array. + ```cadence // Declare an array of integers. let numbers = [42, 23] @@ -1224,6 +1234,8 @@ It is invalid to use one of these functions on a fixed-sized array. The array must not be empty. If the array is empty, the program aborts. + This function [mutates](../access-control) the array. + ```cadence // Declare an array of integers. let numbers = [42, 23] @@ -1398,6 +1410,8 @@ booleans[0] = true if the dictionary contained the key, otherwise `nil`. + This function [mutates](../access-control) the dictionary. + ```cadence // Declare a dictionary mapping strings to integers. let numbers = {"twentyThree": 23} @@ -1420,6 +1434,8 @@ booleans[0] = true if the dictionary contained the key, otherwise `nil`. + This function [mutates](../access-control) the dictionary. + ```cadence // Declare a dictionary mapping strings to integers. let numbers = {"fortyTwo": 42, "twentyThree": 23} diff --git a/runtime/account_test.go b/runtime/account_test.go index 69bbb028fe..6e11fdcb5b 100644 --- a/runtime/account_test.go +++ b/runtime/account_test.go @@ -1289,6 +1289,39 @@ func TestRuntimePublicKey(t *testing.T) { storage: storage, } + _, err := executeScript(script, runtimeInterface) + + require.Error(t, err) + + var checkerErr *sema.CheckerError + require.ErrorAs(t, err, &checkerErr) + errs := checkerErr.Errors + require.Len(t, errs, 1) + + assert.IsType(t, &sema.ExternalMutationError{}, errs[0]) + }) + + t.Run("raw-key reference mutability", func(t *testing.T) { + script := ` + pub fun main(): PublicKey { + let publicKey = PublicKey( + publicKey: "0102".decodeHex(), + signatureAlgorithm: SignatureAlgorithm.ECDSA_P256 + ) + + var publickeyRef = &publicKey.publicKey as &[UInt8] + publickeyRef[0] = 3 + + return publicKey + } + ` + + storage := newTestLedger(nil, nil) + + runtimeInterface := &testRuntimeInterface{ + storage: storage, + } + value, err := executeScript(script, runtimeInterface) require.NoError(t, err) @@ -1297,15 +1330,12 @@ func TestRuntimePublicKey(t *testing.T) { Fields: []cadence.Value{ // Public key (bytes) newBytesValue([]byte{1, 2}), - // Signature Algo newSignAlgoValue(sema.SignatureAlgorithmECDSA_P256), - // valid cadence.Bool(false), }, } - assert.Equal(t, expected, value) }) @@ -1368,7 +1398,6 @@ func TestAuthAccountContracts(t *testing.T) { transaction { prepare(signer: AuthAccount) { signer.contracts.names[0] = "baz" - assert(signer.contracts.names[0] == "foo") } } `) @@ -1393,7 +1422,52 @@ func TestAuthAccountContracts(t *testing.T) { Location: nextTransactionLocation(), }, ) + require.Error(t, err) + var checkerErr *sema.CheckerError + require.ErrorAs(t, err, &checkerErr) + errs := checkerErr.Errors + require.Len(t, errs, 1) + + assert.IsType(t, &sema.ExternalMutationError{}, errs[0]) + }) + + t.Run("update names through reference", func(t *testing.T) { + t.Parallel() + + rt := newTestInterpreterRuntime() + + script := []byte(` + transaction { + prepare(signer: AuthAccount) { + var namesRef = &signer.contracts.names as &[String] + namesRef[0] = "baz" + + assert(signer.contracts.names[0] == "foo") + } + } + `) + + runtimeInterface := &testRuntimeInterface{ + getSigningAccounts: func() ([]Address, error) { + return []Address{{42}}, nil + }, + getAccountContractNames: func(_ Address) ([]string, error) { + return []string{"foo", "bar"}, nil + }, + } + + nextTransactionLocation := newTransactionLocationGenerator() + + err := rt.ExecuteTransaction( + Script{ + Source: script, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) require.NoError(t, err) }) } @@ -1571,7 +1645,7 @@ func TestPublicAccountContracts(t *testing.T) { nextTransactionLocation := newTransactionLocationGenerator() - result, err := rt.ExecuteScript( + _, err := rt.ExecuteScript( Script{ Source: script, }, @@ -1581,14 +1655,58 @@ func TestPublicAccountContracts(t *testing.T) { }, ) - require.NoError(t, err) + require.Error(t, err) - require.IsType(t, cadence.Array{}, result) - array := result.(cadence.Array) + var checkerErr *sema.CheckerError + require.ErrorAs(t, err, &checkerErr) + errs := checkerErr.Errors + require.Len(t, errs, 1) - require.Len(t, array.Values, 2) - assert.Equal(t, cadence.String("foo"), array.Values[0]) - assert.Equal(t, cadence.String("bar"), array.Values[1]) + assert.IsType(t, &sema.ExternalMutationError{}, errs[0]) + }) + + t.Run("append names", func(t *testing.T) { + t.Parallel() + + rt := newTestInterpreterRuntime() + + script := []byte(` + pub fun main(): [String] { + let acc = getAccount(0x02) + acc.contracts.names.append("baz") + return acc.contracts.names + } + `) + + runtimeInterface := &testRuntimeInterface{ + getSigningAccounts: func() ([]Address, error) { + return []Address{{42}}, nil + }, + getAccountContractNames: func(_ Address) ([]string, error) { + return []string{"foo", "bar"}, nil + }, + } + + nextTransactionLocation := newTransactionLocationGenerator() + + _, err := rt.ExecuteScript( + Script{ + Source: script, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + + require.Error(t, err) + + var checkerErr *sema.CheckerError + require.ErrorAs(t, err, &checkerErr) + errs := checkerErr.Errors + require.Len(t, errs, 1) + + assert.IsType(t, &sema.ExternalMutationError{}, errs[0]) }) } diff --git a/runtime/common/declarationkind.go b/runtime/common/declarationkind.go index 268e210790..dd3021f849 100644 --- a/runtime/common/declarationkind.go +++ b/runtime/common/declarationkind.go @@ -149,7 +149,7 @@ func (k DeclarationKind) Keywords() string { case DeclarationKindVariable: return "var" case DeclarationKindConstant: - return "const" + return "let" case DeclarationKindStructure: return "struct" case DeclarationKindResource: diff --git a/runtime/resourcedictionary_test.go b/runtime/resourcedictionary_test.go index b53ec7ea94..7ef0915936 100644 --- a/runtime/resourcedictionary_test.go +++ b/runtime/resourcedictionary_test.go @@ -58,7 +58,7 @@ const resourceDictionaryContract = ` pub resource C { - pub let rs: @{String: R} + pub(set) var rs: @{String: R} init() { self.rs <- {} @@ -410,7 +410,7 @@ func TestRuntimeResourceDictionaryValues_Nested(t *testing.T) { pub resource C2 { - pub let rs: @{String: R} + pub(set) var rs: @{String: R} init() { self.rs <- {} @@ -431,7 +431,7 @@ func TestRuntimeResourceDictionaryValues_Nested(t *testing.T) { pub resource C { - pub let c2s: @{String: C2} + pub(set) var c2s: @{String: C2} init() { self.c2s <- {} @@ -602,7 +602,7 @@ func TestRuntimeResourceDictionaryValues_DictionaryTransfer(t *testing.T) { pub resource C { - pub let rs: @{String: R} + pub(set) var rs: @{String: R} init() { self.rs <- {} diff --git a/runtime/runtime_test.go b/runtime/runtime_test.go index 89b69c935f..24db90400e 100644 --- a/runtime/runtime_test.go +++ b/runtime/runtime_test.go @@ -1685,7 +1685,7 @@ func TestRuntimeStorageMultipleTransactionsResourceWithArray(t *testing.T) { container := []byte(` pub resource Container { - pub let values: [Int] + pub(set) var values: [Int] init() { self.values = [] diff --git a/runtime/sema/check_assignment.go b/runtime/sema/check_assignment.go index cd7fc6dcdb..a4d89d120c 100644 --- a/runtime/sema/check_assignment.go +++ b/runtime/sema/check_assignment.go @@ -212,6 +212,23 @@ func (checker *Checker) visitIndexExpressionAssignment( elementType = checker.visitIndexExpression(target, true) + targetExpression := target.TargetExpression + switch targetExpression := targetExpression.(type) { + case *ast.MemberExpression: + // calls to this method are cached, so this performs no computation + _, member, _ := checker.visitMember(targetExpression) + if !checker.isMutatableMember(member) { + checker.report( + &ExternalMutationError{ + Name: member.Identifier.Identifier, + DeclarationKind: member.DeclarationKind, + Range: ast.NewRangeFromPositioned(targetExpression), + ContainerType: member.ContainerType, + }, + ) + } + } + if elementType == nil { return InvalidType } diff --git a/runtime/sema/check_member_expression.go b/runtime/sema/check_member_expression.go index 1a937a49a1..1448138f9f 100644 --- a/runtime/sema/check_member_expression.go +++ b/runtime/sema/check_member_expression.go @@ -163,6 +163,21 @@ func (checker *Checker) visitMember(expression *ast.MemberExpression) (accessedT } targetRange := ast.NewRangeFromPositioned(expression.Expression) member = resolver.Resolve(identifier, targetRange, checker.report) + switch targetExpression := accessedExpression.(type) { + case *ast.MemberExpression: + // calls to this method are cached, so this performs no computation + _, m, _ := checker.visitMember(targetExpression) + if !checker.isMutatableMember(m) && resolver.Mutating { + checker.report( + &ExternalMutationError{ + Name: m.Identifier.Identifier, + DeclarationKind: m.DeclarationKind, + Range: ast.NewRangeFromPositioned(targetRange), + ContainerType: m.ContainerType, + }, + ) + } + } } // Get the member from the accessed value based @@ -312,6 +327,13 @@ func (checker *Checker) isWriteableMember(member *Member) bool { checker.containerTypes[member.ContainerType] } +// isMutatableMember returns true if the given member can be mutated +// in the current location of the checker. Currently equivalent to +// isWriteableMember above, but separate in case this changes +func (checker *Checker) isMutatableMember(member *Member) bool { + return checker.isWriteableMember(member) +} + // containingContractKindedType returns the containing contract-kinded type // of the given type, if any. // diff --git a/runtime/sema/errors.go b/runtime/sema/errors.go index 50cbafe3ed..8bd703e662 100644 --- a/runtime/sema/errors.go +++ b/runtime/sema/errors.go @@ -2950,3 +2950,31 @@ func (e *InvalidEntryPointTypeError) Error() string { e.Type.QualifiedString(), ) } + +// ImportedProgramError + +type ExternalMutationError struct { + Name string + ContainerType Type + DeclarationKind common.DeclarationKind + ast.Range +} + +func (e *ExternalMutationError) Error() string { + return fmt.Sprintf( + "cannot mutate `%s`: %s is only mutable inside `%s`", + e.Name, + e.DeclarationKind.Name(), + e.ContainerType.QualifiedString(), + ) +} + +func (e *ExternalMutationError) SecondaryError() string { + return fmt.Sprintf( + "Consider adding a setter for `%s` to `%s`", + e.Name, + e.ContainerType.QualifiedString(), + ) +} + +func (*ExternalMutationError) isSemanticError() {} diff --git a/runtime/sema/type.go b/runtime/sema/type.go index fc821a3804..8db45a9341 100644 --- a/runtime/sema/type.go +++ b/runtime/sema/type.go @@ -182,8 +182,9 @@ type ValueIndexableType interface { } type MemberResolver struct { - Kind common.DeclarationKind - Resolve func(identifier string, targetRange ast.Range, report func(error)) *Member + Kind common.DeclarationKind + Mutating bool + Resolve func(identifier string, targetRange ast.Range, report func(error)) *Member } // ContainedType is a type which might have a container type @@ -1628,7 +1629,8 @@ func getArrayMembers(arrayType ArrayType) map[string]MemberResolver { if _, ok := arrayType.(*VariableSizedType); ok { members["append"] = MemberResolver{ - Kind: common.DeclarationKindFunction, + Kind: common.DeclarationKindFunction, + Mutating: true, Resolve: func(identifier string, targetRange ast.Range, report func(error)) *Member { elementType := arrayType.ElementType(false) return NewPublicFunctionMember( @@ -1641,7 +1643,8 @@ func getArrayMembers(arrayType ArrayType) map[string]MemberResolver { } members["appendAll"] = MemberResolver{ - Kind: common.DeclarationKindFunction, + Kind: common.DeclarationKindFunction, + Mutating: true, Resolve: func(identifier string, targetRange ast.Range, report func(error)) *Member { elementType := arrayType.ElementType(false) @@ -1718,7 +1721,8 @@ func getArrayMembers(arrayType ArrayType) map[string]MemberResolver { } members["insert"] = MemberResolver{ - Kind: common.DeclarationKindFunction, + Kind: common.DeclarationKindFunction, + Mutating: true, Resolve: func(identifier string, _ ast.Range, _ func(error)) *Member { elementType := arrayType.ElementType(false) @@ -1733,7 +1737,8 @@ func getArrayMembers(arrayType ArrayType) map[string]MemberResolver { } members["remove"] = MemberResolver{ - Kind: common.DeclarationKindFunction, + Kind: common.DeclarationKindFunction, + Mutating: true, Resolve: func(identifier string, _ ast.Range, _ func(error)) *Member { elementType := arrayType.ElementType(false) @@ -1748,7 +1753,8 @@ func getArrayMembers(arrayType ArrayType) map[string]MemberResolver { } members["removeFirst"] = MemberResolver{ - Kind: common.DeclarationKindFunction, + Kind: common.DeclarationKindFunction, + Mutating: true, Resolve: func(identifier string, _ ast.Range, _ func(error)) *Member { elementType := arrayType.ElementType(false) @@ -1764,7 +1770,8 @@ func getArrayMembers(arrayType ArrayType) map[string]MemberResolver { } members["removeLast"] = MemberResolver{ - Kind: common.DeclarationKindFunction, + Kind: common.DeclarationKindFunction, + Mutating: true, Resolve: func(identifier string, _ ast.Range, _ func(error)) *Member { elementType := arrayType.ElementType(false) @@ -4337,7 +4344,8 @@ func (t *DictionaryType) initializeMemberResolvers() { }, }, "insert": { - Kind: common.DeclarationKindFunction, + Kind: common.DeclarationKindFunction, + Mutating: true, Resolve: func(identifier string, _ ast.Range, _ func(error)) *Member { return NewPublicFunctionMember(t, identifier, @@ -4347,7 +4355,8 @@ func (t *DictionaryType) initializeMemberResolvers() { }, }, "remove": { - Kind: common.DeclarationKindFunction, + Kind: common.DeclarationKindFunction, + Mutating: true, Resolve: func(identifier string, _ ast.Range, _ func(error)) *Member { return NewPublicFunctionMember(t, identifier, diff --git a/runtime/tests/checker/external_mutation_test.go b/runtime/tests/checker/external_mutation_test.go new file mode 100644 index 0000000000..e3d8073ec3 --- /dev/null +++ b/runtime/tests/checker/external_mutation_test.go @@ -0,0 +1,864 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright 2019-2021 Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package checker + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/sema" +) + +func TestCheckArrayUpdateIndexAccess(t *testing.T) { + + t.Parallel() + + accessModifiers := []ast.Access{ + ast.AccessPublic, + ast.AccessAccount, + ast.AccessContract, + } + + declarationKinds := []common.DeclarationKind{ + common.DeclarationKindConstant, + common.DeclarationKindVariable, + } + + valueKinds := []common.CompositeKind{ + common.CompositeKindStructure, + common.CompositeKindResource, + } + + runTest := func(access ast.Access, declaration common.DeclarationKind, valueKind common.CompositeKind) { + testName := fmt.Sprintf("%s %s %s", access.Keyword(), valueKind.Keyword(), declaration.Keywords()) + + assignmentOp := "=" + var destroyStatement string + if valueKind == common.CompositeKindResource { + assignmentOp = "<- create" + destroyStatement = "destroy foo" + } + + t.Run(testName, func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + fmt.Sprintf(` + pub contract C { + pub %s Foo { + %s %s x: [Int] + + init() { + self.x = [3] + } + } + + pub fun bar() { + let foo %s Foo() + foo.x[0] = 3 + %s + } + } + `, valueKind.Keyword(), access.Keyword(), declaration.Keywords(), assignmentOp, destroyStatement), + ) + + errs := ExpectCheckerErrors(t, err, 1) + var externalMutationError *sema.ExternalMutationError + require.ErrorAs(t, errs[0], &externalMutationError) + }) + } + + for _, access := range accessModifiers { + for _, kind := range declarationKinds { + for _, value := range valueKinds { + runTest(access, kind, value) + } + } + } +} + +func TestCheckDictionaryUpdateIndexAccess(t *testing.T) { + + t.Parallel() + + accessModifiers := []ast.Access{ + ast.AccessPublic, + ast.AccessAccount, + ast.AccessContract, + } + + declarationKinds := []common.DeclarationKind{ + common.DeclarationKindConstant, + common.DeclarationKindVariable, + } + + valueKinds := []common.CompositeKind{ + common.CompositeKindStructure, + common.CompositeKindResource, + } + + runTest := func(access ast.Access, declaration common.DeclarationKind, valueKind common.CompositeKind) { + testName := fmt.Sprintf("%s %s %s", access.Keyword(), valueKind.Keyword(), declaration.Keywords()) + + assignmentOp := "=" + var destroyStatement string + if valueKind == common.CompositeKindResource { + assignmentOp = "<- create" + destroyStatement = "destroy foo" + } + + t.Run(testName, func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + fmt.Sprintf(` + pub contract C { + pub %s Foo { + %s %s x: {Int: Int} + + init() { + self.x = {0: 3} + } + } + + pub fun bar() { + let foo %s Foo() + foo.x[0] = 3 + %s + } + } + `, valueKind.Keyword(), access.Keyword(), declaration.Keywords(), assignmentOp, destroyStatement), + ) + + errs := ExpectCheckerErrors(t, err, 1) + var externalMutationError *sema.ExternalMutationError + require.ErrorAs(t, errs[0], &externalMutationError) + }) + } + + for _, access := range accessModifiers { + for _, kind := range declarationKinds { + for _, value := range valueKinds { + runTest(access, kind, value) + } + } + } +} + +func TestCheckNestedArrayUpdateIndexAccess(t *testing.T) { + + t.Parallel() + + accessModifiers := []ast.Access{ + ast.AccessPublic, + ast.AccessAccount, + ast.AccessContract, + } + + declarationKinds := []common.DeclarationKind{ + common.DeclarationKindConstant, + common.DeclarationKindVariable, + } + + runTest := func(access ast.Access, declaration common.DeclarationKind) { + testName := fmt.Sprintf("%s %s", access.Keyword(), declaration.Keywords()) + + t.Run(testName, func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + fmt.Sprintf(` + pub contract C { + pub struct Bar { + pub let foo: Foo + init() { + self.foo = Foo() + } + } + + pub struct Foo { + %s %s x: [Int] + + init() { + self.x = [3] + } + } + + pub fun bar() { + let bar = Bar() + bar.foo.x[0] = 3 + } + } + `, access.Keyword(), declaration.Keywords()), + ) + + errs := ExpectCheckerErrors(t, err, 1) + var externalMutationError *sema.ExternalMutationError + require.ErrorAs(t, errs[0], &externalMutationError) + }) + } + + for _, access := range accessModifiers { + for _, kind := range declarationKinds { + runTest(access, kind) + } + } +} + +func TestCheckNestedDictionaryUpdateIndexAccess(t *testing.T) { + + t.Parallel() + + accessModifiers := []ast.Access{ + ast.AccessPublic, + ast.AccessAccount, + ast.AccessContract, + } + + declarationKinds := []common.DeclarationKind{ + common.DeclarationKindConstant, + common.DeclarationKindVariable, + } + + runTest := func(access ast.Access, declaration common.DeclarationKind) { + testName := fmt.Sprintf("%s %s", access.Keyword(), declaration.Keywords()) + + t.Run(testName, func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + fmt.Sprintf(` + pub contract C { + pub struct Bar { + pub let foo: Foo + init() { + self.foo = Foo() + } + } + + pub struct Foo { + %s %s x: {Int: Int} + + init() { + self.x = {3: 3} + } + } + + pub fun bar() { + let bar = Bar() + bar.foo.x[0] = 3 + } + } + `, access.Keyword(), declaration.Keywords()), + ) + + errs := ExpectCheckerErrors(t, err, 1) + var externalMutationError *sema.ExternalMutationError + require.ErrorAs(t, errs[0], &externalMutationError) + }) + } + + for _, access := range accessModifiers { + for _, kind := range declarationKinds { + runTest(access, kind) + } + } +} + +func TestCheckMutateContractIndexAccess(t *testing.T) { + + t.Parallel() + + accessModifiers := []ast.Access{ + ast.AccessPublic, + ast.AccessAccount, + ast.AccessContract, + } + + declarationKinds := []common.DeclarationKind{ + common.DeclarationKindConstant, + common.DeclarationKindVariable, + } + + runTest := func(access ast.Access, declaration common.DeclarationKind) { + testName := fmt.Sprintf("%s %s", access.Keyword(), declaration.Keywords()) + + t.Run(testName, func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + fmt.Sprintf(` + pub contract Foo { + %s %s x: [Int] + + init() { + self.x = [3] + } + } + + pub fun bar() { + Foo.x[0] = 1 + } + `, access.Keyword(), declaration.Keywords()), + ) + + expectedErrors := 1 + if access == ast.AccessContract { + expectedErrors++ + } + + errs := ExpectCheckerErrors(t, err, expectedErrors) + if expectedErrors > 1 { + var accessError *sema.InvalidAccessError + require.ErrorAs(t, errs[expectedErrors-2], &accessError) + } + var externalMutationError *sema.ExternalMutationError + require.ErrorAs(t, errs[expectedErrors-1], &externalMutationError) + }) + } + + for _, access := range accessModifiers { + for _, kind := range declarationKinds { + runTest(access, kind) + } + } +} + +func TestCheckContractNestedStructIndexAccess(t *testing.T) { + + t.Parallel() + + accessModifiers := []ast.Access{ + ast.AccessPublic, + ast.AccessAccount, + ast.AccessContract, + } + + declarationKinds := []common.DeclarationKind{ + common.DeclarationKindConstant, + common.DeclarationKindVariable, + } + + runTest := func(access ast.Access, declaration common.DeclarationKind) { + testName := fmt.Sprintf("%s %s", access.Keyword(), declaration.Keywords()) + + t.Run(testName, func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + fmt.Sprintf(` + pub contract Foo { + pub let x: S + + pub struct S { + %s %s y: [Int] + init() { + self.y = [3] + } + } + + init() { + self.x = S() + } + } + + pub fun bar() { + Foo.x.y[0] = 1 + } + `, access.Keyword(), declaration.Keywords()), + ) + + expectedErrors := 1 + if access == ast.AccessContract { + expectedErrors++ + } + + errs := ExpectCheckerErrors(t, err, expectedErrors) + if expectedErrors > 1 { + var accessError *sema.InvalidAccessError + require.ErrorAs(t, errs[expectedErrors-2], &accessError) + } + var externalMutationError *sema.ExternalMutationError + require.ErrorAs(t, errs[expectedErrors-1], &externalMutationError) + }) + } + + for _, access := range accessModifiers { + for _, kind := range declarationKinds { + runTest(access, kind) + } + } +} + +func TestCheckContractStructInitIndexAccess(t *testing.T) { + + t.Parallel() + + accessModifiers := []ast.Access{ + ast.AccessPublic, + ast.AccessAccount, + ast.AccessContract, + } + + declarationKinds := []common.DeclarationKind{ + common.DeclarationKindConstant, + common.DeclarationKindVariable, + } + + runTest := func(access ast.Access, declaration common.DeclarationKind) { + testName := fmt.Sprintf("%s %s", access.Keyword(), declaration.Keywords()) + + t.Run(testName, func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + fmt.Sprintf(` + pub contract Foo { + pub let x: S + + pub struct S { + %s %s y: [Int] + init() { + self.y = [3] + } + } + + init() { + self.x = S() + self.x.y[1] = 2 + } + } + `, access.Keyword(), declaration.Keywords()), + ) + + errs := ExpectCheckerErrors(t, err, 1) + var externalMutationError *sema.ExternalMutationError + require.ErrorAs(t, errs[0], &externalMutationError) + }) + } + + for _, access := range accessModifiers { + for _, kind := range declarationKinds { + runTest(access, kind) + } + } +} + +func TestCheckArrayUpdateMethodCall(t *testing.T) { + + t.Parallel() + + accessModifiers := []ast.Access{ + ast.AccessPublic, + ast.AccessAccount, + ast.AccessContract, + } + + declarationKinds := []common.DeclarationKind{ + common.DeclarationKindConstant, + common.DeclarationKindVariable, + } + + valueKinds := []common.CompositeKind{ + common.CompositeKindStructure, + common.CompositeKindResource, + } + + type MethodCall = struct { + Mutating bool + Code string + Name string + } + + memberExpressions := []MethodCall{ + {Mutating: true, Code: ".append(3)", Name: "append"}, + {Mutating: false, Code: ".length", Name: "length"}, + {Mutating: false, Code: ".concat([3])", Name: "concat"}, + {Mutating: false, Code: ".contains(3)", Name: "contains"}, + {Mutating: true, Code: ".appendAll([3])", Name: "appendAll"}, + {Mutating: true, Code: ".insert(at: 0, 3)", Name: "insert"}, + {Mutating: true, Code: ".remove(at: 0)", Name: "remove"}, + {Mutating: true, Code: ".removeFirst()", Name: "removeFirst"}, + {Mutating: true, Code: ".removeLast()", Name: "removeLast"}, + } + + runTest := func(access ast.Access, declaration common.DeclarationKind, valueKind common.CompositeKind, member MethodCall) { + testName := fmt.Sprintf("%s %s %s %s", access.Keyword(), valueKind.Keyword(), declaration.Keywords(), member.Name) + + assignmentOp := "=" + var destroyStatement string + if valueKind == common.CompositeKindResource { + assignmentOp = "<- create" + destroyStatement = "destroy foo" + } + + t.Run(testName, func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + fmt.Sprintf(` + pub contract C { + pub %s Foo { + %s %s x: [Int] + + init() { + self.x = [3] + } + } + + pub fun bar() { + let foo %s Foo() + foo.x%s + %s + } + } + `, valueKind.Keyword(), access.Keyword(), declaration.Keywords(), assignmentOp, member.Code, destroyStatement), + ) + + if member.Mutating { + errs := ExpectCheckerErrors(t, err, 1) + var externalMutationError *sema.ExternalMutationError + require.ErrorAs(t, errs[0], &externalMutationError) + } else { + require.NoError(t, err) + } + }) + } + + for _, access := range accessModifiers { + for _, kind := range declarationKinds { + for _, value := range valueKinds { + for _, member := range memberExpressions { + runTest(access, kind, value, member) + } + } + } + } +} + +func TestCheckDictionaryUpdateMethodCall(t *testing.T) { + + t.Parallel() + + accessModifiers := []ast.Access{ + ast.AccessPublic, + ast.AccessAccount, + ast.AccessContract, + } + + declarationKinds := []common.DeclarationKind{ + common.DeclarationKindConstant, + common.DeclarationKindVariable, + } + + valueKinds := []common.CompositeKind{ + common.CompositeKindStructure, + common.CompositeKindResource, + } + type MethodCall = struct { + Mutating bool + Code string + Name string + } + + memberExpressions := []MethodCall{ + {Mutating: true, Code: ".insert(key:3, 3)", Name: "insert"}, + {Mutating: false, Code: ".length", Name: "length"}, + {Mutating: false, Code: ".keys", Name: "keys"}, + {Mutating: false, Code: ".values", Name: "values"}, + {Mutating: false, Code: ".containsKey(3)", Name: "containsKey"}, + {Mutating: true, Code: ".remove(key: 0)", Name: "remove"}, + } + + runTest := func(access ast.Access, declaration common.DeclarationKind, valueKind common.CompositeKind, member MethodCall) { + testName := fmt.Sprintf("%s %s %s %s", access.Keyword(), valueKind.Keyword(), declaration.Keywords(), member.Name) + + assignmentOp := "=" + var destroyStatement string + if valueKind == common.CompositeKindResource { + assignmentOp = "<- create" + destroyStatement = "destroy foo" + } + + t.Run(testName, func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + fmt.Sprintf(` + pub contract C { + pub %s Foo { + %s %s x: {Int: Int} + + init() { + self.x = {3: 3} + } + } + + pub fun bar() { + let foo %s Foo() + foo.x%s + %s + } + } + `, valueKind.Keyword(), access.Keyword(), declaration.Keywords(), assignmentOp, member.Code, destroyStatement), + ) + + if member.Mutating { + errs := ExpectCheckerErrors(t, err, 1) + var externalMutationError *sema.ExternalMutationError + require.ErrorAs(t, errs[0], &externalMutationError) + } else { + require.NoError(t, err) + } + }) + } + + for _, access := range accessModifiers { + for _, kind := range declarationKinds { + for _, value := range valueKinds { + for _, member := range memberExpressions { + runTest(access, kind, value, member) + } + } + } + } +} + +func TestCheckPubSetAccessModifier(t *testing.T) { + + t.Parallel() + t.Run("pub set dict", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + pub contract C { + pub struct Foo { + pub(set) var x: {Int: Int} + + init() { + self.x = {3: 3} + } + } + + pub fun bar() { + let foo = Foo() + foo.x[0] = 3 + } + } + `, + ) + require.NoError(t, err) + + }) +} + +func TestCheckPubSetNestedAccessModifier(t *testing.T) { + + t.Parallel() + t.Run("pub set nested", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + pub contract C { + pub struct Bar { + pub let foo: Foo + init() { + self.foo = Foo() + } + } + + pub struct Foo { + pub(set) var x: [Int] + + init() { + self.x = [3] + } + } + + pub fun bar() { + let bar = Bar() + bar.foo.x[0] = 3 + } + } + `, + ) + require.NoError(t, err) + + }) +} + +func TestCheckSelfContainingStruct(t *testing.T) { + + t.Parallel() + + t.Run("pub let", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + pub contract C { + pub struct Foo { + pub let x: {Int: Int} + + init() { + self.x = {3: 3} + } + + pub fun bar() { + let foo = Foo() + foo.x[0] = 3 + } + } + } + `, + ) + require.NoError(t, err) + + }) +} + +func TestCheckMutationThroughReference(t *testing.T) { + + t.Parallel() + + t.Run("pub let", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + pub fun main() { + let foo = Foo() + foo.ref.arr.append("y") + } + + pub struct Foo { + pub let ref: &Bar + init() { + self.ref = &Bar() as &Bar + } + } + + pub struct Bar { + pub let arr: [String] + init() { + self.arr = ["x"] + } + } + `, + ) + errs := ExpectCheckerErrors(t, err, 1) + var externalMutationError *sema.ExternalMutationError + require.ErrorAs(t, errs[0], &externalMutationError) + }) +} + +func TestCheckMutationThroughInnerReference(t *testing.T) { + + t.Parallel() + + t.Run("pub let", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + pub fun main() { + let foo = Foo() + var arrayRef = &foo.ref.arr as &[String] + arrayRef[0] = "y" + } + + pub struct Foo { + pub let ref: &Bar + init() { + self.ref = &Bar() as &Bar + } + } + + pub struct Bar { + pub let arr: [String] + init() { + self.arr = ["x"] + } + } + `, + ) + require.NoError(t, err) + }) +} + +func TestCheckMutationThroughAccess(t *testing.T) { + + t.Parallel() + + t.Run("pub let", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, + ` + pub contract C { + pub struct Foo { + pub let arr: [Int] + init() { + self.arr = [3] + } + } + + priv let foo : Foo + + init() { + self.foo = Foo() + } + + pub fun getFoo(): Foo { + return self.foo + } + } + + pub fun main() { + let a = C.getFoo() + a.arr.append(0) // a.arr is now [3, 0] + } + `, + ) + errs := ExpectCheckerErrors(t, err, 1) + var externalMutationError *sema.ExternalMutationError + require.ErrorAs(t, errs[0], &externalMutationError) + }) +} diff --git a/runtime/tests/interpreter/resources_test.go b/runtime/tests/interpreter/resources_test.go index 7b49097dfd..b695dafff9 100644 --- a/runtime/tests/interpreter/resources_test.go +++ b/runtime/tests/interpreter/resources_test.go @@ -395,7 +395,7 @@ func TestInterpretImplicitResourceRemovalFromContainer(t *testing.T) { } resource R1 { - var r2s: @{Int: R2} + pub(set) var r2s: @{Int: R2} init() { self.r2s <- {}