omena-scss-eval 0.3.0

Oracle-first SCSS and Less value evaluator rail for Omena CSS
Documentation
use std::collections::BTreeMap;

use omena_parser::{StyleDialect, parse};
use omena_syntax::SyntaxKind;

use super::{
    SCSS_CALL_RETURN_RECURSION_LIMIT,
    cst_tokens::{cst_next_non_trivia_token_index, cst_token_ranges},
    model::{OmenaScssEvalCallReturnEdgeV0, OmenaScssEvalCallReturnNodeV0},
};

pub(super) fn static_scss_value_contains_function_call(value: &str, function_name: &str) -> bool {
    let canonical_function_name = canonical_scss_callable_name(function_name);
    let parsed = parse(value, StyleDialect::Scss);
    let root = parsed.syntax();
    let Some(tokens) = cst_token_ranges(&root) else {
        return false;
    };
    for (index, token) in tokens.iter().enumerate() {
        if token.kind != SyntaxKind::Ident
            || canonical_scss_callable_name(token.text.as_str()) != canonical_function_name
        {
            continue;
        }
        let Some(left_paren_index) = cst_next_non_trivia_token_index(tokens.as_slice(), index + 1)
        else {
            continue;
        };
        if tokens
            .get(left_paren_index)
            .is_some_and(|candidate| candidate.kind == SyntaxKind::LeftParen)
        {
            return true;
        }
    }
    false
}

pub(super) fn canonical_scss_callable_name(name: &str) -> String {
    name.trim().replace('_', "-")
}

pub(super) fn scss_visible_function_declaration_exists(
    name: &str,
    position: usize,
    nodes: &[OmenaScssEvalCallReturnNodeV0],
) -> bool {
    nodes.iter().any(|node| {
        node.kind == "functionDeclaration"
            && node.source_span_start <= position
            && node
                .name
                .as_deref()
                .is_some_and(|candidate| canonical_scss_callable_name(candidate) == name)
    })
}

pub(super) fn scss_visible_mixin_declaration_exists(
    name: &str,
    position: usize,
    nodes: &[OmenaScssEvalCallReturnNodeV0],
) -> bool {
    nodes.iter().any(|node| {
        node.kind == "mixinDeclaration"
            && node.source_span_start <= position
            && node
                .name
                .as_deref()
                .is_some_and(|candidate| canonical_scss_callable_name(candidate) == name)
    })
}

pub(super) fn max_call_stack_depth_observed(
    nodes: &[OmenaScssEvalCallReturnNodeV0],
    edges: &[OmenaScssEvalCallReturnEdgeV0],
) -> usize {
    let call_graph = declaration_call_graph(nodes, edges);
    let mut max_depth = 0usize;
    for node_key in call_graph.keys() {
        let mut stack = Vec::new();
        max_depth = max_depth.max(call_stack_depth_from(node_key, &call_graph, &mut stack));
    }
    max_depth
}

pub(super) fn declaration_call_graph(
    nodes: &[OmenaScssEvalCallReturnNodeV0],
    edges: &[OmenaScssEvalCallReturnEdgeV0],
) -> BTreeMap<String, Vec<String>> {
    let containing_declarations = nodes
        .iter()
        .filter_map(|node| {
            Some((
                node.node_key.0.clone(),
                node.containing_declaration_node_key.as_ref()?.0.clone(),
            ))
        })
        .collect::<BTreeMap<_, _>>();
    let mut graph = BTreeMap::<String, Vec<String>>::new();
    for edge in edges {
        if !matches!(edge.kind, "mixinCall" | "functionCall") {
            continue;
        }
        let Some(source_declaration) = containing_declarations.get(&edge.source_node_key.0) else {
            continue;
        };
        graph
            .entry(source_declaration.clone())
            .or_default()
            .push(edge.target_node_key.0.clone());
    }
    graph
}

pub(super) fn call_stack_depth_from(
    node_key: &str,
    graph: &BTreeMap<String, Vec<String>>,
    stack: &mut Vec<String>,
) -> usize {
    if stack.iter().any(|entry| entry == node_key) {
        return SCSS_CALL_RETURN_RECURSION_LIMIT;
    }
    if stack.len() >= SCSS_CALL_RETURN_RECURSION_LIMIT {
        return SCSS_CALL_RETURN_RECURSION_LIMIT;
    }
    stack.push(node_key.to_string());
    let depth = graph
        .get(node_key)
        .into_iter()
        .flat_map(|targets| targets.iter())
        .map(|target| call_stack_depth_from(target, graph, stack))
        .max()
        .unwrap_or(stack.len());
    stack.pop();
    depth.min(SCSS_CALL_RETURN_RECURSION_LIMIT)
}