Skip to content

Commit 0fb4723

Browse files
committed
Fix nested paths in flow cadence lint
1 parent da43284 commit 0fb4723

File tree

2 files changed

+116
-10
lines changed

2 files changed

+116
-10
lines changed

internal/cadence/lint_test.go

Lines changed: 67 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,43 @@ 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+
441+
383442
rw := afero.Afero{Fs: mockFs}
384443
state, err := flowkit.Init(rw)
385444
require.NoError(t, err)
@@ -389,6 +448,14 @@ func setupMockState(t *testing.T) *flowkit.State {
389448
Name: "NoError",
390449
Location: "NoError.cdc",
391450
})
451+
state.Contracts().AddOrUpdate(config.Contract{
452+
Name: "Helper",
453+
Location: "Helper.cdc",
454+
})
455+
state.Contracts().AddOrUpdate(config.Contract{
456+
Name: "ContractWithNestedImports",
457+
Location: "ContractWithNestedImports.cdc",
458+
})
392459

393460
return state
394461
}

internal/cadence/linter.go

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,21 @@ func (l *linter) handleImport(
196196
Elaboration: helpersChecker.Elaboration,
197197
}, nil
198198
default:
199+
// Normalize path-based imports relative to the current checker location
200+
// This ensures relative imports are resolved correctly
201+
if l.isPathLocation(importedLocation) {
202+
importedLocation = l.normalizePathLocation(checker.Location, importedLocation)
203+
}
204+
199205
filepath, err := l.resolveImportFilepath(importedLocation, checker.Location)
200206
if err != nil {
201207
return nil, err
202208
}
203209

210+
// Use the filepath as the location for caching and SubChecker
211+
// This ensures nested imports are resolved relative to the actual file location
212+
fileLocation := common.StringLocation(filepath)
213+
204214
importedChecker, ok := l.checkers[filepath]
205215
if !ok {
206216
code, err := l.state.ReadFile(filepath)
@@ -219,7 +229,9 @@ func (l *linter) handleImport(
219229
}
220230
}
221231

222-
importedChecker, err = checker.SubChecker(importedProgram, importedLocation)
232+
// Use the file location for the subchecker
233+
// This ensures nested imports within the imported file are resolved correctly
234+
importedChecker, err = checker.SubChecker(importedProgram, fileLocation)
223235
if err != nil {
224236
return nil, err
225237
}
@@ -237,6 +249,38 @@ func (l *linter) handleImport(
237249
}
238250
}
239251

252+
// isPathLocation returns true if the location is a file path (contains .cdc)
253+
func (l *linter) isPathLocation(location common.Location) bool {
254+
stringLocation, ok := location.(common.StringLocation)
255+
if !ok {
256+
return false
257+
}
258+
return strings.Contains(stringLocation.String(), ".cdc")
259+
}
260+
261+
// normalizePathLocation normalizes a relative path import against a base location
262+
// This matches the behavior of the language server
263+
func (l *linter) normalizePathLocation(base, relative common.Location) common.Location {
264+
baseString, baseOk := base.(common.StringLocation)
265+
relativeString, relativeOk := relative.(common.StringLocation)
266+
267+
if !baseOk || !relativeOk {
268+
return relative
269+
}
270+
271+
basePath := baseString.String()
272+
relativePath := relativeString.String()
273+
274+
// If the relative path is absolute, return it as-is
275+
if filepath.IsAbs(relativePath) {
276+
return relative
277+
}
278+
279+
// Join relative to the parent directory of the base
280+
normalizedPath := filepath.Join(filepath.Dir(basePath), relativePath)
281+
return common.StringLocation(normalizedPath)
282+
}
283+
240284
func (l *linter) resolveImportFilepath(
241285
location common.Location,
242286
parentLocation common.Location,
@@ -246,7 +290,7 @@ func (l *linter) resolveImportFilepath(
246290
) {
247291
switch location := location.(type) {
248292
case common.StringLocation:
249-
// If the location is not a cadence file try getting the code by identifier
293+
// If the location is not a cadence file, try getting the code by identifier
250294
if !strings.Contains(location.String(), ".cdc") {
251295
contract, err := l.state.Contracts().ByName(location.String())
252296
if err != nil {
@@ -256,14 +300,9 @@ func (l *linter) resolveImportFilepath(
256300
return contract.Location, nil
257301
}
258302

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
303+
// If the location is a cadence file, it should already be normalized
304+
// by this point, so just return it
305+
return location.String(), nil
267306
default:
268307
return "", fmt.Errorf("unsupported location: %T", location)
269308
}

0 commit comments

Comments
 (0)