relay-knowledge 1.1.16

Graph-database-based knowledge graph project.
Documentation
//! Cross-language manual syntax-node extraction.

use tree_sitter::Node;

use crate::code::config_files::{self, ConfigFact, ConfigReference};

use super::{
    super::CodeIndexError,
    FileParseContext, FileParseOutput, languages,
    nodes::{SyntaxRange, last_identifier_text, node_text, push_children_reverse, syntax_range},
    records::{
        reference_record, symbol_record, symbol_record_with_qualified_suffix, upsert_reference,
        upsert_symbol,
    },
};

pub(super) fn collect_manual_nodes(
    context: &FileParseContext<'_>,
    root: Node<'_>,
    config_definitions: &[ConfigFact],
    config_references: &[ConfigReference],
    output: &mut FileParseOutput,
) -> Result<(), CodeIndexError> {
    let mut stack = Vec::with_capacity(root.child_count().saturating_add(1));
    stack.push(root);
    while let Some(node) = stack.pop() {
        if languages::manual_definition_candidate(context.language_id, node.kind()) {
            for (name, kind, range) in
                manual_definitions(context.content, context.language_id, node)
            {
                upsert_symbol(output, symbol_record(context, &name, kind, &range)?);
            }
        }
        if let Some((name, kind, range)) =
            enum_member_definition(context.content, context.language_id, node)
        {
            upsert_symbol(output, symbol_record(context, &name, kind, &range)?);
        }
        if let Some((name, range)) = manual_call(context, node) {
            upsert_reference(output, reference_record(context, &name, "call", &range)?);
        }
        for (name, kind, range) in manual_references(context, node) {
            upsert_reference(output, reference_record(context, &name, kind, &range)?);
        }
        push_children_reverse(node, &mut stack);
    }
    for (name, qualified_suffix, kind, range) in
        languages::language_manual_file_definitions(context.content, context.language_id)
    {
        upsert_symbol(
            output,
            symbol_record_with_qualified_suffix(
                context,
                &name,
                kind,
                &range,
                qualified_suffix.as_deref().unwrap_or(&name),
            )?,
        );
    }
    for (name, kind, range) in
        languages::language_manual_file_references(context.content, context.language_id)
    {
        upsert_reference(output, reference_record(context, &name, kind, &range)?);
    }
    for definition in config_definitions {
        upsert_symbol(
            output,
            symbol_record(
                context,
                &definition.name,
                definition.kind,
                &definition.range.into(),
            )?,
        );
    }
    for reference in config_references {
        upsert_reference(
            output,
            reference_record(
                context,
                &reference.name,
                reference.kind,
                &reference.range.into(),
            )?,
        );
    }

    Ok(())
}

impl From<config_files::ConfigRange> for SyntaxRange {
    fn from(range: config_files::ConfigRange) -> Self {
        Self {
            byte_start: range.byte_start,
            byte_end: range.byte_end,
            line_start: range.line_start,
            line_end: range.line_end,
        }
    }
}

pub(super) fn manual_definitions(
    content: &str,
    language_id: &str,
    node: Node<'_>,
) -> Vec<(String, &'static str, SyntaxRange)> {
    let definitions = languages::language_manual_definitions(content, language_id, node);
    if !definitions.is_empty() {
        return definitions;
    }

    generic_manual_definition(content, language_id, node)
        .into_iter()
        .collect()
}

fn generic_manual_definition(
    content: &str,
    language_id: &str,
    node: Node<'_>,
) -> Option<(String, &'static str, SyntaxRange)> {
    if languages::generic_manual_definition_is_rejected(content, language_id, node) {
        return None;
    }
    let kind = languages::definition_kind(language_id, node.kind())?;
    let name = node.child_by_field_name("name")?;
    let range = languages::language_exported_declaration_range(language_id, node)
        .unwrap_or_else(|| syntax_range(node));

    Some((node_text(content, name), kind, range))
}

