relay-knowledge 1.1.17

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

use crate::{
    code::source_roots::{
        normalized_module_candidates, source_module_candidates, source_relative_path,
    },
    domain::RepositoryCodeFileRecord,
};

use super::{ImportContext, ModuleFileResolution};

impl ImportContext<'_> {
    pub(in crate::code::identity) fn module_file_exists(&self, module_path: &str) -> bool {
        normalized_module_candidates(module_path)
            .iter()
            .any(|candidate| self.module_paths.contains_key(candidate))
    }

    pub(in crate::code::identity) fn any_module_file_exists(
        &self,
        module_paths: &[String],
    ) -> bool {
        module_paths
            .iter()
            .any(|module_path| self.module_file_exists(module_path))
    }

    pub(in crate::code::identity) fn resolve_first_module_file(
        &self,
        module_paths: &[String],
        allow_source_root_match: bool,
    ) -> ModuleFileResolution {
        for module_path in module_paths {
            match self.resolve_module_file(module_path, allow_source_root_match) {
                ModuleFileResolution::Resolved(path) => {
                    return ModuleFileResolution::Resolved(path);
                }
                ModuleFileResolution::Ambiguous => return ModuleFileResolution::Ambiguous,
                ModuleFileResolution::Unresolved => {}
            }
        }

        ModuleFileResolution::Unresolved
    }

    pub(in crate::code::identity) fn resolve_first_exact_module_file(
        &self,
        module_paths: &[String],
    ) -> ModuleFileResolution {
        for module_path in module_paths {
            if self.file_languages.contains_key(module_path.as_str()) {
                return ModuleFileResolution::Resolved(module_path.clone());
            }
        }

        ModuleFileResolution::Unresolved
    }

    pub(in crate::code::identity) fn directory_has_language_files(
        &self,
        directory_path: &str,
        language_id: &str,
    ) -> bool {
        normalized_module_candidates(directory_path)
            .iter()
            .any(|directory| {
                directory_has_language_files(&self.module_paths, directory, language_id)
            })
    }

    pub(in crate::code::identity) fn resolve_go_directory_with_language_files(
        &self,
        directory_path: &str,
    ) -> ModuleFileResolution {
        resolve_directory_from_modules(
            &self.go_module_paths,
            &normalized_module_candidates(directory_path),
            "go",
        )
    }

    pub(in crate::code::identity) fn resolve_directory_with_language_files(
        &self,
        directory_path: &str,
        language_id: &str,
    ) -> ModuleFileResolution {
        resolve_directory_from_modules(
            &self.module_paths,
            &normalized_module_candidates(directory_path),
            language_id,
        )
    }

    pub(in crate::code::identity) fn resolve_directory_tree_with_language_files(
        &self,
        directory_path: &str,
        language_id: &str,
    ) -> ModuleFileResolution {
        resolve_directory_tree_from_modules(
            &self.module_paths,
            &normalized_module_candidates(directory_path),
            language_id,
        )
    }
}

impl ImportContext<'_> {
    fn resolve_module_file(
        &self,
        module_path: &str,
        allow_source_root_match: bool,
    ) -> ModuleFileResolution {
        for normalized_path in normalized_module_candidates(module_path) {
            let Some(files) = self.module_paths.get(&normalized_path) else {
                continue;
            };
            if let Some(path) = unique_file_match(
                files
                    .iter()
                    .copied()
                    .filter(|file| file.path == module_path),
            ) {
                return ModuleFileResolution::Resolved(path);
            }
            if !allow_source_root_match {
                if files.len() == 1 && normalized_path == module_path {
                    return ModuleFileResolution::Resolved(files[0].path.clone());
                }
                continue;
            }
            if let Some(path) = unique_file_match(files.iter().copied().filter(|file| {
                source_module_candidates(&file.path)
                    .iter()
                    .any(|candidate| candidate == &normalized_path)
            })) {
                return ModuleFileResolution::Resolved(path);
            }
            if files.len() == 1 {
                return ModuleFileResolution::Resolved(files[0].path.clone());
            }

            return ModuleFileResolution::Ambiguous;
        }

        ModuleFileResolution::Unresolved
    }
}

fn unique_file_match<'a>(
    files: impl IntoIterator<Item = &'a RepositoryCodeFileRecord>,
) -> Option<String> {
    let mut matches = files.into_iter();
    let first = matches.next()?;
    matches.next().is_none().then(|| first.path.clone())
}

pub(in crate::code::identity) fn parse_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])
}

pub(in crate::code::identity) fn parent_dir(path: &str) -> &str {
    path.rsplit_once('/').map_or("", |(parent, _)| parent)
}

