Skip to content

Commit f6405d9

Browse files
committed
Fix nested import paths in flow cadence lint
1 parent 275e592 commit f6405d9

File tree

2 files changed

+107
-10
lines changed

2 files changed

+107
-10
lines changed

internal/cadence/lint_test.go

Lines changed: 66 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,42 @@ 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+
383441
rw := afero.Afero{Fs: mockFs}
384442
state, err := flowkit.Init(rw)
385443
require.NoError(t, err)
@@ -389,6 +447,14 @@ func setupMockState(t *testing.T) *flowkit.State {
389447
Name: "NoError",
390448
Location: "NoError.cdc",
391449
})
450+
state.Contracts().AddOrUpdate(config.Contract{
451+
Name: "Helper",
452+
Location: "Helper.cdc",
453+
})
454+
state.Contracts().AddOrUpdate(config.Contract{
455+
Name: "ContractWithNestedImports",
456+
Location: "ContractWithNestedImports.cdc",
457+
})
392458

393459
return state
394460
}

internal/cadence/linter.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,18 @@ func (l *linter) handleImport(
196196
Elaboration: helpersChecker.Elaboration,
197197
}, nil
198198
default:
199+
// Normalize relative path imports to absolute paths
200+
if l.isPathLocation(importedLocation) {
201+
importedLocation = l.normalizePathLocation(checker.Location, importedLocation)
202+
}
203+
199204
filepath, err := l.resolveImportFilepath(importedLocation, checker.Location)
200205
if err != nil {
201206
return nil, err
202207
}
203208

209+
fileLocation := common.StringLocation(filepath)
210+
204211
importedChecker, ok := l.checkers[filepath]
205212
if !ok {
206213
code, err := l.state.ReadFile(filepath)
@@ -219,7 +226,7 @@ func (l *linter) handleImport(
219226
}
220227
}
221228

222-
importedChecker, err = checker.SubChecker(importedProgram, importedLocation)
229+
importedChecker, err = checker.SubChecker(importedProgram, fileLocation)
223230
if err != nil {
224231
return nil, err
225232
}
@@ -237,6 +244,37 @@ func (l *linter) handleImport(
237244
}
238245
}
239246

247+
// isPathLocation returns true if the location is a file path (contains .cdc)
248+
func (l *linter) isPathLocation(location common.Location) bool {
249+
stringLocation, ok := location.(common.StringLocation)
250+
if !ok {
251+
return false
252+
}
253+
return strings.Contains(stringLocation.String(), ".cdc")
254+
}
255+
256+
// normalizePathLocation normalizes a relative path import against a base location
257+
func (l *linter) normalizePathLocation(base, relative common.Location) common.Location {
258+
baseString, baseOk := base.(common.StringLocation)
259+
relativeString, relativeOk := relative.(common.StringLocation)
260+
261+
if !baseOk || !relativeOk {
262+
return relative
263+
}
264+
265+
basePath := baseString.String()
266+
relativePath := relativeString.String()
267+
268+
// If the relative path is absolute, return it as-is
269+
if filepath.IsAbs(relativePath) {
270+
return relative
271+
}
272+
273+
// Join relative to the parent directory of the base
274+
normalizedPath := filepath.Join(filepath.Dir(basePath), relativePath)
275+
return common.StringLocation(normalizedPath)
276+
}
277+
240278
func (l *linter) resolveImportFilepath(
241279
location common.Location,
242280
parentLocation common.Location,
@@ -246,7 +284,7 @@ func (l *linter) resolveImportFilepath(
246284
) {
247285
switch location := location.(type) {
248286
case common.StringLocation:
249-
// If the location is not a cadence file try getting the code by identifier
287+
// Resolve by contract name from flowkit config
250288
if !strings.Contains(location.String(), ".cdc") {
251289
contract, err := l.state.Contracts().ByName(location.String())
252290
if err != nil {
@@ -256,14 +294,7 @@ func (l *linter) resolveImportFilepath(
256294
return contract.Location, nil
257295
}
258296

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
297+
return location.String(), nil
267298
default:
268299
return "", fmt.Errorf("unsupported location: %T", location)
269300
}

0 commit comments

Comments
 (0)