fn enum_member_definition(
    content: &str,
    language_id: &str,
    node: Node<'_>,
) -> Option<(String, &'static str, SyntaxRange)> {
    languages::language_enum_member_definition(content, language_id, node)
}

fn manual_call(context: &FileParseContext<'_>, node: Node<'_>) -> Option<(String, SyntaxRange)> {
    if node.kind() == "preproc_call" {
        let name = node
            .child_by_field_name("directive")
            .or_else(|| node.child_by_field_name("name"))
            .or_else(|| super::nodes::first_named_child_of_kind(node, "identifier"))?;
        return Some((node_text(context.content, name), syntax_range(name)));
    }
    if let Some(call) = languages::language_manual_call(context.content, context.language_id, node)
    {
        return Some(call);
    }
    if !languages::is_call_node(context.language_id, node.kind()) {
        return None;
    }
    if let Some(call) = constructed_type_call(context.content, node) {
        return Some(call);
    }
    let function = node
        .child_by_field_name("function")
        .or_else(|| node.child_by_field_name("constructor"))
        .or_else(|| node.child(0))?;

    callable_expression_name(context.content, function)
}

fn callable_expression_name(content: &str, function: Node<'_>) -> Option<(String, SyntaxRange)> {
    if function.kind() == "subscript_expression" {
        let argument = function.child_by_field_name("argument");
        for index in 0..function.named_child_count() {
            let child = function.named_child(u32::try_from(index).ok()?)?;
            if argument.is_some_and(|argument| node_contains(argument, child)) {
                continue;
            }
            if let Some(callable) = callable_expression_name(content, child) {
                return Some(callable);
            }
        }
    }

    last_identifier_text(content, function).map(|name| (name, syntax_range(function)))
}

fn manual_references(
    context: &FileParseContext<'_>,
    node: Node<'_>,
) -> Vec<(String, &'static str, SyntaxRange)> {
    languages::language_manual_references(context.content, context.language_id, node)
}

fn node_contains(parent: Node<'_>, child: Node<'_>) -> bool {
    parent.start_byte() <= child.start_byte() && parent.end_byte() >= child.end_byte()
}

fn constructed_type_call(content: &str, node: Node<'_>) -> Option<(String, SyntaxRange)> {
    let type_node = node.child_by_field_name("type")?;
    let name = constructed_type_name(content, type_node)?;

    Some((name, syntax_range(type_node)))
}

fn constructed_type_name(content: &str, node: Node<'_>) -> Option<String> {
    match node.kind() {
        "generic_type" => first_constructed_type_child(node)
            .and_then(|inner| constructed_type_name(content, inner)),
        "array_type" => node
            .child_by_field_name("element")
            .and_then(|inner| constructed_type_name(content, inner)),
        "scoped_type_identifier" => last_type_identifier_text(content, node),
        "identifier" | "type_identifier" => Some(node_text(content, node)),
        _ => last_type_identifier_text(content, node),
    }
}

fn first_constructed_type_child(node: Node<'_>) -> Option<Node<'_>> {
    (0..node.child_count()).find_map(|index| {
        let index = u32::try_from(index).ok()?;
        let child = node.child(index)?;
        constructed_type_node(child.kind()).then_some(child)
    })
}

fn last_type_identifier_text(content: &str, node: Node<'_>) -> Option<String> {
    let mut stack = Vec::with_capacity(node.child_count().saturating_add(1));
    stack.push(node);
    let mut last = None;
    while let Some(current) = stack.pop() {
        if current.kind() == "type_arguments" {
            continue;
        }
        if matches!(current.kind(), "identifier" | "type_identifier") {
            last = Some(node_text(content, current));
            continue;
        }
        push_children_reverse(current, &mut stack);
    }

    last
}

fn constructed_type_node(kind: &str) -> bool {
    matches!(
        kind,
        "array_type" | "generic_type" | "identifier" | "scoped_type_identifier" | "type_identifier"
    )
}