@@ -42,10 +42,12 @@ import (
4242)
4343
4444type linter struct {
45- checkers map [string ]* sema.Checker
46- state * flowkit.State
47- checkerStandardConfig * sema.Config
48- checkerScriptConfig * sema.Config
45+ checkers map [string ]* sema.Checker
46+ exportedIdentifiers map [string ][]ast.Identifier
47+ state * flowkit.State
48+ checkerStandardConfig * sema.Config
49+ checkerScriptConfig * sema.Config
50+ currentLocation common.Location
4951}
5052
5153type positionedError interface {
@@ -64,8 +66,9 @@ var analyzers = maps.Values(cdclint.Analyzers)
6466
6567func newLinter (state * flowkit.State ) * linter {
6668 l := & linter {
67- checkers : make (map [string ]* sema.Checker ),
68- state : state ,
69+ checkers : make (map [string ]* sema.Checker ),
70+ exportedIdentifiers : make (map [string ][]ast.Identifier ),
71+ state : state ,
6972 }
7073
7174 // Create checker configs for both standard and script
@@ -102,6 +105,8 @@ func (l *linter) lintCode(
102105 diagnostics = make ([]analysis.Diagnostic , 0 )
103106 codeStr := string (code )
104107
108+ l .currentLocation = location
109+
105110 // Parse program & convert any parsing errors to diagnostics
106111 program , parseProgramErr := parser .ParseProgram (nil , code , parser.Config {})
107112 if parseProgramErr != nil {
@@ -332,6 +337,7 @@ func (l *linter) newCheckerConfig(standardLibrary *util.StandardLibrary) *sema.C
332337 PositionInfoEnabled : true , // Must be enabled for linters
333338 ExtendedElaborationEnabled : true , // Must be enabled for linters
334339 ImportHandler : l .handleImport ,
340+ LocationHandler : l .resolveLocation ,
335341 SuggestionsEnabled : true , // Must be enabled to offer semantic suggestions
336342 }
337343}
@@ -437,7 +443,13 @@ func (l *linter) handleImport(
437443 }
438444
439445 l .checkers [filepath ] = importedChecker
446+ // Pre-populate so resolveLocation doesn't re-parse during sub-checking
447+ l .exportedIdentifiers [filepath ] = exportedIdentifiersFromProgram (importedProgram )
448+
449+ prevLocation := l .currentLocation
450+ l .currentLocation = fileLocation
440451 err = importedChecker .Check ()
452+ l .currentLocation = prevLocation
441453 if err != nil {
442454 return nil , err
443455 }
@@ -474,6 +486,82 @@ func (l *linter) resolveImportFilepath(
474486 }
475487}
476488
489+ // resolveLocation is the LocationHandler for the sema.Checker config.
490+ // For implicit imports (no explicit identifiers), it resolves the exported names
491+ // from the imported file so the unused-import analyzer can track usage.
492+ func (l * linter ) resolveLocation (
493+ identifiers []ast.Identifier ,
494+ location common.Location ,
495+ ) ([]sema.ResolvedLocation , error ) {
496+ defaultResolution := []sema.ResolvedLocation {{
497+ Location : location ,
498+ Identifiers : identifiers ,
499+ }}
500+
501+ // Explicit imports already carry their identifiers — nothing to add
502+ if len (identifiers ) > 0 {
503+ return defaultResolution , nil
504+ }
505+
506+ // Only handle string locations (path-based or contract-name imports)
507+ if _ , ok := location .(common.StringLocation ); ! ok {
508+ return defaultResolution , nil
509+ }
510+
511+ // Normalize relative path imports against the current file's location,
512+ // then resolve to a file path (handles both .cdc paths and contract names)
513+ resolvedLoc := location
514+ if l .currentLocation != nil && util .IsPathLocation (location ) {
515+ resolvedLoc = util .NormalizePathLocation (l .currentLocation , location )
516+ }
517+
518+ filePath , err := l .resolveImportFilepath (resolvedLoc , l .currentLocation )
519+ if err != nil {
520+ return defaultResolution , nil
521+ }
522+
523+ exportedIdents := l .getExportedIdentifiers (filePath )
524+ if len (exportedIdents ) == 0 {
525+ return defaultResolution , nil
526+ }
527+
528+ return []sema.ResolvedLocation {{
529+ Location : location ,
530+ Identifiers : exportedIdents ,
531+ }}, nil
532+ }
533+
534+ func (l * linter ) getExportedIdentifiers (filePath string ) []ast.Identifier {
535+ if cached , ok := l .exportedIdentifiers [filePath ]; ok {
536+ return cached
537+ }
538+
539+ code , err := l .state .ReadFile (filePath )
540+ if err != nil {
541+ return nil
542+ }
543+
544+ program , err := parser .ParseProgram (nil , code , parser.Config {})
545+ if err != nil || program == nil {
546+ return nil
547+ }
548+
549+ identifiers := exportedIdentifiersFromProgram (program )
550+ l .exportedIdentifiers [filePath ] = identifiers
551+ return identifiers
552+ }
553+
554+ func exportedIdentifiersFromProgram (program * ast.Program ) []ast.Identifier {
555+ var identifiers []ast.Identifier
556+ for _ , decl := range program .CompositeDeclarations () {
557+ identifiers = append (identifiers , decl .Identifier )
558+ }
559+ for _ , decl := range program .InterfaceDeclarations () {
560+ identifiers = append (identifiers , decl .Identifier )
561+ }
562+ return identifiers
563+ }
564+
477565// helpers
478566
479567func getDiagnosticsFromParentError (err cdcerrors.ParentError , location common.Location , code string ) ([]analysis.Diagnostic , error ) {
0 commit comments