relay-knowledge 1.1.5

Graph-database-based knowledge graph project.
Documentation
use crate::domain::CodeImportRecord;

use super::super::import_resolution::{
    ImportContext, ImportResolution, combined_resolution, normalize_join, parent_dir,
    strip_source_root,
};

pub(in crate::code::identity) fn resolve_import(
    import: &CodeImportRecord,
    context: &ImportContext<'_>,
) -> Option<ImportResolution> {
    let request = PythonImportRequest::parse(&import.path, &import.module)?;

    Some(resolve_python_import(&request, context))
}

struct PythonImportRequest {
    module_paths: Vec<String>,
    imported_names: Vec<String>,
}

impl PythonImportRequest {
    fn parse(import_path: &str, statement: &str) -> Option<Self> {
        if !(import_path.ends_with(".py") || import_path.ends_with(".pyw")) {
            return None;
        }
        let statement = statement.trim().trim_end_matches(';').trim();
        if let Some(body) = statement.strip_prefix("from ") {
            let (module, names) = body.split_once(" import ")?;
            let module_paths = python_module_path_candidates(import_path, module.trim());
            let imported_names = parse_python_imported_names(names);
            if module_paths.is_empty() && imported_names.is_empty() {
                return None;
            }

            return Some(Self {
                module_paths,
                imported_names,
            });
        }
        if let Some(body) = statement.strip_prefix("import ") {
            let module_paths = body
                .split(',')
                .filter_map(|part| {
                    let module = part
                        .trim()
                        .split_once(" as ")
                        .map_or(part.trim(), |(module, _)| module.trim());
                    python_module_path_candidates(import_path, module)
                        .into_iter()
                        .next()
                })
                .collect::<Vec<_>>();
            if module_paths.is_empty() {
                return None;
            }

            return Some(Self {
                module_paths,
                imported_names: Vec::new(),
            });
        }

        None
    }
}

fn resolve_python_import(
    request: &PythonImportRequest,
    context: &ImportContext<'_>,
) -> ImportResolution {
    if request.imported_names.is_empty() {
        return if request
            .module_paths
            .iter()
            .any(|module_path| python_module_exists(context, module_path))
        {
            ImportResolution::Resolved
        } else {
            ImportResolution::Unresolved
        };
    }

    combined_resolution(
        request.imported_names.iter().map(|name| {
            resolve_python_imported_name(context, name, request.module_paths.as_slice())
        }),
    )
}

fn resolve_python_imported_name(
    context: &ImportContext<'_>,
    name: &str,
    module_paths: &[String],
) -> ImportResolution {
    let symbol_paths = module_paths
        .iter()
        .flat_map(|module_path| python_module_files(module_path))
        .collect::<Vec<_>>();
    match context.resolve_name_in_paths(name, &symbol_paths) {
        ImportResolution::Unresolved => {
            if module_paths
                .iter()
                .any(|module_path| python_module_exists(context, &format!("{module_path}/{name}")))
            {
                ImportResolution::Resolved
            } else {
                ImportResolution::Unresolved
            }
        }
        resolution => resolution,
    }
}

fn python_module_exists(context: &ImportContext<'_>, module_path: &str) -> bool {
    python_module_files(module_path)
        .iter()
        .any(|file_path| context.module_file_exists(file_path))
}

fn python_module_files(module_path: &str) -> Vec<String> {
    vec![
        format!("{module_path}.py"),
        format!("{module_path}.pyw"),
        format!("{module_path}/__init__.py"),
    ]
}

fn python_module_path_candidates(import_path: &str, module: &str) -> Vec<String> {
    let module = module.trim();
    if module.is_empty() {
        return Vec::new();
    }
    let mut candidates = Vec::new();
    if module.starts_with('.') {
        if let Some(relative) = relative_python_module_path(import_path, module) {
            candidates.push(relative);
        }
    } else {
        candidates.push(module.replace('.', "/"));
    }
    candidates.sort();
    candidates.dedup();

    candidates
}

fn relative_python_module_path(import_path: &str, module: &str) -> Option<String> {
    let dot_count = module
        .chars()
        .take_while(|character| *character == '.')
        .count();
    let remainder = module[dot_count..].replace('.', "/");
    let import_path = strip_source_root(import_path);
    let mut package = parent_dir(&import_path)
        .split('/')
        .filter(|part| !part.is_empty())
        .collect::<Vec<_>>();
    let drop_count = dot_count.saturating_sub(1);
    if drop_count > package.len() {
        return None;
    }
    for _ in 0..drop_count {
        package.pop();
    }
    let base = package.join("/");
    if remainder.is_empty() {
        return (!base.is_empty()).then_some(base);
    }

    normalize_join(&base, &remainder)
}

fn parse_python_imported_names(names: &str) -> Vec<String> {
    names
        .replace(['(', ')', '\\'], " ")
        .split(',')
        .filter_map(|part| {
            let name = part
                .trim()
                .split_once(" as ")
                .map_or(part.trim(), |(name, _)| name.trim());
            let name = name.trim_start_matches('.');
            (!name.is_empty() && name != "*").then(|| name.to_owned())
        })
        .collect()
}