diff --git a/tools/analysis/analysis_test.go b/tools/analysis/analysis_test.go index 779cf921bf..fb5d885d05 100644 --- a/tools/analysis/analysis_test.go +++ b/tools/analysis/analysis_test.go @@ -76,8 +76,8 @@ func TestNeedSyntaxAndImport(t *testing.T) { return []byte(contractCode), nil default: - require.FailNow(t, - "import of unknown location: %s", + require.FailNowf(t, + "import of unknown location", "location: %s", location, ) @@ -194,10 +194,9 @@ func TestParseError(t *testing.T) { switch location { case contractLocation: return []byte(contractCode), nil - default: - require.FailNow(t, - "import of unknown location: %s", + require.FailNowf(t, + "import of unknown location", "location: %s", location, ) @@ -242,8 +241,8 @@ func TestCheckError(t *testing.T) { return []byte(contractCode), nil default: - require.FailNow(t, - "import of unknown location: %s", + require.FailNowf(t, + "import of unknown location", "location: %s", location, ) @@ -259,6 +258,185 @@ func TestCheckError(t *testing.T) { require.ErrorAs(t, err, &checkerError) } +func TestHandledParserError(t *testing.T) { + + t.Parallel() + + contractAddress := common.MustBytesToAddress([]byte{0x1}) + contractLocation := common.AddressLocation{ + Address: contractAddress, + Name: "ContractA", + } + const contractCode = ` + access(all) contract ContractA { + init() { + ??? + } + } + ` + + handlerCalls := 0 + config := &analysis.Config{ + Mode: analysis.NeedSyntax, + ResolveCode: func( + location common.Location, + importingLocation common.Location, + importRange ast.Range, + ) ([]byte, error) { + switch location { + case contractLocation: + return []byte(contractCode), nil + + default: + require.FailNowf(t, + "import of unknown location", + "location: %s", + location, + ) + return nil, nil + } + }, + HandleParserError: func(err analysis.ParsingCheckingError, _ *ast.Program) error { + require.Error(t, err) + handlerCalls++ + return nil + }, + } + + programs, err := analysis.Load(config, contractLocation) + require.NoError(t, err) + + require.Equal(t, 1, handlerCalls) + + var parserError parser.Error + require.ErrorAs(t, programs[contractLocation].LoadError, &parserError) +} + +func TestHandledCheckerError(t *testing.T) { + + t.Parallel() + + contractAddress := common.MustBytesToAddress([]byte{0x1}) + contractLocation := common.AddressLocation{ + Address: contractAddress, + Name: "ContractA", + } + const contractCode = ` + access(all) contract ContractA { + init() { + X + } + } + ` + + handlerCalls := 0 + config := &analysis.Config{ + Mode: analysis.NeedTypes, + ResolveCode: func( + location common.Location, + importingLocation common.Location, + importRange ast.Range, + ) ([]byte, error) { + switch location { + case contractLocation: + return []byte(contractCode), nil + default: + require.FailNowf(t, + "import of unknown location", + "location: %s", + location, + ) + return nil, nil + } + }, + HandleCheckerError: func(err analysis.ParsingCheckingError, _ *sema.Checker) error { + require.Error(t, err) + handlerCalls++ + return nil + }, + } + + programs, err := analysis.Load(config, contractLocation) + require.Equal(t, 1, handlerCalls) + require.NoError(t, err) + + var checkerError *sema.CheckerError + require.ErrorAs(t, programs[contractLocation].LoadError, &checkerError) +} + +// Tests that an error handled by the custom error handler is not returned +// However, it must set LoadError to the handled error so that checkers later importing the program can see it +func TestHandledLoadErrorImportedProgram(t *testing.T) { + + t.Parallel() + + contract1Address := common.MustBytesToAddress([]byte{0x1}) + contract1Location := common.AddressLocation{ + Address: contract1Address, + Name: "ContractA", + } + const contract1Code = ` + import ContractB from 0x2 + + access(all) contract ContractA { + init() {} + } + ` + contract2Address := common.MustBytesToAddress([]byte{0x2}) + contract2Location := common.AddressLocation{ + Address: contract2Address, + Name: "ContractB", + } + const contract2Code = ` + access(all) contract ContractB { + init() { + X + } + } + ` + + handlerCalls := 0 + config := &analysis.Config{ + Mode: analysis.NeedTypes, + ResolveCode: func( + location common.Location, + importingLocation common.Location, + importRange ast.Range, + ) ([]byte, error) { + switch location { + case contract1Location: + return []byte(contract1Code), nil + case contract2Location: + return []byte(contract2Code), nil + default: + require.FailNowf(t, + "import of unknown location", + "location: %s", + location, + ) + return nil, nil + } + }, + HandleCheckerError: func(err analysis.ParsingCheckingError, _ *sema.Checker) error { + require.Error(t, err) + handlerCalls++ + return nil + }, + } + + programs, err := analysis.Load(config, contract1Location) + require.Equal(t, 2, handlerCalls) + require.NoError(t, err) + + var checkerError *sema.CheckerError + require.ErrorAs(t, programs[contract1Location].LoadError, &checkerError) + require.ErrorAs(t, programs[contract2Location].LoadError, &checkerError) + + // Validate that parent checker receives the imported program error despite it being handled + var importedProgramErr *sema.ImportedProgramError + require.ErrorAs(t, programs[contract1Location].LoadError, &importedProgramErr) +} + func TestStdlib(t *testing.T) { t.Parallel() @@ -283,8 +461,8 @@ func TestStdlib(t *testing.T) { return []byte(code), nil default: - require.FailNow(t, - "import of unknown location: %s", + require.FailNowf(t, + "import of unknown location", "location: %s", location, ) @@ -349,8 +527,8 @@ func TestCyclicImports(t *testing.T) { return []byte(barContractCode), nil default: - require.FailNow(t, - "import of unknown location: %s", + require.FailNowf(t, + "import of unknown location", "location: %s", location, ) diff --git a/tools/analysis/config.go b/tools/analysis/config.go index 020384f3ea..ffaafd7c93 100644 --- a/tools/analysis/config.go +++ b/tools/analysis/config.go @@ -24,6 +24,7 @@ import ( "github.com/onflow/cadence/runtime/ast" "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/sema" ) // A Config specifies details about how programs should be loaded. @@ -40,6 +41,10 @@ type Config struct { ) ([]byte, error) // Mode controls the level of information returned for each program Mode LoadMode + // HandleParserError is called when a parser error occurs instead of returning it + HandleParserError func(err ParsingCheckingError, program *ast.Program) error + // HandleCheckerError is called when a checker error occurs instead of returning it + HandleCheckerError func(err ParsingCheckingError, checker *sema.Checker) error } func NewSimpleConfig( diff --git a/tools/analysis/program.go b/tools/analysis/program.go index b4612a01e4..d234adf74c 100644 --- a/tools/analysis/program.go +++ b/tools/analysis/program.go @@ -27,10 +27,11 @@ import ( ) type Program struct { - Location common.Location - Program *ast.Program - Checker *sema.Checker - Code []byte + Location common.Location + Program *ast.Program + Checker *sema.Checker + Code []byte + LoadError error } // Run runs the given DAG of analyzers in parallel diff --git a/tools/analysis/programs.go b/tools/analysis/programs.go index 2aaa20e13a..767dea6465 100644 --- a/tools/analysis/programs.go +++ b/tools/analysis/programs.go @@ -50,11 +50,11 @@ func (programs Programs) load( importRange ast.Range, seenImports importResolutionResults, ) error { - if programs[location] != nil { return nil } + var loadError error wrapError := func(err error) ParsingCheckingError { return ParsingCheckingError{ error: err, @@ -69,22 +69,47 @@ func (programs Programs) load( program, err := parser.ParseProgram(nil, code, parser.Config{}) if err != nil { - return wrapError(err) + wrappedErr := wrapError(err) + loadError = wrappedErr + + // If a custom error handler is set, use it to potentially handle the error + if config.HandleParserError != nil { + err = config.HandleParserError(wrappedErr, program) + if err != nil { + return err + } + } else { + return wrappedErr + } } var checker *sema.Checker if config.Mode&NeedTypes != 0 { checker, err = programs.check(config, program, location, seenImports) if err != nil { - return wrapError(err) + wrappedErr := wrapError(err) + if loadError == nil { + loadError = wrappedErr + } + + // If a custom error handler is set, use it to potentially handle the error + if config.HandleCheckerError != nil { + err = config.HandleCheckerError(wrappedErr, checker) + if err != nil { + return err + } + } else { + return wrappedErr + } } } programs[location] = &Program{ - Location: location, - Code: code, - Program: program, - Checker: checker, + Location: location, + Code: code, + Program: program, + Checker: checker, + LoadError: loadError, } return nil @@ -125,6 +150,8 @@ func (programs Programs) check( ) (sema.Import, error) { var elaboration *sema.Elaboration + var loadError error + switch importedLocation { case stdlib.CryptoCheckerLocation: cryptoChecker := stdlib.CryptoChecker() @@ -145,7 +172,20 @@ func (programs Programs) check( return nil, err } - elaboration = programs[importedLocation].Checker.Elaboration + program := programs[importedLocation] + checker := program.Checker + + // If the imported program has a checker, use its elaboration for the import + if checker != nil { + elaboration = checker.Elaboration + } + + // If the imported program had an error while loading, record it + loadError = program.LoadError + } + + if loadError != nil { + return nil, loadError } return sema.ElaborationImport{ @@ -160,7 +200,7 @@ func (programs Programs) check( err = checker.Check() if err != nil { - return nil, err + return checker, err } return checker, nil