OXYGENHUB

Coverage / crates/hub/src/repos/functions.rs 94.9%

75 of 79 lines run, 19 of 19 functions · run #542 on main · run missed

1
//! Function boundaries on file pages (hub spec §6.6): tree-sitter finds
2
//! the functions and methods of a Rust, TypeScript, Python or shell file,
3
//! with the lines they span, so a function's history (Oxygen spec §27) is
4
//! one click away.
5
6
/// A function or method and the lines it spans (1-based, inclusive).
7
#[derive(Clone, Debug, PartialEq, Eq)]
8
pub struct Function {
9
    /// Qualified by its enclosing type or class where there is one: `Repo::log`.
10
    pub name: String,
11
    pub start: usize,
12
    pub end: usize,
13
}
14
15
/// The grammar for a path, by its extension, and the node kinds that are
16
/// functions in it.
1714×
fn grammar(path: &str) -> Option<(tree_sitter::Language, &'static [&'static str])> {
1814×
    let ext = path.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase())?;
1913×
    Some(match ext.as_str() {
2013×
        "rs" => (tree_sitter_rust::LANGUAGE.into(), &["function_item"][..]),
216×
        "ts" | "mts" | "cts" => (
221×
            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
231×
            &["function_declaration", "method_definition", "generator_function_declaration"][..],
241×
        ),
255×
        "tsx" => (
260×
            tree_sitter_typescript::LANGUAGE_TSX.into(),
270×
            &["function_declaration", "method_definition", "generator_function_declaration"][..],
280×
        ),
295×
        "py" => (tree_sitter_python::LANGUAGE.into(), &["function_definition"][..]),
304×
        "sh" | "bash" => (tree_sitter_bash::LANGUAGE.into(), &["function_definition"][..]),
313×
        _ => return None,
32
    })
3314×
}
34
35
/// Node kinds whose name qualifies the functions inside them.
36
const CONTAINERS: &[&str] = &["impl_item", "trait_item", "class_declaration", "class_definition", "mod_item"];
37
38
/// The functions of `text`, in file order; empty for a language without a
39
/// grammar here or text that does not parse.
4014×
pub fn functions(path: &str, text: &str) -> Vec<Function> {
4114×
    let Some((language, kinds)) = grammar(path) else { return vec![] };
4210×
    let mut parser = tree_sitter::Parser::new();
4310×
    if parser.set_language(&language).is_err() {
440×
        return vec![];
4510×
    }
4610×
    let Some(tree) = parser.parse(text, None) else { return vec![] };
4710×
    let mut out = Vec::new();
4810×
    walk(tree.root_node(), text.as_bytes(), kinds, &mut Vec::new(), &mut out);
4910×
    out
5014×
}
51
5222×
fn name_of(node: tree_sitter::Node, src: &[u8]) -> Option<String> {
5322×
    let n = node.child_by_field_name("name").or_else(|| node.child_by_field_name("type"))?;
5422×
    n.utf8_text(src).ok().map(|s| s.split_whitespace().collect::<Vec<_>>().join(" "))
5522×
}
56
57335×
fn walk(node: tree_sitter::Node, src: &[u8], kinds: &[&str], scope: &mut Vec<String>, out: &mut Vec<Function>) {
58335×
    let kind = node.kind();
59335×
    let mut pushed = false;
60335×
    if kinds.contains(&kind) {
6116×
        if let Some(name) = name_of(node, src) {
6216×
            let mut full = scope.clone();
6316×
            full.push(name);
6416×
            out.push(Function { name: full.join("::"), start: node.start_position().row + 1, end: node.end_position().row + 1 });
6516×
        }
66319×
    } else if CONTAINERS.contains(&kind) {
676×
        if let Some(name) = name_of(node, src) {
686×
            scope.push(name);
696×
            pushed = true;
706×
        }
71313×
    }
72335×
    let mut cursor = node.walk();
73335×
    for child in node.children(&mut cursor) {
74325×
        walk(child, src, kinds, scope, out);
75325×
    }
76335×
    if pushed {
776×
        scope.pop();
78329×
    }
79335×
}
80
81
#[cfg(test)]
82
mod tests {
83
    use super::*;
84
855×
    fn names(path: &str, text: &str) -> Vec<(String, usize, usize)> {
868×
        functions(path, text).into_iter().map(|f| (f.name, f.start, f.end)).collect()
875×
    }
88
89
    #[test]
901×
    fn rust_functions_and_methods() {
911×
        let src = "use x;\n\nfn free() {\n    a();\n}\n\nimpl Repo {\n    pub fn log(&self) -> u8 {\n        1\n    }\n}\n\ntrait T {\n    fn t(&self);\n}\n";
921×
        assert_eq!(names("src/lib.rs", src), [("free".to_string(), 3, 5), ("Repo::log".to_string(), 8, 10)]);
931×
        let generic = "impl<T> Wrap<T> {\n    fn get(&self) {}\n}\n";
941×
        assert_eq!(names("a.rs", generic), [("Wrap<T>::get".to_string(), 2, 2)]);
951×
    }
96
97
    #[test]
981×
    fn typescript_python_and_shell() {
991×
        let ts = "export function activate(ctx: X): void {\n  go();\n}\nclass Repo {\n  refresh(): void {\n    x();\n  }\n}\n";
1001×
        assert_eq!(names("src/extension.ts", ts), [("activate".to_string(), 1, 3), ("Repo::refresh".to_string(), 5, 7)]);
1011×
        let py = "def top():\n    pass\n\nclass Recipe:\n    def main(self):\n        return 1\n";
1021×
        assert_eq!(names("images/rust/recipe.py", py), [("top".to_string(), 1, 2), ("Recipe::main".to_string(), 5, 6)]);
1031×
        let sh = "#!/bin/sh\nbuild() {\n  cargo build\n}\n";
1041×
        assert_eq!(names("build.sh", sh), [("build".to_string(), 2, 4)]);
1051×
    }
106
107
    #[test]
1081×
    fn other_files_have_none() {
1091×
        assert!(functions("README.md", "# fn main() {}\n").is_empty());
1101×
        assert!(functions("Makefile", "all:\n").is_empty());
1111×
    }
112
}