omena-scss-eval 0.3.0

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

use cstree::syntax::SyntaxNode;
use omena_abstract_value::AbstractCssValueV0;
use omena_parser::{ParsedSassSymbolFactKind, StyleDialect, collect_style_facts, parse};
use omena_syntax::SyntaxKind;

use super::analysis_model::ScssGlobalVariableDeclaration;
use super::call_resolution::canonical_scss_callable_name;
use super::header_values::static_scss_header_abstract_value;
use super::symbol_candidates::cst_variable_declaration_parts;
use super::variables::canonical_scss_variable_name;

pub(super) fn collect_lexical_scss_bindings(
    source: &str,
    dialect: StyleDialect,
) -> LexicalScssBindings {
    let parsed = parse(source, dialect);
    let syntax = parsed.syntax();
    let Some(scopes) = collect_lexical_scss_scopes_from_cst(source.len(), &syntax) else {
        return LexicalScssBindings::new(Vec::new());
    };
    let facts = collect_style_facts(source, dialect);
    let mut bindings = LexicalScssBindings::new(scopes);
    for symbol in &facts.sass_symbols {
        match symbol.kind {
            ParsedSassSymbolFactKind::FunctionDeclaration => bindings.push_callable(
                LexicalScssCallableKind::Function,
                symbol.name.as_str(),
                symbol.range.start().into(),
            ),
            ParsedSassSymbolFactKind::MixinDeclaration => bindings.push_callable(
                LexicalScssCallableKind::Mixin,
                symbol.name.as_str(),
                symbol.range.start().into(),
            ),
            ParsedSassSymbolFactKind::FunctionCall
            | ParsedSassSymbolFactKind::MixinInclude
            | ParsedSassSymbolFactKind::VariableDeclaration
            | ParsedSassSymbolFactKind::VariableReference => {}
        }
    }
    for declaration in cst_scss_variable_declaration_nodes(&syntax) {
        let Some((name, declaration_start, _, value_text)) =
            cst_variable_declaration_parts(source, declaration)
        else {
            continue;
        };
        if value_text.is_empty() {
            continue;
        }
        let Some(scope_id) = lexical_scss_scope_for_position(&bindings.scopes, declaration_start)
        else {
            continue;
        };
        bindings.push(
            name.as_str(),
            declaration_start,
            scope_id,
            static_scss_header_abstract_value(value_text.as_str()),
        );
    }
    bindings
}

pub(super) fn collect_scss_global_variable_declarations(
    source: &str,
    dialect: StyleDialect,
) -> Vec<ScssGlobalVariableDeclaration> {
    let parsed = parse(source, dialect);
    let syntax = parsed.syntax();
    let Some(scopes) = collect_lexical_scss_scopes_from_cst(source.len(), &syntax) else {
        return Vec::new();
    };
    cst_scss_variable_declaration_nodes(&syntax)
        .filter_map(|declaration| {
            let (name, declaration_start, _, _) =
                cst_variable_declaration_parts(source, declaration)?;
            let scope_id = lexical_scss_scope_for_position(&scopes, declaration_start)?;
            (scope_id == 0).then(|| ScssGlobalVariableDeclaration {
                name: canonical_scss_variable_name(name.as_str()),
                declaration_start,
            })
        })
        .collect()
}

pub(super) fn scss_global_variable_metadata_exists(
    name: &str,
    position: usize,
    declarations: &[ScssGlobalVariableDeclaration],
) -> Option<bool> {
    let canonical_name = canonical_scss_variable_name(name);
    if declarations.iter().any(|declaration| {
        declaration.name == canonical_name && declaration.declaration_start <= position
    }) {
        return Some(true);
    }
    if declarations.iter().any(|declaration| {
        declaration.name == canonical_name && declaration.declaration_start > position
    }) {
        return None;
    }
    Some(false)
}

pub(super) fn static_scss_metadata_exists_call_may_need_resolution(value: &str) -> bool {
    const NAMES: [&str; 8] = [
        "meta.function-exists(",
        "function-exists(",
        "meta.mixin-exists(",
        "mixin-exists(",
        "meta.variable-exists(",
        "variable-exists(",
        "meta.global-variable-exists(",
        "global-variable-exists(",
    ];
    let lower = value.to_ascii_lowercase();
    NAMES.iter().any(|name| lower.contains(name))
}

fn collect_lexical_scss_scopes_from_cst(
    source_len: usize,
    root: &SyntaxNode<SyntaxKind>,
) -> Option<Vec<LexicalScssScope>> {
    let mut scopes = vec![LexicalScssScope {
        parent_id: None,
        body_start: 0,
        end: source_len,
    }];
    let mut stack = vec![0usize];
    for token in root
        .descendants_with_tokens()
        .filter_map(|element| element.into_token())
    {
        match token.kind() {
            SyntaxKind::LeftBrace | SyntaxKind::SassIndent => {
                let parent_id = *stack.last()?;
                let scope_id = scopes.len();
                scopes.push(LexicalScssScope {
                    parent_id: Some(parent_id),
                    body_start: u32::from(token.text_range().end()) as usize,
                    end: source_len,
                });
                stack.push(scope_id);
            }
            SyntaxKind::RightBrace | SyntaxKind::SassDedent => {
                let scope_id = stack.pop()?;
                if scope_id == 0 {
                    return None;
                }
                scopes.get_mut(scope_id)?.end = u32::from(token.text_range().start()) as usize;
            }
            _ => {}
        }
    }

    (stack.len() == 1).then_some(scopes)
}

