omena-semantic 0.3.0

Semantic boundary crate for Omena CSS Modules style analysis
Documentation
use std::collections::{BTreeMap, BTreeSet};

use omena_parser::ParsedCst;
use omena_syntax::{SyntaxKind, SyntaxNode, css_keyword};

use crate::{ParserByteSpanV0, StyleLayerBlockBindingV0, StyleLayerOrderNodeV0};

pub(crate) struct LayerOrderFactsV0 {
    pub(crate) order_nodes: Vec<StyleLayerOrderNodeV0>,
    pub(crate) block_bindings: Vec<StyleLayerBlockBindingV0>,
    pub(crate) unresolved_topology_count: usize,
    pub(crate) topology_complete: bool,
}

#[derive(Clone)]
struct LayerBlockDraftV0 {
    context_id: String,
    node: SyntaxNode,
    local_path: Option<String>,
    canonical_name: Option<String>,
}

#[derive(Clone)]
struct LayerNodeDraftV0 {
    canonical_name: String,
    local_name: String,
    parent_name: Option<String>,
    first_source_order: usize,
    implicit_prefix: bool,
}

pub(crate) fn summarize_layer_order_from_cst(_source: &str, cst: &ParsedCst) -> LayerOrderFactsV0 {
    let mut all_context_order = 0usize;
    let mut blocks = Vec::<LayerBlockDraftV0>::new();
    for node in cst.root().descendants().filter(|node| {
        matches!(
            node.kind(),
            SyntaxKind::LayerRule | SyntaxKind::ContainerRule | SyntaxKind::ScopeRule
        ) && node_has_block(node)
    }) {
        if node.kind() == SyntaxKind::LayerRule {
            let names = layer_names(node);
            blocks.push(LayerBlockDraftV0 {
                context_id: format!("layer:{all_context_order}"),
                node: node.clone(),
                local_path: (names.len() == 1).then(|| names[0].clone()),
                canonical_name: None,
            });
        }
        all_context_order = all_context_order.saturating_add(1);
    }

    blocks.sort_by_key(|block| {
        let range = block.node.text_range();
        (
            u32::from(range.start()) as usize,
            usize::MAX.saturating_sub(u32::from(range.end()) as usize),
        )
    });

    let mut unresolved_topology_count = 0usize;
    for index in 0..blocks.len() {
        let parent = nearest_enclosing_block(index, blocks.as_slice());
        let parent_name = parent.and_then(|parent| blocks[parent].canonical_name.as_deref());
        let Some(local_path) = blocks[index].local_path.as_deref() else {
            unresolved_topology_count = unresolved_topology_count.saturating_add(1);
            continue;
        };
        if parent.is_some() && parent_name.is_none() {
            unresolved_topology_count = unresolved_topology_count.saturating_add(1);
            continue;
        }
        blocks[index].canonical_name = canonical_layer_path(parent_name, local_path);
        if blocks[index].canonical_name.is_none() {
            unresolved_topology_count = unresolved_topology_count.saturating_add(1);
        }
    }

    let mut events = cst
        .root()
        .descendants()
        .filter(|node| node.kind() == SyntaxKind::LayerRule)
        .collect::<Vec<_>>();
    events.sort_by_key(|node| u32::from(node.text_range().start()) as usize);

    let mut nodes = BTreeMap::<String, LayerNodeDraftV0>::new();
    let mut source_order = 0usize;
    for event in events {
        let parent = nearest_enclosing_block_for_node(event, blocks.as_slice());
        if parent.is_some_and(|block| block.canonical_name.is_none()) {
            unresolved_topology_count = unresolved_topology_count.saturating_add(1);
            continue;
        }
        let parent_name = parent.and_then(|block| block.canonical_name.clone());
        let names = layer_names(event);
        let has_block = node_has_block(event);
        if names.is_empty() {
            if !has_block {
                unresolved_topology_count = unresolved_topology_count.saturating_add(1);
            }
            continue;
        }
        if has_block && names.len() != 1 {
            continue;
        }
        for name in names {
            let Some(canonical_name) = canonical_layer_path(parent_name.as_deref(), name.as_str())
            else {
                unresolved_topology_count = unresolved_topology_count.saturating_add(1);
                continue;
            };
            register_layer_path(&mut nodes, canonical_name.as_str(), source_order);
            source_order = source_order.saturating_add(1);
        }
    }

    let ranks = cascade_ranks(nodes.values());
    let mut order_nodes = nodes
        .into_values()
        .map(|node| StyleLayerOrderNodeV0 {
            cascade_rank: ranks
                .get(node.canonical_name.as_str())
                .copied()
                .unwrap_or(0),
            nesting_depth: node.canonical_name.split('.').count().saturating_sub(1),
            canonical_name: node.canonical_name,
            local_name: node.local_name,
            parent_name: node.parent_name,
            first_source_order: node.first_source_order,
            implicit_prefix: node.implicit_prefix,
        })
        .collect::<Vec<_>>();
    order_nodes.sort_by_key(|node| node.cascade_rank);

    let mut block_bindings = blocks
        .iter()
        .filter_map(|block| {
            let canonical_name = block.canonical_name.as_ref()?;
            let range = block.node.text_range();
            Some(StyleLayerBlockBindingV0 {
                context_id: block.context_id.clone(),
                canonical_name: canonical_name.clone(),
                cascade_rank: ranks.get(canonical_name.as_str()).copied().unwrap_or(0),
                nesting_depth: canonical_name.split('.').count().saturating_sub(1),
                byte_span: ParserByteSpanV0 {
                    start: u32::from(range.start()) as usize,
                    end: u32::from(range.end()) as usize,
                },
            })
        })
        .collect::<Vec<_>>();
    block_bindings.sort_by_key(|binding| (binding.byte_span.start, binding.byte_span.end));

    LayerOrderFactsV0 {
        topology_complete: unresolved_topology_count == 0,
        order_nodes,
        block_bindings,
        unresolved_topology_count,
    }
}

