relay-knowledge 1.1.17

Graph-database-based knowledge graph project.
Documentation
use tree_sitter::Node;

use super::super::super::super::nodes::{
    SyntaxRange, node_text, push_children_reverse, syntax_range,
};
use super::super::super::super::recovery::{
    code_contains_char, decorated_function_error_body_is_statement_like,
    decorated_function_head_has_recoverable_tail, decorated_function_head_text,
    scan_code_line_indices, token_starts_in_angle_arguments,
};
use super::lexical::{
    CppHeadToken, cpp_builtin_type_token, cpp_declaration_prefix_token, cpp_decorator_token,
    cpp_head_tokens, cpp_tokens_joined_by_qualifier, cpp_type_name_candidate,
};

pub(super) fn function_definition_symbol(
    content: &str,
    node: Node<'_>,
) -> Option<(String, &'static str, SyntaxRange)> {
    node.child_by_field_name("declarator")
        .and_then(|declarator| declarator_name(content, declarator))
        .or_else(|| gcc_decorated_function_name(content, node))
        .map(|name| (name, "function", syntax_range(node)))
}

pub(super) fn gcc_decorated_function_symbol(
    content: &str,
    node: Node<'_>,
) -> Option<(String, &'static str, SyntaxRange)> {
    gcc_decorated_function_name(content, node).map(|name| (name, "function", syntax_range(node)))
}

pub(in crate::code::parser) fn function_definition_is_destructor(
    content: &str,
    node: Node<'_>,
) -> bool {
    let text = node_text(content, node);
    let Some(head) = decorated_function_head_text(&text) else {
        return false;
    };
    let Some(parameter_start) = cpp_last_top_level_parenthesis_start(head) else {
        return false;
    };
    head[..parameter_start]
        .trim_end()
        .rsplit("::")
        .next()
        .is_some_and(|tail| tail.trim_start().starts_with('~'))
}

fn cpp_last_top_level_parenthesis_start(head: &str) -> Option<usize> {
    let mut depth = 0usize;
    let mut candidate = None;
    let literals_closed = scan_code_line_indices(head, |index, character| match character {
        '(' => {
            if depth == 0 {
                candidate = Some(index);
            }
            depth += 1;
        }
        ')' => depth = depth.saturating_sub(1),
        _ => {}
    });

    literals_closed.then_some(candidate).flatten()
}

pub(super) fn gcc_decorated_function_name(content: &str, node: Node<'_>) -> Option<String> {
    let text = node_text(content, node);
    if !text.contains('{') {
        return None;
    }
    if !decorated_function_error_body_is_statement_like(&text) {
        return None;
    }
    let head = decorated_function_head_text(&text)?;
    if !cpp_function_head_has_recovery_decorator(head) {
        return None;
    }
    if !decorated_function_head_has_recoverable_tail(head, true, true, true) {
        return None;
    }
    if head.contains("::~") {
        return None;
    }
    let (name_start, name_end) = cpp_function_name_bounds_from_decorated_head(head)?;
    let name = &head[name_start..name_end];
    if !cpp_function_name_candidate(name) || !cpp_function_head_is_declaration(head, name) {
        return None;
    }

    Some(name.to_owned())
}

fn cpp_function_head_has_recovery_decorator(head: &str) -> bool {
    let Some(parameter_start) = cpp_top_level_parameter_start(head) else {
        return false;
    };
    let parameter_end = cpp_closing_parenthesis_index(&head[parameter_start..])
        .map_or(head.len(), |index| parameter_start + index + 1);
    cpp_head_tokens(&head[..parameter_start])
        .iter()
        .chain(cpp_head_tokens(&head[parameter_end..]).iter())
        .any(|token| {
            cpp_decorator_token(token.text)
                || matches!(
                    token.text,
                    "__always_inline" | "__inline" | "__inline__" | "always_inline"
                )
        })
}

fn cpp_function_name_bounds_from_decorated_head(head: &str) -> Option<(usize, usize)> {
    let parameter_start = cpp_top_level_parameter_start(head)?;
    cpp_name_bounds_before_open(head, parameter_start)
}

