Skip to content

Commit

Permalink
Merge pull request #3083 from onflow/jribbink/analysis-error-handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
turbolent authored Feb 14, 2024
2 parents 58cc20f + b714e99 commit ff5f509
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 24 deletions.
200 changes: 189 additions & 11 deletions tools/analysis/analysis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
)
Expand All @@ -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()
Expand All @@ -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,
)
Expand Down Expand Up @@ -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,
)
Expand Down
5 changes: 5 additions & 0 deletions tools/analysis/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(
Expand Down
9 changes: 5 additions & 4 deletions tools/analysis/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 49 additions & 9 deletions tools/analysis/programs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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{
Expand All @@ -160,7 +200,7 @@ func (programs Programs) check(

err = checker.Check()
if err != nil {
return nil, err
return checker, err
}

return checker, nil
Expand Down

0 comments on commit ff5f509

Please sign in to comment.