fn nearest_enclosing_block(index: usize, blocks: &[LayerBlockDraftV0]) -> Option<usize> {
    let range = blocks[index].node.text_range();
    blocks
        .iter()
        .enumerate()
        .filter(|(candidate_index, candidate)| {
            *candidate_index != index
                && candidate.node.text_range().start() < range.start()
                && range.end() < candidate.node.text_range().end()
        })
        .min_by_key(|(_, candidate)| {
            u32::from(candidate.node.text_range().end())
                .saturating_sub(u32::from(candidate.node.text_range().start()))
        })
        .map(|(candidate_index, _)| candidate_index)
}

fn nearest_enclosing_block_for_node<'a>(
    node: &SyntaxNode,
    blocks: &'a [LayerBlockDraftV0],
) -> Option<&'a LayerBlockDraftV0> {
    let range = node.text_range();
    blocks
        .iter()
        .filter(|block| {
            block.node.text_range() != range
                && block.node.text_range().start() < range.start()
                && range.end() < block.node.text_range().end()
        })
        .min_by_key(|block| {
            u32::from(block.node.text_range().end())
                .saturating_sub(u32::from(block.node.text_range().start()))
        })
}

fn canonical_layer_path(parent: Option<&str>, local_path: &str) -> Option<String> {
    let local_path = local_path.trim();
    if !plain_layer_path(local_path) {
        return None;
    }
    Some(match parent {
        Some(parent) => format!("{parent}.{local_path}"),
        None => local_path.to_string(),
    })
}

fn plain_layer_path(path: &str) -> bool {
    path.split('.').all(|segment| {
        !segment.is_empty()
            && segment
                .bytes()
                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
    })
}

fn register_layer_path(
    nodes: &mut BTreeMap<String, LayerNodeDraftV0>,
    canonical_name: &str,
    source_order: usize,
) {
    let segments = canonical_name.split('.').collect::<Vec<_>>();
    for length in 1..=segments.len() {
        let name = segments[..length].join(".");
        let parent_name = (length > 1).then(|| segments[..length - 1].join("."));
        let implicit_prefix = length != segments.len();
        nodes.entry(name.clone()).or_insert(LayerNodeDraftV0 {
            canonical_name: name,
            local_name: segments[length - 1].to_string(),
            parent_name,
            first_source_order: source_order,
            implicit_prefix,
        });
    }
}

fn cascade_ranks<'a>(nodes: impl Iterator<Item = &'a LayerNodeDraftV0>) -> BTreeMap<String, usize> {
    let nodes = nodes
        .map(|node| (node.canonical_name.clone(), node.clone()))
        .collect::<BTreeMap<_, _>>();
    let mut children = BTreeMap::<Option<String>, Vec<String>>::new();
    for node in nodes.values() {
        children
            .entry(node.parent_name.clone())
            .or_default()
            .push(node.canonical_name.clone());
    }
    for names in children.values_mut() {
        names.sort_by_key(|name| {
            nodes
                .get(name)
                .map(|node| (node.first_source_order, node.canonical_name.clone()))
                .unwrap_or((usize::MAX, name.clone()))
        });
    }

    let mut ordered = Vec::new();
    append_postorder(None, &children, &mut ordered, &mut BTreeSet::new());
    ordered
        .into_iter()
        .enumerate()
        .map(|(rank, name)| (name, rank))
        .collect()
}

fn append_postorder(
    parent: Option<&str>,
    children: &BTreeMap<Option<String>, Vec<String>>,
    ordered: &mut Vec<String>,
    visited: &mut BTreeSet<String>,
) {
    let key = parent.map(ToString::to_string);
    let Some(names) = children.get(&key) else {
        return;
    };
    for name in names {
        if !visited.insert(name.clone()) {
            continue;
        }
        append_postorder(Some(name.as_str()), children, ordered, visited);
        ordered.push(name.clone());
    }
}

fn layer_names(node: &SyntaxNode) -> Vec<String> {
    let text = syntax_node_text(node);
    let Some(rest) = css_keyword(text.trim_start()).strip_prefix("@layer") else {
        return Vec::new();
    };
    rest.split(['{', ';', '\n'])
        .next()
        .unwrap_or_default()
        .split(',')
        .filter_map(|name| {
            let name = name.trim();
            (!name.is_empty()).then(|| name.to_string())
        })
        .collect()
}

fn node_has_block(node: &SyntaxNode) -> bool {
    node.descendants_with_tokens()
        .filter_map(|element| element.into_token())
        .any(|token| matches!(token.kind(), SyntaxKind::LeftBrace | SyntaxKind::SassIndent))
}

fn syntax_node_text(node: &SyntaxNode) -> String {
    node.try_resolved()
        .map(|resolved| resolved.text().to_string())
        .unwrap_or_default()
}