omena-scss-eval 0.3.0

Oracle-first SCSS and Less value evaluator rail for Omena CSS
Documentation
use omena_parser::{LexedToken, ParsedSassSymbolFact, ParsedSassSymbolFactKind};
use omena_syntax::SyntaxKind;

use crate::abstract_css_value_kind;

use super::{
    analysis_model::ScssCallReturnCandidate,
    arguments::split_scss_call_arguments,
    model::{OmenaScssEvalCallLocalBindingV0, OmenaScssEvalCallParameterValueV0},
    return_candidates::static_scss_return_abstract_value,
    scanner_tokens::{
        declaration_end_token_index, matching_block_end_token_index,
        matching_right_paren_token_index, next_block_start_token_index,
        next_non_trivia_token_index, token_range_end, token_range_start,
    },
    symbol_candidates::{
        scss_call_argument_value_from_text, scss_parameter_name_from_text,
        scss_parameter_value_from_text,
    },
};

#[derive(Debug, Clone, PartialEq, Eq)]
struct ScssCallLocalBindingScope {
    end_index: usize,
    span_start: usize,
    span_end: usize,
}

pub(super) fn call_return_candidate_from_sass_symbol_scanner_oracle(
    source: &str,
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
) -> Option<ScssCallReturnCandidate> {
    let (kind, symbol_kind, role) = sass_symbol_candidate_shape(symbol)?;
    Some(ScssCallReturnCandidate {
        kind,
        symbol_kind,
        role,
        name: Some(symbol.name.clone()),
        namespace: symbol.namespace.clone(),
        parameter_names: scss_declaration_parameter_names_from_symbol(source, tokens, symbol),
        parameter_values: scss_declaration_parameter_values_from_symbol(source, tokens, symbol),
        local_binding_values: scss_declaration_local_bindings_from_symbol(source, tokens, symbol),
        argument_values: scss_call_argument_values_from_symbol(source, tokens, symbol),
        return_text: None,
        return_value: None,
        body_has_control_flow: scss_declaration_body_has_control_flow_scanner_oracle(
            tokens, symbol,
        ),
        body_has_loop_control_flow: scss_declaration_body_has_loop_control_flow_scanner_oracle(
            tokens, symbol,
        ),
        return_inside_loop_control_flow: false,
        return_loop_header_text: None,
        return_loop_header_texts: Vec::new(),
        return_loop_body_texts: Vec::new(),
        return_condition_text: None,
        return_negated_condition_texts: Vec::new(),
        source_span_start: symbol.range.start().into(),
        source_span_end: symbol.range.end().into(),
    })
}

fn sass_symbol_candidate_shape(
    symbol: &ParsedSassSymbolFact,
) -> Option<(&'static str, &'static str, &'static str)> {
    match symbol.kind {
        ParsedSassSymbolFactKind::MixinDeclaration => {
            Some(("mixinDeclaration", "mixin", "declaration"))
        }
        ParsedSassSymbolFactKind::MixinInclude => Some(("mixinInclude", "mixin", "call")),
        ParsedSassSymbolFactKind::FunctionDeclaration => {
            Some(("functionDeclaration", "function", "declaration"))
        }
        ParsedSassSymbolFactKind::FunctionCall => Some(("functionCall", "function", "call")),
        ParsedSassSymbolFactKind::VariableDeclaration
        | ParsedSassSymbolFactKind::VariableReference => None,
    }
}

fn scss_call_argument_values_from_symbol(
    source: &str,
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
) -> Vec<super::model::OmenaScssEvalCallArgumentValueV0> {
    if !matches!(
        symbol.kind,
        ParsedSassSymbolFactKind::FunctionCall | ParsedSassSymbolFactKind::MixinInclude
    ) {
        return Vec::new();
    }
    let Some(arguments) = scss_call_argument_texts_from_symbol(source, tokens, symbol) else {
        return Vec::new();
    };
    arguments
        .into_iter()
        .filter_map(|text| scss_call_argument_value_from_text(text.as_str()))
        .collect()
}

