From 6d56a062dd9ee14e1cb84fa57544f4860415208c Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Wed, 20 Mar 2024 07:26:15 -0700 Subject: [PATCH 1/3] Consider inherited interfaces when checking for conformances --- ..._to_v1_contract_upgrade_validation_test.go | 78 ++++++++++++++++ ..._v0.42_to_v1_contract_upgrade_validator.go | 88 ++++++++++++++++++- runtime/stdlib/contract_update_validation.go | 27 ++++-- 3 files changed, 183 insertions(+), 10 deletions(-) diff --git a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go index a90d0f6be0..a5670fd38b 100644 --- a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go +++ b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go @@ -1829,3 +1829,81 @@ func TestTypeRequirementRemoval(t *testing.T) { require.ErrorAs(t, cause, &declKindChangeError) }) } + +func TestInterfaceConformanceChange(t *testing.T) { + + t.Parallel() + + t.Run("local inherited interface", func(t *testing.T) { + + t.Parallel() + + const oldCode = ` + pub contract Test { + pub resource interface A {} + + pub resource R: A {} + } + ` + + const newCode = ` + access(all) contract Test { + access(all) resource interface A {} + access(all) resource interface B: A {} + + // Also conforms to 'A' via inheritance. + // Therefore, existing conformance is not removed. + access(all) resource R: B {} + } + ` + + err := testContractUpdate(t, oldCode, newCode) + require.NoError(t, err) + }) + + t.Run("imported inherited interface", func(t *testing.T) { + + t.Parallel() + + const oldCode = ` + import TestImport from 0x02 + + pub contract Test { + pub resource R: TestImport.A {} + } + ` + + const newImport = ` + access(all) contract TestImport { + access(all) resource interface A {} + + access(all) resource interface B: A {} + } + ` + + const newCode = ` + import TestImport from 0x02 + + access(all) contract Test { + // Also conforms to 'TestImport.A' via inheritance. + // Therefore, existing conformance is not removed. + access(all) resource R: TestImport.B {} + } + ` + + err := testContractUpdateWithImports( + t, + "Test", + oldCode, + newCode, + map[common.Location]string{ + common.AddressLocation{ + Name: "TestImport", + Address: common.MustBytesToAddress([]byte{0x2}), + }: newImport, + }, + ) + + require.NoError(t, err) + }) +} diff --git a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go index 23d0619f88..ee30f9a8a1 100644 --- a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go +++ b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go @@ -97,7 +97,12 @@ func (validator *CadenceV042ToV1ContractUpdateValidator) Validate() error { validator.TypeComparator.expectedIdentifierImportLocations = collectImports(validator, underlyingValidator.oldProgram) validator.TypeComparator.foundIdentifierImportLocations = collectImports(validator, underlyingValidator.newProgram) - checkDeclarationUpdatability(validator, oldRootDecl, newRootDecl) + checkDeclarationUpdatability( + validator, + oldRootDecl, + newRootDecl, + validator.checkConformanceV1, + ) if underlyingValidator.hasErrors() { return underlyingValidator.getContractUpdateError() @@ -231,7 +236,7 @@ func (validator *CadenceV042ToV1ContractUpdateValidator) requireEqualAccess( } func (validator *CadenceV042ToV1ContractUpdateValidator) expectedAuthorizationOfComposite(composite *ast.NominalType) sema.Access { - // if this field is set, we are currently upgrading a formerly legacy restricted type into a reference to a composite + // If this field is set, we are currently upgrading a former legacy restricted type into a reference to a composite // in this case, the expected entitlements are based not on the underlying composite type, // but instead the types previously in the restriction set if validator.currentRestrictedTypeUpgradeRestrictions != nil { @@ -499,6 +504,85 @@ func (validator *CadenceV042ToV1ContractUpdateValidator) checkDeclarationKindCha return false } +func (validator *CadenceV042ToV1ContractUpdateValidator) checkConformanceV1( + oldDecl *ast.CompositeDeclaration, + newDecl *ast.CompositeDeclaration, +) { + + // Here it is assumed enums will always have one and only one conformance. + // This is enforced by the checker. + // Therefore, below check for multiple conformances is only applicable + // for non-enum type composite declarations. i.e: structs, resources, etc. + + oldConformances := oldDecl.Conformances + + location := validator.underlyingUpdateValidator.location + + elaboration := validator.newElaborations[location] + newDeclType := elaboration.CompositeDeclarationType(newDecl) + + // A conformance may not be explicitly defined in the current declaration, + // but they could be available via inheritance. + newConformances := newDeclType.EffectiveInterfaceConformances() + + // All the existing conformances must have a match. Order is not important. + // Having extra new conformance is OK. See: https://github.com/onflow/cadence/issues/1394 + + // Note: Removing a conformance is NOT OK. That could lead to type-safety issues. + // e.g: + // - Someone stores an array of type `[{I}]` with `T:I` objects inside. + // - Later T’s conformance to `I` is removed. + // - Now `[{I}]` contains objects if `T` that does not conform to `I`. + + for _, oldConformance := range oldConformances { + found := false + for index, newConformance := range newConformances { + nominalType := semaConformanceToASTNominalType(newConformance) + + err := oldConformance.CheckEqual(nominalType, validator) + if err == nil { + found = true + + // Remove the matched conformance, so we don't have to check it again. + // i.e: optimization + newConformances = append(newConformances[:index], newConformances[index+1:]...) + break + } + } + + if !found { + validator.report(&ConformanceMismatchError{ + DeclName: newDecl.Identifier.Identifier, + Range: ast.NewUnmeteredRangeFromPositioned(newDecl.Identifier), + }) + + return + } + } +} + +func semaConformanceToASTNominalType(newConformance sema.Conformance) *ast.NominalType { + interfaceType := newConformance.InterfaceType + containerType := interfaceType.GetContainerType() + + identifier := ast.Identifier{ + Identifier: interfaceType.Identifier, + } + + if containerType == nil { + return ast.NewNominalType(nil, identifier, nil) + } + + return ast.NewNominalType( + nil, + ast.Identifier{ + Identifier: containerType.String(), + }, + []ast.Identifier{identifier}, + ) + +} + var builtinTypes = map[string]struct{}{} func init() { diff --git a/runtime/stdlib/contract_update_validation.go b/runtime/stdlib/contract_update_validation.go index a47b842b85..44577cd082 100644 --- a/runtime/stdlib/contract_update_validation.go +++ b/runtime/stdlib/contract_update_validation.go @@ -45,6 +45,11 @@ type UpdateValidator interface { ) bool } +type checkConformanceFunc func( + oldDecl *ast.CompositeDeclaration, + newDecl *ast.CompositeDeclaration, +) + type ContractUpdateValidator struct { TypeComparator @@ -114,7 +119,12 @@ func (validator *ContractUpdateValidator) Validate() error { return validator.getContractUpdateError() } - checkDeclarationUpdatability(validator, oldRootDecl, newRootDecl) + checkDeclarationUpdatability( + validator, + oldRootDecl, + newRootDecl, + validator.checkConformance, + ) if validator.hasErrors() { return validator.getContractUpdateError() @@ -203,6 +213,7 @@ func checkDeclarationUpdatability( validator UpdateValidator, oldDeclaration ast.Declaration, newDeclaration ast.Declaration, + checkConformance checkConformanceFunc, ) { if !validator.checkDeclarationKindChange(oldDeclaration, newDeclaration) { @@ -217,11 +228,11 @@ func checkDeclarationUpdatability( checkFields(validator, oldDeclaration, newDeclaration) - checkNestedDeclarations(validator, oldDeclaration, newDeclaration) + checkNestedDeclarations(validator, oldDeclaration, newDeclaration, checkConformance) if newDecl, ok := newDeclaration.(*ast.CompositeDeclaration); ok { if oldDecl, ok := oldDeclaration.(*ast.CompositeDeclaration); ok { - checkConformance(validator, oldDecl, newDecl) + checkConformance(oldDecl, newDecl) } } } @@ -293,6 +304,7 @@ func checkNestedDeclarations( validator UpdateValidator, oldDeclaration ast.Declaration, newDeclaration ast.Declaration, + checkConformance checkConformanceFunc, ) { oldNominalTypeDecls := getNestedNominalTypeDecls(oldDeclaration) @@ -306,7 +318,7 @@ func checkNestedDeclarations( continue } - checkDeclarationUpdatability(validator, oldNestedDecl, newNestedDecl) + checkDeclarationUpdatability(validator, oldNestedDecl, newNestedDecl, checkConformance) // If there's a matching new decl, then remove the old one from the map. delete(oldNominalTypeDecls, newNestedDecl.Identifier.Identifier) @@ -321,7 +333,7 @@ func checkNestedDeclarations( continue } - checkDeclarationUpdatability(validator, oldNestedDecl, newNestedDecl) + checkDeclarationUpdatability(validator, oldNestedDecl, newNestedDecl, checkConformance) // If there's a matching new decl, then remove the old one from the map. delete(oldNominalTypeDecls, newNestedDecl.Identifier.Identifier) @@ -336,7 +348,7 @@ func checkNestedDeclarations( continue } - checkDeclarationUpdatability(validator, oldNestedDecl, newNestedDecl) + checkDeclarationUpdatability(validator, oldNestedDecl, newNestedDecl, checkConformance) // If there's a matching new decl, then remove the old one from the map. delete(oldNominalTypeDecls, newNestedDecl.Identifier.Identifier) @@ -444,8 +456,7 @@ func checkEnumCases( } } -func checkConformance( - validator UpdateValidator, +func (validator *ContractUpdateValidator) checkConformance( oldDecl *ast.CompositeDeclaration, newDecl *ast.CompositeDeclaration, ) { From a78ac56a86e2c074ea51041b60bce7ffb76bde32 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Wed, 20 Mar 2024 08:30:22 -0700 Subject: [PATCH 2/3] Consider custom composite/interface type change rules --- ..._to_v1_contract_upgrade_validation_test.go | 75 ++++++++++++++++++- ..._v0.42_to_v1_contract_upgrade_validator.go | 43 +++++++---- 2 files changed, 102 insertions(+), 16 deletions(-) diff --git a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go index a5670fd38b..9db4d47172 100644 --- a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go +++ b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go @@ -759,13 +759,13 @@ func TestContractUpgradeFieldType(t *testing.T) { Address: common.MustBytesToAddress([]byte{0x1}), } - nftLocation := common.AddressLocation{ + ftLocation := common.AddressLocation{ Name: "FungibleToken", Address: common.MustBytesToAddress([]byte{0x2}), } imports := map[common.Location]string{ - nftLocation: newImport, + ftLocation: newImport, } oldProgram, newProgram, elaborations := parseAndCheckPrograms(t, location, oldCode, newCode, imports) @@ -1906,4 +1906,75 @@ func TestInterfaceConformanceChange(t *testing.T) { require.NoError(t, err) }) + + t.Run("with custom rules", func(t *testing.T) { + t.Parallel() + + const oldCode = ` + import NonFungibleToken from 0x02 + + pub contract Test { + pub resource R: NonFungibleToken.INFT {} + } + ` + + const newImport = ` + access(all) contract NonFungibleToken { + access(all) resource interface NFT {} + } + ` + + const newCode = ` + import NonFungibleToken from 0x02 + + access(all) contract Test { + access(all) resource R: NonFungibleToken.NFT {} + } + ` + + nftLocation := common.AddressLocation{ + Name: "NonFungibleToken", + Address: common.MustBytesToAddress([]byte{0x2}), + } + + imports := map[common.Location]string{ + nftLocation: newImport, + } + + const contractName = "Test" + location := common.AddressLocation{ + Name: contractName, + Address: common.MustBytesToAddress([]byte{0x1}), + } + + oldProgram, newProgram, elaborations := parseAndCheckPrograms(t, location, oldCode, newCode, imports) + + inftTypeID := common.NewTypeIDFromQualifiedName(nil, nftLocation, "NonFungibleToken.INFT") + nftTypeID := common.NewTypeIDFromQualifiedName(nil, nftLocation, "NonFungibleToken.NFT") + + upgradeValidator := stdlib.NewCadenceV042ToV1ContractUpdateValidator( + location, + contractName, + &runtime_utils.TestRuntimeInterface{ + OnGetAccountContractNames: func(address runtime.Address) ([]string, error) { + return []string{"TestImport"}, nil + }, + }, + oldProgram, + newProgram, + elaborations, + ).WithUserDefinedTypeChangeChecker( + func(oldTypeID common.TypeID, newTypeID common.TypeID) (checked, valid bool) { + switch oldTypeID { + case inftTypeID: + return true, newTypeID == nftTypeID + } + + return false, false + }, + ) + + err := upgradeValidator.Validate() + require.NoError(t, err) + }) } diff --git a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go index ee30f9a8a1..61de58cb33 100644 --- a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go +++ b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go @@ -394,17 +394,7 @@ typeSwitch: } if _, isbuiltinType := builtinTypes[oldType.String()]; !isbuiltinType { - oldTypeID, err := validator.typeIDFromType(oldType) - if err != nil { - break - } - - newTypeID, err := validator.typeIDFromType(newType) - if err != nil { - break - } - - checked, valid := validator.checkUserDefinedType(oldTypeID, newTypeID) + checked, valid := validator.checkUserDefinedTypeCustomRules(oldType, newType) // If there are no custom rules for this type, // do the default type comparison. @@ -430,6 +420,24 @@ typeSwitch: } +func (validator *CadenceV042ToV1ContractUpdateValidator) checkUserDefinedTypeCustomRules( + oldType ast.Type, + newType ast.Type, +) (checked, valid bool) { + + oldTypeID, err := validator.typeIDFromType(oldType) + if err != nil { + return false, false + } + + newTypeID, err := validator.typeIDFromType(newType) + if err != nil { + return false, false + } + + return validator.checkUserDefinedType(oldTypeID, newTypeID) +} + func isNonStorableType(typ ast.Type) bool { switch typ := typ.(type) { case *ast.ReferenceType, *ast.FunctionType: @@ -537,10 +545,17 @@ func (validator *CadenceV042ToV1ContractUpdateValidator) checkConformanceV1( for _, oldConformance := range oldConformances { found := false for index, newConformance := range newConformances { - nominalType := semaConformanceToASTNominalType(newConformance) + newConformanceNominalType := semaConformanceToASTNominalType(newConformance) + + err := oldConformance.CheckEqual(newConformanceNominalType, validator) + + var customRuleChecked, customRuleValid bool + if err != nil { + customRuleChecked, customRuleValid = + validator.checkUserDefinedTypeCustomRules(oldConformance, newConformanceNominalType) + } - err := oldConformance.CheckEqual(nominalType, validator) - if err == nil { + if err == nil || (customRuleChecked && customRuleValid) { found = true // Remove the matched conformance, so we don't have to check it again. From a179da75e74b27c08f2d0c8ba2d27c83cdb8db3f Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Wed, 20 Mar 2024 11:18:59 -0700 Subject: [PATCH 3/3] Improve typeID resolving --- runtime/stdlib/account.go | 2 +- ..._to_v1_contract_upgrade_validation_test.go | 98 ++++++++++++++++++- ..._v0.42_to_v1_contract_upgrade_validator.go | 39 ++++++-- 3 files changed, 124 insertions(+), 15 deletions(-) diff --git a/runtime/stdlib/account.go b/runtime/stdlib/account.go index 1868725195..aec228ceb8 100644 --- a/runtime/stdlib/account.go +++ b/runtime/stdlib/account.go @@ -1625,7 +1625,7 @@ func changeAccountContracts( contractName, handler, oldProgram, - program.Program, + program, inter.AllElaborations(), ) } else { diff --git a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go index 9db4d47172..4a4290fd9d 100644 --- a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go +++ b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go @@ -27,6 +27,7 @@ import ( "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/ast" "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/interpreter" "github.com/onflow/cadence/runtime/old_parser" "github.com/onflow/cadence/runtime/parser" "github.com/onflow/cadence/runtime/sema" @@ -55,12 +56,14 @@ func testContractUpdate(t *testing.T, oldCode string, newCode string) error { err = checker.Check() require.NoError(t, err) + program := interpreter.ProgramFromChecker(checker) + upgradeValidator := stdlib.NewCadenceV042ToV1ContractUpdateValidator( utils.TestLocation, "Test", &runtime_utils.TestRuntimeInterface{}, oldProgram, - newProgram, + program, map[common.Location]*sema.Elaboration{ utils.TestLocation: checker.Elaboration, }) @@ -104,7 +107,7 @@ func parseAndCheckPrograms( newImports map[common.Location]string, ) ( oldProgram *ast.Program, - newProgram *ast.Program, + newProgram *interpreter.Program, elaborations map[common.Location]*sema.Elaboration, ) { @@ -112,7 +115,7 @@ func parseAndCheckPrograms( oldProgram, err = old_parser.ParseProgram(nil, []byte(oldCode), old_parser.Config{}) require.NoError(t, err) - newProgram, err = parser.ParseProgram(nil, []byte(newCode), parser.Config{}) + program, err := parser.ParseProgram(nil, []byte(newCode), parser.Config{}) require.NoError(t, err) elaborations = map[common.Location]*sema.Elaboration{} @@ -139,7 +142,7 @@ func parseAndCheckPrograms( } checker, err := sema.NewChecker( - newProgram, + program, location, nil, &sema.Config{ @@ -174,7 +177,7 @@ func parseAndCheckPrograms( err = checker.Check() require.NoError(t, err) - elaborations[location] = checker.Elaboration + newProgram = interpreter.ProgramFromChecker(checker) return } @@ -1977,4 +1980,89 @@ func TestInterfaceConformanceChange(t *testing.T) { err := upgradeValidator.Validate() require.NoError(t, err) }) + + t.Run("with custom rules and changed import", func(t *testing.T) { + t.Parallel() + + const oldCode = ` + import MetadataViews from 0x02 + + pub contract Test { + pub resource R: MetadataViews.Resolver {} + } + ` + + const newImport = ` + access(all) contract ViewResolver { + access(all) resource interface Resolver {} + } + ` + + const newCode = ` + import ViewResolver from 0x02 + + access(all) contract Test { + access(all) resource R: ViewResolver.Resolver {} + } + ` + + viewResolverLocation := common.AddressLocation{ + Name: "ViewResolver", + Address: common.MustBytesToAddress([]byte{0x2}), + } + + metadatViewsLocation := common.AddressLocation{ + Name: "MetadataViews", + Address: common.MustBytesToAddress([]byte{0x2}), + } + + imports := map[common.Location]string{ + viewResolverLocation: newImport, + } + + const contractName = "Test" + location := common.AddressLocation{ + Name: contractName, + Address: common.MustBytesToAddress([]byte{0x1}), + } + + oldProgram, newProgram, elaborations := parseAndCheckPrograms(t, location, oldCode, newCode, imports) + + metadataViewsResolverTypeID := common.NewTypeIDFromQualifiedName( + nil, + metadatViewsLocation, + "MetadataViews.Resolver", + ) + + viewResolverResolverTypeID := common.NewTypeIDFromQualifiedName( + nil, + viewResolverLocation, + "ViewResolver.Resolver", + ) + + upgradeValidator := stdlib.NewCadenceV042ToV1ContractUpdateValidator( + location, + contractName, + &runtime_utils.TestRuntimeInterface{ + OnGetAccountContractNames: func(address runtime.Address) ([]string, error) { + return []string{"TestImport"}, nil + }, + }, + oldProgram, + newProgram, + elaborations, + ).WithUserDefinedTypeChangeChecker( + func(oldTypeID common.TypeID, newTypeID common.TypeID) (checked, valid bool) { + switch oldTypeID { + case metadataViewsResolverTypeID: + return true, newTypeID == viewResolverResolverTypeID + } + + return false, false + }, + ) + + err := upgradeValidator.Validate() + require.NoError(t, err) + }) } diff --git a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go index 61de58cb33..5ce8350a9a 100644 --- a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go +++ b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go @@ -25,6 +25,7 @@ import ( "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" ) @@ -46,11 +47,20 @@ func NewCadenceV042ToV1ContractUpdateValidator( contractName string, provider AccountContractNamesProvider, oldProgram *ast.Program, - newProgram *ast.Program, + newProgram *interpreter.Program, newElaborations map[common.Location]*sema.Elaboration, ) *CadenceV042ToV1ContractUpdateValidator { - underlyingValidator := NewContractUpdateValidator(location, contractName, provider, oldProgram, newProgram) + underlyingValidator := NewContractUpdateValidator( + location, + contractName, + provider, + oldProgram, + newProgram.Program, + ) + + // Also add the elaboration of the current program. + newElaborations[location] = newProgram.Elaboration return &CadenceV042ToV1ContractUpdateValidator{ underlyingUpdateValidator: underlyingValidator, @@ -157,17 +167,24 @@ func (validator *CadenceV042ToV1ContractUpdateValidator) idAndLocationOfQualifie // and in 1 and 2 we don't need to do anything typIdentifier := typ.Identifier.Identifier rootIdentifier := validator.TypeComparator.RootDeclIdentifier.Identifier - location := validator.underlyingUpdateValidator.location - foundLocations := validator.TypeComparator.foundIdentifierImportLocations + newImportLocations := validator.TypeComparator.foundIdentifierImportLocations + oldImportLocations := validator.TypeComparator.expectedIdentifierImportLocations - if typIdentifier != rootIdentifier && foundLocations[typIdentifier] == nil { - qualifiedString = fmt.Sprintf("%s.%s", rootIdentifier, qualifiedString) - return common.NewTypeIDFromQualifiedName(nil, location, qualifiedString), location + // Here we only need to find the qualified type ID. + // So check in both old imports as well as in new imports. + location, wasImported := newImportLocations[typIdentifier] + if !wasImported { + location, wasImported = oldImportLocations[typIdentifier] + } + + if !wasImported { + location = validator.underlyingUpdateValidator.location } - if loc := foundLocations[typIdentifier]; loc != nil { - location = loc + if typIdentifier != rootIdentifier && !wasImported { + qualifiedString = fmt.Sprintf("%s.%s", rootIdentifier, qualifiedString) + return common.NewTypeIDFromQualifiedName(nil, location, qualifiedString), location } return common.NewTypeIDFromQualifiedName(nil, location, qualifiedString), location @@ -425,6 +442,10 @@ func (validator *CadenceV042ToV1ContractUpdateValidator) checkUserDefinedTypeCus newType ast.Type, ) (checked, valid bool) { + if validator.checkUserDefinedType == nil { + return false, false + } + oldTypeID, err := validator.typeIDFromType(oldType) if err != nil { return false, false