use sinter_core::{CorpusScope, Node, NodeId, SymbolKind};
pub fn node_scopes(language: &str, source: &str, nodes: &[Node]) -> Vec<(NodeId, CorpusScope)> {
if is_generated_header(source) {
return nodes
.iter()
.filter(|n| n.kind == SymbolKind::File)
.map(|n| (n.id.clone(), CorpusScope::Generated))
.collect();
}
let mut marked: Vec<((u64, u64), CorpusScope)> = Vec::new();
let mut out = Vec::new();
for node in nodes {
if node.kind == SymbolKind::File {
continue;
}
let own = match language {
"rust" => rust_marker(source, node.span.start as usize),
"python" => python_marker(node),
_ => None,
};
let scope = own.or_else(|| {
marked
.iter()
.filter(|((s, e), _)| *s <= node.span.start && node.span.end <= *e)
.min_by_key(|((s, e), _)| e - s)
.map(|(_, scope)| *scope)
});
if let Some(scope) = scope {
if own.is_some() {
marked.push(((node.span.start, node.span.end), scope));
}
out.push((node.id.clone(), scope));
}
}
let items = nodes.iter().filter(|n| n.kind != SymbolKind::File).count();
if items > 0 && out.len() == items && out.iter().all(|(_, s)| *s == CorpusScope::Test) {
out.extend(
nodes
.iter()
.filter(|n| n.kind == SymbolKind::File)
.map(|n| (n.id.clone(), CorpusScope::Test)),
);
}
out
}
fn is_generated_header(source: &str) -> bool {
let mut cut = source.len().min(2048);
while !source.is_char_boundary(cut) {
cut -= 1;
}
let head = &source[..cut];
head.contains("@generated")
|| (head.contains("Code generated") && head.contains("DO NOT EDIT"))
|| head.contains("<auto-generated")
}
fn rust_marker(source: &str, start: usize) -> Option<CorpusScope> {
let mut rest = source[..start].trim_end();
let mut found = None;
loop {
if rest.ends_with("*/") {
let Some(open) = rest.rfind("/*") else { break };
rest = rest[..open].trim_end();
continue;
}
let line_start = rest.rfind('\n').map_or(0, |i| i + 1);
if rest[line_start..].trim_start().starts_with("//") {
rest = rest[..line_start].trim_end();
continue;
}
if !rest.ends_with(']') {
break;
}
let Some(open) = rest.rfind("#[") else { break };
let attr = &rest[open + 2..rest.len() - 1];
let attr: String = attr.chars().filter(|c| !c.is_whitespace()).collect();
if attr == "cfg(test)" || attr == "test" || attr.ends_with("::test") {
found = Some(CorpusScope::Test);
} else if attr == "automatically_derived" && found.is_none() {
found = Some(CorpusScope::Generated);
}
rest = rest[..open].trim_end();
}
found
}
fn python_marker(node: &Node) -> Option<CorpusScope> {
let test = match node.kind {
SymbolKind::Function | SymbolKind::Method => node.name.starts_with("test_"),
SymbolKind::Class => node.name.starts_with("Test"),
_ => false,
};
test.then_some(CorpusScope::Test)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Extractor, spec_for_path};
fn scopes_of(file: &str, src: &str) -> Vec<(String, CorpusScope)> {
let spec = spec_for_path(file).unwrap();
let facts = Extractor::new(spec).unwrap().extract(file, src).unwrap();
facts
.scopes
.iter()
.map(|(id, s)| (id.qualified().to_string(), *s))
.collect()
}
#[test]
fn rust_cfg_test_module_and_test_attrs_are_test_scope() {
let src = "pub fn run() {}\n\n/// doc\n#[tokio::test]\nasync fn solo() {}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n fn helper() {}\n #[test]\n fn it_runs() { run(); }\n}\n";
let got = scopes_of("src/lib.rs", src);
assert_eq!(
got,
vec![
("solo".into(), CorpusScope::Test),
("tests".into(), CorpusScope::Test),
("tests::helper".into(), CorpusScope::Test),
("tests::it_runs".into(), CorpusScope::Test),
]
);
}
#[test]
fn file_with_only_test_items_marks_file_node_test() {
let src =
"use super::*;\n\n#[cfg(test)]\nmod tests {\n #[test]\n fn it_runs() {}\n}\n";
let got = scopes_of("src/lib/tests_only.rs", src);
assert!(got.iter().all(|(_, s)| *s == CorpusScope::Test));
assert!(got.iter().any(|(q, _)| q == "src/lib/tests_only.rs"));
let got = scopes_of(
"src/lib.rs",
"pub fn run() {}\n#[cfg(test)]\nmod tests {}\n",
);
assert!(!got.iter().any(|(q, _)| q == "src/lib.rs"));
}
#[test]
fn generated_banner_marks_every_node() {
let got = scopes_of("src/gen.rs", "// @generated by tool\npub fn a() {}\n");
assert_eq!(got, vec![("src/gen.rs".into(), CorpusScope::Generated)]);
let go = scopes_of(
"x.go",
"// Code generated by protoc. DO NOT EDIT.\npackage x\nfunc A() {}\n",
);
assert!(!go.is_empty() && go.iter().all(|(_, s)| *s == CorpusScope::Generated));
}
#[test]
fn python_pytest_names_are_test_scope() {
let got = scopes_of(
"pkg/mod.py",
"def test_it():\n pass\n\ndef run():\n pass\n\nclass TestThing:\n def test_m(self):\n pass\n",
);
assert!(got.iter().any(|(q, _)| q == "test_it"));
assert!(got.iter().any(|(q, _)| q == "TestThing"));
assert!(!got.iter().any(|(q, _)| q == "run"));
}
}