fn scss_call_argument_texts_from_symbol(
    source: &str,
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
) -> Option<Vec<String>> {
    let token_index = token_index_for_symbol_range(tokens, symbol)?;
    match symbol.kind {
        ParsedSassSymbolFactKind::FunctionCall => {
            let left_paren_index = next_non_trivia_token_index(tokens, token_index + 1)?;
            if tokens.get(left_paren_index)?.kind != SyntaxKind::LeftParen {
                return None;
            }
            let right_paren_index = matching_right_paren_token_index(tokens, left_paren_index)?;
            split_scss_call_arguments(source.get(
                token_range_end(&tokens[left_paren_index])
                    ..token_range_start(&tokens[right_paren_index]),
            )?)
        }
        ParsedSassSymbolFactKind::MixinInclude => {
            let next_index = next_non_trivia_token_index(tokens, token_index + 1)?;
            if tokens.get(next_index)?.kind == SyntaxKind::LeftParen {
                let right_paren_index = matching_right_paren_token_index(tokens, next_index)?;
                return split_scss_call_arguments(source.get(
                    token_range_end(&tokens[next_index])
                        ..token_range_start(&tokens[right_paren_index]),
                )?);
            }
            let argument_start = token_range_end(&tokens[token_index]);
            let argument_end = tokens
                .iter()
                .skip(token_index + 1)
                .find(|candidate| {
                    matches!(
                        candidate.kind,
                        SyntaxKind::Semicolon
                            | SyntaxKind::SassOptionalSemicolon
                            | SyntaxKind::SassIndentedNewline
                            | SyntaxKind::LeftBrace
                            | SyntaxKind::RightBrace
                    )
                })
                .map(token_range_start)
                .unwrap_or(argument_start);
            split_scss_call_arguments(source.get(argument_start..argument_end)?)
        }
        _ => None,
    }
}

fn scss_declaration_parameter_names_from_symbol(
    source: &str,
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
) -> Vec<String> {
    if !matches!(
        symbol.kind,
        ParsedSassSymbolFactKind::FunctionDeclaration | ParsedSassSymbolFactKind::MixinDeclaration
    ) {
        return Vec::new();
    }
    let Some(parameters) = scss_declaration_parameter_texts_from_symbol(source, tokens, symbol)
    else {
        return Vec::new();
    };
    parameters
        .into_iter()
        .filter_map(|parameter| scss_parameter_name_from_text(parameter.as_str()))
        .collect()
}

fn scss_declaration_parameter_values_from_symbol(
    source: &str,
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
) -> Vec<OmenaScssEvalCallParameterValueV0> {
    if !matches!(
        symbol.kind,
        ParsedSassSymbolFactKind::FunctionDeclaration | ParsedSassSymbolFactKind::MixinDeclaration
    ) {
        return Vec::new();
    }
    let Some(parameters) = scss_declaration_parameter_texts_from_symbol(source, tokens, symbol)
    else {
        return Vec::new();
    };
    parameters
        .into_iter()
        .filter_map(|parameter| scss_parameter_value_from_text(parameter.as_str()))
        .collect()
}

fn scss_declaration_local_bindings_from_symbol(
    source: &str,
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
) -> Vec<OmenaScssEvalCallLocalBindingV0> {
    if !matches!(symbol.kind, ParsedSassSymbolFactKind::FunctionDeclaration) {
        return Vec::new();
    }
    let Some((body_start, body_end)) = scss_declaration_body_token_range(tokens, symbol) else {
        return Vec::new();
    };
    let mut bindings = Vec::new();
    let mut scope_stack = Vec::<ScssCallLocalBindingScope>::new();
    let Some(function_scope_start) = tokens
        .get(body_start)
        .map(token_range_start)
        .or_else(|| tokens.get(body_end).map(token_range_start))
    else {
        return Vec::new();
    };
    let function_scope_end = tokens
        .get(body_end)
        .map(token_range_start)
        .unwrap_or(function_scope_start);
    let mut index = body_start;
    while index < body_end {
        while scope_stack
            .last()
            .is_some_and(|scope| index > scope.end_index)
        {
            scope_stack.pop();
        }
        let Some(token) = tokens.get(index) else {
            break;
        };
        match token.kind {
            SyntaxKind::LeftBrace | SyntaxKind::SassIndent => {
                let Some(scope_end_index) = matching_block_end_token_index(tokens, index) else {
                    index += 1;
                    continue;
                };
                scope_stack.push(ScssCallLocalBindingScope {
                    end_index: scope_end_index,
                    span_start: token_range_end(token),
                    span_end: token_range_start(&tokens[scope_end_index]),
                });
                index += 1;
                continue;
            }
            SyntaxKind::RightBrace | SyntaxKind::SassDedent => {
                if scope_stack
                    .last()
                    .is_some_and(|scope| scope.end_index == index)
                {
                    scope_stack.pop();
                }
                index += 1;
                continue;
            }
            SyntaxKind::ScssVariable => {
                let Some(colon_index) = next_non_trivia_token_index(tokens, index + 1) else {
                    index += 1;
                    continue;
                };
                if tokens.get(colon_index).map(|token| token.kind) != Some(SyntaxKind::Colon) {
                    index += 1;
                    continue;
                }
                let Some(end_index) = declaration_end_token_index(tokens, colon_index + 1) else {
                    index += 1;
                    continue;
                };
                if end_index >= body_end {
                    break;
                }
                let value_start = token_range_end(&tokens[colon_index]);
                let value_end = token_range_start(&tokens[end_index]);
                if let Some(value_text) = source.get(value_start..value_end).map(str::trim)
                    && !value_text.is_empty()
                {
                    let value = static_scss_return_abstract_value(value_text);
                    let (scope_span_start, scope_span_end) = scope_stack
                        .last()
                        .map(|scope| (scope.span_start, scope.span_end))
                        .unwrap_or((function_scope_start, function_scope_end));
                    bindings.push(OmenaScssEvalCallLocalBindingV0 {
                        name: token.text.clone(),
                        source_span_start: token.range.start().into(),
                        source_span_end: token.range.end().into(),
                        scope_span_start,
                        scope_span_end,
                        value_text: value_text.to_string(),
                        value_kind: abstract_css_value_kind(&value),
                        value,
                    });
                }
                index = end_index + 1;
                continue;
            }
            _ => {}
        }
        index += 1;
    }
    bindings
}

