@@ -36,6 +36,7 @@ func extractFileAST(src []byte, relFile string, crates []crateInfo, moduleDirs m
3636 crateDir : nearestCrateDir (dir , crates ),
3737 crates : crates ,
3838 moduleDirs : moduleDirs ,
39+ fileRefIdx : - 1 ,
3940 }
4041 w .walkSourceFile (root )
4142
@@ -78,6 +79,14 @@ type astWalker struct {
7879 // reallocate its backing array and strand a raw pointer.
7980 ownerStack []int
8081
82+ // fileRefIdx: index into out of the lazily-created file-scope reference
83+ // fact (facts.KindFileRef), or -1 until first used. Catches calls/
84+ // references made in macro content with no enclosing symbol — a
85+ // macro_rules! template body, or an item-level macro invocation standing
86+ // in for a whole function (e.g. `ffi_fn! { fn foo() { ... } }`) — which
87+ // emitEdge would otherwise silently drop for lack of an owner.
88+ fileRefIdx int
89+
8190 // modStack/typeStack hold the enclosing inline-`mod { }` and
8291 // impl/trait-block names, so a nested declaration's canonical name is
8392 // "<dir>.<mod1>.<mod2>...<Type>.<name>" — the same qualification scheme
@@ -93,6 +102,10 @@ type astWalker struct {
93102 // mod/file scope, order-independent. Parallel to modStack.
94103 modFnStack []map [string ]bool
95104
105+ // modSubmoduleStack[len-1]: body-less `mod foo;` names declared directly
106+ // in the enclosing mod/file scope. Parallel to modStack.
107+ modSubmoduleStack []map [string ]bool
108+
96109 // decisions counts cyclomatic decision points in the function/method body
97110 // currently being walked; saved/restored around nested function items.
98111 decisions int
@@ -174,7 +187,9 @@ func (w *astWalker) currentMethods() map[string]bool {
174187
175188func (w * astWalker ) walkSourceFile (root * sitter.Node ) {
176189 w .modFnStack = append (w .modFnStack , collectFnNames (root , w .src ))
190+ w .modSubmoduleStack = append (w .modSubmoduleStack , collectSubmoduleNames (root , w .src ))
177191 w .walkItemsTrackingAttrs (root )
192+ w .modSubmoduleStack = w .modSubmoduleStack [:len (w .modSubmoduleStack )- 1 ]
178193 w .modFnStack = w .modFnStack [:len (w .modFnStack )- 1 ]
179194}
180195
@@ -190,6 +205,18 @@ func (w *astWalker) isKnownFn(name string) bool {
190205 return false
191206}
192207
208+ // isKnownSubmodule checks every enclosing mod/file scope for a body-less
209+ // `mod name;` declaration, so `use name::item;` (no self::/crate:: prefix)
210+ // is recognized as a reference to that sibling file, not an external crate.
211+ func (w * astWalker ) isKnownSubmodule (name string ) bool {
212+ for i := len (w .modSubmoduleStack ) - 1 ; i >= 0 ; i -- {
213+ if w.modSubmoduleStack [i ][name ] {
214+ return true
215+ }
216+ }
217+ return false
218+ }
219+
193220// walkItemsTrackingAttrs iterates parent's children like walkChild, but first
194221// tracks preceding attribute_item siblings so a `#[cfg(test)] mod { ... }` can
195222// be detected and routed into test mode (enterTestMod) instead of being walked
@@ -322,7 +349,9 @@ func (w *astWalker) handleMod(node *sitter.Node) {
322349 }
323350 w .modStack = append (w .modStack , name )
324351 w .modFnStack = append (w .modFnStack , collectFnNames (body , w .src ))
352+ w .modSubmoduleStack = append (w .modSubmoduleStack , collectSubmoduleNames (body , w .src ))
325353 w .walkItemsTrackingAttrs (body )
354+ w .modSubmoduleStack = w .modSubmoduleStack [:len (w .modSubmoduleStack )- 1 ]
326355 w .modFnStack = w .modFnStack [:len (w .modFnStack )- 1 ]
327356 w .modStack = w .modStack [:len (w .modStack )- 1 ]
328357}
@@ -585,7 +614,18 @@ func (w *astWalker) emitDependency(segs []string, line int) {
585614 if len (segs ) == 0 || segs [len (segs )- 1 ] == "" {
586615 return
587616 }
588- target , source := classifyUsePath (segs , w .dir , w .crateDir , w .crates , w .moduleDirs )
617+ var target , source string
618+ switch {
619+ case segs [0 ] != "self" && segs [0 ] != "super" && segs [0 ] != "crate" && w .isKnownSubmodule (segs [0 ]):
620+ // An unprefixed `use foo::bar;` where "foo" is a body-less `mod foo;`
621+ // declared in this same file/mod scope is a reference to that sibling
622+ // file, not an external crate — classifyUsePath can't see that on its
623+ // own since foo.rs shares its parent's directory (no subdirectory to
624+ // find in moduleDirs).
625+ target , source = joinRustPath (w .dir , segs , w .moduleDirs ), "internal"
626+ default :
627+ target , source = classifyUsePath (segs , w .dir , w .crateDir , w .crates , w .moduleDirs )
628+ }
589629 raw := strings .Join (segs , "::" )
590630 w .out = append (w .out , facts.Fact {
591631 Kind : facts .KindDependency ,
@@ -840,7 +880,9 @@ func (w *astWalker) handleCallExpression(node *sitter.Node) {
840880// while walking a #[cfg(test)] module (w.inTestMod) — as a deduplicated
841881// RelCalls reference into testRefRels. KindTestRef carries only RelCalls (per
842882// its doc comment), so a would-be RelInstantiates from a test still just
843- // proves the target is used, not constructed for real.
883+ // proves the target is used, not constructed for real. With no owner and not
884+ // in test mode, it's file-scope macro content (see fileRefIdx) — recorded
885+ // there instead of dropped.
844886func (w * astWalker ) emitEdge (kind , target string ) {
845887 if w .inTestMod {
846888 if w .testRefSeen == nil {
@@ -855,22 +897,40 @@ func (w *astWalker) emitEdge(kind, target string) {
855897 }
856898 owner := w .currentOwner ()
857899 if owner == nil {
900+ idx := w .ensureFileRefFact ()
901+ w .out [idx ].Relations = append (w .out [idx ].Relations , facts.Relation {Kind : kind , Target : target })
858902 return
859903 }
860904 owner .Relations = append (owner .Relations , facts.Relation {Kind : kind , Target : target })
861905}
862906
907+ // ensureFileRefFact returns the index of this file's lazily-created
908+ // file-scope reference fact (facts.KindFileRef), creating it on first use.
909+ func (w * astWalker ) ensureFileRefFact () int {
910+ if w .fileRefIdx < 0 {
911+ w .out = append (w .out , facts.Fact {
912+ Kind : facts .KindFileRef ,
913+ Name : w .relFile ,
914+ File : w .relFile ,
915+ Props : map [string ]any {"language" : "rust" },
916+ })
917+ w .fileRefIdx = len (w .out ) - 1
918+ }
919+ return w .fileRefIdx
920+ }
921+
863922// attrFnRefKeys: field-attribute options whose value names a function —
864- // serde's #[serde(default = "some_fn")] (string) or clap's
865- // #[arg(value_parser = some_fn)] (bare path) — resolved by that macro.
923+ // serde's #[serde(default = "some_fn")] (string), clap's
924+ // #[arg(value_parser = some_fn)] (bare path), or the merge crate's
925+ // #[merge(strategy = mod::path)] (scoped path) — resolved by that macro.
866926var attrFnRefKeys = map [string ]bool {
867927 "default" : true , "skip_serializing_if" : true ,
868928 "serialize_with" : true , "deserialize_with" : true , "with" : true ,
869- "value_parser" : true ,
929+ "value_parser" : true , "strategy" : true ,
870930}
871931
872932// attrFnRefMacros: attribute macro names worth scanning for attrFnRefKeys.
873- var attrFnRefMacros = map [string ]bool {"serde" : true , "arg" : true }
933+ var attrFnRefMacros = map [string ]bool {"serde" : true , "arg" : true , "merge" : true }
874934
875935// scanAttributeFnRefs walks a struct/enum body for #[serde(...)]/#[arg(...)]
876936// attributes referencing a function by name.
@@ -882,6 +942,13 @@ func (w *astWalker) scanAttributeFnRefs(body *sitter.Node) {
882942 walk = func (n * sitter.Node ) {
883943 if n .Kind () == "attribute" {
884944 w .scanAttribute (n )
945+ // Beyond the curated key=value macros above, an attribute can embed
946+ // an ordinary call — thiserror's #[error("{}", helper(x))] — using
947+ // the exact same flattened shape as a macro invocation's token_tree,
948+ // so the same scan applies regardless of which macro this is.
949+ if tree := findChildByKind (n , "token_tree" ); tree != nil {
950+ w .scanTokenTreeCalls (tree )
951+ }
885952 return
886953 }
887954 for i := uint (0 ); i < uint (n .ChildCount ()); i ++ {
@@ -912,15 +979,24 @@ func (w *astWalker) scanAttribute(attr *sitter.Node) {
912979 if content := findChildByKind (v , "string_content" ); content != nil {
913980 name = nodeText (content , w .src )
914981 }
915- case "identifier" , "scoped_identifier" :
982+ case "identifier" :
983+ // A scoped path (mod::path::fn) is flattened into separate
984+ // identifier/"::" siblings inside a macro-like token_tree, not a
985+ // single scoped_identifier node — walk to the last segment.
986+ j := i + 2
987+ for j + 2 < n && tree .Child (j + 1 ).Kind () == "::" && tree .Child (j + 2 ).Kind () == "identifier" {
988+ j += 2
989+ }
990+ name = nodeText (tree .Child (j ), w .src )
991+ case "scoped_identifier" :
916992 name = nodeText (v , w .src )
993+ if idx := strings .LastIndex (name , "::" ); idx >= 0 {
994+ name = name [idx + 2 :]
995+ }
917996 }
918997 if name == "" {
919998 continue
920999 }
921- if idx := strings .LastIndex (name , "::" ); idx >= 0 {
922- name = name [idx + 2 :]
923- }
9241000 w .emitEdge (facts .RelCalls , name )
9251001 }
9261002}
@@ -1059,6 +1135,28 @@ func collectFnNames(body *sitter.Node, src []byte) map[string]bool {
10591135 return names
10601136}
10611137
1138+ // collectSubmoduleNames returns the names declared by body-less `mod foo;`
1139+ // items directly in body — a file-based submodule (foo.rs or foo/mod.rs),
1140+ // as opposed to an inline `mod foo { ... }` block. Used to recognize an
1141+ // unprefixed `use foo::bar;` as a reference to that sibling file rather than
1142+ // an external crate.
1143+ func collectSubmoduleNames (body * sitter.Node , src []byte ) map [string ]bool {
1144+ names := make (map [string ]bool )
1145+ if body == nil {
1146+ return names
1147+ }
1148+ for i := uint (0 ); i < uint (body .ChildCount ()); i ++ {
1149+ c := body .Child (i )
1150+ if c .Kind () != "mod_item" || c .ChildByFieldName ("body" ) != nil {
1151+ continue
1152+ }
1153+ if n := c .ChildByFieldName ("name" ); n != nil {
1154+ names [nodeText (n , src )] = true
1155+ }
1156+ }
1157+ return names
1158+ }
1159+
10621160// simpleTypeName returns a type node's simple (rightmost) identifier, e.g.
10631161// "Wrapper" for "Wrapper<T>" or "MyError" for "&MyError". Used for impl-block
10641162// types/traits, where generics and references must be stripped.
0 commit comments