Walking a type to find every class name
The analyser wants to extract every class-like name a type references — for refactoring, for symbol tracking, for diagnostics. The recipe uses inspect with a closure that collects into a RefCell.
The recipe
use std::cell::RefCell;
use suffete::{TypeId, ElementKind, inspect};
use suffete::interner::interner;
use mago_atom::Atom;
fn collect_class_names(ty: TypeId) -> Vec<Atom> {
let names = RefCell::new(Vec::new());
inspect::any(ty, |elem| {
let i = interner();
match elem.kind() {
ElementKind::Object => {
names.borrow_mut().push(i.get_object(elem).name);
}
ElementKind::Enum => {
names.borrow_mut().push(i.get_enum(elem).name);
}
ElementKind::ClassLikeString => {
use suffete::element::payload::ClassLikeStringSpecifier;
let info = i.get_class_like_string(elem);
if let ClassLikeStringSpecifier::Literal(name) = info.specifier {
names.borrow_mut().push(name);
}
}
_ => {}
}
false // don't short-circuit; visit every Element
});
names.into_inner()
}
Returning false from the predicate keeps the walker going. If you want to short-circuit on the first hit (e.g. "does this type reference any class?"), return true after a match.
How inspect::any enumerates
The walker is post-order and recurses into every nested-type carrier (inspect chapter has the full list). For each Element:
- Recurse into nested types (object args, list element, callable params, etc.).
- Call the closure on this Element.
- If the closure returns
true, short-circuit; otherwise continue.
A type like Box<Map<string, Foo|Bar>> | null produces this enumeration order (post-order):
string, Foo, Bar, Map, Box, null
The recipe above pushes only the class-like Elements (Foo, Bar, Map, Box), giving [Foo, Bar, Map, Box].
Worked example
use suffete::{TypeBuilder, ElementId};
use mago_atom::atom;
let inner = TypeBuilder::new()
.push(ElementId::named_object(atom("Foo")))
.push(ElementId::named_object(atom("Bar")))
.build();
let map_t = TypeBuilder::new().push(
ElementId::named_object_with_args(atom("Map"), &[suffete::prelude::TYPE_STRING, inner])
).build();
let outer = TypeBuilder::new().push(
ElementId::named_object_with_args(atom("Box"), &[map_t])
).push(suffete::prelude::NULL).build();
let names = collect_class_names(outer);
// names == [Foo, Bar, Map, Box] (in some order)
Deduplicating
The walker visits the same ElementId once per occurrence, not per ID. If a class appears multiple times in the tree, the closure runs multiple times. To dedupe:
use std::collections::HashSet;
fn collect_unique_class_names(ty: TypeId) -> HashSet<Atom> {
let names = RefCell::new(HashSet::new());
inspect::any(ty, |elem| {
let i = interner();
match elem.kind() {
ElementKind::Object => { names.borrow_mut().insert(i.get_object(elem).name); }
ElementKind::Enum => { names.borrow_mut().insert(i.get_enum(elem).name); }
_ => {}
}
false
});
names.into_inner()
}
HashSet::insert does the dedup; the closure runs as many times as there are occurrences, but only unique names are stored.
Reach into intersection conjuncts
The walker recurses into Object's intersections automatically. For Foo & Bar, the recipe collects both Foo and Bar. For deeply nested intersections (e.g. Foo & Bar & Baz<Qux>), the recipe collects all four.
Filtering by class_like_kind
If you want only class names (not interfaces, traits, enums), the recipe can filter:
fn collect_class_only<W: World>(ty: TypeId, world: &W) -> Vec<Atom> {
let names = RefCell::new(Vec::new());
inspect::any(ty, |elem| {
if elem.kind() == ElementKind::Object {
let name = interner().get_object(elem).name;
// Filter: only keep classes, exclude interfaces/traits.
if !world.is_interface(name) && !world.is_trait(name) {
names.borrow_mut().push(name);
}
}
false
});
names.into_inner()
}
The world query keeps the recipe codebase-aware.
Performance
inspect::any is O(tree size) when the closure never short-circuits. For typical analyser types (tree size of a few dozen Elements), the walk is sub-microsecond.
The closure cost is the dominant factor. The recipes above do one interner().get_object(elem) per Object Element, which is one &'static arena read per element ; sub-nanosecond.
Allocating the result vector or hashset has its own cost; for hot paths, reuse a pre-allocated buffer.
A subtle case: ClassLikeString specifier
ClassLikeString Elements may carry their class name in the Literal(name) specifier, in the OfType { constraint } (a generic constraint), or in the Generic { constraint } (a ::class lookup on a generic). The recipe above handles only the Literal case.
For OfType and Generic, the constraint is a TypeId ; the walker descends into it automatically and the closure sees any Object Elements inside the constraint.
Variant: collect class names with positions
If the analyser wants to know where in the type each class name appeared (e.g. for diagnostic spans), the closure can track context using a stack in its environment:
let names_with_positions = RefCell::new(Vec::new());
let position = RefCell::new(0u32);
inspect::any(ty, |elem| {
*position.borrow_mut() += 1;
if elem.kind() == ElementKind::Object {
let name = interner().get_object(elem).name;
names_with_positions.borrow_mut().push((name, *position.borrow()));
}
false
});
The position counter increments on every visit, giving each class a stable position in the walk.
See also: Inspection for the walker; Element kinds for the full list of payload-bearing kinds and their nested-type carriers; World for codebase-aware filtering.