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..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" @@ -196,11 +195,18 @@ func (l *linter) handleImport( Elaboration: helpersChecker.Elaboration, }, nil default: + // Normalize relative path imports to absolute paths + if util.IsPathLocation(importedLocation) { + importedLocation = util.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 +225,7 @@ func (l *linter) handleImport( } } - importedChecker, err = checker.SubChecker(importedProgram, importedLocation) + importedChecker, err = checker.SubChecker(importedProgram, fileLocation) if err != nil { return nil, err } @@ -246,7 +252,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 +262,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) } 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) +}