@@ -45,7 +45,10 @@ fn node_kind_to_symbol_kind(node_kind: &str, in_class: bool) -> SymbolKind {
4545 // `impl`/`impl Trait for` block without a corresponding consumer.
4646 "impl_item" => SymbolKind :: Impl ,
4747 "interface_declaration" => SymbolKind :: Interface ,
48- "type_declaration" => SymbolKind :: Type ,
48+ // Go's `type_declaration` and TS's `type_alias_declaration` are
49+ // distinct grammar node kinds for the same concept (a named type),
50+ // so both map to the one SymbolKind here.
51+ "type_declaration" | "type_alias_declaration" => SymbolKind :: Type ,
4952 // Explicit method nodes are always Method regardless of scope.
5053 "method_declaration" | "method_definition" => SymbolKind :: Method ,
5154 // JS/TS `const foo = () => {}` — treated as a variable holding a function.
@@ -630,6 +633,14 @@ pub struct RawCall {
630633 /// bare-name fan-out candidates whose own signature can't possibly be
631634 /// `Option`/`Result` — see its `MAX_CALLEE_CANDIDATES` fallback.
632635 pub looks_option_or_result_chained : bool ,
636+ /// The immediate module-path segment just before the callee, when the
637+ /// whole callee expression is a lowercase-qualified `::`-path
638+ /// (`crate::telemetry::timed_tool` → `Some("telemetry")`) with no `use`
639+ /// bringing the name into `file_symbols`/`import_map` — see
640+ /// `module_hint_of`. `None` for a `.`-receiver call, a type-qualified
641+ /// `Type::method()` call (already carried via `receiver`), or a bare
642+ /// unqualified name.
643+ pub module_hint : Option < String > ,
633644 pub line : usize ,
634645}
635646
@@ -671,6 +682,37 @@ fn split_receiver_callee(raw: &str) -> Option<(Option<String>, String, bool)> {
671682 }
672683}
673684
685+ /// The module-path segment `split_receiver_callee` discards for a lowercase
686+ /// (non-type-like) `::`-qualified callee, e.g. `crate::telemetry::timed_tool`
687+ /// or `telemetry::timed_tool` → `Some("telemetry")`.
688+ ///
689+ /// Without this, a fully-qualified call to a module-level function with no
690+ /// `use` importing it is textually indistinguishable from a bare unqualified
691+ /// call of the same name — `resolve_tier1` matches purely on bare name, and
692+ /// `rebuild_graph`'s same-file preference (see its doc comment) can then bind
693+ /// to an unrelated same-named symbol that merely happens to share the
694+ /// caller's own file, silently misresolving the edge (in the worst case, to
695+ /// a phantom self-recursive edge on the caller itself) instead of the module
696+ /// actually named in the source. Preserved separately from `receiver` so
697+ /// tier-2 method resolution (which expects a variable/type name, not a
698+ /// module) is unaffected — this is consumed only by `rebuild_graph`'s
699+ /// candidate selection, as a same-strength-as-`same_file` tiebreak that
700+ /// takes priority when present, since an explicit qualifier in the source
701+ /// text is stronger evidence than incidental file collocation.
702+ fn module_hint_of ( raw : & str ) -> Option < String > {
703+ if raw. contains ( '.' ) {
704+ return None ; // dot-form's own receiver already covers this call
705+ }
706+ let idx = raw. rfind ( "::" ) ?;
707+ let ( left, _) = raw. split_at ( idx) ;
708+ let seg = left. rsplit ( "::" ) . next ( ) . and_then ( leading_ident) ?;
709+ if is_type_like ( & seg) {
710+ None // type-qualified — already carried via `receiver`/`receiver_is_type_path`
711+ } else {
712+ Some ( seg)
713+ }
714+ }
715+
674716/// Heuristic: a path segment is "type-like" when it starts with an uppercase
675717/// letter, matching Rust/C#/Java/Kotlin/Swift convention for types/classes
676718/// (vs. snake_case modules or lowerCamelCase namespaces/packages). Not
@@ -795,6 +837,7 @@ fn walk_calls(
795837 callee,
796838 receiver,
797839 receiver_is_type_path,
840+ module_hint : module_hint_of ( & source[ fn_node. byte_range ( ) ] ) ,
798841 looks_option_or_result_chained : looks_option_or_result_chained ( node, source) ,
799842 line : node. start_position ( ) . row + 1 ,
800843 } ) ;
@@ -1686,6 +1729,31 @@ function standalone() {}
16861729 assert_eq ! ( find( & symbols, "standalone" ) . kind, SymbolKind :: Function ) ;
16871730 }
16881731
1732+ /// Regression: a TS/DTO-only file (all `export interface`/`export type`,
1733+ /// no functions/classes) used to extract 0 symbols — `interface_declaration`
1734+ /// and `type_alias_declaration` were missing from `function_node_types`,
1735+ /// making such a file entirely invisible to `file_overview`/`search`.
1736+ #[ test]
1737+ fn test_typescript_interface_and_type_alias_extracted ( ) {
1738+ let code = r#"
1739+ export interface FooRequest {
1740+ id: string;
1741+ count: number;
1742+ }
1743+
1744+ export type BarResponse = {
1745+ ok: boolean;
1746+ };
1747+
1748+ export type Baz = string | number;
1749+ "# ;
1750+ let symbols = extract_symbols ( code, "typescript" , "mcp_types.ts" ) . unwrap ( ) ;
1751+ assert_eq ! ( symbols. len( ) , 3 , "all three type-level declarations must be extracted" ) ;
1752+ assert_eq ! ( find( & symbols, "FooRequest" ) . kind, SymbolKind :: Interface ) ;
1753+ assert_eq ! ( find( & symbols, "BarResponse" ) . kind, SymbolKind :: Type ) ;
1754+ assert_eq ! ( find( & symbols, "Baz" ) . kind, SymbolKind :: Type ) ;
1755+ }
1756+
16891757 #[ test]
16901758 fn test_python_entry_point_decorator ( ) {
16911759 let code = r#"
0 commit comments