pub(in crate::code::identity) fn normalize_join(parent: &str, child: &str) -> Option<String> {
    let mut parts = Vec::<&str>::new();
    if child.starts_with('/') {
        return None;
    }
    for part in parent
        .split('/')
        .chain(child.split('/'))
        .filter(|part| !part.is_empty() && *part != ".")
    {
        if part == ".." {
            parts.pop()?;
        } else {
            parts.push(part);
        }
    }
    if parts.is_empty() {
        return None;
    }

    Some(parts.join("/"))
}

pub(in crate::code::identity) fn strip_source_root(path: &str) -> String {
    source_relative_path(path)
}

fn resolve_directory_from_modules(
    module_paths: &BTreeMap<String, Vec<&RepositoryCodeFileRecord>>,
    directories: &[String],
    language_id: &str,
) -> ModuleFileResolution {
    let mut resolved_directories = Vec::new();
    for directory in directories {
        match resolve_single_directory_from_modules(module_paths, directory, language_id) {
            ModuleFileResolution::Resolved(directory) => {
                if !resolved_directories.contains(&directory) {
                    resolved_directories.push(directory);
                }
                if resolved_directories.len() > 1 {
                    return ModuleFileResolution::Ambiguous;
                }
            }
            ModuleFileResolution::Ambiguous => return ModuleFileResolution::Ambiguous,
            ModuleFileResolution::Unresolved => {}
        }
    }

    match resolved_directories.as_slice() {
        [directory] => ModuleFileResolution::Resolved(directory.clone()),
        [] => ModuleFileResolution::Unresolved,
        _ => ModuleFileResolution::Ambiguous,
    }
}

fn resolve_single_directory_from_modules(
    module_paths: &BTreeMap<String, Vec<&RepositoryCodeFileRecord>>,
    directory: &str,
    language_id: &str,
) -> ModuleFileResolution {
    let prefix = if directory.is_empty() {
        String::new()
    } else {
        format!("{directory}/")
    };
    let mut matching_directories = Vec::new();
    for (module_path, files) in module_paths
        .range(prefix.clone()..)
        .take_while(|(path, _)| prefix.is_empty() || path.starts_with(&prefix))
    {
        if parent_dir(module_path) != directory {
            continue;
        }
        for file in files.iter().filter(|file| file.language_id == language_id) {
            let directory = parent_dir(&file.path).to_owned();
            if !matching_directories.contains(&directory) {
                matching_directories.push(directory);
            }
            if matching_directories.len() > 1 {
                return ModuleFileResolution::Ambiguous;
            }
        }
    }

    match matching_directories.as_slice() {
        [directory] => ModuleFileResolution::Resolved(directory.clone()),
        [] => ModuleFileResolution::Unresolved,
        _ => ModuleFileResolution::Ambiguous,
    }
}

fn directory_has_language_files(
    module_paths: &BTreeMap<String, Vec<&RepositoryCodeFileRecord>>,
    directory: &str,
    language_id: &str,
) -> bool {
    let prefix = if directory.is_empty() {
        String::new()
    } else {
        format!("{directory}/")
    };
    module_paths
        .range(prefix.clone()..)
        .take_while(|(path, _)| prefix.is_empty() || path.starts_with(&prefix))
        .any(|(_, files)| files.iter().any(|file| file.language_id == language_id))
}

fn resolve_directory_tree_from_modules(
    module_paths: &BTreeMap<String, Vec<&RepositoryCodeFileRecord>>,
    directories: &[String],
    language_id: &str,
) -> ModuleFileResolution {
    let mut matching_roots = Vec::new();
    for directory in directories {
        let prefix = if directory.is_empty() {
            String::new()
        } else {
            format!("{directory}/")
        };
        for (module_path, files) in module_paths
            .range(prefix.clone()..)
            .take_while(|(path, _)| prefix.is_empty() || path.starts_with(&prefix))
        {
            for file in files.iter().filter(|file| file.language_id == language_id) {
                let Some(root) = physical_directory_tree_root(&file.path, module_path, directory)
                else {
                    continue;
                };
                if !matching_roots.contains(&root) {
                    matching_roots.push(root);
                }
                if matching_roots.len() > 1 {
                    return ModuleFileResolution::Ambiguous;
                }
            }
        }
    }

    match matching_roots.as_slice() {
        [root] => ModuleFileResolution::Resolved(root.clone()),
        [] => ModuleFileResolution::Unresolved,
        _ => ModuleFileResolution::Ambiguous,
    }
}

fn physical_directory_tree_root(
    file_path: &str,
    module_path: &str,
    directory: &str,
) -> Option<String> {
    let suffix = module_path.strip_prefix(directory)?.trim_start_matches('/');
    if suffix.is_empty() {
        return Some(file_path.to_owned());
    }
    let root_len = file_path.len().checked_sub(suffix.len() + 1)?;
    Some(file_path[..root_len].to_owned())
}

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