fn cpp_top_level_parameter_start(head: &str) -> Option<usize> {
    let mut depth = 0usize;
    let mut candidate = None;
    let literals_closed = scan_code_line_indices(head, |index, character| match character {
        '(' => {
            if depth == 0 && cpp_parameter_open_looks_like_declarator(head, index) {
                candidate = Some(index);
            }
            depth += 1;
        }
        ')' => depth = depth.saturating_sub(1),
        _ => {}
    });

    literals_closed.then_some(candidate).flatten()
}

fn cpp_parameter_open_looks_like_declarator(head: &str, parameter_start: usize) -> bool {
    let Some((name_start, name_end)) = cpp_name_bounds_before_open(head, parameter_start) else {
        return false;
    };
    let token = &head[name_start..name_end];
    cpp_function_name_candidate(token) && !cpp_declaration_prefix_token(token)
}

fn cpp_name_bounds_before_open(head: &str, parameter_start: usize) -> Option<(usize, usize)> {
    if let Some(bounds) = cpp_operator_bounds_before_open(head, parameter_start) {
        return Some(bounds);
    }
    let name_end = head[..parameter_start].trim_end().len();
    let name_start = head[..name_end]
        .char_indices()
        .rev()
        .find(|(_, character)| !(character.is_ascii_alphanumeric() || *character == '_'))
        .map_or(0, |(index, character)| index + character.len_utf8());
    if head[..name_start].trim_end().ends_with('~') {
        return None;
    }
    (name_start < name_end).then_some((name_start, name_end))
}

fn cpp_operator_bounds_before_open(head: &str, parameter_start: usize) -> Option<(usize, usize)> {
    let name_end = head[..parameter_start].trim_end().len();
    let prefix = &head[..name_end];
    let operator_start = prefix.rfind("operator")?;
    if prefix[..operator_start]
        .chars()
        .next_back()
        .is_some_and(|character| character.is_ascii_alphanumeric() || character == '_')
    {
        return None;
    }
    let suffix = prefix[operator_start + "operator".len()..].trim();
    (cpp_punctuation_operator_suffix(suffix) || cpp_conversion_operator_suffix(suffix))
        .then_some((operator_start, name_end))
}

fn cpp_punctuation_operator_suffix(suffix: &str) -> bool {
    !suffix.is_empty()
        && suffix
            .chars()
            .all(|character| character.is_ascii_punctuation() || character.is_ascii_whitespace())
}

fn cpp_conversion_operator_suffix(suffix: &str) -> bool {
    let tokens = cpp_head_tokens(suffix);
    !tokens.is_empty()
        && tokens.iter().all(|token| {
            cpp_declaration_prefix_token(token.text)
                || cpp_type_name_candidate(token.text)
                || cpp_builtin_type_token(token.text)
        })
        && suffix.chars().all(|character| {
            character.is_ascii_alphanumeric()
                || character.is_ascii_whitespace()
                || matches!(character, '_' | ':' | '*' | '&' | '<' | '>' | ',')
        })
}

fn cpp_function_head_is_declaration(head: &str, name: &str) -> bool {
    if name.is_empty() {
        return false;
    }
    let Some((name_start, name_end)) = cpp_function_name_bounds_from_decorated_head(head) else {
        return false;
    };
    if &head[name_start..name_end] != name {
        return false;
    }
    if code_contains_char(&head[..name_start], '=') {
        return false;
    }
    let prefix = cpp_strip_trailing_qualified_declarator_scope(&head[..name_start]);
    let tokens = cpp_head_tokens(prefix);
    if cpp_conversion_operator_name(name) {
        return tokens
            .iter()
            .all(|token| cpp_declaration_prefix_token(token.text));
    }
    let Some(type_index) = tokens.iter().rposition(|token| {
        !cpp_declaration_prefix_token(token.text)
            && !token_starts_in_angle_arguments(prefix, token.start)
    }) else {
        return false;
    };
    if !cpp_function_return_type_at(prefix, &tokens, type_index) {
        return false;
    }
    let type_start = cpp_qualified_return_type_start(prefix, &tokens, type_index);

    tokens[..type_start].iter().all(|token| {
        cpp_declaration_prefix_token(token.text) || cpp_function_return_type_token(token.text)
    })
}

