relay-knowledge 1.1.16

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

use super::super::{generated_detection, languages::language_id, safe_git_blob_path};
use super::SourceGrepRequest;

pub(crate) const SOURCE_GREP_CANDIDATE_FILE_LIMIT: usize = 256;
const SOURCE_GREP_MATCHES_PER_CANDIDATE_FILE: usize = 2;
const SOURCE_GREP_CANDIDATE_MATCH_LIMIT: usize =
    SOURCE_GREP_CANDIDATE_FILE_LIMIT * SOURCE_GREP_MATCHES_PER_CANDIDATE_FILE;

pub(crate) fn bounded_source_grep_candidate_match_limit(
    result_limit: usize,
    candidate_path_count: usize,
) -> usize {
    let fair_candidate_limit = candidate_path_count
        .min(SOURCE_GREP_CANDIDATE_FILE_LIMIT)
        .saturating_mul(SOURCE_GREP_MATCHES_PER_CANDIDATE_FILE);

    result_limit
        .max(fair_candidate_limit)
        .min(SOURCE_GREP_CANDIDATE_MATCH_LIMIT)
}

pub(super) struct CandidatePaths {
    pub(super) paths: Vec<String>,
    pub(super) degraded_reason: Option<String>,
}

pub(super) fn selected_candidate_paths(request: &SourceGrepRequest) -> CandidatePaths {
    let mut paths = Vec::new();
    let mut seen = BTreeSet::new();
    let mut exhausted = false;
    for path in &request.paths {
        if paths.len() >= SOURCE_GREP_CANDIDATE_FILE_LIMIT {
            exhausted = true;
            break;
        }
        if !safe_git_blob_path(path) || !path_filter_allows(path, &request.path_filters) {
            continue;
        }
        if request.exclude_generated && generated_detection::path_has_generated_signal(path) {
            continue;
        }
        let language = language_id(path).unwrap_or("unknown");
        if !language_filter_allows(path, language, &request.language_filters) {
            continue;
        }
        if seen.insert(path.clone()) {
            paths.push(path.clone());
        }
    }
    CandidatePaths {
        paths,
        degraded_reason: exhausted
            .then(|| "source fallback candidate file budget exhausted".to_owned()),
    }
}

fn path_filter_allows(path: &str, filters: &[String]) -> bool {
    filters.is_empty()
        || filters.iter().any(|filter| {
            let filter = normalize_filter_path(filter);
            filter == "." || path == filter || path.starts_with(&format!("{filter}/"))
        })
}

fn language_filter_allows(path: &str, language_id: &str, filters: &[String]) -> bool {
    filters.is_empty()
        || filters.iter().any(|filter| {
            filter == language_id
                || c_cpp_header_only_filter_allows(path, language_id, filter)
                || cxx_header_filter_allows(path, language_id, filter)
                || unknown_filter_allows_document_path(path, language_id, filter)
        })
}

fn c_cpp_header_only_filter_allows(path: &str, language_id: &str, filter: &str) -> bool {
    filter == "__relay_c_cpp_header_only__"
        && matches!(language_id, "c" | "cpp")
        && c_cpp_header_path(path)
}

fn cxx_header_filter_allows(path: &str, language_id: &str, filter: &str) -> bool {
    filter == "cpp" && language_id == "c" && path.to_ascii_lowercase().ends_with(".h")
}

fn c_cpp_header_path(path: &str) -> bool {
    matches!(
        std::path::Path::new(path)
            .extension()
            .and_then(|extension| extension.to_str())
            .map(str::to_ascii_lowercase)
            .as_deref(),
        Some("h" | "hh" | "hpp" | "hxx")
    )
}

fn unknown_filter_allows_document_path(path: &str, language_id: &str, filter: &str) -> bool {
    filter == "unknown" && document_like_language_path(path, language_id)
}

fn document_like_language_path(path: &str, language_id: &str) -> bool {
    matches!(
        language_id,
        "markdown" | "json" | "yaml" | "toml" | "xml" | "ini" | "properties"
    ) || matches!(
        path.rsplit('.')
            .next()
            .map(str::to_ascii_lowercase)
            .as_deref(),
        Some("md" | "markdown" | "txt" | "rst" | "adoc")
    )
}

fn normalize_filter_path(filter: &str) -> &str {
    let mut filter = filter.trim_end_matches(['/', '\\']);
    while let Some(stripped) = filter.strip_prefix("./") {
        filter = stripped;
    }

    filter
}

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