relay-knowledge 1.1.5

Graph-database-based knowledge graph project.
Documentation
use crate::{
    domain::{GraphVersion, SoftwareBuildTarget},
    storage::StorageError,
};

use super::{
    IndexedDocument, build_input, clean_scalar, file_name, first_call_arg, gradle_plugin,
    indentation, json_string_pair, json_string_value, key_value, push_build_target, strip_comment,
    toml_section, toml_value,
};

pub(super) fn collect(
    document: &IndexedDocument,
    graph_version: GraphVersion,
    targets: &mut Vec<SoftwareBuildTarget>,
) -> Result<(), StorageError> {
    let file_name = file_name(&document.path);
    match file_name.as_deref() {
        Some("Cargo.toml") => collect_cargo(document, graph_version, targets),
        Some("package.json") => collect_package_json(document, graph_version, targets),
        Some("pyproject.toml") => collect_pyproject(document, graph_version, targets),
        Some("go.mod") => collect_go_mod(document, graph_version, targets),
        Some("CMakeLists.txt") => collect_cmake(document, graph_version, targets),
        Some("Makefile") | Some("makefile") | Some("GNUmakefile") => {
            collect_makefile(document, graph_version, targets)
        }
        Some("build.gradle") | Some("build.gradle.kts") => {
            collect_gradle(document, graph_version, targets)
        }
        Some(".gitlab-ci.yml") | Some(".gitlab-ci.yaml") => {
            collect_ci_jobs(document, graph_version, "gitlab-ci", targets)
        }
        _ if document.path.starts_with(".github/workflows/") => {
            collect_ci_jobs(document, graph_version, "github-actions", targets)
        }
        _ => Ok(()),
    }
}

fn collect_cargo(
    document: &IndexedDocument,
    graph_version: GraphVersion,
    targets: &mut Vec<SoftwareBuildTarget>,
) -> Result<(), StorageError> {
    let mut section = "";
    for line in &document.lines {
        let trimmed = strip_comment(&line.text, '#').trim();
        if let Some(next) = toml_section(trimmed) {
            section = next;
            continue;
        }
        if matches!(section, "package" | "lib" | "bin")
            && let Some(name) = toml_value(trimmed, "name")
        {
            let kind = match section {
                "lib" => "library",
                "bin" => "binary",
                _ => "package",
            };
            push_build_target(
                targets,
                build_input(
                    document,
                    graph_version,
                    "rust",
                    kind,
                    &name,
                    "Cargo.toml",
                    line,
                ),
            )?;
        }
        if section == "features"
            && let Some((name, _)) = key_value(trimmed, '=')
        {
            push_build_target(
                targets,
                build_input(
                    document,
                    graph_version,
                    "rust",
                    "feature",
                    name,
                    "Cargo.toml",
                    line,
                ),
            )?;
        }
    }
    Ok(())
}

fn collect_package_json(
    document: &IndexedDocument,
    graph_version: GraphVersion,
    targets: &mut Vec<SoftwareBuildTarget>,
) -> Result<(), StorageError> {
    let mut in_scripts = false;
    for line in &document.lines {
        let trimmed = line.text.trim();
        if let Some(name) = json_string_value(trimmed, "name") {
            push_build_target(
                targets,
                build_input(
                    document,
                    graph_version,
                    "npm",
                    "package",
                    &name,
                    "package.json",
                    line,
                ),
            )?;
        }
        if trimmed.starts_with("\"scripts\"") && trimmed.contains('{') {
            in_scripts = true;
            continue;
        }
        if in_scripts && trimmed.starts_with('}') {
            in_scripts = false;
        }
        if in_scripts && let Some((name, command)) = json_string_pair(trimmed) {
            let mut input = build_input(
                document,
                graph_version,
                "npm",
                "script",
                &name,
                "package.json",
                line,
            );
            input.command = Some(command);
            push_build_target(targets, input)?;
        }
    }
    Ok(())
}

fn collect_pyproject(
    document: &IndexedDocument,
    graph_version: GraphVersion,
    targets: &mut Vec<SoftwareBuildTarget>,
) -> Result<(), StorageError> {
    let mut section = "";
    for line in &document.lines {
        let trimmed = strip_comment(&line.text, '#').trim();
        if let Some(next) = toml_section(trimmed) {
            section = next;
            continue;
        }
        if matches!(section, "project" | "tool.poetry")
            && let Some(name) = toml_value(trimmed, "name")
        {
            push_build_target(
                targets,
                build_input(
                    document,
                    graph_version,
                    "python",
                    "package",
                    &name,
                    "pyproject.toml",
                    line,
                ),
            )?;
        }
        if matches!(section, "project.scripts" | "tool.poetry.scripts")
            && let Some((name, command)) = key_value(trimmed, '=')
        {
            let mut input = build_input(
                document,
                graph_version,
                "python",
                "script",
                name,
                "pyproject.toml",
                line,
            );
            input.command = Some(clean_scalar(command));
            push_build_target(targets, input)?;
        }
    }
    Ok(())
}

