relay-knowledge 1.1.9

Graph-database-based knowledge graph project.
Documentation
use super::super::{
    model::{ConfigFact, ConfigImport, ConfigRange, ConfigReference},
    source::{push_definition, push_import, push_reference, source_lines},
};

pub(in crate::code::config_files) fn facts(
    content: &str,
    definitions: &mut Vec<ConfigFact>,
    references: &mut Vec<ConfigReference>,
) {
    let mut stages = Vec::<String>::new();
    for line in logical_lines(content) {
        let words = line.text.split_whitespace().collect::<Vec<_>>();
        if words
            .first()
            .is_some_and(|word| word.eq_ignore_ascii_case("FROM"))
        {
            if let Some(stage) = stage_name(&words) {
                push_definition(definitions, stage, "stage", line.range);
                stages.push(stage.to_owned());
            }
        }
        for source in copy_from_sources(&words) {
            if stages.iter().any(|stage| stage == source) {
                push_reference(references, source, "stage", line.range);
            }
        }
    }
}

pub(in crate::code::config_files) fn imports(content: &str, imports: &mut Vec<ConfigImport>) {
    let mut stages = Vec::<String>::new();
    for line in logical_lines(content) {
        let words = line.text.split_whitespace().collect::<Vec<_>>();
        if words
            .first()
            .is_some_and(|word| word.eq_ignore_ascii_case("FROM"))
        {
            if let Some(image) = from_image(&words)
                && !stages.iter().any(|stage| stage == image)
            {
                push_import(imports, image, line.range);
            }
            if let Some(stage) = stage_name(&words) {
                stages.push(stage.to_owned());
            }
        }
        for source in copy_from_sources(&words) {
            if !stages.iter().any(|stage| stage == source) {
                push_import(imports, source, line.range);
            }
        }
    }
}

struct LogicalLine {
    text: String,
    range: ConfigRange,
}

fn logical_lines(content: &str) -> Vec<LogicalLine> {
    let mut lines = Vec::new();
    let mut text = String::new();
    let mut range = None;
    for line in source_lines(content) {
        let raw = line.text.trim_end();
        let continued = raw.ends_with('\\');
        let segment = raw
            .strip_suffix('\\')
            .map(str::trim_end)
            .unwrap_or(raw)
            .trim();
        let current = range.get_or_insert_with(|| line.range());
        current.byte_end = line.byte_end;
        current.line_end = line.number;
        if !text.is_empty() && !segment.is_empty() {
            text.push(' ');
        }
        text.push_str(segment);
        if continued {
            continue;
        }
        if !text.is_empty() {
            lines.push(LogicalLine {
                text: std::mem::take(&mut text),
                range: range
                    .take()
                    .expect("Dockerfile logical line should have range"),
            });
        } else {
            range = None;
        }
    }
    if !text.is_empty()
        && let Some(range) = range
    {
        lines.push(LogicalLine { text, range });
    }
    lines
}

fn from_image<'a>(words: &[&'a str]) -> Option<&'a str> {
    let image = words
        .iter()
        .skip(1)
        .copied()
        .find(|word| !word.starts_with("--"))?;
    (!image.contains('$')).then_some(image)
}

fn stage_name<'a>(words: &[&'a str]) -> Option<&'a str> {
    let stage_index = words
        .iter()
        .position(|word| word.eq_ignore_ascii_case("AS"))?;
    words.get(stage_index + 1).copied()
}

fn copy_from_sources<'a>(words: &[&'a str]) -> Vec<&'a str> {
    if !words
        .first()
        .is_some_and(|word| word.eq_ignore_ascii_case("COPY"))
    {
        return Vec::new();
    }

    let mut sources = Vec::new();
    let mut index = 1usize;
    while let Some(word) = words.get(index).copied() {
        if let Some(source) = word.strip_prefix("--from=") {
            if !source.contains('$') {
                sources.push(source);
            }
        } else if word == "--from"
            && let Some(source) = words.get(index + 1).copied()
        {
            if !source.contains('$') {
                sources.push(source);
            }
            index += 1;
        }
        index += 1;
    }

    sources
}