From 85ca59d0590cb7ba310094fdd818f0253f0b0c5f Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 18 Dec 2025 13:29:01 -0800 Subject: [PATCH 1/3] Fix panic in cadence lint and add proper account access checking Previously, when the Cadence semantic checker encountered certain code patterns (like optional chaining on reference types), it would panic instead of returning an error, causing the entire lint command to crash. This fix: 1. Adds panic recovery to gracefully handle internal Cadence checker errors 2. Implements proper MemberAccountAccessHandler that checks if contracts are deployed to the same account across networks (similar to PR #531 in cadence-tools for the language server) 3. Uses AccessCheckModeNotSpecifiedUnrestricted to be permissive during linting Now `flow cadence lint` always completes successfully and correctly validates access(account) usage based on deployment configuration. --- internal/cadence/lint.go | 14 +++- internal/cadence/linter.go | 143 ++++++++++++++++++++++++++++++++++--- 2 files changed, 147 insertions(+), 10 deletions(-) diff --git a/internal/cadence/lint.go b/internal/cadence/lint.go index 7aa700348..83b853e06 100644 --- a/internal/cadence/lint.go +++ b/internal/cadence/lint.go @@ -29,6 +29,8 @@ import ( "github.com/logrusorgru/aurora/v4" "github.com/spf13/cobra" + "github.com/onflow/cadence/ast" + "github.com/onflow/cadence/common" "github.com/onflow/cadence/tools/analysis" "github.com/onflow/flowkit/v2" "github.com/onflow/flowkit/v2/output" @@ -118,7 +120,17 @@ func lintFiles( for _, location := range filePaths { diagnostics, err := l.lintFile(location) if err != nil { - return nil, err + // If there's an internal error (like a panic), convert it to a diagnostic + // and continue processing other files + diagnostics = []analysis.Diagnostic{ + { + Location: common.StringLocation(location), + Category: ErrorCategory, + Message: err.Error(), + Range: ast.Range{}, + }, + } + exitCode = 1 } // Sort for consistent output diff --git a/internal/cadence/linter.go b/internal/cadence/linter.go index d3815d156..d42b56054 100644 --- a/internal/cadence/linter.go +++ b/internal/cadence/linter.go @@ -21,6 +21,7 @@ package cadence import ( "errors" "fmt" + "path/filepath" "strings" "github.com/onflow/flow-cli/internal/util" @@ -75,16 +76,21 @@ func newLinter(state *flowkit.State) *linter { func (l *linter) lintFile( filePath string, -) ( - []analysis.Diagnostic, - error, -) { - diagnostics := make([]analysis.Diagnostic, 0) +) (diagnostics []analysis.Diagnostic, err error) { + // Recover from panics in the Cadence checker + defer func() { + if r := recover(); r != nil { + // Convert panic to error instead of crashing + err = fmt.Errorf("internal error: %v", r) + } + }() + + diagnostics = make([]analysis.Diagnostic, 0) location := common.StringLocation(filePath) - code, err := l.state.ReadFile(filePath) - if err != nil { - return nil, err + code, readErr := l.state.ReadFile(filePath) + if readErr != nil { + return nil, readErr } codeStr := string(code) @@ -151,13 +157,132 @@ func (l *linter) lintFile( return diagnostics, nil } +// isContractName returns true if the location string is a contract name (not a file path) +func isContractName(locationString string) bool { + return !strings.HasSuffix(locationString, ".cdc") +} + +// resolveContractName attempts to resolve a location to a contract name +func (l *linter) resolveContractName(location common.StringLocation) string { + locationString := location.String() + + // If it's already a contract name, return it + if isContractName(locationString) { + return locationString + } + + // Otherwise, try to find the contract by file path + if l.state == nil { + return "" + } + + contracts := l.state.Contracts() + if contracts == nil { + return "" + } + + // Normalize the location path + absLocation, err := filepath.Abs(locationString) + if err != nil { + absLocation = locationString + } + + // Search for matching contract + for _, contract := range *contracts { + contractPath := contract.Location + absContractPath, err := filepath.Abs(contractPath) + if err != nil { + absContractPath = contractPath + } + + if absLocation == absContractPath { + return contract.Name + } + } + + return "" +} + +// checkAccountAccess determines if checker and member locations are on the same account +func (l *linter) checkAccountAccess(checker *sema.Checker, memberLocation common.Location) bool { + // If both are AddressLocation, directly compare addresses + if checkerAddr, ok := checker.Location.(common.AddressLocation); ok { + if memberAddr, ok := memberLocation.(common.AddressLocation); ok { + return checkerAddr.Address == memberAddr.Address + } + } + + // For StringLocations, resolve to contract names and check deployments + checkerLocation, ok := checker.Location.(common.StringLocation) + if !ok { + return false + } + + memberStringLocation, ok := memberLocation.(common.StringLocation) + if !ok { + return false + } + + checkerContractName := l.resolveContractName(checkerLocation) + if checkerContractName == "" { + return false + } + + memberContractName := l.resolveContractName(memberStringLocation) + if memberContractName == "" { + return false + } + + if l.state == nil { + return false + } + + // Check across all networks if they're deployed to the same account + networks := l.state.Networks() + if networks == nil { + return false + } + + for _, network := range *networks { + checkerContract, err := l.state.Contracts().ByName(checkerContractName) + if err != nil { + continue + } + + memberContract, err := l.state.Contracts().ByName(memberContractName) + if err != nil { + continue + } + + checkerAddr, err := l.state.ContractAddress(checkerContract, network) + if err != nil || checkerAddr == nil { + continue + } + + memberAddr, err := l.state.ContractAddress(memberContract, network) + if err != nil || memberAddr == nil { + continue + } + + // If they're on the same account for this network, allow access + if *checkerAddr == *memberAddr { + return true + } + } + + return false +} + // Create a new checker config with the given standard library func (l *linter) newCheckerConfig(standardLibrary *util.StandardLibrary) *sema.Config { return &sema.Config{ BaseValueActivationHandler: func(location common.Location) *sema.VariableActivation { return standardLibrary.BaseValueActivation }, - AccessCheckMode: sema.AccessCheckModeStrict, + MemberAccountAccessHandler: func(checker *sema.Checker, memberLocation common.Location) bool { + return l.checkAccountAccess(checker, memberLocation) + }, + AccessCheckMode: sema.AccessCheckModeNotSpecifiedUnrestricted, PositionInfoEnabled: true, // Must be enabled for linters ExtendedElaborationEnabled: true, // Must be enabled for linters ImportHandler: l.handleImport, From aedecbdf373437c050ef85877c13edb472b0e947 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink <17958158+jribbink@users.noreply.github.com> Date: Thu, 18 Dec 2025 22:16:40 -0800 Subject: [PATCH 2/3] Update internal/cadence/linter.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/cadence/linter.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/cadence/linter.go b/internal/cadence/linter.go index d42b56054..a1603e94b 100644 --- a/internal/cadence/linter.go +++ b/internal/cadence/linter.go @@ -243,17 +243,17 @@ func (l *linter) checkAccountAccess(checker *sema.Checker, memberLocation common return false } - for _, network := range *networks { - checkerContract, err := l.state.Contracts().ByName(checkerContractName) - if err != nil { - continue - } + checkerContract, err := l.state.Contracts().ByName(checkerContractName) + if err != nil { + return false + } - memberContract, err := l.state.Contracts().ByName(memberContractName) - if err != nil { - continue - } + memberContract, err := l.state.Contracts().ByName(memberContractName) + if err != nil { + return false + } + for _, network := range *networks { checkerAddr, err := l.state.ContractAddress(checkerContract, network) if err != nil || checkerAddr == nil { continue From 8d9f293b5988aa1db610979f4f06d8414c7cb821 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Fri, 19 Dec 2025 01:35:41 -0800 Subject: [PATCH 3/3] Fix lint --- internal/tools/wallet.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/tools/wallet.go b/internal/tools/wallet.go index d117678a4..59ff91443 100644 --- a/internal/tools/wallet.go +++ b/internal/tools/wallet.go @@ -82,6 +82,8 @@ func wallet( fmt.Printf("%s Starting dev wallet server on port %d\n", output.SuccessEmoji(), walletFlags.Port) fmt.Printf("%s Make sure the emulator is running\n", output.WarningEmoji()) - srv.Start() + if err := srv.Start(); err != nil { + return nil, err + } return nil, nil }