relay-knowledge 1.1.16

Graph-database-based knowledge graph project.
Documentation
use std::collections::BTreeSet;

use tree_sitter::Node;

use crate::{
    code::config_files,
    domain::{CodeImportRecord, RepositoryCodeRange},
};

use super::{
    super::{CodeIndexError, SnapshotBuild, stable_id},
    languages,
    nodes::{SyntaxRange, compact_whitespace, node_text, push_children_reverse, syntax_range},
};

pub(super) fn collect_imports(
    build: &SnapshotBuild,
    path: &str,
    file_id: &str,
    language_id: &str,
    content: &str,
    root: Node<'_>,
) -> Result<Vec<CodeImportRecord>, CodeIndexError> {
    let mut imports = ImportCollector::new(build, path, file_id);
    let mut stack = Vec::with_capacity(root.child_count().saturating_add(1));
    stack.push(root);
    while let Some(node) = stack.pop() {
        if let Some((module, range)) =
            languages::javascript_like_dynamic_import(language_id, content, node)
        {
            imports.push_record(module, &range)?;
        } else if let Some((module, range)) =
            languages::javascript_like_re_export(language_id, content, node)
        {
            imports.push_record(module, &range)?;
        } else if language_id == "markdown" {
            imports.push_markdown_imports(content, node)?;
        } else if is_import_node(language_id, node) {
            imports.push_records(language_id, content, node)?;
        }
        push_children_reverse(node, &mut stack);
    }
    imports.push_line_imports(language_id, content)?;
    imports.push_configuration_imports(language_id, content)?;

    Ok(imports.into_records())
}

struct ImportCollector<'a> {
    build: &'a SnapshotBuild,
    path: &'a str,
    file_id: &'a str,
    seen_import_ids: BTreeSet<String>,
    records: Vec<CodeImportRecord>,
}

impl<'a> ImportCollector<'a> {
    fn new(build: &'a SnapshotBuild, path: &'a str, file_id: &'a str) -> Self {
        Self {
            build,
            path,
            file_id,
            seen_import_ids: BTreeSet::new(),
            records: Vec::new(),
        }
    }

    fn into_records(self) -> Vec<CodeImportRecord> {
        self.records
    }

    fn push_records(
        &mut self,
        language_id: &str,
        content: &str,
        node: Node<'_>,
    ) -> Result<(), CodeIndexError> {
        let module_text = node_text(content, node);
        let modules = if language_id == "go" {
            languages::go::import_specs(&module_text)
        } else {
            Vec::new()
        };
        let modules = if modules.is_empty() {
            vec![compact_whitespace(&module_text)]
        } else {
            modules
        };
        let range = syntax_range(node);
        for module in modules {
            self.push_record(module, &range)?;
        }

        Ok(())
    }

    fn push_record(&mut self, module: String, range: &SyntaxRange) -> Result<(), CodeIndexError> {
        let module = module.trim().to_owned();
        if module.is_empty() || module == "import" {
            return Ok(());
        }
        let byte_start = range.byte_start.to_string();
        let byte_end = range.byte_end.to_string();
        let line_start = range.line_start.to_string();
        let line_end = range.line_end.to_string();
        let import_id = stable_id(
            "import",
            [
                &self.build.repository_id,
                &self.build.source_scope,
                self.path,
                &module,
                &byte_start,
                &byte_end,
                &line_start,
                &line_end,
            ],
        );
        if !self.seen_import_ids.insert(import_id.clone()) {
            return Ok(());
        }

        self.records.push(CodeImportRecord {
            repository_id: self.build.repository_id.clone(),
            source_scope: self.build.source_scope.clone(),
            import_id,
            file_id: self.file_id.to_owned(),
            path: self.path.to_owned(),
            module,
            target_hint: None,
            resolution_state: "unresolved".to_owned(),
            confidence_basis_points: 10_000,
            confidence_tier: "extracted".to_owned(),
            line_range: RepositoryCodeRange::new("line_range", range.line_start, range.line_end)
                .map_err(|error| CodeIndexError::InvalidInput(error.to_string()))?,
        });

        Ok(())
    }

    fn push_markdown_imports(
        &mut self,
        content: &str,
        node: Node<'_>,
    ) -> Result<(), CodeIndexError> {
        for (module, range) in languages::markdown::imports(content, node) {
            self.push_record(module, &range)?;
        }

        Ok(())
    }

    fn push_line_imports(
        &mut self,
        language_id: &str,
        content: &str,
    ) -> Result<(), CodeIndexError> {
        let imports = match language_id {
            "bash" => languages::bash::line_imports(content),
            "ruby" => languages::ruby::line_imports(content),
            _ => Vec::new(),
        };
        for import in imports {
            self.push_record(import.module, &import.range)?;
        }

        Ok(())
    }

    fn push_configuration_imports(
        &mut self,
        language_id: &str,
        content: &str,
    ) -> Result<(), CodeIndexError> {
        for import in config_files::structured_imports(self.path, language_id, content) {
            let range = SyntaxRange {
                byte_start: import.range.byte_start,
                byte_end: import.range.byte_end,
                line_start: import.range.line_start,
                line_end: import.range.line_end,
            };
            self.push_record(import.module, &range)?;
        }

        Ok(())
    }
}

pub(in crate::code::parser) struct ScriptLine<'a> {
    pub(in crate::code::parser) number: usize,
    pub(in crate::code::parser) byte_start: usize,
    pub(in crate::code::parser) byte_end: usize,
    pub(in crate::code::parser) text: &'a str,
}

pub(in crate::code::parser) struct ScriptLineImport {
    pub(in crate::code::parser) module: String,
    pub(in crate::code::parser) range: SyntaxRange,
}

pub(in crate::code::parser) fn source_lines(content: &str) -> Vec<ScriptLine<'_>> {
    let mut lines = Vec::new();
    let mut byte_start = 0usize;
    for (index, raw_line) in content.split_inclusive('\n').enumerate() {
        let without_lf = raw_line.strip_suffix('\n').unwrap_or(raw_line);
        let text = without_lf.strip_suffix('\r').unwrap_or(without_lf);
        lines.push(ScriptLine {
            number: index + 1,
            byte_start,
            byte_end: byte_start + text.len(),
            text,
        });
        byte_start += raw_line.len();
    }

    lines
}

pub(in crate::code::parser) fn line_range(
    start: &ScriptLine<'_>,
    end: &ScriptLine<'_>,
) -> SyntaxRange {
    SyntaxRange {
        byte_start: start.byte_start,
        byte_end: end.byte_end,
        line_start: start.number,
        line_end: end.number,
    }
}

pub(in crate::code::parser) fn quoted_specifier(statement: &str) -> Option<&str> {
    let start = statement.find(['"', '\''])?;
    let quote = statement.as_bytes()[start] as char;
    let rest = &statement[start + 1..];
    let end = rest.find(quote)?;

    Some(&rest[..end])
}

fn is_import_node(language_id: &str, node: Node<'_>) -> bool {
    match node.kind() {
        "import" => {
            node.is_named() && !matches!(language_id, "javascript" | "jsx" | "typescript" | "tsx")
        }
        "import_declaration"
        | "import_from_statement"
        | "import_statement"
        | "namespace_use_declaration"
        | "preproc_include"
        | "use_declaration"
        | "using_declaration"
        | "using_directive" => true,
        _ => false,
    }
}