helm-schema-ir 0.0.4

Generate an accurate JSON schema for any helm chart
Documentation
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};

use helm_schema_ast::TemplateExpr;

use crate::abstract_value::AbstractValue;
use crate::expr_eval::literal_helper_call_callee;
use crate::fragment_expr_eval::FragmentEvalContext;
use crate::helper_meta::HelperOutputMeta;
use crate::node_eval::{NodeAction, node_action};

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum StaticTemplateSource {
    File { path: String },
    ValuesDefault { path: String, program: String },
    Constructed { program: String },
}

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct StaticTemplateProgram {
    pub(crate) source: StaticTemplateSource,
    pub(crate) dot: Option<AbstractValue>,
}

#[derive(Clone, Debug)]
pub(crate) struct LiteralHelperCall {
    pub(crate) name: String,
    pub(crate) arg: Option<TemplateExpr>,
}

pub(crate) fn literal_helper_calls_from_exprs(exprs: &[TemplateExpr]) -> Vec<LiteralHelperCall> {
    let mut out = Vec::new();
    for expr in exprs {
        expr.walk(|node| {
            let TemplateExpr::Call { function, args } = node else {
                return;
            };
            let Some(name) = literal_helper_call_callee(function, args) else {
                return;
            };
            out.push(LiteralHelperCall {
                name: name.to_string(),
                arg: args.get(1).cloned(),
            });
        });
    }
    out.sort_by(|left, right| {
        left.name
            .cmp(&right.name)
            .then_with(|| format!("{:?}", left.arg).cmp(&format!("{:?}", right.arg)))
    });
    out.dedup_by(|left, right| left.name == right.name && left.arg == right.arg);
    out
}

pub(crate) fn collect_template_requests_from_helper(
    name: &str,
    helper_dot: Option<&AbstractValue>,
    context: FragmentEvalContext<'_>,
) -> BTreeSet<StaticTemplateProgram> {
    let Some(body) = context.analysis_db.parsed_helper_body(name) else {
        return BTreeSet::new();
    };

    let locals = HashMap::new();
    let local_output_meta = HashMap::new();
    let mut requests = BTreeSet::new();
    walk_template_exprs(body.source, body.tree.root_node(), &mut |expr| {
        requests.extend(collect_template_requests_from_exprs(
            std::slice::from_ref(expr),
            helper_dot,
            &locals,
            &local_output_meta,
            context,
        ));
    });
    requests
}

pub(crate) fn collect_template_requests_from_exprs(
    exprs: &[TemplateExpr],
    current_dot: Option<&AbstractValue>,
    locals: &HashMap<String, AbstractValue>,
    local_output_meta: &HashMap<String, BTreeMap<String, HelperOutputMeta>>,
    context: FragmentEvalContext<'_>,
) -> BTreeSet<StaticTemplateProgram> {
    let mut requests = BTreeSet::new();
    for expr in exprs {
        let mut seen = HashSet::new();
        let mut resolve_fragment_value = |expr: &TemplateExpr| {
            context.fragment_value_from_expr_with_meta(
                expr,
                locals,
                local_output_meta,
                current_dot,
                &mut seen,
            )
        };
        expr.walk(|node| {
            if let TemplateExpr::Call { function, args } = node
                && function == "tpl"
                && let Some(template_arg) = args.first()
            {
                let dot = args.get(1).and_then(&mut resolve_fragment_value);
                let mut paths = BTreeSet::new();
                collect_files_get_paths(template_arg, &mut resolve_fragment_value, &mut paths);
                for path in paths {
                    requests.insert(StaticTemplateProgram {
                        source: StaticTemplateSource::File { path },
                        dot: dot.clone(),
                    });
                }
                if let Some(value) = resolve_fragment_value(template_arg) {
                    for path in value.fragment_source_paths() {
                        let Some(program) = context.analysis_db.chart_default_string(&path) else {
                            continue;
                        };
                        requests.insert(StaticTemplateProgram {
                            source: StaticTemplateSource::ValuesDefault {
                                path,
                                program: program.to_string(),
                            },
                            dot: dot.clone(),
                        });
                    }
                    for program in value.strings() {
                        if !matches!(
                            helm_schema_ast::contains_template_action(&program),
                            Ok(true)
                        ) {
                            continue;
                        }
                        requests.insert(StaticTemplateProgram {
                            source: StaticTemplateSource::Constructed { program },
                            dot: dot.clone(),
                        });
                    }
                }
            }
        });
    }
    requests
}

fn walk_template_exprs(
    source: &str,
    node: tree_sitter::Node<'_>,
    visit: &mut impl FnMut(&TemplateExpr),
) {
    match node_action(source, node) {
        NodeAction::Assignment(Some(exprs)) | NodeAction::Output(Some(exprs)) => {
            for expr in &exprs {
                visit(expr);
            }
        }
        NodeAction::If(Some(header))
        | NodeAction::With(Some(header))
        | NodeAction::Range(Some(header)) => visit(header.expr()),
        NodeAction::Text
        | NodeAction::Suppressed
        | NodeAction::Assignment(None)
        | NodeAction::If(None)
        | NodeAction::With(None)
        | NodeAction::Range(None)
        | NodeAction::Output(None)
        | NodeAction::Descend => {}
    }

    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        walk_template_exprs(source, child, visit);
    }
}

fn collect_files_get_paths<F>(
    expr: &TemplateExpr,
    resolve_fragment_value: &mut F,
    out: &mut BTreeSet<String>,
) where
    F: FnMut(&TemplateExpr) -> Option<AbstractValue>,
{
    expr.walk(|node| {
        if let TemplateExpr::Call { function, args } = node
            && is_static_files_get_call(function)
            && let Some(path_arg) = args.first()
            && let Some(binding) = resolve_fragment_value(path_arg)
        {
            out.extend(binding.strings());
        }
    });
}

fn is_static_files_get_call(function: &str) -> bool {
    function == "Files.Get" || function.ends_with(".Files.Get")
}