sinter-extract 0.57.4

Language-agnostic tree-sitter extraction: languages are data, the engine never forks
Documentation
//! Node-level scope detection from source text: a file's path says
//! `production`, but an inline `#[cfg(test)] mod tests` or a
//! `@generated` header says otherwise for some or all of its nodes.
//!
//! Only overrides are emitted; a node without one inherits its file scope.

use sinter_core::{CorpusScope, Node, NodeId, SymbolKind};

/// Sparse `(node, scope)` overrides for `nodes` extracted from `source`.
pub fn node_scopes(language: &str, source: &str, nodes: &[Node]) -> Vec<(NodeId, CorpusScope)> {
    // A generated banner scopes the whole file: marking the file node is
    // enough, every node in the file resolves through it.
    if is_generated_header(source) {
        return nodes
            .iter()
            .filter(|n| n.kind == SymbolKind::File)
            .map(|n| (n.id.clone(), CorpusScope::Generated))
            .collect();
    }
    // (span, scope) of each node that carries its own marker; enclosed
    // nodes inherit the innermost marked ancestor.
    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));
        }
    }
    // A file whose every item is test-scoped (e.g. only `#[cfg(test)] mod
    // tests`) is a test file: mark the file node so file-level scope follows.
    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
}

/// Conventional generated-file banners, checked in the first 2 KiB.
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")
}

/// Outer attributes immediately preceding byte `start` (`#[...]` lines,
/// skipping whitespace and doc/line comments between them).
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
}

/// pytest discovery convention: `test_*` functions, `Test*` classes.
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"));
        // a production item keeps the file production
        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"));
    }
}