fn cst_scss_variable_declaration_nodes(
    root: &SyntaxNode<SyntaxKind>,
) -> impl Iterator<Item = &SyntaxNode<SyntaxKind>> {
    root.descendants()
        .filter(|node| node.kind() == SyntaxKind::ScssVariableDeclaration)
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(super) struct LexicalScssBindings {
    bindings: Vec<LexicalScssBinding>,
    callables: Vec<LexicalScssCallableDeclaration>,
    scopes: Vec<LexicalScssScope>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct LexicalScssBinding {
    name: String,
    declaration_start: usize,
    scope_id: usize,
    value: AbstractCssValueV0,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LexicalScssCallableKind {
    Function,
    Mixin,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct LexicalScssCallableDeclaration {
    kind: LexicalScssCallableKind,
    name: String,
    declaration_start: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct LexicalScssScope {
    parent_id: Option<usize>,
    body_start: usize,
    end: usize,
}

impl LexicalScssBindings {
    fn new(scopes: Vec<LexicalScssScope>) -> Self {
        Self {
            bindings: Vec::new(),
            callables: Vec::new(),
            scopes,
        }
    }

    fn push(
        &mut self,
        name: &str,
        declaration_start: usize,
        scope_id: usize,
        value: AbstractCssValueV0,
    ) {
        self.bindings.push(LexicalScssBinding {
            name: canonical_scss_variable_name(name),
            declaration_start,
            scope_id,
            value,
        });
    }

    pub(super) fn push_root_binding(&mut self, name: &str, value: AbstractCssValueV0) {
        self.push(name, 0, 0, value);
    }

    fn push_callable(
        &mut self,
        kind: LexicalScssCallableKind,
        name: &str,
        declaration_start: usize,
    ) {
        self.callables.push(LexicalScssCallableDeclaration {
            kind,
            name: canonical_scss_callable_name(name),
            declaration_start,
        });
    }

    pub(super) fn visible_function_metadata_exists(
        &self,
        name: &str,
        position: usize,
    ) -> Option<bool> {
        self.visible_callable_metadata_exists(LexicalScssCallableKind::Function, name, position)
    }

    pub(super) fn visible_mixin_metadata_exists(
        &self,
        name: &str,
        position: usize,
    ) -> Option<bool> {
        self.visible_callable_metadata_exists(LexicalScssCallableKind::Mixin, name, position)
    }

    fn visible_callable_metadata_exists(
        &self,
        kind: LexicalScssCallableKind,
        name: &str,
        position: usize,
    ) -> Option<bool> {
        let canonical_name = canonical_scss_callable_name(name);
        self.callables
            .iter()
            .any(|callable| {
                callable.kind == kind
                    && callable.name == canonical_name
                    && callable.declaration_start <= position
            })
            .then_some(true)
    }

    pub(super) fn visible_at(&self, position: usize) -> BTreeMap<String, AbstractCssValueV0> {
        let Some(scope_id) = lexical_scss_scope_for_position(&self.scopes, position) else {
            return BTreeMap::new();
        };
        let mut visible = BTreeMap::new();
        for binding in self.bindings.iter() {
            if binding.declaration_start > position {
                continue;
            }
            if lexical_scss_scope_is_ancestor_or_self(&self.scopes, binding.scope_id, scope_id) {
                visible.insert(binding.name.clone(), binding.value.clone());
            } else {
                visible.insert(binding.name.clone(), AbstractCssValueV0::Top);
            }
        }
        visible
    }

    pub(super) fn visible_variable_metadata_exists(
        &self,
        name: &str,
        position: usize,
    ) -> Option<bool> {
        let canonical_name = canonical_scss_variable_name(name);
        let scope_id = lexical_scss_scope_for_position(&self.scopes, position)?;
        if self.bindings.iter().any(|binding| {
            binding.name == canonical_name
                && binding.declaration_start <= position
                && lexical_scss_scope_is_ancestor_or_self(&self.scopes, binding.scope_id, scope_id)
        }) {
            return Some(true);
        }
        if self.bindings.iter().any(|binding| {
            binding.name == canonical_name
                && binding.declaration_start > position
                && lexical_scss_scope_is_ancestor_or_self(&self.scopes, binding.scope_id, scope_id)
        }) {
            return None;
        }
        Some(false)
    }

    pub(super) fn global_variable_metadata_exists(
        &self,
        name: &str,
        position: usize,
    ) -> Option<bool> {
        let canonical_name = canonical_scss_variable_name(name);
        if self.bindings.iter().any(|binding| {
            binding.name == canonical_name
                && binding.scope_id == 0
                && binding.declaration_start <= position
        }) {
            return Some(true);
        }
        if self.bindings.iter().any(|binding| {
            binding.name == canonical_name
                && binding.scope_id == 0
                && binding.declaration_start > position
        }) {
            return None;
        }
        Some(false)
    }
}

fn lexical_scss_scope_for_position(scopes: &[LexicalScssScope], position: usize) -> Option<usize> {
    scopes
        .iter()
        .enumerate()
        .rev()
        .find_map(|(scope_id, scope)| {
            (position >= scope.body_start && position < scope.end).then_some(scope_id)
        })
}

fn lexical_scss_scope_is_ancestor_or_self(
    scopes: &[LexicalScssScope],
    ancestor_id: usize,
    mut scope_id: usize,
) -> bool {
    loop {
        if scope_id == ancestor_id {
            return true;
        }
        let Some(parent_id) = scopes.get(scope_id).and_then(|scope| scope.parent_id) else {
            return false;
        };
        scope_id = parent_id;
    }
}