From f6405d9c3a1a78630441da80d9a03290d4440567 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 8 Dec 2025 19:24:58 -0800 Subject: [PATCH 1/2] Fix nested import paths in `flow cadence lint` --- internal/cadence/lint_test.go | 66 +++++++++++++++++++++++++++++++++++ internal/cadence/linter.go | 51 +++++++++++++++++++++------ 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/internal/cadence/lint_test.go b/internal/cadence/lint_test.go index 234af4507..af12a1b24 100644 --- a/internal/cadence/lint_test.go +++ b/internal/cadence/lint_test.go @@ -308,6 +308,28 @@ func Test_Lint(t *testing.T) { results, ) }) + + t.Run("resolves nested imports when contract imported by name", func(t *testing.T) { + t.Parallel() + + state := setupMockState(t) + + results, err := lintFiles(state, "TransactionImportingContractWithNestedImports.cdc") + require.NoError(t, err) + + require.Equal(t, + &lintResult{ + Results: []fileResult{ + { + FilePath: "TransactionImportingContractWithNestedImports.cdc", + Diagnostics: []analysis.Diagnostic{}, + }, + }, + exitCode: 0, + }, + results, + ) + }) } func setupMockState(t *testing.T) *flowkit.State { @@ -380,6 +402,42 @@ func setupMockState(t *testing.T) *flowkit.State { log(RLP.getType()) }`), 0644) + // Regression test files for nested import bug + _ = afero.WriteFile(mockFs, "Helper.cdc", []byte(` + access(all) contract Helper { + access(all) let name: String + + init() { + self.name = "Helper" + } + + access(all) fun greet(): String { + return "Hello from ".concat(self.name) + } + } + `), 0644) + + _ = afero.WriteFile(mockFs, "ContractWithNestedImports.cdc", []byte(` + import Helper from "./Helper.cdc" + + access(all) contract ContractWithNestedImports { + access(all) fun test(): String { + return Helper.greet() + } + init() {} + } + `), 0644) + + _ = afero.WriteFile(mockFs, "TransactionImportingContractWithNestedImports.cdc", []byte(` + import ContractWithNestedImports from "ContractWithNestedImports" + + transaction() { + prepare(signer: auth(Storage) &Account) { + log(ContractWithNestedImports.test()) + } + } + `), 0644) + rw := afero.Afero{Fs: mockFs} state, err := flowkit.Init(rw) require.NoError(t, err) @@ -389,6 +447,14 @@ func setupMockState(t *testing.T) *flowkit.State { Name: "NoError", Location: "NoError.cdc", }) + state.Contracts().AddOrUpdate(config.Contract{ + Name: "Helper", + Location: "Helper.cdc", + }) + state.Contracts().AddOrUpdate(config.Contract{ + Name: "ContractWithNestedImports", + Location: "ContractWithNestedImports.cdc", + }) return state } diff --git a/internal/cadence/linter.go b/internal/cadence/linter.go index c561a72be..000f073f6 100644 --- a/internal/cadence/linter.go +++ b/internal/cadence/linter.go @@ -196,11 +196,18 @@ func (l *linter) handleImport( Elaboration: helpersChecker.Elaboration, }, nil default: + // Normalize relative path imports to absolute paths + if l.isPathLocation(importedLocation) { + importedLocation = l.normalizePathLocation(checker.Location, importedLocation) + } + filepath, err := l.resolveImportFilepath(importedLocation, checker.Location) if err != nil { return nil, err } + fileLocation := common.StringLocation(filepath) + importedChecker, ok := l.checkers[filepath] if !ok { code, err := l.state.ReadFile(filepath) @@ -219,7 +226,7 @@ func (l *linter) handleImport( } } - importedChecker, err = checker.SubChecker(importedProgram, importedLocation) + importedChecker, err = checker.SubChecker(importedProgram, fileLocation) if err != nil { return nil, err } @@ -237,6 +244,37 @@ func (l *linter) handleImport( } } +// isPathLocation returns true if the location is a file path (contains .cdc) +func (l *linter) isPathLocation(location common.Location) bool { + stringLocation, ok := location.(common.StringLocation) + if !ok { + return false + } + return strings.Contains(stringLocation.String(), ".cdc") +} + +// normalizePathLocation normalizes a relative path import against a base location +func (l *linter) normalizePathLocation(base, relative common.Location) common.Location { + baseString, baseOk := base.(common.StringLocation) + relativeString, relativeOk := relative.(common.StringLocation) + + if !baseOk || !relativeOk { + return relative + } + + basePath := baseString.String() + relativePath := relativeString.String() + + // If the relative path is absolute, return it as-is + if filepath.IsAbs(relativePath) { + return relative + } + + // Join relative to the parent directory of the base + normalizedPath := filepath.Join(filepath.Dir(basePath), relativePath) + return common.StringLocation(normalizedPath) +} + func (l *linter) resolveImportFilepath( location common.Location, parentLocation common.Location, @@ -246,7 +284,7 @@ func (l *linter) resolveImportFilepath( ) { switch location := location.(type) { case common.StringLocation: - // If the location is not a cadence file try getting the code by identifier + // Resolve by contract name from flowkit config if !strings.Contains(location.String(), ".cdc") { contract, err := l.state.Contracts().ByName(location.String()) if err != nil { @@ -256,14 +294,7 @@ func (l *linter) resolveImportFilepath( return contract.Location, nil } - // If the location is a cadence file, resolve relative to the parent location - parentPath := "" - if parentLocation != nil { - parentPath = parentLocation.String() - } - - resolvedPath := filepath.Join(filepath.Dir(parentPath), location.String()) - return resolvedPath, nil + return location.String(), nil default: return "", fmt.Errorf("unsupported location: %T", location) } From a16401ce37379365821fd27d78e6d7a7cbb31f07 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 9 Dec 2025 11:43:15 -0800 Subject: [PATCH 2/2] Consolidate utils --- internal/cadence/linter.go | 36 ++---------------------------------- internal/test/test.go | 12 ++---------- internal/util/files.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 44 deletions(-) diff --git a/internal/cadence/linter.go b/internal/cadence/linter.go index 000f073f6..d3815d156 100644 --- a/internal/cadence/linter.go +++ b/internal/cadence/linter.go @@ -21,7 +21,6 @@ package cadence import ( "errors" "fmt" - "path/filepath" "strings" "github.com/onflow/flow-cli/internal/util" @@ -197,8 +196,8 @@ func (l *linter) handleImport( }, nil default: // Normalize relative path imports to absolute paths - if l.isPathLocation(importedLocation) { - importedLocation = l.normalizePathLocation(checker.Location, importedLocation) + if util.IsPathLocation(importedLocation) { + importedLocation = util.NormalizePathLocation(checker.Location, importedLocation) } filepath, err := l.resolveImportFilepath(importedLocation, checker.Location) @@ -244,37 +243,6 @@ func (l *linter) handleImport( } } -// isPathLocation returns true if the location is a file path (contains .cdc) -func (l *linter) isPathLocation(location common.Location) bool { - stringLocation, ok := location.(common.StringLocation) - if !ok { - return false - } - return strings.Contains(stringLocation.String(), ".cdc") -} - -// normalizePathLocation normalizes a relative path import against a base location -func (l *linter) normalizePathLocation(base, relative common.Location) common.Location { - baseString, baseOk := base.(common.StringLocation) - relativeString, relativeOk := relative.(common.StringLocation) - - if !baseOk || !relativeOk { - return relative - } - - basePath := baseString.String() - relativePath := relativeString.String() - - // If the relative path is absolute, return it as-is - if filepath.IsAbs(relativePath) { - return relative - } - - // Join relative to the parent directory of the base - normalizedPath := filepath.Join(filepath.Dir(basePath), relativePath) - return common.StringLocation(normalizedPath) -} - func (l *linter) resolveImportFilepath( location common.Location, parentLocation common.Location, diff --git a/internal/test/test.go b/internal/test/test.go index 0d84f825e..71ef29b4e 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -453,7 +453,7 @@ func importResolver(scriptPath string, state *flowkit.State) cdcTests.ImportReso relativePath := location.String() if strings.Contains(relativePath, helperScriptSubstr) { - importedScriptFilePath := absolutePath(scriptPath, relativePath) + importedScriptFilePath := util.AbsolutePath(scriptPath, relativePath) scriptCode, err := state.ReadFile(importedScriptFilePath) if err != nil { return "", nil @@ -482,7 +482,7 @@ func importResolver(scriptPath string, state *flowkit.State) cdcTests.ImportReso func fileResolver(scriptPath string, state *flowkit.State) cdcTests.FileResolver { return func(path string) (string, error) { - importFilePath := absolutePath(scriptPath, path) + importFilePath := util.AbsolutePath(scriptPath, path) content, err := state.ReadFile(importFilePath) if err != nil { @@ -493,14 +493,6 @@ func fileResolver(scriptPath string, state *flowkit.State) cdcTests.FileResolver } } -func absolutePath(basePath, filePath string) string { - if filepath.IsAbs(filePath) { - return filePath - } - - return filepath.Join(filepath.Dir(basePath), filePath) -} - type result struct { Results map[string]cdcTests.Results CoverageReport *runtime.CoverageReport diff --git a/internal/util/files.go b/internal/util/files.go index 8c2e72c3b..956f16316 100644 --- a/internal/util/files.go +++ b/internal/util/files.go @@ -22,6 +22,8 @@ import ( "fmt" "path/filepath" "strings" + + "github.com/onflow/cadence/common" ) func AddCDCExtension(name string) string { @@ -34,3 +36,35 @@ func AddCDCExtension(name string) string { func StripCDCExtension(name string) string { return strings.TrimSuffix(name, filepath.Ext(name)) } + +// AbsolutePath resolves a relative path against a base file path. +// If the relative path is already absolute, it returns it as-is. +// Otherwise, it joins the relative path to the parent directory of the base path. +func AbsolutePath(basePath, relativePath string) string { + if filepath.IsAbs(relativePath) { + return relativePath + } + return filepath.Join(filepath.Dir(basePath), relativePath) +} + +// IsPathLocation returns true if the location is a file path (contains .cdc) +func IsPathLocation(location common.Location) bool { + stringLocation, ok := location.(common.StringLocation) + if !ok { + return false + } + return strings.Contains(stringLocation.String(), ".cdc") +} + +// NormalizePathLocation normalizes a relative path import against a base location +func NormalizePathLocation(base, relative common.Location) common.Location { + baseString, baseOk := base.(common.StringLocation) + relativeString, relativeOk := relative.(common.StringLocation) + + if !baseOk || !relativeOk { + return relative + } + + normalizedPath := AbsolutePath(baseString.String(), relativeString.String()) + return common.StringLocation(normalizedPath) +}