use ripvec_core::repo_map::{self, CallRef, Definition, FileNode, ImportRef};
use std::path::Path;
fn parse_and_extract(source: &str) -> Vec<Definition> {
use streaming_iterator::StreamingIterator as _;
let lang_config = ripvec_core::languages::config_for_extension("rs").expect("rs lang config");
let call_config =
ripvec_core::languages::call_query_for_extension("rs").expect("rs call config");
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&lang_config.language)
.expect("set rs lang");
let tree = parser.parse(source, None).expect("parse source");
let mut defs = Vec::new();
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&lang_config.query, tree.root_node(), source.as_bytes());
while let Some(m) = matches.next() {
let mut name = String::new();
let mut def_node = None;
for cap in m.captures {
let cap_name = &lang_config.query.capture_names()[cap.index as usize];
if *cap_name == "name" {
name = source[cap.node.start_byte()..cap.node.end_byte()].to_string();
} else if *cap_name == "def" {
def_node = Some(cap.node);
}
}
if let Some(node) = def_node {
#[allow(clippy::cast_possible_truncation)]
defs.push(Definition {
name,
kind: node.kind().to_string(),
start_line: node.start_position().row as u32 + 1,
end_line: node.end_position().row as u32 + 1,
scope: String::new(),
signature: None,
start_byte: node.start_byte() as u32,
end_byte: node.end_byte() as u32,
calls: vec![],
});
}
}
repo_map::extract_calls_pub(source, &call_config, &mut defs);
defs
}
#[test]
fn test_extract_calls_captures_scoped_identifier_path() {
let source = r"
fn caller() {
let x = a::b::foo();
}
";
let defs = parse_and_extract(source);
let caller = defs
.iter()
.find(|d| d.name == "caller")
.expect("caller def");
assert!(
!caller.calls.is_empty(),
"caller should have at least one call"
);
let call = caller
.calls
.iter()
.find(|c| c.name == "foo")
.expect("should find call with bare name 'foo'; got: {:?}");
assert_eq!(
call.qualified_path,
Some("a::b::foo".to_string()),
"qualified_path must be Some(\"a::b::foo\") for scoped call; got {:?}",
call.qualified_path
);
}
#[test]
fn test_resolve_calls_prefers_qualified_match() {
let file_a = FileNode {
path: "moduleA.rs".to_string(),
defs: vec![Definition {
name: "process".to_string(),
kind: "function_item".to_string(),
start_line: 1,
end_line: 3,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 60,
calls: vec![],
}],
imports: vec![],
};
let file_b = FileNode {
path: "moduleB.rs".to_string(),
defs: vec![Definition {
name: "process".to_string(),
kind: "function_item".to_string(),
start_line: 1,
end_line: 3,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 60,
calls: vec![],
}],
imports: vec![],
};
let file_c = FileNode {
path: "caller.rs".to_string(),
defs: vec![Definition {
name: "do_work".to_string(),
kind: "function_item".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 100,
calls: vec![CallRef {
name: "process".to_string(),
qualified_path: Some("moduleA::process".to_string()),
byte_offset: 10,
resolved: None,
receiver_type: None,
}],
}],
imports: vec![],
};
let mut files = vec![file_a, file_b, file_c];
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_pub(&mut files, &def_index);
let resolved = files[2].defs[0].calls[0].resolved;
assert_eq!(
resolved,
Some((0u32, 0u16)),
"qualified call 'moduleA::process' must resolve to (file=0, def=0), got {resolved:?}"
);
}
#[test]
fn test_resolve_calls_falls_back_to_bare_name_when_no_qualified() {
let file_a = FileNode {
path: "utils.rs".to_string(),
defs: vec![Definition {
name: "helper".to_string(),
kind: "function_item".to_string(),
start_line: 1,
end_line: 3,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 60,
calls: vec![],
}],
imports: vec![],
};
let file_b = FileNode {
path: "main.rs".to_string(),
defs: vec![Definition {
name: "run".to_string(),
kind: "function_item".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 100,
calls: vec![CallRef {
name: "helper".to_string(),
qualified_path: None,
byte_offset: 10,
resolved: None,
receiver_type: None,
}],
}],
imports: vec![ripvec_core::repo_map::ImportRef {
raw_path: "use crate::utils::helper;".to_string(),
resolved_idx: Some(0),
}],
};
let mut files = vec![file_a, file_b];
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_pub(&mut files, &def_index);
let resolved = files[1].defs[0].calls[0].resolved;
assert_eq!(
resolved,
Some((0u32, 0u16)),
"bare-name call 'helper' with one imported candidate must resolve; got {resolved:?}"
);
}
#[test]
#[ignore = "runs on full ripvec codebase — run with --ignored after G1 is complete"]
fn def_rank_variance_after_g1() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap();
let graph = repo_map::build_graph(root).expect("build_graph");
let nonzero: Vec<f32> = graph
.def_ranks
.iter()
.copied()
.filter(|&r| r > 1e-9)
.collect();
assert!(!nonzero.is_empty(), "no nonzero def ranks found");
let max_rank = nonzero.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let min_rank = nonzero.iter().copied().fold(f32::INFINITY, f32::min);
let ratio = max_rank / min_rank;
eprintln!("G1 variance: max={max_rank:.8}, min={min_rank:.8}, ratio={ratio:.1}× (need ≥10×)");
assert!(
ratio >= 10.0,
"G1: (max/min nonzero def_rank) = {ratio:.1}×, need ≥10×"
);
}
#[test]
#[ignore = "runs on full ripvec codebase — run with --ignored after G1 is complete"]
fn distinct_def_ranks_after_g1() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap();
let graph = repo_map::build_graph(root).expect("build_graph");
let mut distinct: std::collections::HashSet<u32> = std::collections::HashSet::new();
for &r in &graph.def_ranks {
if r > 1e-9 {
distinct.insert(r.to_bits());
}
}
let count = distinct.len();
eprintln!("G1 distinct nonzero def_ranks: {count} (need ≥50)");
assert!(
count >= 50,
"G1: only {count} distinct nonzero def_rank values, need ≥50"
);
}
#[test]
fn test_extract_calls_captures_self_method_receiver() {
let source = r"
struct Foo;
impl Foo {
fn bar(&self) {
self.baz();
}
}
";
let defs = parse_and_extract(source);
let bar = defs.iter().find(|d| d.name == "bar").expect("bar def");
let baz_call = bar
.calls
.iter()
.find(|c| c.name == "baz")
.expect("baz call in bar");
assert_eq!(
baz_call.receiver_type,
Some("Foo".to_string()),
"self.baz() inside impl Foo must have receiver_type = Some(\"Foo\"); got {:?}",
baz_call.receiver_type
);
}
#[test]
fn test_extract_calls_captures_typed_param_receiver() {
let source = r"
fn f(x: Bar) {
x.method();
}
";
let defs = parse_and_extract(source);
let f = defs.iter().find(|d| d.name == "f").expect("f def");
let method_call = f
.calls
.iter()
.find(|c| c.name == "method")
.expect("method call in f");
assert_eq!(
method_call.receiver_type,
Some("Bar".to_string()),
"x.method() where x: Bar must have receiver_type = Some(\"Bar\"); got {:?}",
method_call.receiver_type
);
}
#[test]
fn test_extract_calls_captures_constructor_init_receiver() {
let source = r"
fn setup() {
let x = Foo::new();
x.bar();
}
";
let defs = parse_and_extract(source);
let setup = defs.iter().find(|d| d.name == "setup").expect("setup def");
let bar_call = setup
.calls
.iter()
.find(|c| c.name == "bar")
.expect("bar call in setup");
assert_eq!(
bar_call.receiver_type,
Some("Foo".to_string()),
"x.bar() after let x = Foo::new() must have receiver_type = Some(\"Foo\"); got {:?}",
bar_call.receiver_type
);
}
#[test]
fn test_resolve_calls_prefers_typed_receiver() {
let file_foo = FileNode {
path: "foo.rs".to_string(),
defs: vec![
Definition {
name: "Foo".to_string(),
kind: "impl_item".to_string(),
start_line: 1,
end_line: 6,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 100,
calls: vec![],
},
Definition {
name: "crunch".to_string(),
kind: "function_item".to_string(),
start_line: 2,
end_line: 5,
scope: "impl_item Foo".to_string(),
signature: None,
start_byte: 10,
end_byte: 90,
calls: vec![],
},
],
imports: vec![],
};
let file_bar = FileNode {
path: "bar.rs".to_string(),
defs: vec![
Definition {
name: "Bar".to_string(),
kind: "impl_item".to_string(),
start_line: 1,
end_line: 6,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 100,
calls: vec![],
},
Definition {
name: "crunch".to_string(),
kind: "function_item".to_string(),
start_line: 2,
end_line: 5,
scope: "impl_item Bar".to_string(),
signature: None,
start_byte: 10,
end_byte: 90,
calls: vec![],
},
],
imports: vec![],
};
let file_c = FileNode {
path: "caller.rs".to_string(),
defs: vec![Definition {
name: "run".to_string(),
kind: "function_item".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 120,
calls: vec![CallRef {
name: "crunch".to_string(),
qualified_path: None,
receiver_type: Some("Foo".to_string()),
byte_offset: 50,
resolved: None,
}],
}],
imports: vec![
ripvec_core::repo_map::ImportRef {
raw_path: "use crate::foo::Foo;".to_string(),
resolved_idx: Some(0),
},
ripvec_core::repo_map::ImportRef {
raw_path: "use crate::bar::Bar;".to_string(),
resolved_idx: Some(1),
},
],
};
let mut files = vec![file_foo, file_bar, file_c];
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_pub(&mut files, &def_index);
let resolved = files[2].defs[0].calls[0].resolved;
assert_eq!(
resolved,
Some((0u32, 1u16)),
"receiver_type=Foo must bind to Foo's crunch (file=0,def=1), got {resolved:?}"
);
}
#[test]
fn test_resolve_calls_falls_back_when_receiver_unknown() {
let file_a = FileNode {
path: "processor.rs".to_string(),
defs: vec![Definition {
name: "process".to_string(),
kind: "function_item".to_string(),
start_line: 1,
end_line: 3,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 60,
calls: vec![],
}],
imports: vec![],
};
let file_b = FileNode {
path: "main.rs".to_string(),
defs: vec![Definition {
name: "main".to_string(),
kind: "function_item".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 100,
calls: vec![CallRef {
name: "process".to_string(),
qualified_path: None,
receiver_type: None, byte_offset: 10,
resolved: None,
}],
}],
imports: vec![ripvec_core::repo_map::ImportRef {
raw_path: "use crate::processor::process;".to_string(),
resolved_idx: Some(0),
}],
};
let mut files = vec![file_a, file_b];
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_pub(&mut files, &def_index);
let resolved = files[1].defs[0].calls[0].resolved;
assert_eq!(
resolved,
Some((0u32, 0u16)),
"unknown receiver with single imported candidate must resolve; got {resolved:?}"
);
}
#[test]
#[ignore = "runs on full ripvec codebase — run with --ignored after G2 is complete"]
fn def_rank_variance_after_g2() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap();
let graph = repo_map::build_graph(root).expect("build_graph");
let nonzero: Vec<f32> = graph
.def_ranks
.iter()
.copied()
.filter(|&r| r > 1e-9)
.collect();
let max_rank = nonzero.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let min_rank = nonzero.iter().copied().fold(f32::INFINITY, f32::min);
let ratio = max_rank / min_rank;
eprintln!("G2 variance: max={max_rank:.8}, min={min_rank:.8}, ratio={ratio:.1}× (need ≥20×)");
assert!(
ratio >= 20.0,
"G2: (max/min nonzero def_rank) = {ratio:.1}×, need ≥20×"
);
}
#[test]
#[ignore = "runs on full ripvec codebase — run with --ignored after G2 is complete"]
fn method_call_resolution_rate() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap();
let graph = repo_map::build_graph(root).expect("build_graph");
let mut method_calls_total = 0usize;
let mut method_calls_resolved = 0usize;
'outer: for file in &graph.files {
for def in &file.defs {
for call in &def.calls {
if call.receiver_type.is_some() {
method_calls_total += 1;
if call.resolved.is_some() {
method_calls_resolved += 1;
}
if method_calls_total >= 20 {
break 'outer;
}
}
}
}
}
let rate = if method_calls_total > 0 {
method_calls_resolved as f64 / method_calls_total as f64
} else {
0.0
};
eprintln!(
"G2 method resolution: {method_calls_resolved}/{method_calls_total} = {:.0}% (need ≥40%)",
rate * 100.0
);
assert!(
method_calls_total > 0,
"no method calls (receiver_type=Some) found in corpus"
);
assert!(
rate >= 0.40,
"G2: method call resolution rate = {:.0}% (need ≥40%)",
rate * 100.0
);
}
#[test]
fn test_build_graph_links_impl_method_to_trait_method() {
let trait_file = FileNode {
path: "foo_trait.rs".to_string(),
defs: vec![
Definition {
name: "Foo".to_string(),
kind: "trait_item".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 80,
calls: vec![],
},
Definition {
name: "bar".to_string(),
kind: "function_signature_item".to_string(),
start_line: 2,
end_line: 4,
scope: "trait_item Foo".to_string(),
signature: None,
start_byte: 10,
end_byte: 70,
calls: vec![],
},
],
imports: vec![],
};
let impl_file = FileNode {
path: "baz_impl.rs".to_string(),
defs: vec![
Definition {
name: "Baz".to_string(),
kind: "impl_item".to_string(),
start_line: 1,
end_line: 6,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 100,
calls: vec![],
},
Definition {
name: "bar".to_string(),
kind: "function_item".to_string(),
start_line: 2,
end_line: 5,
scope: "impl_item Baz".to_string(),
signature: None,
start_byte: 10,
end_byte: 90,
calls: vec![],
},
],
imports: vec![ripvec_core::repo_map::ImportRef {
raw_path: "use crate::foo_trait::Foo;".to_string(),
resolved_idx: Some(0),
}],
};
let files = vec![trait_file, impl_file];
let def_edges = repo_map::build_trait_impl_edges_pub(&files);
let trait_bar: repo_map::DefId = (0, 1);
let impl_bar: repo_map::DefId = (1, 1);
let has_trait_to_impl = def_edges
.iter()
.any(|&(src, dst, _)| src == trait_bar && dst == impl_bar);
let has_impl_to_trait = def_edges
.iter()
.any(|&(src, dst, _)| src == impl_bar && dst == trait_bar);
assert!(
has_trait_to_impl,
"must have edge trait_bar → impl_bar; edges: {def_edges:?}"
);
assert!(
has_impl_to_trait,
"must have edge impl_bar → trait_bar; edges: {def_edges:?}"
);
}
#[test]
#[expect(
clippy::too_many_lines,
reason = "G3 test: full trait/impl/caller fixture must be inline"
)]
fn test_impl_trait_edge_propagates_pagerank() {
use ripvec_core::repo_map::build_graph_from_files_pub;
let trait_file = FileNode {
path: "foo_trait.rs".to_string(),
defs: vec![
Definition {
name: "Foo".to_string(),
kind: "trait_item".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 80,
calls: vec![],
},
Definition {
name: "work".to_string(),
kind: "function_signature_item".to_string(),
start_line: 2,
end_line: 4,
scope: "trait_item Foo".to_string(),
signature: None,
start_byte: 10,
end_byte: 70,
calls: vec![],
},
],
imports: vec![],
};
let impl_file = FileNode {
path: "bar_impl.rs".to_string(),
defs: vec![
Definition {
name: "Bar".to_string(),
kind: "impl_item".to_string(),
start_line: 1,
end_line: 6,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 100,
calls: vec![],
},
Definition {
name: "work".to_string(),
kind: "function_item".to_string(),
start_line: 2,
end_line: 5,
scope: "impl_item Bar".to_string(),
signature: None,
start_byte: 10,
end_byte: 90,
calls: vec![],
},
],
imports: vec![ripvec_core::repo_map::ImportRef {
raw_path: "use crate::foo_trait::Foo;".to_string(),
resolved_idx: Some(0),
}],
};
let callers: Vec<FileNode> = (0..3)
.map(|i| FileNode {
path: format!("caller_{i}.rs"),
defs: vec![Definition {
name: format!("run_{i}"),
kind: "function_item".to_string(),
start_line: 1,
end_line: 3,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 60,
calls: vec![CallRef {
name: "work".to_string(),
qualified_path: Some("Foo::work".to_string()),
receiver_type: None,
byte_offset: 10,
resolved: Some((0u32, 1u16)),
}],
}],
imports: vec![ripvec_core::repo_map::ImportRef {
raw_path: "use crate::foo_trait::Foo;".to_string(),
resolved_idx: Some(0),
}],
})
.collect();
let mut files = vec![trait_file, impl_file];
files.extend(callers);
let graph = build_graph_from_files_pub(files);
let trait_work_rank = graph.def_rank((0u32, 1u16));
let impl_work_rank = graph.def_rank((1u32, 1u16));
eprintln!(
"G3 propagation: trait::work rank = {trait_work_rank:.8}, impl::work rank = {impl_work_rank:.8}"
);
assert!(
impl_work_rank > 1e-7,
"impl::work must have non-trivial rank after G3; got {impl_work_rank}"
);
assert!(
impl_work_rank >= trait_work_rank * 0.1,
"impl::work ({impl_work_rank:.8}) must be ≥ 10% of trait::work ({trait_work_rank:.8})"
);
}
#[test]
fn test_unmatched_impl_method_no_edge() {
let impl_file = FileNode {
path: "widget.rs".to_string(),
defs: vec![
Definition {
name: "Widget".to_string(),
kind: "impl_item".to_string(),
start_line: 1,
end_line: 6,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 100,
calls: vec![],
},
Definition {
name: "render".to_string(),
kind: "function_item".to_string(),
start_line: 2,
end_line: 5,
scope: "impl_item Widget".to_string(),
signature: None,
start_byte: 10,
end_byte: 90,
calls: vec![],
},
],
imports: vec![],
};
let files = vec![impl_file];
let def_edges = repo_map::build_trait_impl_edges_pub(&files);
assert!(
def_edges.is_empty(),
"inherent impl method must not produce trait edges; got: {def_edges:?}"
);
}
#[test]
#[ignore = "runs on full ripvec codebase — run with --ignored after G3 is complete"]
fn def_rank_variance_after_g3() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap();
let graph = repo_map::build_graph(root).expect("build_graph");
let nonzero: Vec<f32> = graph
.def_ranks
.iter()
.copied()
.filter(|&r| r > 1e-9)
.collect();
let max_rank = nonzero.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let min_rank = nonzero.iter().copied().fold(f32::INFINITY, f32::min);
let ratio = max_rank / min_rank;
eprintln!("G3 variance: max={max_rank:.8}, min={min_rank:.8}, ratio={ratio:.1}× (need ≥30×)");
assert!(
ratio >= 30.0,
"G3: (max/min nonzero def_rank) = {ratio:.1}×, need ≥30×"
);
}
#[test]
#[ignore = "runs on full ripvec codebase — run with --ignored after G3 is complete"]
fn trait_dispatch_propagation_rate() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap();
let graph = repo_map::build_graph(root).expect("build_graph");
let mut pairs: Vec<(repo_map::DefId, repo_map::DefId)> = Vec::new();
for (ti, trait_file) in graph.files.iter().enumerate() {
for (tdi, trait_def) in trait_file.defs.iter().enumerate() {
if trait_def.kind != "function_signature_item" {
continue;
}
for (ii, impl_file) in graph.files.iter().enumerate() {
for (idi, impl_def) in impl_file.defs.iter().enumerate() {
if impl_def.kind == "function_item"
&& impl_def.name == trait_def.name
&& impl_def.scope.starts_with("impl_item")
{
#[allow(clippy::cast_possible_truncation)]
pairs.push(((ti as u32, tdi as u16), (ii as u32, idi as u16)));
if pairs.len() >= 10 {
break;
}
}
}
if pairs.len() >= 10 {
break;
}
}
if pairs.len() >= 10 {
break;
}
}
if pairs.len() >= 10 {
break;
}
}
if pairs.is_empty() {
eprintln!("G3: no trait/impl pairs found in corpus — skipping rate check");
return;
}
let mut within_20pct = 0usize;
for &(trait_id, impl_id) in &pairs {
let tr = graph.def_rank(trait_id);
let ir = graph.def_rank(impl_id);
if tr > 0.0 && ir > 0.0 {
let ratio = if tr > ir { ir / tr } else { tr / ir };
if ratio >= 0.20 {
within_20pct += 1;
}
}
}
eprintln!(
"G3 propagation: {within_20pct}/{} pairs within 20% rank of each other (need ≥7)",
pairs.len()
);
assert!(
within_20pct >= 7,
"G3: only {within_20pct}/{} trait/impl pairs have ranks within 20% of each other",
pairs.len()
);
}
fn parse_and_extract_python(source: &str) -> Vec<Definition> {
use streaming_iterator::StreamingIterator as _;
let lang_config = ripvec_core::languages::config_for_extension("py").expect("py lang config");
let call_config =
ripvec_core::languages::call_query_for_extension("py").expect("py call config");
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&lang_config.language)
.expect("set py lang");
let tree = parser.parse(source, None).expect("parse source");
let mut defs = Vec::new();
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&lang_config.query, tree.root_node(), source.as_bytes());
while let Some(m) = matches.next() {
let mut name = String::new();
let mut def_node = None;
for cap in m.captures {
let cap_name = &lang_config.query.capture_names()[cap.index as usize];
if *cap_name == "name" {
name = source[cap.node.start_byte()..cap.node.end_byte()].to_string();
} else if *cap_name == "def" {
def_node = Some(cap.node);
}
}
if let Some(node) = def_node {
#[allow(clippy::cast_possible_truncation)]
defs.push(Definition {
name,
kind: node.kind().to_string(),
start_line: node.start_position().row as u32 + 1,
end_line: node.end_position().row as u32 + 1,
scope: ripvec_core::chunk::build_scope_chain(node, source),
signature: None,
start_byte: node.start_byte() as u32,
end_byte: node.end_byte() as u32,
calls: vec![],
});
}
}
repo_map::extract_calls_pub(source, &call_config, &mut defs);
defs
}
fn parse_and_extract_go(source: &str) -> Vec<Definition> {
use streaming_iterator::StreamingIterator as _;
let lang_config = ripvec_core::languages::config_for_extension("go").expect("go lang config");
let call_config =
ripvec_core::languages::call_query_for_extension("go").expect("go call config");
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&lang_config.language)
.expect("set go lang");
let tree = parser.parse(source, None).expect("parse source");
let mut defs = Vec::new();
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&lang_config.query, tree.root_node(), source.as_bytes());
while let Some(m) = matches.next() {
let mut name = String::new();
let mut def_node = None;
for cap in m.captures {
let cap_name = &lang_config.query.capture_names()[cap.index as usize];
if *cap_name == "name" {
name = source[cap.node.start_byte()..cap.node.end_byte()].to_string();
} else if *cap_name == "def" {
def_node = Some(cap.node);
}
}
if let Some(node) = def_node {
#[allow(clippy::cast_possible_truncation)]
defs.push(Definition {
name,
kind: node.kind().to_string(),
start_line: node.start_position().row as u32 + 1,
end_line: node.end_position().row as u32 + 1,
scope: ripvec_core::chunk::build_scope_chain(node, source),
signature: None,
start_byte: node.start_byte() as u32,
end_byte: node.end_byte() as u32,
calls: vec![],
});
}
}
repo_map::extract_calls_pub(source, &call_config, &mut defs);
defs
}
#[test]
fn python_extract_calls_self_method() {
let source = r"
class Foo:
def bar(self):
self.baz()
";
let defs = parse_and_extract_python(source);
let bar = defs.iter().find(|d| d.name == "bar").expect("bar def");
assert!(
!bar.calls.is_empty(),
"bar must have at least one call; got: {:?}",
bar.calls
);
let baz_call = bar
.calls
.iter()
.find(|c| c.name == "baz")
.expect("baz call in bar");
assert_eq!(
baz_call.receiver_type,
Some("Foo".to_string()),
"self.baz() inside class Foo must have receiver_type=Some(\"Foo\"); got {:?}",
baz_call.receiver_type
);
}
#[test]
fn python_extract_calls_module_function() {
let source = r"
def run():
helper()
";
let defs = parse_and_extract_python(source);
let run = defs.iter().find(|d| d.name == "run").expect("run def");
let helper_call = run
.calls
.iter()
.find(|c| c.name == "helper")
.expect("helper call in run");
assert_eq!(
helper_call.receiver_type, None,
"free call helper() must have receiver_type=None; got {:?}",
helper_call.receiver_type
);
}
#[test]
fn python_extract_calls_class_method() {
let source = r"
def run():
instance = MyClass()
instance.process()
";
let defs = parse_and_extract_python(source);
let run = defs.iter().find(|d| d.name == "run").expect("run def");
let process_call = run
.calls
.iter()
.find(|c| c.name == "process")
.expect("process call in run");
assert_eq!(
process_call.receiver_type,
Some("MyClass".to_string()),
"instance.process() after instance=MyClass() must have receiver_type=Some(\"MyClass\"); got {:?}",
process_call.receiver_type
);
}
#[test]
fn go_extract_calls_package_function() {
let source = r"
func run() {
helper()
}
";
let defs = parse_and_extract_go(source);
let run = defs.iter().find(|d| d.name == "run").expect("run def");
let helper_call = run
.calls
.iter()
.find(|c| c.name == "helper")
.expect("helper call in run");
assert_eq!(
helper_call.receiver_type, None,
"free call helper() must have receiver_type=None; got {:?}",
helper_call.receiver_type
);
}
#[test]
fn go_extract_calls_receiver_method() {
let source = r"
func (r *Foo) Bar() {
r.Baz()
}
";
let defs = parse_and_extract_go(source);
let bar = defs.iter().find(|d| d.name == "Bar").expect("Bar def");
assert!(
!bar.calls.is_empty(),
"Bar must have at least one call; got: {:?}",
bar.calls
);
let baz_call = bar
.calls
.iter()
.find(|c| c.name == "Baz")
.expect("Baz call in Bar");
assert_eq!(
baz_call.receiver_type,
Some("Foo".to_string()),
"r.Baz() inside func (r *Foo) must have receiver_type=Some(\"Foo\"); got {:?}",
baz_call.receiver_type
);
}
#[test]
fn go_extract_calls_interface_method() {
let source = r#"
func writeAll(w Writer, data []byte) {
w.Write(data)
}
"#;
let defs = parse_and_extract_go(source);
let f = defs
.iter()
.find(|d| d.name == "writeAll")
.expect("writeAll def");
let write_call = f
.calls
.iter()
.find(|c| c.name == "Write")
.expect("Write call in writeAll");
assert_eq!(
write_call.receiver_type, None,
"Write call on interface param must have receiver_type=None (parameter annotation not inferred); got {:?}",
write_call.receiver_type
);
}
#[test]
fn python_resolve_self_method_via_class_scope() {
let file_foo = FileNode {
path: "foo.py".to_string(),
defs: vec![
Definition {
name: "Foo".to_string(),
kind: "class_definition".to_string(),
start_line: 1,
end_line: 8,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 120,
calls: vec![],
},
Definition {
name: "do_work".to_string(),
kind: "function_definition".to_string(),
start_line: 3,
end_line: 6,
scope: "class_definition Foo".to_string(),
signature: None,
start_byte: 20,
end_byte: 100,
calls: vec![],
},
],
imports: vec![],
};
let file_caller = FileNode {
path: "caller.py".to_string(),
defs: vec![Definition {
name: "run".to_string(),
kind: "function_definition".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 80,
calls: vec![CallRef {
name: "do_work".to_string(),
qualified_path: None,
receiver_type: Some("Foo".to_string()),
byte_offset: 20,
resolved: None,
}],
}],
imports: vec![ImportRef {
raw_path: "from foo import Foo".to_string(),
resolved_idx: Some(0),
}],
};
let mut files = vec![file_foo, file_caller];
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_pub(&mut files, &def_index);
let resolved = files[1].defs[0].calls[0].resolved;
assert_eq!(
resolved,
Some((0u32, 1u16)),
"receiver_type=Foo must resolve to Foo.do_work (file=0,def=1); got {resolved:?}"
);
}
#[test]
fn go_resolve_receiver_method_via_signature() {
let file_foo = FileNode {
path: "foo.go".to_string(),
defs: vec![Definition {
name: "Process".to_string(),
kind: "method_declaration".to_string(),
start_line: 5,
end_line: 10,
scope: "method_declaration Foo".to_string(),
signature: None,
start_byte: 60,
end_byte: 120,
calls: vec![],
}],
imports: vec![],
};
let file_caller = FileNode {
path: "caller.go".to_string(),
defs: vec![Definition {
name: "Run".to_string(),
kind: "function_declaration".to_string(),
start_line: 1,
end_line: 6,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 80,
calls: vec![CallRef {
name: "Process".to_string(),
qualified_path: None,
receiver_type: Some("Foo".to_string()),
byte_offset: 20,
resolved: None,
}],
}],
imports: vec![ImportRef {
raw_path: "\"pkg/foo\"".to_string(),
resolved_idx: Some(0),
}],
};
let mut files = vec![file_foo, file_caller];
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_pub(&mut files, &def_index);
let resolved = files[1].defs[0].calls[0].resolved;
assert_eq!(
resolved,
Some((0u32, 0u16)),
"receiver_type=Foo must resolve to Foo.Process (file=0,def=0); got {resolved:?}"
);
}
#[test]
#[ignore = "live corpus test — run manually with --ignored"]
fn flask_incoming_calls_non_empty() {
let root = std::path::Path::new("/Users/rwaugh/src/mine/ripvec/tests/corpus/code/flask");
let graph = ripvec_core::repo_map::build_graph(root).expect("build_graph");
let mut total_calls = 0usize;
let mut resolved_calls = 0usize;
let mut add_url_rule_call_count = 0usize;
let mut add_url_rule_resolved_count = 0usize;
for file in &graph.files {
for def in &file.defs {
for call in &def.calls {
total_calls += 1;
if call.resolved.is_some() {
resolved_calls += 1;
}
if call.name == "add_url_rule" {
add_url_rule_call_count += 1;
if call.resolved.is_some() {
add_url_rule_resolved_count += 1;
}
eprintln!(
"add_url_rule call in {}/{}: resolved={:?} recv={:?}",
file.path, def.name, call.resolved, call.receiver_type
);
}
}
}
}
for (fi, file) in graph.files.iter().enumerate() {
for (di, def) in file.defs.iter().enumerate() {
if def.name == "add_url_rule" {
#[allow(clippy::cast_possible_truncation)]
let did = (fi as u32, di as u16);
let rank = graph.def_rank(did);
let flat = graph.def_offsets[fi] + di;
let callers = graph.def_callers.get(flat).cloned().unwrap_or_default();
eprintln!(
"DEF add_url_rule in {} ({},{}) rank={:.8} callers={:?}",
file.path, fi, di, rank, callers
);
}
}
}
eprintln!(
"Total calls: {total_calls}, resolved: {resolved_calls} ({:.1}%)",
100.0 * resolved_calls as f64 / total_calls.max(1) as f64
);
eprintln!(
"add_url_rule: {add_url_rule_call_count} calls, {add_url_rule_resolved_count} resolved"
);
assert!(
add_url_rule_call_count > 0,
"expected calls to add_url_rule in flask corpus; found none"
);
}
fn parse_and_extract_go_with_scope(source: &str) -> Vec<Definition> {
use streaming_iterator::StreamingIterator as _;
let lang_config = ripvec_core::languages::config_for_extension("go").expect("go lang config");
let call_config =
ripvec_core::languages::call_query_for_extension("go").expect("go call config");
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&lang_config.language)
.expect("set go lang");
let tree = parser.parse(source, None).expect("parse source");
let mut defs = Vec::new();
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&lang_config.query, tree.root_node(), source.as_bytes());
while let Some(m) = matches.next() {
let mut name = String::new();
let mut def_node = None;
for cap in m.captures {
let cap_name = &lang_config.query.capture_names()[cap.index as usize];
if *cap_name == "name" {
name = source[cap.node.start_byte()..cap.node.end_byte()].to_string();
} else if *cap_name == "def" {
def_node = Some(cap.node);
}
}
if let Some(node) = def_node {
#[allow(clippy::cast_possible_truncation)]
defs.push(Definition {
name,
kind: node.kind().to_string(),
start_line: node.start_position().row as u32 + 1,
end_line: node.end_position().row as u32 + 1,
scope: ripvec_core::chunk::build_scope_chain(node, source),
signature: None,
start_byte: node.start_byte() as u32,
end_byte: node.end_byte() as u32,
calls: vec![],
});
}
}
repo_map::enrich_go_method_def_scopes_pub(source, &mut defs);
repo_map::extract_calls_pub(source, &call_config, &mut defs);
defs
}
#[test]
fn go_inverse_call_graph_intra_file() {
use ripvec_core::repo_map::build_graph_from_files_pub;
let source = r#"package main
type Foo struct{}
func (f *Foo) Bar() {
f.Baz()
}
func (f *Foo) Baz() {}
"#;
let defs = parse_and_extract_go_with_scope(source);
let bar = defs.iter().find(|d| d.name == "Bar").expect("Bar def");
let baz_call = bar
.calls
.iter()
.find(|c| c.name == "Baz")
.expect("Baz call");
assert_eq!(
baz_call.receiver_type,
Some("Foo".to_string()),
"Baz call in Bar must have receiver_type=Some(\"Foo\"); got {:?}",
baz_call.receiver_type
);
let baz_def = defs.iter().find(|d| d.name == "Baz").expect("Baz def");
assert!(
baz_def.scope.contains("Foo"),
"Baz method_declaration scope must contain receiver type 'Foo'; got {:?}",
baz_def.scope
);
let file = FileNode {
path: "main.go".to_string(),
defs,
imports: vec![],
};
let graph = build_graph_from_files_pub(vec![file]);
let bar_did = graph.find_def("main.go", "Bar").expect("Bar def in graph");
let baz_did = graph.find_def("main.go", "Baz").expect("Baz def in graph");
let has_forward = graph
.def_edges
.iter()
.any(|&(src, dst, _)| src == bar_did && dst == baz_did);
assert!(
has_forward,
"def_edges must contain Bar → Baz; edges: {:?}",
graph.def_edges
);
let baz_flat = graph.def_offsets[baz_did.0 as usize] + baz_did.1 as usize;
let callers = graph.def_callers.get(baz_flat).cloned().unwrap_or_default();
assert!(
callers.contains(&bar_did),
"def_callers[Baz] must include Bar; got: {:?}",
callers
);
}
#[test]
fn go_inverse_call_graph_intra_package() {
use ripvec_core::repo_map::build_graph_from_files_pub;
let file_baz = FileNode {
path: "baz.go".to_string(),
defs: vec![Definition {
name: "Baz".to_string(),
kind: "method_declaration".to_string(),
start_line: 5,
end_line: 10,
scope: "method_declaration Foo".to_string(),
signature: None,
start_byte: 30,
end_byte: 100,
calls: vec![],
}],
imports: vec![],
};
let file_bar = FileNode {
path: "bar.go".to_string(),
defs: vec![Definition {
name: "Bar".to_string(),
kind: "method_declaration".to_string(),
start_line: 5,
end_line: 12,
scope: "method_declaration Foo".to_string(),
signature: None,
start_byte: 30,
end_byte: 120,
calls: vec![CallRef {
name: "Baz".to_string(),
qualified_path: None,
receiver_type: Some("Foo".to_string()),
byte_offset: 60,
resolved: None,
}],
}],
imports: vec![],
};
let graph = build_graph_from_files_pub(vec![file_baz, file_bar]);
let bar_did: repo_map::DefId = (1, 0);
let baz_did: repo_map::DefId = (0, 0);
let has_forward = graph
.def_edges
.iter()
.any(|&(src, dst, _)| src == bar_did && dst == baz_did);
assert!(
has_forward,
"def_edges must contain Bar (file=1,def=0) → Baz (file=0,def=0); edges: {:?}",
graph.def_edges
);
let baz_flat = graph.def_offsets[baz_did.0 as usize] + baz_did.1 as usize;
let callers = graph.def_callers.get(baz_flat).cloned().unwrap_or_default();
assert!(
callers.contains(&bar_did),
"def_callers[Baz] must include Bar (file=1,def=0); got: {:?}",
callers
);
}
#[test]
fn go_lsp_incoming_calls_returns_real_callers() {
use ripvec_core::repo_map::build_graph_from_files_pub;
let file_process = FileNode {
path: "process.go".to_string(),
defs: vec![Definition {
name: "processItem".to_string(),
kind: "method_declaration".to_string(),
start_line: 1,
end_line: 10,
scope: "method_declaration Processor".to_string(),
signature: Some("func (p *Processor) processItem(item Item)".to_string()),
start_byte: 0,
end_byte: 200,
calls: vec![],
}],
imports: vec![],
};
let file_handle = FileNode {
path: "handle.go".to_string(),
defs: vec![Definition {
name: "HandleRequest".to_string(),
kind: "method_declaration".to_string(),
start_line: 1,
end_line: 15,
scope: "method_declaration Processor".to_string(),
signature: Some("func (p *Processor) HandleRequest(req Request)".to_string()),
start_byte: 0,
end_byte: 300,
calls: vec![CallRef {
name: "processItem".to_string(),
qualified_path: None,
receiver_type: Some("Processor".to_string()),
byte_offset: 100,
resolved: None,
}],
}],
imports: vec![],
};
let graph = build_graph_from_files_pub(vec![file_process, file_handle]);
let process_item_did: repo_map::DefId = (0, 0);
let handle_request_did: repo_map::DefId = (1, 0);
let has_forward = graph
.def_edges
.iter()
.any(|&(src, dst, _)| src == handle_request_did && dst == process_item_did);
assert!(
has_forward,
"HandleRequest → processItem edge must be in def_edges; edges: {:?}",
graph.def_edges
);
let process_flat = graph.def_offsets[process_item_did.0 as usize] + process_item_did.1 as usize;
let callers = graph
.def_callers
.get(process_flat)
.cloned()
.unwrap_or_default();
assert!(
!callers.is_empty(),
"lsp_incoming_calls simulation: def_callers[processItem] must be non-empty; got: {:?}",
callers
);
assert!(
callers.contains(&handle_request_did),
"def_callers[processItem] must include HandleRequest; got: {:?}",
callers
);
}
fn python_class_hierarchy_from_source(
source: &str,
) -> std::collections::HashMap<String, Vec<String>> {
ripvec_core::repo_map::extract_python_class_hierarchy(source)
}
#[test]
fn python_mro_self_method_through_parent_class() {
let file_parent = FileNode {
path: "parent.py".to_string(),
defs: vec![
Definition {
name: "A".to_string(),
kind: "class_definition".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: Some("A:".to_string()),
start_byte: 0,
end_byte: 80,
calls: vec![],
},
Definition {
name: "foo".to_string(),
kind: "function_definition".to_string(),
start_line: 2,
end_line: 4,
scope: "class_definition A".to_string(),
signature: None,
start_byte: 20,
end_byte: 60,
calls: vec![],
},
],
imports: vec![],
};
let file_decoy = FileNode {
path: "decoy.py".to_string(),
defs: vec![Definition {
name: "foo".to_string(),
kind: "function_definition".to_string(),
start_line: 1,
end_line: 3,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 40,
calls: vec![],
}],
imports: vec![],
};
let file_child = FileNode {
path: "child.py".to_string(),
defs: vec![
Definition {
name: "B".to_string(),
kind: "class_definition".to_string(),
start_line: 1,
end_line: 6,
scope: String::new(),
signature: Some("B(A):".to_string()),
start_byte: 0,
end_byte: 120,
calls: vec![],
},
Definition {
name: "caller".to_string(),
kind: "function_definition".to_string(),
start_line: 2,
end_line: 5,
scope: "class_definition B".to_string(),
signature: None,
start_byte: 20,
end_byte: 100,
calls: vec![CallRef {
name: "foo".to_string(),
qualified_path: None,
receiver_type: Some("B".to_string()),
byte_offset: 40,
resolved: None,
}],
},
],
imports: vec![
ImportRef {
raw_path: "from parent import A".to_string(),
resolved_idx: Some(0),
},
ImportRef {
raw_path: "from decoy import foo".to_string(),
resolved_idx: Some(1),
},
],
};
let mut files = vec![file_parent, file_decoy, file_child];
let mut hierarchy = std::collections::HashMap::new();
hierarchy.insert("A".to_string(), Vec::<String>::new());
hierarchy.insert("B".to_string(), vec!["A".to_string()]);
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_with_python_mro_pub(&mut files, &def_index, &hierarchy);
let resolved = files[2].defs[1].calls[0].resolved;
assert_eq!(
resolved,
Some((0u32, 1u16)),
"self.foo() in B(A) must resolve to A.foo via MRO walk (file=0,def=1); got {resolved:?}"
);
}
#[test]
#[expect(
clippy::too_many_lines,
reason = "synthetic FileNode fixtures for a four-class MRO setup are inherently \
long; splitting hurts readability of the receiver-type → scope linkage"
)]
fn python_mro_mixin_method_resolution() {
let file_mixin = FileNode {
path: "mixin.py".to_string(),
defs: vec![
Definition {
name: "Mixin".to_string(),
kind: "class_definition".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: Some("Mixin:".to_string()),
start_byte: 0,
end_byte: 80,
calls: vec![],
},
Definition {
name: "helper".to_string(),
kind: "function_definition".to_string(),
start_line: 2,
end_line: 4,
scope: "class_definition Mixin".to_string(),
signature: None,
start_byte: 20,
end_byte: 60,
calls: vec![],
},
],
imports: vec![],
};
let file_base = FileNode {
path: "base.py".to_string(),
defs: vec![Definition {
name: "Base".to_string(),
kind: "class_definition".to_string(),
start_line: 1,
end_line: 3,
scope: String::new(),
signature: Some("Base:".to_string()),
start_byte: 0,
end_byte: 40,
calls: vec![],
}],
imports: vec![],
};
let file_child = FileNode {
path: "child.py".to_string(),
defs: vec![
Definition {
name: "OtherMixin".to_string(),
kind: "class_definition".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: Some("OtherMixin:".to_string()),
start_byte: 0,
end_byte: 80,
calls: vec![],
},
Definition {
name: "helper".to_string(),
kind: "function_definition".to_string(),
start_line: 2,
end_line: 4,
scope: "class_definition OtherMixin".to_string(),
signature: None,
start_byte: 20,
end_byte: 60,
calls: vec![],
},
Definition {
name: "C".to_string(),
kind: "class_definition".to_string(),
start_line: 7,
end_line: 14,
scope: String::new(),
signature: Some("C(Base, Mixin):".to_string()),
start_byte: 100,
end_byte: 220,
calls: vec![],
},
Definition {
name: "caller".to_string(),
kind: "function_definition".to_string(),
start_line: 8,
end_line: 13,
scope: "class_definition C".to_string(),
signature: None,
start_byte: 120,
end_byte: 210,
calls: vec![CallRef {
name: "helper".to_string(),
qualified_path: None,
receiver_type: Some("C".to_string()),
byte_offset: 150,
resolved: None,
}],
},
],
imports: vec![
ImportRef {
raw_path: "from base import Base".to_string(),
resolved_idx: Some(1),
},
ImportRef {
raw_path: "from mixin import Mixin".to_string(),
resolved_idx: Some(0),
},
],
};
let mut files = vec![file_mixin, file_base, file_child];
let mut hierarchy = std::collections::HashMap::new();
hierarchy.insert("Mixin".to_string(), Vec::<String>::new());
hierarchy.insert("Base".to_string(), Vec::<String>::new());
hierarchy.insert("OtherMixin".to_string(), Vec::<String>::new());
hierarchy.insert(
"C".to_string(),
vec!["Base".to_string(), "Mixin".to_string()],
);
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_with_python_mro_pub(&mut files, &def_index, &hierarchy);
let resolved = files[2].defs[3].calls[0].resolved;
assert_eq!(
resolved,
Some((0u32, 1u16)),
"self.helper() in C(Base, Mixin) must resolve to Mixin.helper via MRO walk \
(file=0,def=1) — not to same-file OtherMixin.helper; got {resolved:?}"
);
}
#[test]
#[expect(
clippy::too_many_lines,
reason = "synthetic FileNode fixtures for a three-class MRO setup are inherently \
long; splitting hurts readability of the receiver-type → scope linkage"
)]
fn python_mro_multiple_inheritance_resolves_correct_def() {
let file_a = FileNode {
path: "a.py".to_string(),
defs: vec![
Definition {
name: "A".to_string(),
kind: "class_definition".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: Some("A:".to_string()),
start_byte: 0,
end_byte: 80,
calls: vec![],
},
Definition {
name: "method".to_string(),
kind: "function_definition".to_string(),
start_line: 2,
end_line: 4,
scope: "class_definition A".to_string(),
signature: None,
start_byte: 20,
end_byte: 60,
calls: vec![],
},
],
imports: vec![],
};
let file_b = FileNode {
path: "b.py".to_string(),
defs: vec![
Definition {
name: "B".to_string(),
kind: "class_definition".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: Some("B:".to_string()),
start_byte: 0,
end_byte: 80,
calls: vec![],
},
Definition {
name: "method".to_string(),
kind: "function_definition".to_string(),
start_line: 2,
end_line: 4,
scope: "class_definition B".to_string(),
signature: None,
start_byte: 20,
end_byte: 60,
calls: vec![],
},
],
imports: vec![],
};
let file_d = FileNode {
path: "d.py".to_string(),
defs: vec![
Definition {
name: "D".to_string(),
kind: "class_definition".to_string(),
start_line: 1,
end_line: 6,
scope: String::new(),
signature: Some("D(A, B):".to_string()),
start_byte: 0,
end_byte: 120,
calls: vec![],
},
Definition {
name: "caller".to_string(),
kind: "function_definition".to_string(),
start_line: 2,
end_line: 5,
scope: "class_definition D".to_string(),
signature: None,
start_byte: 20,
end_byte: 100,
calls: vec![CallRef {
name: "method".to_string(),
qualified_path: None,
receiver_type: Some("D".to_string()),
byte_offset: 40,
resolved: None,
}],
},
],
imports: vec![
ImportRef {
raw_path: "from a import A".to_string(),
resolved_idx: Some(0),
},
ImportRef {
raw_path: "from b import B".to_string(),
resolved_idx: Some(1),
},
],
};
let mut files = vec![file_a, file_b, file_d];
let mut hierarchy = std::collections::HashMap::new();
hierarchy.insert("A".to_string(), Vec::<String>::new());
hierarchy.insert("B".to_string(), Vec::<String>::new());
hierarchy.insert("D".to_string(), vec!["A".to_string(), "B".to_string()]);
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_with_python_mro_pub(&mut files, &def_index, &hierarchy);
let resolved = files[2].defs[1].calls[0].resolved;
assert_eq!(
resolved,
Some((0u32, 1u16)),
"self.method() in D(A, B) must resolve to A.method (left-first MRO; file=0,def=1); \
got {resolved:?}"
);
}
#[test]
#[ignore = "live corpus test — run manually with --ignored"]
fn mnemosyne_error_handler_mixin_recall() {
let root = std::path::Path::new("/Users/rwaugh/src/mine/mnemosyne");
let graph = ripvec_core::repo_map::build_graph(root).expect("build_graph mnemosyne");
let mut handle_error_did: Option<(u32, u16)> = None;
let mut handle_error_path = String::new();
for (fi, file) in graph.files.iter().enumerate() {
for (di, def) in file.defs.iter().enumerate() {
if def.name == "handle_error"
&& def.scope.contains("ErrorHandlerMixin")
&& file.path.contains("mixins/error_handling.py")
{
#[allow(clippy::cast_possible_truncation)]
{
handle_error_did = Some((fi as u32, di as u16));
}
handle_error_path = file.path.clone();
break;
}
}
if handle_error_did.is_some() {
break;
}
}
let (fi, di) = handle_error_did
.expect("ErrorHandlerMixin.handle_error must be present in mnemosyne corpus");
eprintln!("Found ErrorHandlerMixin.handle_error at {handle_error_path} ({fi},{di})");
let flat = graph.def_offsets[fi as usize] + di as usize;
let callers = graph.def_callers.get(flat).cloned().unwrap_or_default();
eprintln!(
"def_callers[ErrorHandlerMixin.handle_error] = {} entries (capped at MAX_NEIGHBORS)",
callers.len()
);
let mut total_handle_error_calls = 0usize;
let mut resolved_to_target = 0usize;
let mut distinct_caller_defs: std::collections::HashSet<(u32, u16)> =
std::collections::HashSet::new();
for (fi2, file) in graph.files.iter().enumerate() {
for (di2, def) in file.defs.iter().enumerate() {
for call in &def.calls {
if call.name == "handle_error" {
total_handle_error_calls += 1;
if call.resolved == handle_error_did {
resolved_to_target += 1;
#[allow(clippy::cast_possible_truncation)]
distinct_caller_defs.insert((fi2 as u32, di2 as u16));
}
}
}
}
}
eprintln!(
"Total handle_error call sites: {total_handle_error_calls}; resolved to \
ErrorHandlerMixin.handle_error: {resolved_to_target}; distinct caller defs: {}",
distinct_caller_defs.len()
);
assert!(
resolved_to_target >= 20,
"post-MRO recall regression: expected ≥20 calls resolved to \
ErrorHandlerMixin.handle_error, got {resolved_to_target} of {total_handle_error_calls}"
);
}
#[test]
fn python_class_hierarchy_extraction_basic() {
let source = r"
class A:
pass
class B(A):
pass
class C(B, A):
pass
class D(module.Base, Mixin, metaclass=Meta):
pass
";
let h = python_class_hierarchy_from_source(source);
assert_eq!(h.get("A"), Some(&Vec::<String>::new()), "A has no parents");
assert_eq!(
h.get("B"),
Some(&vec!["A".to_string()]),
"B inherits A; got {:?}",
h.get("B")
);
assert_eq!(
h.get("C"),
Some(&vec!["B".to_string(), "A".to_string()]),
"C inherits B, A in order; got {:?}",
h.get("C")
);
assert_eq!(
h.get("D"),
Some(&vec!["Base".to_string(), "Mixin".to_string()]),
"D inherits Base (from module.Base) and Mixin; metaclass keyword is dropped; got {:?}",
h.get("D")
);
}
#[test]
fn test_hcl_chunk_carries_qualified_name() {
use ripvec_core::chunk::{ChunkConfig, chunk_file};
use ripvec_core::languages::config_for_extension;
let source = r#"resource "aws_iam_role" "loader" {
assume_role_policy = "x"
}
"#;
let cfg = config_for_extension("tf").expect("tf lang config");
let chunks = chunk_file(
std::path::Path::new("iam.tf"),
source,
&cfg,
&ChunkConfig::default(),
);
let loader_chunk = chunks
.iter()
.find(|c| c.name == "loader")
.expect("expected a chunk with bare name 'loader'");
assert_eq!(
loader_chunk.qualified_name.as_deref(),
Some("aws_iam_role.loader"),
"qualified_name must be Some(\"aws_iam_role.loader\"); got {:?}",
loader_chunk.qualified_name
);
}
#[test]
fn test_hcl_locals_each_extracted_as_separate_def() {
use ripvec_core::chunk::{ChunkConfig, chunk_file};
use ripvec_core::languages::config_for_extension;
let source = r#"locals {
data_kms_key_arn = "arn:aws:kms:us-east-1:111111111111:key/abcd1234"
loader_role_arn = "arn:aws:iam::111111111111:role/loader"
another = 42
}
"#;
let cfg = config_for_extension("tf").expect("tf lang config");
let chunks = chunk_file(
std::path::Path::new("locals.tf"),
source,
&cfg,
&ChunkConfig::default(),
);
let names: Vec<&str> = chunks
.iter()
.filter(|c| c.kind == "local_attribute")
.map(|c| c.name.as_str())
.collect();
assert!(
names.contains(&"data_kms_key_arn"),
"expected local 'data_kms_key_arn' as its own chunk; got: {names:?}"
);
assert!(
names.contains(&"loader_role_arn"),
"expected local 'loader_role_arn' as its own chunk; got: {names:?}"
);
assert!(
names.contains(&"another"),
"expected local 'another' as its own chunk; got: {names:?}"
);
let dk = chunks
.iter()
.find(|c| c.kind == "local_attribute" && c.name == "data_kms_key_arn")
.expect("data_kms_key_arn chunk");
assert_eq!(
dk.qualified_name.as_deref(),
Some("local.data_kms_key_arn"),
"qualified_name on a local chunk must be 'local.<name>'"
);
}
#[test]
fn test_hcl_locals_kind_is_constant() {
use ripvec_core::languages::{lsp_symbol_kind, lsp_symbol_kind_for_node_kind};
assert_eq!(
lsp_symbol_kind_for_node_kind("local_attribute"),
lsp_symbol_kind::CONSTANT,
"local_attribute must map to SymbolKind::Constant (14)"
);
}
#[test]
fn test_tfvars_classified_as_meta_content_kind() {
use ripvec_core::chunk::ContentKind;
assert_eq!(
ContentKind::from_extension("tfvars"),
ContentKind::Meta,
".tfvars must be ContentKind::Meta; .tfvars is environment variable \
configuration, not application code"
);
}
#[test]
fn test_chunks_from_tfvars_file_carry_meta_content_kind() {
use ripvec_core::chunk::{ChunkConfig, ContentKind, chunk_file};
use ripvec_core::languages::config_for_extension;
let source = r#"region = "us-east-1"
project_name = "aurora"
environment = "dev-rob"
"#;
let cfg = config_for_extension("tfvars").expect("tfvars lang config");
let chunks = chunk_file(
std::path::Path::new("shared.tfvars"),
source,
&cfg,
&ChunkConfig::default(),
);
assert!(!chunks.is_empty(), "expected at least one tfvars chunk");
assert!(
chunks.iter().all(|c| c.content_kind == ContentKind::Meta),
"all .tfvars chunks must be Meta; got: {:?}",
chunks.iter().map(|c| c.content_kind).collect::<Vec<_>>()
);
}
#[test]
fn test_hcl_terraform_remote_state_emits_call_edge() {
use ripvec_core::repo_map::{Definition, extract_calls_pub};
let source = r#"locals {
data_kms_key_arn = data.terraform_remote_state.shared.outputs.data_kms_key_arn
loader_role_arn = data.terraform_remote_state.shared.outputs.loader_role_arn
}
"#;
#[allow(clippy::cast_possible_truncation)]
let mut defs = vec![Definition {
name: "locals".to_string(),
kind: "block".to_string(),
start_line: 1,
end_line: source.lines().count() as u32,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: source.len() as u32,
calls: vec![],
}];
let call_cfg = ripvec_core::languages::call_query_for_extension("tf").expect("tf call config");
extract_calls_pub(source, &call_cfg, &mut defs);
let calls = &defs[0].calls;
assert!(
calls.iter().any(
|c| c.qualified_path.as_deref() == Some("terraform_remote_state.shared")
|| c.name == "shared"
&& c.qualified_path
.as_deref()
.is_some_and(|q| q.contains("terraform_remote_state"))
),
"expected at least one CallRef pointing at terraform_remote_state.shared; \
got: {:?}",
calls
.iter()
.map(|c| (c.name.clone(), c.qualified_path.clone()))
.collect::<Vec<_>>()
);
}
#[test]
fn test_hcl_module_block_emits_call_edge() {
use ripvec_core::repo_map::{Definition, extract_calls_pub};
let source = r#"module "shared" {
source = "../shared"
}
module "glue" {
source = "../glue-iceberg"
}
"#;
#[allow(clippy::cast_possible_truncation)]
let mut defs = vec![Definition {
name: "file".to_string(),
kind: "file".to_string(),
start_line: 1,
end_line: source.lines().count() as u32,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: source.len() as u32,
calls: vec![],
}];
let call_cfg = ripvec_core::languages::call_query_for_extension("tf").expect("tf call config");
extract_calls_pub(source, &call_cfg, &mut defs);
let calls = &defs[0].calls;
let module_names: Vec<&str> = calls
.iter()
.filter(|c| {
c.qualified_path
.as_deref()
.is_some_and(|q| q.starts_with("module."))
})
.map(|c| c.name.as_str())
.collect();
assert!(
module_names.contains(&"shared"),
"expected module.shared call-edge; got: {:?}",
calls
.iter()
.map(|c| (c.name.clone(), c.qualified_path.clone()))
.collect::<Vec<_>>()
);
assert!(
module_names.contains(&"glue"),
"expected module.glue call-edge; got: {module_names:?}"
);
}
fn parse_and_extract_sql(filename: &str, source: &str) -> Vec<Definition> {
use streaming_iterator::StreamingIterator as _;
let lang_config = ripvec_core::languages::config_for_extension("sql").expect("sql lang config");
let call_config =
ripvec_core::languages::call_query_for_extension("sql").expect("sql call config");
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&lang_config.language)
.expect("set sql lang");
let tree = parser.parse(source, None).expect("parse source");
let mut defs = Vec::new();
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&lang_config.query, tree.root_node(), source.as_bytes());
while let Some(m) = matches.next() {
let mut name = String::new();
let mut def_node = None;
for cap in m.captures {
let cap_name = &lang_config.query.capture_names()[cap.index as usize];
if *cap_name == "name" {
name = source[cap.node.start_byte()..cap.node.end_byte()].to_string();
} else if *cap_name == "def" {
def_node = Some(cap.node);
}
}
if let Some(node) = def_node {
#[allow(clippy::cast_possible_truncation)]
defs.push(Definition {
name,
kind: node.kind().to_string(),
start_line: node.start_position().row as u32 + 1,
end_line: node.end_position().row as u32 + 1,
scope: ripvec_core::chunk::build_scope_chain(node, source),
signature: None,
start_byte: node.start_byte() as u32,
end_byte: node.end_byte() as u32,
calls: vec![],
});
}
}
repo_map::enrich_sql_file_def_pub(filename, source, &mut defs);
repo_map::extract_calls_pub(source, &call_config, &mut defs);
defs
}
#[test]
fn sql_from_clause_emits_call_edge() {
let defs = parse_and_extract_sql(
"user_query.sql",
"SELECT * FROM users; CREATE TABLE users (id int);",
);
assert!(
defs.iter()
.any(|d| d.name == "users" && d.kind == "create_table"),
"expected CREATE TABLE def for 'users' (kind=create_table); got: {:?}",
defs.iter()
.map(|d| (d.name.clone(), d.kind.clone()))
.collect::<Vec<_>>()
);
let file_def = defs
.iter()
.find(|d| d.name == "user_query" && d.kind == "sql_file")
.expect("file-level sql_file def 'user_query' must exist");
assert!(
file_def.calls.iter().any(|c| c.name == "users"),
"file-level def 'user_query' must have a CallRef to 'users' from FROM clause; \
got: {:?}",
file_def
.calls
.iter()
.map(|c| c.name.clone())
.collect::<Vec<_>>()
);
}
#[test]
fn sql_join_clause_emits_call_edge() {
let defs = parse_and_extract_sql(
"join_query.sql",
"SELECT u.id FROM users u JOIN orders o ON u.id = o.uid;
CREATE TABLE users (id int);
CREATE TABLE orders (uid int);",
);
let file_def = defs
.iter()
.find(|d| d.name == "join_query" && d.kind == "sql_file")
.expect("file-level sql_file def 'join_query' must exist");
let call_names: Vec<&str> = file_def.calls.iter().map(|c| c.name.as_str()).collect();
assert!(
call_names.contains(&"users"),
"file-level def 'join_query' must have CallRef to 'users' (FROM); \
got: {call_names:?}"
);
assert!(
call_names.contains(&"orders"),
"file-level def 'join_query' must have CallRef to 'orders' (JOIN); \
got: {call_names:?}"
);
}
#[test]
fn sql_cte_reference_emits_call_edge() {
let defs = parse_and_extract_sql(
"cte_query.sql",
"WITH tmp AS (SELECT * FROM users) SELECT id FROM tmp;
CREATE TABLE users (id int);",
);
let cte_def = defs
.iter()
.find(|d| d.name == "tmp" && d.kind == "cte")
.unwrap_or_else(|| {
panic!(
"expected CTE def 'tmp' (kind=cte); got: {:?}",
defs.iter()
.map(|d| (d.name.clone(), d.kind.clone()))
.collect::<Vec<_>>()
)
});
let cte_call_names: Vec<&str> = cte_def.calls.iter().map(|c| c.name.as_str()).collect();
assert!(
cte_call_names.contains(&"users"),
"CTE def 'tmp' must have CallRef to 'users' from inner FROM; \
got: {cte_call_names:?}"
);
let file_def = defs
.iter()
.find(|d| d.name == "cte_query" && d.kind == "sql_file")
.expect("file-level sql_file def 'cte_query' must exist");
let file_call_names: Vec<&str> = file_def.calls.iter().map(|c| c.name.as_str()).collect();
assert!(
file_call_names.contains(&"tmp"),
"file-level def 'cte_query' must have CallRef to 'tmp' from outer FROM; \
got: {file_call_names:?}"
);
}
#[test]
fn sql_sqlmesh_templated_schema_emits_call_edge() {
let source = "MODEL (\n \
name @{athena_sqlmesh_gold_schema}.issuer_returns,\n \
kind FULL,\n \
gateway athena,\n \
grain (fund_id, issuer_id, strategy, position_date),\n \
table_format iceberg,\n \
audits (\n \
assert_unrealized_pnl_matches,\n \
assert_no_duplicate_grain,\n \
assert_exposure_sums_to_one\n )\n);\n\n\
SELECT\n \
fund_id, fund_name, issuer_id, issuer_name, strategy,\n \
point_person AS analyst,\n \
as_of_date AS position_date,\n \
market_value, cost_basis, unrealized_pnl, realized_pnl\n\
FROM @{athena_sqlmesh_silver_schema}.issuer_returns\n\
WHERE valid_to IS NULL\n";
let defs = parse_and_extract_sql("gold_issuer_returns.sql", source);
let file_def = defs
.iter()
.find(|d| d.name == "gold_issuer_returns" && d.kind == "sql_file")
.expect("file-level sql_file def 'gold_issuer_returns' must exist");
let names: Vec<&str> = file_def.calls.iter().map(|c| c.name.as_str()).collect();
assert!(
names.contains(&"issuer_returns"),
"FROM @{{schema}}.issuer_returns inside a full sqlmesh MODEL header must \
produce CallRef name='issuer_returns' on the file-level def; got: {names:?}"
);
}
#[test]
fn sql_suffix_match_resolves_bare_name_to_prefixed_model() {
let gold = FileNode {
path: "gold/gold_issuer_returns.sql".to_string(),
defs: vec![Definition {
name: "gold_issuer_returns".to_string(),
kind: "sql_file".to_string(),
start_line: 1,
end_line: 10,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 300,
calls: vec![CallRef {
name: "issuer_returns".to_string(),
qualified_path: None,
receiver_type: None,
byte_offset: 0,
resolved: None,
}],
}],
imports: vec![],
};
let silver = FileNode {
path: "silver/silver_issuer_returns.sql".to_string(),
defs: vec![Definition {
name: "silver_issuer_returns".to_string(),
kind: "sql_file".to_string(),
start_line: 1,
end_line: 20,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 500,
calls: vec![],
}],
imports: vec![],
};
let mut files = vec![gold, silver];
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_pub(&mut files, &def_index);
let resolved = files[0].defs[0].calls[0].resolved;
assert!(
resolved.is_some(),
"CallRef(name='issuer_returns') in sql_file 'gold_issuer_returns' must resolve \
to the sql_file def 'silver_issuer_returns' via suffix-match; got None"
);
let (f_idx, d_idx) = resolved.unwrap();
assert_eq!(
files[f_idx as usize].defs[d_idx as usize].name, "silver_issuer_returns",
"resolved def must be 'silver_issuer_returns'"
);
}
#[test]
fn sql_suffix_match_ambiguous_leaves_unresolved() {
let gold = FileNode {
path: "gold/gold_issuer_returns.sql".to_string(),
defs: vec![Definition {
name: "gold_issuer_returns".to_string(),
kind: "sql_file".to_string(),
start_line: 1,
end_line: 10,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 300,
calls: vec![CallRef {
name: "issuer_returns".to_string(),
qualified_path: None,
receiver_type: None,
byte_offset: 0,
resolved: None,
}],
}],
imports: vec![],
};
let silver = FileNode {
path: "silver/silver_issuer_returns.sql".to_string(),
defs: vec![Definition {
name: "silver_issuer_returns".to_string(),
kind: "sql_file".to_string(),
start_line: 1,
end_line: 20,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 500,
calls: vec![],
}],
imports: vec![],
};
let bronze = FileNode {
path: "bronze/bronze_issuer_returns.sql".to_string(),
defs: vec![Definition {
name: "bronze_issuer_returns".to_string(),
kind: "sql_file".to_string(),
start_line: 1,
end_line: 30,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 700,
calls: vec![],
}],
imports: vec![],
};
let mut files = vec![gold, silver, bronze];
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_pub(&mut files, &def_index);
let resolved = files[0].defs[0].calls[0].resolved;
assert!(
resolved.is_none(),
"ambiguous suffix-match (silver_issuer_returns AND bronze_issuer_returns) \
must leave resolved = None; got: {resolved:?}"
);
}
#[test]
fn sql_suffix_match_non_sql_caller_unchanged() {
let rust_file = FileNode {
path: "src/returns.rs".to_string(),
defs: vec![Definition {
name: "returns".to_string(),
kind: "function_item".to_string(),
start_line: 1,
end_line: 5,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 100,
calls: vec![CallRef {
name: "issuer_returns".to_string(),
qualified_path: None,
receiver_type: None,
byte_offset: 0,
resolved: None,
}],
}],
imports: vec![],
};
let sql_file = FileNode {
path: "silver/silver_issuer_returns.sql".to_string(),
defs: vec![Definition {
name: "silver_issuer_returns".to_string(),
kind: "sql_file".to_string(),
start_line: 1,
end_line: 20,
scope: String::new(),
signature: None,
start_byte: 0,
end_byte: 500,
calls: vec![],
}],
imports: vec![],
};
let mut files = vec![rust_file, sql_file];
let def_index = repo_map::build_def_index_pub(&files);
repo_map::resolve_calls_pub(&mut files, &def_index);
let resolved = files[0].defs[0].calls[0].resolved;
assert!(
resolved.is_none(),
"non-sql_file caller (Rust function_item) must NOT resolve 'issuer_returns' \
to 'silver_issuer_returns' via suffix-match; got: {resolved:?}"
);
}
fn kind_for_first_python_decorated_def(source: &str) -> u32 {
use streaming_iterator::StreamingIterator as _;
let cfg =
ripvec_core::languages::config_for_extension("py").expect("Python config must compile");
let mut parser = tree_sitter::Parser::new();
parser.set_language(&cfg.language).expect("set language");
let tree = parser.parse(source, None).expect("parse");
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());
while let Some(m) = matches.next() {
for cap in m.captures {
let cap_name = &cfg.query.capture_names()[cap.index as usize];
if *cap_name == "def" && cap.node.kind() == "decorated_definition" {
return ripvec_core::languages::lsp_symbol_kind_for_node(
&cap.node,
source.as_bytes(),
);
}
}
}
panic!("no decorated_definition @def found in source: {source}");
}
#[test]
fn test_python_property_decorator_classifies_as_property() {
let source =
"class MyModel:\n @property\n def foo(self) -> str:\n return self._foo\n";
let kind = kind_for_first_python_decorated_def(source);
assert_eq!(
kind,
ripvec_core::languages::lsp_symbol_kind::PROPERTY,
"@property must map to SymbolKind::Property (7); got {kind}"
);
}
#[test]
fn test_python_classmethod_decorator_classifies_as_method_or_function() {
let source = "class MyModel:\n @classmethod\n def foo(cls):\n pass\n";
let kind = kind_for_first_python_decorated_def(source);
assert_ne!(
kind,
ripvec_core::languages::lsp_symbol_kind::PROPERTY,
"@classmethod must NOT map to SymbolKind::Property (7); got {kind}"
);
assert!(
kind == ripvec_core::languages::lsp_symbol_kind::METHOD
|| kind == ripvec_core::languages::lsp_symbol_kind::FUNCTION,
"@classmethod must map to Method (6) or Function (12); got {kind}"
);
}
#[test]
fn test_python_staticmethod_decorator_classifies_as_function() {
let source = "class MyModel:\n @staticmethod\n def foo():\n pass\n";
let kind = kind_for_first_python_decorated_def(source);
assert_eq!(
kind,
ripvec_core::languages::lsp_symbol_kind::FUNCTION,
"@staticmethod must map to SymbolKind::Function (12); got {kind}"
);
}
#[test]
fn test_python_cached_property_classifies_as_property() {
let source = "class MyModel:\n @cached_property\n def foo(self):\n return 42\n";
let kind = kind_for_first_python_decorated_def(source);
assert_eq!(
kind,
ripvec_core::languages::lsp_symbol_kind::PROPERTY,
"@cached_property must map to SymbolKind::Property (7); got {kind}"
);
}
#[test]
fn test_python_arbitrary_decorator_falls_through() {
let source =
"import functools\n\ndef other(): pass\n\n@functools.wraps(other)\ndef foo():\n pass\n";
let kind = kind_for_first_python_decorated_def(source);
assert_eq!(
kind,
ripvec_core::languages::lsp_symbol_kind::FUNCTION,
"@functools.wraps(other) must map to SymbolKind::Function (12); got {kind}"
);
}