src/vector/spans.rs
//! **Item spans — the extent of every item, nested.**
//!
//! The span MODEL: where an item starts and ends (1-based inclusive, docs and
//! attributes included), its nesting depth, and whether it is a leaf. Consumed by
//! the sub-clone detector (to label a match with its enclosing item and to run
//! R6) and by the chunker; the warehouse's missing `end_line` is
//! [`ItemSpan::end_line`].
//!
//! The `syn` walker that computed spans (`item_spans` / `leaf_spans`) was removed
//! with Mímir v2 (PLAN-MIMIR-V2 §7.3b); the rustc-driver dump supplies them
//! (lane 3). Until then every consumer is handed no spans and says so.
/// One item's extent, at any nesting depth.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ItemSpan {
/// `fn`, `struct`, `enum`, `impl`, `mod`, `trait`, `method`, `const`, … —
/// the same vocabulary `symbol_facts.item_kind` uses.
pub kind: &'static str,
/// The item's own name (`""` for an item that has none, e.g. a bare `use`).
pub name: String,
/// Dotted container path from the file root: `"Ledger"` for a method of
/// `impl Ledger`, `"inner::Ledger"` inside `mod inner`. Empty at file level.
pub path: String,
/// First source line, 1-based inclusive (includes docs + attributes).
pub start_line: usize,
/// Last source line, 1-based inclusive. **This is the `end_line`
/// `symbol_facts` has never had.**
pub end_line: usize,
/// Nesting depth: 0 for a top-level item, 1 for a method of a top-level
/// `impl`, and so on.
pub depth: usize,
/// No further items were descended into — this is a body, not a container.
pub is_leaf: bool,
}
impl ItemSpan {
/// Source lines covered (1-based inclusive range height).
pub fn lines(&self) -> usize {
self.end_line.saturating_sub(self.start_line) + 1
}
/// `path::name` when nested, plain `name` at file level — a reading label.
pub fn qualified(&self) -> String {
if self.path.is_empty() {
self.name.clone()
} else {
format!("{}::{}", self.path, self.name)
}
}
}
/// The innermost item containing `line`, if any — how a sub-item match gets a
/// human-readable home ("inside `Ledger::reconcile`") without that name ever
/// being an identity input.
pub fn enclosing<'a>(spans: &'a [ItemSpan], line: usize) -> Option<&'a ItemSpan> {
spans
.iter()
.filter(|s| s.start_line <= line && line <= s.end_line)
.max_by_key(|s| (s.depth, std::cmp::Reverse(s.lines())))
}
#[cfg(test)]
mod tests {
use super::*;
fn span(kind: &'static str, name: &str, path: &str, s: usize, e: usize, depth: usize) -> ItemSpan {
ItemSpan { kind, name: name.into(), path: path.into(), start_line: s, end_line: e, depth, is_leaf: depth > 0 }
}
#[test]
fn enclosing_picks_the_innermost_item() {
let spans = vec![
span("impl", "Ledger", "", 3, 12, 0),
span("method", "credit", "Ledger", 4, 6, 1),
span("method", "debit", "Ledger", 8, 11, 1),
];
assert_eq!(enclosing(&spans, 5).map(|s| s.qualified()), Some("Ledger::credit".into()));
assert_eq!(enclosing(&spans, 7).map(|s| s.name.clone()), Some("Ledger".into()));
assert!(enclosing(&spans, 20).is_none());
assert_eq!(spans[2].lines(), 4);
}
}