relay-knowledge 1.1.5

Graph-database-based knowledge graph project.
Documentation
use std::collections::{BTreeMap, BTreeSet};

use rusqlite::{Connection, params};

use crate::{domain::CodeRepositorySetMemberStatus, storage::StorageError};

#[derive(Debug, Clone, Eq, PartialEq)]
pub(super) struct ModulePrefix {
    source_path_prefix: String,
    module_key: String,
}

pub(super) fn manifest_module_prefixes_for_members(
    connection: &mut Connection,
    members: &[CodeRepositorySetMemberStatus],
) -> Result<BTreeMap<String, Vec<ModulePrefix>>, StorageError> {
    let mut prefixes_by_scope = BTreeMap::new();
    for member in members {
        let mut statement = connection.prepare(
            "
            SELECT path, content
            FROM code_repository_chunks
            WHERE source_scope = ?1
              AND (path = 'go.mod' OR path LIKE '%/go.mod')
            ORDER BY path ASC, chunk_id ASC
            ",
        )?;
        let rows = statement.query_map(params![member.member.source_scope], |row| {
            Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
        })?;
        let mut prefixes = Vec::new();
        for row in rows {
            let (path, content) = row?;
            collect_go_module_prefixes(&path, &content, &mut prefixes);
        }
        if !prefixes.is_empty() {
            prefixes_by_scope.insert(member.member.source_scope.clone(), prefixes);
        }
    }

    Ok(prefixes_by_scope)
}

pub(super) fn module_keys_for_path_with_prefixes(
    path: &str,
    prefixes: &[ModulePrefix],
) -> BTreeSet<String> {
    let mut keys = module_keys_for_path(path);
    for prefix in prefixes {
        let Some(relative_path) = path_relative_to_module(path, &prefix.source_path_prefix) else {
            continue;
        };
        extend_with_module_prefix(&mut keys, &prefix.module_key, &relative_path);
    }
    keys
}

pub(super) fn normalize_module_key(value: &str) -> String {
    let mut value = value
        .trim()
        .trim_matches('"')
        .trim_matches('\'')
        .trim_end_matches(';')
        .trim();
    if let Some(stripped) = value.strip_prefix("use ") {
        value = stripped.trim();
    } else if let Some(stripped) = value.strip_prefix("import ") {
        value = stripped.trim();
    }
    value
        .replace("::", ".")
        .replace(['/', '\\', '-'], ".")
        .replace(['{', '}', ','], ".")
        .split_whitespace()
        .next()
        .unwrap_or_default()
        .trim_matches('.')
        .to_lowercase()
}

fn collect_go_module_prefixes(path: &str, content: &str, prefixes: &mut Vec<ModulePrefix>) {
    let source_path_prefix = go_manifest_path_prefix(path);
    for line in content.lines() {
        let Some(module_key) = go_module_prefix(line) else {
            continue;
        };
        let prefix = ModulePrefix {
            source_path_prefix: source_path_prefix.clone(),
            module_key,
        };
        if !prefixes.contains(&prefix) {
            prefixes.push(prefix);
        }
    }
}

fn go_module_prefix(line: &str) -> Option<String> {
    let line = line.split("//").next()?.trim();
    let module = line
        .strip_prefix("module")
        .filter(|rest| rest.starts_with(char::is_whitespace))?
        .trim();
    if module.is_empty() {
        return None;
    }
    let normalized = normalize_module_key(module.trim_matches(['"', '\'', '`']));

    (!normalized.is_empty() && normalized.contains('.')).then_some(normalized)
}

fn go_manifest_path_prefix(path: &str) -> String {
    let path = clean_manifest_path(path);
    path.strip_suffix("/go.mod")
        .filter(|prefix| !prefix.is_empty())
        .unwrap_or_default()
        .to_owned()
}

fn module_keys_for_path(path: &str) -> BTreeSet<String> {
    let without_extension = path
        .rsplit_once('.')
        .map(|(left, _)| left)
        .unwrap_or(path)
        .trim_start_matches("./");
    let normalized = normalize_module_key(without_extension);
    let mut keys = BTreeSet::new();
    keys.insert(normalized.clone());
    if let Some(last) = normalized.rsplit('.').next() {
        keys.insert(last.to_owned());
    }
    keys
}

fn extend_with_module_prefix(keys: &mut BTreeSet<String>, prefix: &str, relative_path: &str) {
    if let Some(package_key) = package_key_for_path(relative_path) {
        if package_key.is_empty() {
            keys.insert(prefix.to_owned());
        } else {
            keys.insert(format!("{prefix}.{package_key}"));
        }
    }
    for key in module_keys_for_path(relative_path) {
        keys.insert(format!("{prefix}.{key}"));
    }
}

fn package_key_for_path(path: &str) -> Option<String> {
    let path = clean_manifest_path(path);
    if path == "go.mod" || path.ends_with("/go.mod") {
        return None;
    }
    let Some((directory, _)) = path.rsplit_once('/') else {
        return Some(String::new());
    };
    Some(normalize_module_key(directory))
}

fn path_relative_to_module(path: &str, source_path_prefix: &str) -> Option<String> {
    let path = clean_manifest_path(path);
    if source_path_prefix.is_empty() {
        return Some(path);
    }
    let stripped = path.strip_prefix(source_path_prefix)?;
    let relative = stripped.strip_prefix('/')?;
    (!relative.is_empty()).then_some(relative.to_owned())
}

fn clean_manifest_path(path: &str) -> String {
    path.replace('\\', "/").trim_start_matches("./").to_owned()
}