fn cpp_closing_parenthesis_index(text: &str) -> Option<usize> {
    if !text.starts_with('(') {
        return None;
    }
    let mut depth = 0isize;
    let mut matched_end = None;
    let literals_closed = scan_code_line_indices(text, |index, character| {
        if matched_end.is_some() {
            return;
        }
        match character {
            '(' => depth += 1,
            ')' => {
                depth -= 1;
                if depth == 0 {
                    matched_end = Some(index);
                }
            }
            _ => {}
        }
    });
    literals_closed.then_some(matched_end).flatten()
}

fn cpp_strip_trailing_qualified_declarator_scope(prefix: &str) -> &str {
    let mut cursor = prefix.trim_end().len();
    let mut stripped_scope = false;
    loop {
        let before_colons = prefix[..cursor].trim_end().len();
        if !prefix[..before_colons].ends_with("::") {
            break;
        }
        let scope_end = before_colons.saturating_sub(2);
        let ident_end = prefix[..scope_end].trim_end().len();
        let ident_start = prefix[..ident_end]
            .char_indices()
            .rev()
            .find(|(_, character)| !(character.is_ascii_alphanumeric() || *character == '_'))
            .map_or(0, |(index, character)| index + character.len_utf8());
        if ident_start >= ident_end {
            break;
        }
        cursor = ident_start;
        stripped_scope = true;
    }

    if stripped_scope {
        &prefix[..cursor]
    } else {
        prefix
    }
}

fn cpp_function_return_type_at(head: &str, tokens: &[CppHeadToken<'_>], type_index: usize) -> bool {
    let token = tokens[type_index].text;
    if cpp_function_return_type_token(token) {
        return true;
    }
    cpp_qualified_return_type_start(head, tokens, type_index) < type_index
        && cpp_type_name_candidate(token)
}

fn cpp_qualified_return_type_start(
    head: &str,
    tokens: &[CppHeadToken<'_>],
    type_index: usize,
) -> usize {
    let mut start = type_index;
    while start > 0
        && cpp_type_name_candidate(tokens[start - 1].text)
        && cpp_tokens_joined_by_qualifier(head, tokens[start - 1], tokens[start])
    {
        start -= 1;
    }
    start
}

fn cpp_function_name_candidate(name: &str) -> bool {
    cpp_type_name_candidate(name) || name.starts_with("operator")
}

fn cpp_conversion_operator_name(name: &str) -> bool {
    name.strip_prefix("operator").is_some_and(|suffix| {
        suffix
            .trim_start()
            .chars()
            .next()
            .is_some_and(|character| character == '_' || character.is_ascii_alphabetic())
    })
}

fn cpp_function_return_type_token(token: &str) -> bool {
    cpp_builtin_type_token(token)
        || (token.ends_with("_t") && cpp_type_name_candidate(token))
        || (token
            .chars()
            .next()
            .is_some_and(|character| character.is_ascii_uppercase())
            && token
                .chars()
                .any(|character| character.is_ascii_lowercase())
            && cpp_type_name_candidate(token))
}

fn declarator_name(content: &str, node: Node<'_>) -> Option<String> {
    let mut stack = vec![node];
    while let Some(current) = stack.pop() {
        if current.kind() == "qualified_identifier" {
            return terminal_qualified_identifier_name(content, current);
        }
        if matches!(
            current.kind(),
            "identifier" | "field_identifier" | "operator_name"
        ) {
            return Some(node_text(content, current));
        }
        if let Some(declarator) = current.child_by_field_name("declarator") {
            stack.push(declarator);
            continue;
        }
        push_children_reverse(current, &mut stack);
    }

    None
}

fn terminal_qualified_identifier_name(content: &str, node: Node<'_>) -> Option<String> {
    let mut stack = vec![node];
    let mut terminal = None;
    while let Some(current) = stack.pop() {
        if matches!(
            current.kind(),
            "identifier" | "field_identifier" | "operator_name"
        ) && terminal
            .as_ref()
            .is_none_or(|(start, _)| current.start_byte() >= *start)
        {
            terminal = Some((current.start_byte(), node_text(content, current)));
        }
        push_children_reverse(current, &mut stack);
    }

    terminal.map(|(_, name)| name)
}

#[cfg(test)]
#[path = "function_definitions_tests.rs"]
mod tests;