git-topology 0.1.2

Shared clustering and topology primitives for git-cognitive and git-semantic
Documentation
use anyhow::{Context, Result};
use tree_sitter::Parser;

use super::languages::SupportedLanguage;
use super::CodeChunk;

const BODY_NODE_TYPES: &[&str] = &[
    "block",
    "statement_block",
    "compound_statement",
    "class_body",
    "enum_body",
    "field_declaration_list",
    "declaration_list",
];

const CHUNK_NODE_TYPES: &[&str] = &[
    "function_item",
    "function_declaration",
    "function_definition",
    "method_declaration",
    "method_definition",
    "class_declaration",
    "class_definition",
    "impl_item",
    "struct_item",
    "enum_item",
    "trait_item",
];

pub fn extract_signature(text: &str, language: SupportedLanguage) -> String {
    let mut parser = tree_sitter::Parser::new();
    let ts_language = language.tree_sitter_language();
    if parser.set_language(&ts_language).is_err() {
        return first_line_fallback(text);
    }

    let tree = match parser.parse(text, None) {
        Some(t) => t,
        None => return first_line_fallback(text),
    };

    let root = tree.root_node();
    let mut cursor = root.walk();

    let top = root
        .children(&mut cursor)
        .find(|n| !n.is_extra() && n.child_count() > 0);

    let node = match top {
        Some(n) => n,
        None => return first_line_fallback(text),
    };

    let mut body_start_byte: Option<usize> = None;
    let mut node_cursor = node.walk();
    for child in node.children(&mut node_cursor) {
        if BODY_NODE_TYPES.contains(&child.kind()) {
            body_start_byte = Some(child.start_byte());
            break;
        }
    }

    match body_start_byte {
        Some(body_start) => {
            let sig = text[..body_start].trim_end_matches([' ', '\t', '{', ':']);
            sig.trim_end().to_string()
        }
        None => text.trim_end_matches('{').trim_end().to_string(),
    }
}

pub fn extract_name(text: &str, language: SupportedLanguage) -> String {
    let mut parser = tree_sitter::Parser::new();
    let ts_language = language.tree_sitter_language();
    if parser.set_language(&ts_language).is_err() {
        return first_line_fallback(text);
    }

    let tree = match parser.parse(text, None) {
        Some(t) => t,
        None => return first_line_fallback(text),
    };

    let root = tree.root_node();
    let mut cursor = root.walk();

    let top = root
        .children(&mut cursor)
        .find(|n| !n.is_extra() && n.child_count() > 0);

    let node = match top {
        Some(n) => n,
        None => return first_line_fallback(text),
    };

    let mut node_cursor = node.walk();
    for child in node.children(&mut node_cursor) {
        if child.kind() == "name"
            || child.kind() == "identifier"
            || child.kind() == "type_identifier"
        {
            if let Ok(name) = child.utf8_text(text.as_bytes()) {
                return name.to_string();
            }
        }
    }

    first_line_fallback(text)
}

fn first_line_fallback(text: &str) -> String {
    text.lines()
        .next()
        .unwrap_or("")
        .trim_end_matches(['{', ':'])
        .trim_end()
        .to_string()
}

pub fn parse(src: &str, language: SupportedLanguage) -> Option<tree_sitter::Tree> {
    let mut parser = tree_sitter::Parser::new();
    parser.set_language(&language.tree_sitter_language()).ok()?;
    parser.parse(src, None)
}

pub fn parse_with_tree_sitter(text: &str, language: SupportedLanguage) -> Result<Vec<CodeChunk>> {
    let mut parser = Parser::new();
    let ts_language = language.tree_sitter_language();

    parser
        .set_language(&ts_language)
        .context("Failed to set tree-sitter language")?;

    let tree = parser
        .parse(text, None)
        .context("Failed to parse code with tree-sitter")?;

    let root_node = tree.root_node();
    let mut chunks = Vec::new();

    walk_tree(text, root_node, &mut chunks);

    if chunks.is_empty() {
        chunks.push(CodeChunk {
            text: text.to_string(),
            start_line: 0,
            end_line: text.lines().count(),
        });
    } else {
        let first_start = chunks.iter().map(|c| c.start_line).min().unwrap_or(0);
        if first_start > 0 {
            let preamble: String = text
                .lines()
                .take(first_start)
                .collect::<Vec<_>>()
                .join("\n");
            if !preamble.trim().is_empty() {
                chunks.insert(
                    0,
                    CodeChunk {
                        text: preamble,
                        start_line: 0,
                        end_line: first_start,
                    },
                );
            }
        }
    }

    Ok(chunks)
}

fn walk_tree(text: &str, node: tree_sitter::Node, chunks: &mut Vec<CodeChunk>) {
    let node_kind = node.kind();

    if CHUNK_NODE_TYPES.contains(&node_kind) || is_top_level_definition(&node) {
        let start_line = node.start_position().row;
        let end_line = node.end_position().row;
        let start_byte = node.start_byte();
        let end_byte = node.end_byte();

        if let Some(chunk_text) = text.get(start_byte..end_byte) {
            chunks.push(CodeChunk {
                text: chunk_text.to_string(),
                start_line,
                end_line,
            });
            return;
        }
    }

    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        walk_tree(text, child, chunks);
    }
}

fn is_top_level_definition(node: &tree_sitter::Node) -> bool {
    matches!(
        node.kind(),
        "function"
            | "class"
            | "method"
            | "struct"
            | "enum"
            | "trait"
            | "impl"
            | "interface"
            | "type_alias"
            | "const_declaration"
    )
}