Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions internal/cadence/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
21 changes: 10 additions & 11 deletions internal/cadence/linter.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ package cadence
import (
"errors"
"fmt"
"path/filepath"
"strings"

"github.com/onflow/flow-cli/internal/util"
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down
12 changes: 2 additions & 10 deletions internal/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
34 changes: 34 additions & 0 deletions internal/util/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"fmt"
"path/filepath"
"strings"

"github.com/onflow/cadence/common"
)

func AddCDCExtension(name string) string {
Expand All @@ -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)
}
Loading