Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
}
51 changes: 41 additions & 10 deletions internal/cadence/linter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down
Loading