relay-knowledge 1.1.6

Graph-database-based knowledge graph project.
Documentation
use std::path::Path;

use tree_sitter::Language;

use super::super::languages::LanguageSpec;

const EMPTY_TAGS_QUERY: &str = "";

pub(in crate::code) fn detect(path: &str) -> Option<LanguageSpec> {
    let file_name = Path::new(path).file_name()?.to_str()?;
    if let Some(spec) = language_for_file_name(path, file_name) {
        return Some(spec);
    }

    let extension = Path::new(path).extension()?.to_str()?.to_ascii_lowercase();
    language_for_extension(path, &extension)
}

pub(super) fn static_template_language(path: &str) -> Option<&'static str> {
    let extension = Path::new(path).extension()?.to_str()?.to_ascii_lowercase();
    match extension.as_str() {
        "json" => Some("json"),
        "toml" => Some("toml"),
        "yaml" | "yml" => Some("yaml"),
        _ => None,
    }
}

pub(in crate::code) fn recoverable_parse_error(language_id: &str, content: &str) -> bool {
    language_id == "cmake" && cmake_recoverable_content(content)
}

pub(in crate::code) fn manual_parse_status(language_id: &str, content: &str) -> bool {
    match language_id {
        "gotemplate" => gotemplate_actions_balanced(content),
        "ninja" => ninja_manifest_shape(content),
        _ => false,
    }
}

fn cmake_recoverable_content(content: &str) -> bool {
    let mut balance = 0i32;
    for line in content.lines() {
        let code = line.split('#').next().unwrap_or(line).trim();
        if code.is_empty() {
            continue;
        }
        if balance == 0 && !cmake_command_start(code) {
            return false;
        }
        for character in code.chars() {
            match character {
                '(' => balance += 1,
                ')' => balance -= 1,
                _ => {}
            }
            if balance < 0 {
                return false;
            }
        }
    }

    balance == 0
}

fn cmake_command_start(line: &str) -> bool {
    let Some((command, _)) = line.split_once('(') else {
        return false;
    };
    let command = command.trim();
    !command.is_empty()
        && command
            .chars()
            .all(|character| character.is_ascii_alphanumeric() || character == '_')
}

fn gotemplate_actions_balanced(content: &str) -> bool {
    let mut rest = content;
    loop {
        let Some(start) = rest.find("{{") else {
            return !rest.contains("}}");
        };
        if rest[..start].contains("}}") {
            return false;
        }
        let after_start = &rest[start + "{{".len()..];
        let Some(end) = after_start.find("}}") else {
            return false;
        };
        rest = &after_start[end + "}}".len()..];
    }
}

fn ninja_manifest_shape(content: &str) -> bool {
    let mut has_manifest_line = false;
    for line in content.lines() {
        let trimmed = line.split('#').next().unwrap_or(line).trim();
        if trimmed.is_empty() || line.starts_with(' ') || line.starts_with('\t') {
            continue;
        }
        if let Some(rest) = trimmed.strip_prefix("build ") {
            if !rest.contains(':') {
                return false;
            }
        } else if !ninja_declaration_line(trimmed) {
            return false;
        }
        has_manifest_line = true;
    }

    has_manifest_line
}

fn ninja_declaration_line(line: &str) -> bool {
    line.split_once('=').is_some()
        || ["rule ", "include ", "subninja ", "default ", "pool "]
            .iter()
            .any(|prefix| line.starts_with(prefix))
}

fn language_for_file_name(path: &str, file_name: &str) -> Option<LanguageSpec> {
    match file_name {
        "BUILD" | "BUILD.bazel" | "WORKSPACE" | "WORKSPACE.bazel" | "MODULE.bazel" => {
            Some(spec("starlark", || tree_sitter_starlark::LANGUAGE.into()))
        }
        "Makefile" | "GNUmakefile" | "BSDmakefile" => {
            Some(spec("make", || tree_sitter_make::LANGUAGE.into()))
        }
        "CMakeLists.txt" => Some(spec("cmake", || tree_sitter_cmake::LANGUAGE.into())),
        "Dockerfile" | "Containerfile" => Some(dockerfile()),
        "go.mod" => Some(spec("gomod", || tree_sitter_gomod_orchard::LANGUAGE.into())),
        "build.ninja" => Some(spec("ninja", || tree_sitter_make::LANGUAGE.into())),
        _ if file_name.starts_with("Dockerfile.")
            || file_name.starts_with("Containerfile.")
            || file_name.ends_with(".Dockerfile")
            || file_name.ends_with(".Containerfile") =>
        {
            Some(dockerfile())
        }
        _ if template_directory_path(path) && content_template_name(file_name) => {
            Some(spec("gotemplate", || tree_sitter_jinja2::LANGUAGE.into()))
        }
        _ => None,
    }
}

fn language_for_extension(path: &str, extension: &str) -> Option<LanguageSpec> {
    match extension {
        "md" | "markdown" => Some(spec("markdown", || tree_sitter_md::LANGUAGE.into())),
        "xml" | "xsd" | "xsl" | "xslt" => {
            Some(spec("xml", || tree_sitter_xml::LANGUAGE_XML.into()))
        }
        "bzl" => Some(spec("starlark", || tree_sitter_starlark::LANGUAGE.into())),
        "mk" => Some(spec("make", || tree_sitter_make::LANGUAGE.into())),
        "cmake" => Some(spec("cmake", || tree_sitter_cmake::LANGUAGE.into())),
        "properties" => Some(spec("properties", || {
            tree_sitter_properties::LANGUAGE.into()
        })),
        "toml" => Some(spec("toml", || tree_sitter_toml_ng::LANGUAGE.into())),
        "conf" | "ini" | "cfg" => Some(spec("ini", || tree_sitter_ini::LANGUAGE.into())),
        "yaml" | "yml" => Some(spec("yaml", || tree_sitter_yaml::LANGUAGE.into())),
        "json" => Some(spec("json", || tree_sitter_json::LANGUAGE.into())),
        "ninja" => Some(spec("ninja", || tree_sitter_make::LANGUAGE.into())),
        "j2" | "jinja" | "jinja2" => Some(spec("jinja2", || tree_sitter_jinja2::LANGUAGE.into())),
        "gotmpl" | "tmpl" | "tpl" => {
            Some(spec("gotemplate", || tree_sitter_jinja2::LANGUAGE.into()))
        }
        _ if template_suffix(path) => Some(spec("jinja2", || tree_sitter_jinja2::LANGUAGE.into())),
        _ => None,
    }
}

fn spec(id: &'static str, language: fn() -> Language) -> LanguageSpec {
    LanguageSpec {
        id,
        language,
        tags_query: EMPTY_TAGS_QUERY,
    }
}

fn dockerfile() -> LanguageSpec {
    spec("dockerfile", || tree_sitter_containerfile::LANGUAGE.into())
}

fn template_suffix(path: &str) -> bool {
    path.ends_with(".yaml.j2")
        || path.ends_with(".yml.j2")
        || path.ends_with(".json.j2")
        || path.ends_with(".toml.j2")
        || path.ends_with(".xml.j2")
}

fn content_template_name(file_name: &str) -> bool {
    file_name.ends_with(".yaml")
        || file_name.ends_with(".yml")
        || file_name.ends_with(".json")
        || file_name.ends_with(".toml")
        || file_name.ends_with(".tpl")
        || file_name == "NOTES.txt"
}

fn template_directory_path(path: &str) -> bool {
    path.starts_with("templates/") || path.contains("/templates/")
}