Root cause. Two bugs compounded in the rust symbol extractor (hilo-graph, tree-sitter-rust 0.23.3):
Root cause. Two bugs compounded in the rust symbol extractor (hilo-graph, tree-sitter-rust 0.23.3):
struct_item, function_item, …). A use_declaration like pub use crate::glob::{Glob, GlobBuilder, GlobMatcher}; was ignored, so globset's lib.rs contributed nothing to the MAP.Fix. Merge re-export extraction into the single AST-order walk collect_rust_symbols, which now interleaves definitions and re-exports exactly as they appear in source. A re-export is a use_declaration carrying a visibility_modifier child (pub, pub(crate), pub(super), pub(in path)); private use is skipped. Names are extracted from:
use_list items — identifier, scoped_identifier (final segment), use_as_clause (alias is the exported name)identifier / scoped_identifieruse_lists (inside scoped_use_list and inside list items)use_wildcard (*) and the self / super / crate keywords// collect_rust_symbols.js (tree-sitter-rust 0.23.3)
const SYMBOL_MAP_CAP = 8;
const DEF_NODE_TYPES = new Set([
"struct_item", "enum_item", "union_item", "function_item", "trait_item",
"type_item", "const_item", "static_item", "mod_item", "impl_item",
]);
const isVisibilityModifier = (n) => n.type === "visibility_modifier";
/** Exported names from a use payload (everything but its visibility_modifier). */
function extractReexportNames(payload, out) {
switch (payload.type) {
case "use_as_clause": { // `Path as Alias` -> alias
const ids = payload.namedChildren.filter((c) => c.type === "identifier");
if (ids.length) out.push(ids[ids.length - 1].text);
return;
}
case "scoped_identifier": { // `a::b::Final` -> Final
const kids = payload.namedChildren;
const last = kids[kids.length - 1];
if (last && last.type === "identifier") out.push(last.text);
return;
}
case "use_list": // `{A, <ip-address>, D as E, ...}`
for (const item of payload.namedChildren) extractReexportNames(item, out);
return;
case "scoped_use_list": { // `crate::glob::{...}`
const list = payload.namedChildren.find((c) => c.type === "use_list");
if (list) extractReexportNames(list, out);
return;
}
case "identifier":
out.push(payload.text);
return;
case "use_wildcard": case "self": case "super": case "crate":
return; // skip globs & path keywords
}
}
function reexportNames(useDecl) {
const payload = useDecl.namedChildren.find((c) => !isVisibilityModifier(c));
const out = [];
if (payload) extractReexportNames(payload, out);
return out;
}
function definitionName(node) {
if (node.type === "impl_item") return null; // boundary, no symbol
const name = node.namedChildren.find(
(c) => c.type === "type_identifier" || c.type === "identifier"
);
return name ? name.text : null;
}
/** SINGLE AST-order walk: defs and public re-exports interleaved in source order. */
function collectRustSymbols(tree) {
const symbols = [];
walk(tree.rootNode);
return symbols;
function walk(node) {
if (node.type === "use_declaration") {
if (node.namedChildren.some(isVisibilityModifier)) { // pub use = re-export
for (const name of reexportNames(node))
symbols.push({ kind: "reexport", name });
}
return; // a use contributes nothing else
}
if (DEF_NODE_TYPES.has(node.type)) {
const name = definitionName(node);
if (name) symbols.push({ kind: "definition", name });
return; // don't descend into item bodies
}
for (const child of node.namedChildren) walk(child);
}
}
/** hilo-graph MAP: first SYMBOL_MAP_CAP symbols, in collection order. */
function buildSymbolMap(tree) {
return collectRustSymbols(tree).slice(0, SYMBOL_MAP_CAP).map((s) => s.name);
}
The old behavior (what got replaced) appended re-exports in a second walk: defs.concat(reexports) — the exact pattern that truncated them. A definition's name is its type_identifier (struct/enum/union/trait/type) or identifier (fn/const/static/mod); impl_item is a boundary so methods are not counted as top-level symbols.
Verified against tree-sitter-rust 0.23.3 (parsed with tree-sitter 0.22.4) in /tmp/rustreex — a 14-test suite, plus a regression harness that reimplements the old two-pass walk. 14/14 pass.
Real-world reproduction — ripgrep's crates/globset/src/lib.rs (fetched from BurntSushi/ripgrep master, 1100+ lines, ~27 collected symbols):
OLD two-pass MAP : ["fnv","glob","pathutil","serde_impl","Error","ErrorKind","new_regex","new_regex_set"]
NEW single-walk : ["Glob","GlobBuilder","GlobMatcher","fnv","glob","pathutil","serde_impl","Error"]
The old map is exactly the reported symptom: pub use crate::glob::{Glob, GlobBuilder, GlobMatcher} (line 136) "extracted nothing" because the 8-symbol cap was already filled by mod fnv; mod glob; … and fn new_regex. The new map lists Glob / GlobBuilder / GlobMatcher first, in true source order — matching the expected understand MAP output.
Edge cases tested (each with an exact expected name list):
| Input | Result |
|---|---|
pub use crate::glob::{Glob, GlobBuilder, GlobMatcher}; |
[Glob, GlobBuilder, GlobMatcher] |
use std::collections::HashMap; (private) |
[] (not a re-export) |
pub use self::foo::Bar as Baz; |
[Baz] (alias is the exported name) |
pub use std::path::{Path as P, PathBuf}; |
[P, PathBuf] |
pub use crate::{self as c}; |
[c] |
pub use std::collections::*; / pub use wild::{A, *}; |
[] / [A] (use_wildcard skipped) |
pub use super::deep::{self, Thing}; |
[Thing] (self keyword skipped) |
pub use simple::Name; / pub use ::leading::Path::Final; |
[Name] / [Final] (plain-path final segment) |
pub(crate) / pub(super) / pub(in crate::x) uses |
extracted — all have visibility_modifier |
pub use nested::{a::{b, c as d}, e}; |
[b, d, e] (nested use_lists) |
| def/reexport interleave | [A, First, B, C, Second, E, third] — source order preserved |
error-tolerant syntax (pub use module { };, fn body use) |
no crash; garbage skipped, fn main still a def |
Regression proof: on the symbol-rich synthetic file and on real globset, the old two-pass implementation fails the same assertions the fixed one passes — Glob/GlobMatcher are absent from the old MAP, present and first in the new one.
{"problem_class":"rust-reexport-symbol-extraction","model":"deepseek-v4-flash","result":"passed","tests":14}
Artifacts: /tmp/rustreex/collect_rust_symbols.js (fix), /tmp/rustreex/test.js (suite), /tmp/rustreex/globset_lib.rs (real-world fixture).
Solved by Pi Agent (deepseek-v4-flash).
Root cause. Two bugs compounded in the rust symbol extractor (hilo-graph, tree-sitter-rust 0.23.3):
struct_item, function_item, …). A use_declaration like pub use crate::glob::{Glob, GlobBuilder, GlobMatcher}; was ignored, so globset's lib.rs contributed nothing to the MAP.Fix. Merge re-export extraction into the single AST-order walk collect_rust_symbols, which now interleaves definitions and re-exports exactly as they appear in source. A re-export is a use_declaration carrying a visibility_modifier child (pub, pub(crate), pub(super), pub(in path)); private use is skipped. Names are extracted from:
use_list items — identifier, scoped_identifier (final segment), use_as_clause (alias is the exported name)identifier / scoped_identifieruse_lists (inside scoped_use_list and inside list items)use_wildcard (*) and the self / super / crate keywords// collect_rust_symbols.js (tree-sitter-rust 0.23.3)
const SYMBOL_MAP_CAP = 8;
const DEF_NODE_TYPES = new Set([
"struct_item", "enum_item", "union_item", "function_item", "trait_item",
"type_item", "const_item", "static_item", "mod_item", "impl_item",
]);
const isVisibilityModifier = (n) => n.type === "visibility_modifier";
/** Exported names from a use payload (everything but its visibility_modifier). */
function extractReexportNames(payload, out) {
switch (payload.type) {
case "use_as_clause": { // `Path as Alias` -> alias
const ids = payload.namedChildren.filter((c) => c.type === "identifier");
if (ids.length) out.push(ids[ids.length - 1].text);
return;
}
case "scoped_identifier": { // `a::b::Final` -> Final
const kids = payload.namedChildren;
const last = kids[kids.length - 1];
if (last && last.type === "identifier") out.push(last.text);
return;
}
case "use_list": // `{A, <ip-address>, D as E, ...}`
for (const item of payload.namedChildren) extractReexportNames(item, out);
return;
case "scoped_use_list": { // `crate::glob::{...}`
const list = payload.namedChildren.find((c) => c.type === "use_list");
if (list) extractReexportNames(list, out);
return;
}
case "identifier":
out.push(payload.text);
return;
case "use_wildcard": case "self": case "super": case "crate":
return; // skip globs & path keywords
}
}
function reexportNames(useDecl) {
const payload = useDecl.namedChildren.find((c) => !isVisibilityModifier(c));
const out = [];
if (payload) extractReexportNames(payload, out);
return out;
}
function definitionName(node) {
if (node.type === "impl_item") return null; // boundary, no symbol
const name = node.namedChildren.find(
(c) => c.type === "type_identifier" || c.type === "identifier"
);
return name ? name.text : null;
}
/** SINGLE AST-order walk: defs and public re-exports interleaved in source order. */
function collectRustSymbols(tree) {
const symbols = [];
walk(tree.rootNode);
return symbols;
function walk(node) {
if (node.type === "use_declaration") {
if (node.namedChildren.some(isVisibilityModifier)) { // pub use = re-export
for (const name of reexportNames(node))
symbols.push({ kind: "reexport", name });
}
return; // a use contributes nothing else
}
if (DEF_NODE_TYPES.has(node.type)) {
const name = definitionName(node);
if (name) symbols.push({ kind: "definition", name });
return; // don't descend into item bodies
}
for (const child of node.namedChildren) walk(child);
}
}
/** hilo-graph MAP: first SYMBOL_MAP_CAP symbols, in collection order. */
function buildSymbolMap(tree) {
return collectRustSymbols(tree).slice(0, SYMBOL_MAP_CAP).map((s) => s.name);
}
The old behavior (what got replaced) appended re-exports in a second walk: defs.concat(reexports) — the exact pattern that truncated them. A definition's name is its type_identifier (struct/enum/union/trait/type) or identifier (fn/const/static/mod); impl_item is a boundary so methods are not counted as top-level symbols.
Verified against tree-sitter-rust 0.23.3 (parsed with tree-sitter 0.22.4) in /tmp/rustreex — a 14-test suite, plus a regression harness that reimplements the old two-pass walk. 14/14 pass.
Real-world reproduction — ripgrep's crates/globset/src/lib.rs (fetched from BurntSushi/ripgrep master, 1100+ lines, ~27 collected symbols):
OLD two-pass MAP : ["fnv","glob","pathutil","serde_impl","Error","ErrorKind","new_regex","new_regex_set"]
NEW single-walk : ["Glob","GlobBuilder","GlobMatcher","fnv","glob","pathutil","serde_impl","Error"]
The old map is exactly the reported symptom: pub use crate::glob::{Glob, GlobBuilder, GlobMatcher} (line 136) "extracted nothing" because the 8-symbol cap was already filled by mod fnv; mod glob; … and fn new_regex. The new map lists Glob / GlobBuilder / GlobMatcher first, in true source order — matching the expected understand MAP output.
Edge cases tested (each with an exact expected name list):
| Input | Result |
|---|---|
pub use crate::glob::{Glob, GlobBuilder, GlobMatcher}; |
[Glob, GlobBuilder, GlobMatcher] |
use std::collections::HashMap; (private) |
[] (not a re-export) |
pub use self::foo::Bar as Baz; |
[Baz] (alias is the exported name) |
pub use std::path::{Path as P, PathBuf}; |
[P, PathBuf] |
pub use crate::{self as c}; |
[c] |
pub use std::collections::*; / pub use wild::{A, *}; |
[] / [A] (use_wildcard skipped) |
pub use super::deep::{self, Thing}; |
[Thing] (self keyword skipped) |
pub use simple::Name; / pub use ::leading::Path::Final; |
[Name] / [Final] (plain-path final segment) |
pub(crate) / pub(super) / pub(in crate::x) uses |
extracted — all have visibility_modifier |
pub use nested::{a::{b, c as d}, e}; |
[b, d, e] (nested use_lists) |
| def/reexport interleave | [A, First, B, C, Second, E, third] — source order preserved |
error-tolerant syntax (pub use module { };, fn body use) |
no crash; garbage skipped, fn main still a def |
Regression proof: on the symbol-rich synthetic file and on real globset, the old two-pass implementation fails the same assertions the fixed one passes — Glob/GlobMatcher are absent from the old MAP, present and first in the new one.
{"problem_class":"rust-reexport-symbol-extraction","model":"deepseek-v4-flash","result":"passed","tests":14}
Artifacts: /tmp/rustreex/collect_rust_symbols.js (fix), /tmp/rustreex/test.js (suite), /tmp/rustreex/globset_lib.rs (real-world fixture).
Solved by Pi Agent (deepseek-v4-flash).