Skip to content

Commit b977a88

Browse files
authored
Fix nested paths in flow cadence lint (#2213)
1 parent b117356 commit b977a88

File tree

4 files changed

+112
-21
lines changed

4 files changed

+112
-21
lines changed

internal/cadence/lint_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,28 @@ func Test_Lint(t *testing.T) {
308308
results,
309309
)
310310
})
311+
312+
t.Run("resolves nested imports when contract imported by name", func(t *testing.T) {
313+
t.Parallel()
314+
315+
state := setupMockState(t)
316+
317+
results, err := lintFiles(state, "TransactionImportingContractWithNestedImports.cdc")
318+
require.NoError(t, err)
319+
320+
require.Equal(t,
321+
&lintResult{
322+
Results: []fileResult{
323+
{
324+
FilePath: "TransactionImportingContractWithNestedImports.cdc",
325+
Diagnostics: []analysis.Diagnostic{},
326+
},
327+
},
328+
exitCode: 0,
329+
},
330+
results,
331+
)
332+
})
311333
}
312334

313335
func setupMockState(t *testing.T) *flowkit.State {
@@ -380,6 +402,42 @@ func setupMockState(t *testing.T) *flowkit.State {
380402
log(RLP.getType())
381403
}`), 0644)
382404

405+
// Regression test files for nested import bug
406+
_ = afero.WriteFile(mockFs, "Helper.cdc", []byte(`
407+
access(all) contract Helper {
408+
access(all) let name: String
409+
410+
init() {
411+
self.name = "Helper"
412+
}
413+
414+
access(all) fun greet(): String {
415+
return "Hello from ".concat(self.name)
416+
}
417+
}
418+
`), 0644)
419+
420+
_ = afero.WriteFile(mockFs, "ContractWithNestedImports.cdc", []byte(`
421+
import Helper from "./Helper.cdc"
422+
423+
access(all) contract ContractWithNestedImports {
424+
access(all) fun test(): String {
425+
return Helper.greet()
426+
}
427+
init() {}
428+
}
429+
`), 0644)
430+
431+
_ = afero.WriteFile(mockFs, "TransactionImportingContractWithNestedImports.cdc", []byte(`
432+
import ContractWithNestedImports from "ContractWithNestedImports"
433+
434+
transaction() {
435+
prepare(signer: auth(Storage) &Account) {
436+
log(ContractWithNestedImports.test())
437+
}
438+
}
439+
`), 0644)
440+
383441
rw := afero.Afero{Fs: mockFs}
384442
state, err := flowkit.Init(rw)
385443
require.NoError(t, err)
@@ -389,6 +447,14 @@ func setupMockState(t *testing.T) *flowkit.State {
389447
Name: "NoError",
390448
Location: "NoError.cdc",
391449
})
450+
state.Contracts().AddOrUpdate(config.Contract{
451+
Name: "Helper",
452+
Location: "Helper.cdc",
453+
})
454+
state.Contracts().AddOrUpdate(config.Contract{
455+
Name: "ContractWithNestedImports",
456+
Location: "ContractWithNestedImports.cdc",
457+
})
392458

393459
return state
394460
}

internal/cadence/linter.go

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

2726
"github.com/onflow/flow-cli/internal/util"
@@ -196,11 +195,18 @@ func (l *linter) handleImport(
196195
Elaboration: helpersChecker.Elaboration,
197196
}, nil
198197
default:
198+
// Normalize relative path imports to absolute paths
199+
if util.IsPathLocation(importedLocation) {
200+
importedLocation = util.NormalizePathLocation(checker.Location, importedLocation)
201+
}
202+
199203
filepath, err := l.resolveImportFilepath(importedLocation, checker.Location)
200204
if err != nil {
201205
return nil, err
202206
}
203207

208+
fileLocation := common.StringLocation(filepath)
209+
204210
importedChecker, ok := l.checkers[filepath]
205211
if !ok {
206212
code, err := l.state.ReadFile(filepath)
@@ -219,7 +225,7 @@ func (l *linter) handleImport(
219225
}
220226
}
221227

222-
importedChecker, err = checker.SubChecker(importedProgram, importedLocation)
228+
importedChecker, err = checker.SubChecker(importedProgram, fileLocation)
223229
if err != nil {
224230
return nil, err
225231
}
@@ -246,7 +252,7 @@ func (l *linter) resolveImportFilepath(
246252
) {
247253
switch location := location.(type) {
248254
case common.StringLocation:
249-
// If the location is not a cadence file try getting the code by identifier
255+
// Resolve by contract name from flowkit config
250256
if !strings.Contains(location.String(), ".cdc") {
251257
contract, err := l.state.Contracts().ByName(location.String())
252258
if err != nil {
@@ -256,14 +262,7 @@ func (l *linter) resolveImportFilepath(
256262
return contract.Location, nil
257263
}
258264

259-
// If the location is a cadence file, resolve relative to the parent location
260-
parentPath := ""
261-
if parentLocation != nil {
262-
parentPath = parentLocation.String()
263-
}
264-
265-
resolvedPath := filepath.Join(filepath.Dir(parentPath), location.String())
266-
return resolvedPath, nil
265+
return location.String(), nil
267266
default:
268267
return "", fmt.Errorf("unsupported location: %T", location)
269268
}

internal/test/test.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ func importResolver(scriptPath string, state *flowkit.State) cdcTests.ImportReso
453453
relativePath := location.String()
454454

455455
if strings.Contains(relativePath, helperScriptSubstr) {
456-
importedScriptFilePath := absolutePath(scriptPath, relativePath)
456+
importedScriptFilePath := util.AbsolutePath(scriptPath, relativePath)
457457
scriptCode, err := state.ReadFile(importedScriptFilePath)
458458
if err != nil {
459459
return "", nil
@@ -482,7 +482,7 @@ func importResolver(scriptPath string, state *flowkit.State) cdcTests.ImportReso
482482

483483
func fileResolver(scriptPath string, state *flowkit.State) cdcTests.FileResolver {
484484
return func(path string) (string, error) {
485-
importFilePath := absolutePath(scriptPath, path)
485+
importFilePath := util.AbsolutePath(scriptPath, path)
486486

487487
content, err := state.ReadFile(importFilePath)
488488
if err != nil {
@@ -493,14 +493,6 @@ func fileResolver(scriptPath string, state *flowkit.State) cdcTests.FileResolver
493493
}
494494
}
495495

496-
func absolutePath(basePath, filePath string) string {
497-
if filepath.IsAbs(filePath) {
498-
return filePath
499-
}
500-
501-
return filepath.Join(filepath.Dir(basePath), filePath)
502-
}
503-
504496
type result struct {
505497
Results map[string]cdcTests.Results
506498
CoverageReport *runtime.CoverageReport

internal/util/files.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"fmt"
2323
"path/filepath"
2424
"strings"
25+
26+
"github.com/onflow/cadence/common"
2527
)
2628

2729
func AddCDCExtension(name string) string {
@@ -34,3 +36,35 @@ func AddCDCExtension(name string) string {
3436
func StripCDCExtension(name string) string {
3537
return strings.TrimSuffix(name, filepath.Ext(name))
3638
}
39+
40+
// AbsolutePath resolves a relative path against a base file path.
41+
// If the relative path is already absolute, it returns it as-is.
42+
// Otherwise, it joins the relative path to the parent directory of the base path.
43+
func AbsolutePath(basePath, relativePath string) string {
44+
if filepath.IsAbs(relativePath) {
45+
return relativePath
46+
}
47+
return filepath.Join(filepath.Dir(basePath), relativePath)
48+
}
49+
50+
// IsPathLocation returns true if the location is a file path (contains .cdc)
51+
func IsPathLocation(location common.Location) bool {
52+
stringLocation, ok := location.(common.StringLocation)
53+
if !ok {
54+
return false
55+
}
56+
return strings.Contains(stringLocation.String(), ".cdc")
57+
}
58+
59+
// NormalizePathLocation normalizes a relative path import against a base location
60+
func NormalizePathLocation(base, relative common.Location) common.Location {
61+
baseString, baseOk := base.(common.StringLocation)
62+
relativeString, relativeOk := relative.(common.StringLocation)
63+
64+
if !baseOk || !relativeOk {
65+
return relative
66+
}
67+
68+
normalizedPath := AbsolutePath(baseString.String(), relativeString.String())
69+
return common.StringLocation(normalizedPath)
70+
}

0 commit comments

Comments
 (0)