fn collect_go_mod(
    document: &IndexedDocument,
    graph_version: GraphVersion,
    targets: &mut Vec<SoftwareBuildTarget>,
) -> Result<(), StorageError> {
    for line in &document.lines {
        if let Some(module) = line.text.trim().strip_prefix("module ").map(str::trim) {
            push_build_target(
                targets,
                build_input(
                    document,
                    graph_version,
                    "go",
                    "module",
                    module,
                    "go.mod",
                    line,
                ),
            )?;
        }
    }
    Ok(())
}

fn collect_cmake(
    document: &IndexedDocument,
    graph_version: GraphVersion,
    targets: &mut Vec<SoftwareBuildTarget>,
) -> Result<(), StorageError> {
    for line in &document.lines {
        let trimmed = strip_comment(&line.text, '#').trim();
        for (prefix, kind) in [
            ("project(", "project"),
            ("add_executable(", "executable"),
            ("add_library(", "library"),
        ] {
            if let Some(name) = first_call_arg(trimmed, prefix) {
                push_build_target(
                    targets,
                    build_input(
                        document,
                        graph_version,
                        "cmake",
                        kind,
                        &name,
                        "CMakeLists.txt",
                        line,
                    ),
                )?;
            }
        }
    }
    Ok(())
}

fn collect_makefile(
    document: &IndexedDocument,
    graph_version: GraphVersion,
    targets: &mut Vec<SoftwareBuildTarget>,
) -> Result<(), StorageError> {
    for line in &document.lines {
        let trimmed = strip_comment(&line.text, '#').trim();
        if trimmed.starts_with('.') || trimmed.starts_with('\t') || trimmed.contains('=') {
            continue;
        }
        let Some((target, _)) = key_value(trimmed, ':') else {
            continue;
        };
        if target.is_empty() || target.contains('%') || target.contains(' ') {
            continue;
        }
        push_build_target(
            targets,
            build_input(
                document,
                graph_version,
                "make",
                "target",
                target,
                "Makefile",
                line,
            ),
        )?;
    }
    Ok(())
}

fn collect_gradle(
    document: &IndexedDocument,
    graph_version: GraphVersion,
    targets: &mut Vec<SoftwareBuildTarget>,
) -> Result<(), StorageError> {
    for line in &document.lines {
        let trimmed = line
            .text
            .split_once("//")
            .map_or(line.text.as_str(), |(value, _)| value)
            .trim();
        if let Some(name) = first_call_arg(trimmed, "rootProject.name =") {
            push_build_target(
                targets,
                build_input(
                    document,
                    graph_version,
                    "gradle",
                    "project",
                    &name,
                    "gradle",
                    line,
                ),
            )?;
        } else if let Some(name) = first_call_arg(trimmed, "tasks.register(") {
            push_build_target(
                targets,
                build_input(
                    document,
                    graph_version,
                    "gradle",
                    "task",
                    &name,
                    "gradle",
                    line,
                ),
            )?;
        } else if let Some(plugin) = gradle_plugin(trimmed) {
            push_build_target(
                targets,
                build_input(
                    document,
                    graph_version,
                    "gradle",
                    "plugin",
                    &plugin,
                    "gradle",
                    line,
                ),
            )?;
        }
    }
    Ok(())
}

fn collect_ci_jobs(
    document: &IndexedDocument,
    graph_version: GraphVersion,
    source_kind: &str,
    targets: &mut Vec<SoftwareBuildTarget>,
) -> Result<(), StorageError> {
    let mut in_jobs = false;
    for line in &document.lines {
        let trimmed = strip_comment(&line.text, '#').trim();
        if trimmed == "jobs:" || trimmed == "stages:" {
            in_jobs = true;
            continue;
        }
        if in_jobs && !line.text.starts_with(' ') && trimmed.ends_with(':') {
            in_jobs = false;
        }
        if in_jobs
            && indentation(&line.text) == 2
            && let Some(name) = trimmed.strip_suffix(':')
            && !name.starts_with('-')
        {
            push_build_target(
                targets,
                build_input(
                    document,
                    graph_version,
                    source_kind,
                    "job",
                    name,
                    source_kind,
                    line,
                ),
            )?;
        }
    }
    Ok(())
}