Skip to content

Commit c8ae793

Browse files
authored
Catch unhandled panics in cadence lint and add proper account access checking (#2227)
1 parent c2efa48 commit c8ae793

3 files changed

Lines changed: 150 additions & 11 deletions

File tree

internal/cadence/lint.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import (
2929
"github.com/logrusorgru/aurora/v4"
3030
"github.com/spf13/cobra"
3131

32+
"github.com/onflow/cadence/ast"
33+
"github.com/onflow/cadence/common"
3234
"github.com/onflow/cadence/tools/analysis"
3335
"github.com/onflow/flowkit/v2"
3436
"github.com/onflow/flowkit/v2/output"
@@ -118,7 +120,17 @@ func lintFiles(
118120
for _, location := range filePaths {
119121
diagnostics, err := l.lintFile(location)
120122
if err != nil {
121-
return nil, err
123+
// If there's an internal error (like a panic), convert it to a diagnostic
124+
// and continue processing other files
125+
diagnostics = []analysis.Diagnostic{
126+
{
127+
Location: common.StringLocation(location),
128+
Category: ErrorCategory,
129+
Message: err.Error(),
130+
Range: ast.Range{},
131+
},
132+
}
133+
exitCode = 1
122134
}
123135

124136
// Sort for consistent output

internal/cadence/linter.go

Lines changed: 134 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package cadence
2121
import (
2222
"errors"
2323
"fmt"
24+
"path/filepath"
2425
"strings"
2526

2627
"github.com/onflow/flow-cli/internal/util"
@@ -75,16 +76,21 @@ func newLinter(state *flowkit.State) *linter {
7576

7677
func (l *linter) lintFile(
7778
filePath string,
78-
) (
79-
[]analysis.Diagnostic,
80-
error,
81-
) {
82-
diagnostics := make([]analysis.Diagnostic, 0)
79+
) (diagnostics []analysis.Diagnostic, err error) {
80+
// Recover from panics in the Cadence checker
81+
defer func() {
82+
if r := recover(); r != nil {
83+
// Convert panic to error instead of crashing
84+
err = fmt.Errorf("internal error: %v", r)
85+
}
86+
}()
87+
88+
diagnostics = make([]analysis.Diagnostic, 0)
8389
location := common.StringLocation(filePath)
8490

85-
code, err := l.state.ReadFile(filePath)
86-
if err != nil {
87-
return nil, err
91+
code, readErr := l.state.ReadFile(filePath)
92+
if readErr != nil {
93+
return nil, readErr
8894
}
8995
codeStr := string(code)
9096

@@ -151,13 +157,132 @@ func (l *linter) lintFile(
151157
return diagnostics, nil
152158
}
153159

160+
// isContractName returns true if the location string is a contract name (not a file path)
161+
func isContractName(locationString string) bool {
162+
return !strings.HasSuffix(locationString, ".cdc")
163+
}
164+
165+
// resolveContractName attempts to resolve a location to a contract name
166+
func (l *linter) resolveContractName(location common.StringLocation) string {
167+
locationString := location.String()
168+
169+
// If it's already a contract name, return it
170+
if isContractName(locationString) {
171+
return locationString
172+
}
173+
174+
// Otherwise, try to find the contract by file path
175+
if l.state == nil {
176+
return ""
177+
}
178+
179+
contracts := l.state.Contracts()
180+
if contracts == nil {
181+
return ""
182+
}
183+
184+
// Normalize the location path
185+
absLocation, err := filepath.Abs(locationString)
186+
if err != nil {
187+
absLocation = locationString
188+
}
189+
190+
// Search for matching contract
191+
for _, contract := range *contracts {
192+
contractPath := contract.Location
193+
absContractPath, err := filepath.Abs(contractPath)
194+
if err != nil {
195+
absContractPath = contractPath
196+
}
197+
198+
if absLocation == absContractPath {
199+
return contract.Name
200+
}
201+
}
202+
203+
return ""
204+
}
205+
206+
// checkAccountAccess determines if checker and member locations are on the same account
207+
func (l *linter) checkAccountAccess(checker *sema.Checker, memberLocation common.Location) bool {
208+
// If both are AddressLocation, directly compare addresses
209+
if checkerAddr, ok := checker.Location.(common.AddressLocation); ok {
210+
if memberAddr, ok := memberLocation.(common.AddressLocation); ok {
211+
return checkerAddr.Address == memberAddr.Address
212+
}
213+
}
214+
215+
// For StringLocations, resolve to contract names and check deployments
216+
checkerLocation, ok := checker.Location.(common.StringLocation)
217+
if !ok {
218+
return false
219+
}
220+
221+
memberStringLocation, ok := memberLocation.(common.StringLocation)
222+
if !ok {
223+
return false
224+
}
225+
226+
checkerContractName := l.resolveContractName(checkerLocation)
227+
if checkerContractName == "" {
228+
return false
229+
}
230+
231+
memberContractName := l.resolveContractName(memberStringLocation)
232+
if memberContractName == "" {
233+
return false
234+
}
235+
236+
if l.state == nil {
237+
return false
238+
}
239+
240+
// Check across all networks if they're deployed to the same account
241+
networks := l.state.Networks()
242+
if networks == nil {
243+
return false
244+
}
245+
246+
checkerContract, err := l.state.Contracts().ByName(checkerContractName)
247+
if err != nil {
248+
return false
249+
}
250+
251+
memberContract, err := l.state.Contracts().ByName(memberContractName)
252+
if err != nil {
253+
return false
254+
}
255+
256+
for _, network := range *networks {
257+
checkerAddr, err := l.state.ContractAddress(checkerContract, network)
258+
if err != nil || checkerAddr == nil {
259+
continue
260+
}
261+
262+
memberAddr, err := l.state.ContractAddress(memberContract, network)
263+
if err != nil || memberAddr == nil {
264+
continue
265+
}
266+
267+
// If they're on the same account for this network, allow access
268+
if *checkerAddr == *memberAddr {
269+
return true
270+
}
271+
}
272+
273+
return false
274+
}
275+
154276
// Create a new checker config with the given standard library
155277
func (l *linter) newCheckerConfig(standardLibrary *util.StandardLibrary) *sema.Config {
156278
return &sema.Config{
157279
BaseValueActivationHandler: func(location common.Location) *sema.VariableActivation {
158280
return standardLibrary.BaseValueActivation
159281
},
160-
AccessCheckMode: sema.AccessCheckModeStrict,
282+
MemberAccountAccessHandler: func(checker *sema.Checker, memberLocation common.Location) bool {
283+
return l.checkAccountAccess(checker, memberLocation)
284+
},
285+
AccessCheckMode: sema.AccessCheckModeNotSpecifiedUnrestricted,
161286
PositionInfoEnabled: true, // Must be enabled for linters
162287
ExtendedElaborationEnabled: true, // Must be enabled for linters
163288
ImportHandler: l.handleImport,

internal/tools/wallet.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ func wallet(
8282
fmt.Printf("%s Starting dev wallet server on port %d\n", output.SuccessEmoji(), walletFlags.Port)
8383
fmt.Printf("%s Make sure the emulator is running\n", output.WarningEmoji())
8484

85-
srv.Start()
85+
if err := srv.Start(); err != nil {
86+
return nil, err
87+
}
8688
return nil, nil
8789
}

0 commit comments

Comments
 (0)