fn scss_declaration_body_token_range(
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
) -> Option<(usize, usize)> {
    let token_index = token_index_for_symbol_range(tokens, symbol)?;
    let block_start_index = next_block_start_token_index(tokens, token_index + 1)?;
    let block_end_index = matching_block_end_token_index(tokens, block_start_index)?;
    Some((block_start_index + 1, block_end_index))
}

fn scss_declaration_body_has_control_flow_scanner_oracle(
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
) -> bool {
    scss_declaration_body_has_matching_control_flow(tokens, symbol, |name| {
        matches!(name, "@if" | "@else" | "@for" | "@each" | "@while")
    })
}

fn scss_declaration_body_has_loop_control_flow_scanner_oracle(
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
) -> bool {
    scss_declaration_body_has_matching_control_flow(tokens, symbol, |name| {
        matches!(name, "@for" | "@each" | "@while")
    })
}

fn scss_declaration_body_has_matching_control_flow(
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
    matches_name: impl Fn(&str) -> bool,
) -> bool {
    if !matches!(
        symbol.kind,
        ParsedSassSymbolFactKind::FunctionDeclaration | ParsedSassSymbolFactKind::MixinDeclaration
    ) {
        return false;
    }
    let Some((body_start, body_end)) = scss_declaration_body_token_range(tokens, symbol) else {
        return false;
    };
    tokens
        .iter()
        .skip(body_start)
        .take(body_end.saturating_sub(body_start))
        .any(|token| {
            token.kind == SyntaxKind::AtKeyword
                && matches_name(token.text.to_ascii_lowercase().as_str())
        })
}

fn scss_declaration_parameter_texts_from_symbol(
    source: &str,
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
) -> Option<Vec<String>> {
    let token_index = token_index_for_symbol_range(tokens, symbol)?;
    let left_paren_index = next_non_trivia_token_index(tokens, token_index + 1)?;
    if tokens.get(left_paren_index)?.kind != SyntaxKind::LeftParen {
        return Some(Vec::new());
    }
    let right_paren_index = matching_right_paren_token_index(tokens, left_paren_index)?;
    split_scss_call_arguments(source.get(
        token_range_end(&tokens[left_paren_index])..token_range_start(&tokens[right_paren_index]),
    )?)
}

fn token_index_for_symbol_range(
    tokens: &[LexedToken],
    symbol: &ParsedSassSymbolFact,
) -> Option<usize> {
    let start: usize = symbol.range.start().into();
    let end: usize = symbol.range.end().into();
    tokens
        .iter()
        .enumerate()
        .find_map(|(index, token)| {
            (token_range_start(token) == start && token_range_end(token) == end).then_some(index)
        })
        .or_else(|| {
            tokens.iter().enumerate().find_map(|(index, token)| {
                (token_range_start(token) <= start
                    && start < token_range_end(token)
                    && token.text.ends_with(symbol.name.as_str()))
                .then_some(index)
            })
        })
        .or_else(|| {
            tokens.iter().enumerate().find_map(|(index, token)| {
                (token_range_start(token) >= start
                    && token_range_end(token) <= end
                    && token.text.ends_with(symbol.name.as_str()))
                .then_some(index)
            })
        })
}