Skip to main content

omena_parser/facts/
css_modules.rs

1//! Parser facts for CSS Modules `:export`, `:import`, `@value`, and `composes`.
2//!
3//! This module stays syntax-only: it records local edges and references so
4//! query/resolution layers can perform cross-file interpretation later.
5
6use cstree::{syntax::SyntaxNode, text::TextRange};
7use omena_syntax::{SyntaxKind, css_keyword};
8use std::collections::{BTreeMap, BTreeSet};
9
10use crate::{
11    ParseResult, SelectorBranch, Token, css_module_block_scope_marker_in_header,
12    matches_ignore_ascii_case, next_non_trivia_token_index_until, previous_non_trivia_token_index,
13    resolve_selector_header, skip_trivia_tokens, top_level_token_kind_index,
14    top_level_token_text_index,
15};
16
17use super::tokens_from_syntax_node;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct ParsedCssModuleValueFact {
21    pub kind: ParsedCssModuleValueFactKind,
22    pub name: String,
23    pub range: TextRange,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27pub enum ParsedCssModuleValueFactKind {
28    Definition,
29    Reference,
30    ImportSource,
31}
32
33pub(crate) fn collect_css_module_value_facts_from_cst(
34    text: &str,
35    parsed: &ParseResult,
36) -> Vec<ParsedCssModuleValueFact> {
37    css_module_value_facts_from_cst_nodes(text, parsed)
38}
39
40fn css_module_value_facts_from_cst_nodes(
41    text: &str,
42    parsed: &ParseResult,
43) -> Vec<ParsedCssModuleValueFact> {
44    let mut values = Vec::new();
45    let mut seen = BTreeSet::new();
46    let value_path_aliases = collect_css_module_value_path_aliases_from_cst_nodes(text, parsed);
47    for tokens in css_module_value_statement_tokens_from_cst(text, parsed) {
48        collect_css_module_value_statement_facts(
49            &tokens,
50            &value_path_aliases,
51            &mut values,
52            &mut seen,
53        );
54    }
55    let local_value_names = values
56        .iter()
57        .filter(|value| value.kind == ParsedCssModuleValueFactKind::Definition)
58        .map(|value| value.name.clone())
59        .collect::<BTreeSet<_>>();
60    for tokens in css_module_value_reference_declaration_tokens_from_cst(text, parsed) {
61        collect_css_module_value_declaration_reference_facts_from_declaration_tokens(
62            &tokens,
63            &local_value_names,
64            &mut values,
65            &mut seen,
66        );
67    }
68    values
69}
70
71fn collect_css_module_value_statement_facts(
72    tokens: &[Token<'_>],
73    value_path_aliases: &BTreeMap<String, String>,
74    values: &mut Vec<ParsedCssModuleValueFact>,
75    seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
76) {
77    let Some(index) = tokens.iter().position(|token| {
78        token.kind == SyntaxKind::AtKeyword && matches_ignore_ascii_case(token.text, &["@value"])
79    }) else {
80        return;
81    };
82
83    let start = skip_trivia_tokens(tokens, index + 1, tokens.len());
84    let end = css_module_value_statement_end(tokens, start);
85    let colon_index = top_level_token_kind_index(tokens, start, end, SyntaxKind::Colon);
86    let from_index = top_level_token_text_index(tokens, start, end, "from");
87
88    if let Some(from_index) = from_index
89        && match colon_index {
90            Some(colon_index) => from_index < colon_index,
91            None => true,
92        }
93    {
94        collect_css_module_value_import_facts(
95            tokens,
96            start,
97            from_index,
98            end,
99            value_path_aliases,
100            values,
101            seen,
102        );
103        return;
104    }
105
106    if let Some(colon_index) = colon_index {
107        if css_module_value_path_alias_from_tokens(tokens, start, colon_index, end).is_some() {
108            return;
109        }
110        collect_css_module_value_definition_facts(tokens, start, colon_index, values, seen);
111        collect_css_module_value_reference_facts(tokens, colon_index + 1, end, values, seen);
112    } else {
113        collect_css_module_value_definition_facts(tokens, start, end, values, seen);
114    }
115}
116
117fn collect_css_module_value_path_aliases_from_cst_nodes(
118    text: &str,
119    parsed: &ParseResult,
120) -> BTreeMap<String, String> {
121    let mut aliases = BTreeMap::new();
122    for tokens in css_module_value_statement_tokens_from_cst(text, parsed) {
123        collect_css_module_value_path_aliases_from_statement_tokens(&tokens, &mut aliases);
124    }
125    aliases
126}
127
128fn collect_css_module_value_path_aliases_from_statement_tokens(
129    tokens: &[Token<'_>],
130    aliases: &mut BTreeMap<String, String>,
131) {
132    let Some(index) = tokens.iter().position(|token| {
133        token.kind == SyntaxKind::AtKeyword && matches_ignore_ascii_case(token.text, &["@value"])
134    }) else {
135        return;
136    };
137
138    let start = skip_trivia_tokens(tokens, index + 1, tokens.len());
139    let end = css_module_value_statement_end(tokens, start);
140    let Some(colon_index) = top_level_token_kind_index(tokens, start, end, SyntaxKind::Colon)
141    else {
142        return;
143    };
144    if top_level_token_text_index(tokens, start, end, "from").is_some() {
145        return;
146    }
147    if let Some((name, target)) =
148        css_module_value_path_alias_from_tokens(tokens, start, colon_index, end)
149    {
150        aliases.insert(name, target);
151    }
152}
153
154fn css_module_value_path_alias_from_tokens(
155    tokens: &[Token<'_>],
156    start: usize,
157    colon_index: usize,
158    end: usize,
159) -> Option<(String, String)> {
160    let name_index = next_non_trivia_token_index_until(tokens, start, colon_index)?;
161    let name_token = tokens[name_index];
162    if !css_module_value_name_token_can_define(name_token) {
163        return None;
164    }
165    let source_index = next_non_trivia_token_index_until(tokens, colon_index + 1, end)?;
166    let source_token = tokens[source_index];
167    if !matches!(source_token.kind, SyntaxKind::String | SyntaxKind::Url) {
168        return None;
169    }
170    let source = css_module_value_source_name(source_token);
171    css_module_value_source_looks_like_style_request(&source)
172        .then(|| (name_token.text.to_string(), source))
173}
174
175pub(crate) fn css_module_value_statement_end(tokens: &[Token<'_>], start: usize) -> usize {
176    let mut index = start;
177    let mut paren_depth = 0usize;
178    let mut bracket_depth = 0usize;
179    while index < tokens.len() {
180        match tokens[index].kind {
181            SyntaxKind::LeftParen => paren_depth += 1,
182            SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
183            SyntaxKind::LeftBracket => bracket_depth += 1,
184            SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
185            SyntaxKind::Semicolon
186            | SyntaxKind::SassOptionalSemicolon
187            | SyntaxKind::LeftBrace
188            | SyntaxKind::RightBrace
189            | SyntaxKind::SassIndent
190            | SyntaxKind::SassDedent
191                if paren_depth == 0 && bracket_depth == 0 =>
192            {
193                return index;
194            }
195            _ => {}
196        }
197        index += 1;
198    }
199    index
200}
201
202fn collect_css_module_value_import_facts(
203    tokens: &[Token<'_>],
204    start: usize,
205    from_index: usize,
206    end: usize,
207    value_path_aliases: &BTreeMap<String, String>,
208    values: &mut Vec<ParsedCssModuleValueFact>,
209    seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
210) {
211    collect_css_module_value_import_names(tokens, start, from_index, values, seen);
212    if let Some((source_name, source_range)) =
213        css_module_value_import_edge_source(tokens, from_index + 1, end, value_path_aliases)
214    {
215        push_css_module_value_fact(
216            values,
217            seen,
218            ParsedCssModuleValueFactKind::ImportSource,
219            source_name,
220            source_range,
221        );
222    }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct ParsedCssModuleValueImportEdgeFact {
227    pub remote_name: String,
228    pub local_name: String,
229    pub import_source: String,
230    pub local_range: TextRange,
231    pub remote_range: TextRange,
232    pub range: TextRange,
233}
234
235pub(crate) fn collect_css_module_value_import_edge_facts_from_cst(
236    text: &str,
237    parsed: &ParseResult,
238) -> Vec<ParsedCssModuleValueImportEdgeFact> {
239    css_module_value_import_edge_facts_from_cst_nodes(text, parsed)
240}
241
242fn css_module_value_import_edge_facts_from_cst_nodes(
243    text: &str,
244    parsed: &ParseResult,
245) -> Vec<ParsedCssModuleValueImportEdgeFact> {
246    let mut edges = Vec::new();
247    let value_path_aliases = collect_css_module_value_path_aliases_from_cst_nodes(text, parsed);
248    for tokens in css_module_value_statement_tokens_from_cst(text, parsed) {
249        collect_css_module_value_import_edge_statement_facts(
250            &tokens,
251            &value_path_aliases,
252            &mut edges,
253        );
254    }
255    edges
256}
257
258fn collect_css_module_value_import_edge_statement_facts(
259    tokens: &[Token<'_>],
260    value_path_aliases: &BTreeMap<String, String>,
261    edges: &mut Vec<ParsedCssModuleValueImportEdgeFact>,
262) {
263    let Some(index) = tokens.iter().position(|token| {
264        token.kind == SyntaxKind::AtKeyword && matches_ignore_ascii_case(token.text, &["@value"])
265    }) else {
266        return;
267    };
268
269    let start = skip_trivia_tokens(tokens, index + 1, tokens.len());
270    let end = css_module_value_statement_end(tokens, start);
271    let colon_index = top_level_token_kind_index(tokens, start, end, SyntaxKind::Colon);
272    let from_index = top_level_token_text_index(tokens, start, end, "from");
273    let Some(from_index) = from_index else {
274        return;
275    };
276    if colon_index.is_some_and(|colon_index| from_index > colon_index) {
277        return;
278    }
279    let Some((import_source, _source_range)) =
280        css_module_value_import_edge_source(tokens, from_index + 1, end, value_path_aliases)
281    else {
282        return;
283    };
284
285    collect_css_module_value_import_edges(tokens, start, from_index, import_source, edges);
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct ParsedCssModuleValueDefinitionEdgeFact {
290    pub definition_name: String,
291    pub reference_names: Vec<String>,
292    pub range: TextRange,
293}
294
295pub(crate) fn collect_css_module_value_definition_edge_facts_from_cst(
296    text: &str,
297    parsed: &ParseResult,
298) -> Vec<ParsedCssModuleValueDefinitionEdgeFact> {
299    css_module_value_definition_edge_facts_from_cst_nodes(text, parsed)
300}
301
302fn css_module_value_definition_edge_facts_from_cst_nodes(
303    text: &str,
304    parsed: &ParseResult,
305) -> Vec<ParsedCssModuleValueDefinitionEdgeFact> {
306    let mut edges = Vec::new();
307    for tokens in css_module_value_statement_tokens_from_cst(text, parsed) {
308        collect_css_module_value_definition_edge_statement_facts(&tokens, &mut edges);
309    }
310    edges
311}
312
313fn collect_css_module_value_definition_edge_statement_facts(
314    tokens: &[Token<'_>],
315    edges: &mut Vec<ParsedCssModuleValueDefinitionEdgeFact>,
316) {
317    let Some(index) = tokens.iter().position(|token| {
318        token.kind == SyntaxKind::AtKeyword && matches_ignore_ascii_case(token.text, &["@value"])
319    }) else {
320        return;
321    };
322
323    let start = skip_trivia_tokens(tokens, index + 1, tokens.len());
324    let end = css_module_value_statement_end(tokens, start);
325    let colon_index = top_level_token_kind_index(tokens, start, end, SyntaxKind::Colon);
326    let from_index = top_level_token_text_index(tokens, start, end, "from");
327    let Some(colon_index) = colon_index else {
328        return;
329    };
330    if from_index.is_some_and(|from_index| from_index < colon_index) {
331        return;
332    }
333
334    let definition_names = collect_css_module_value_definition_edge_names(
335        tokens,
336        start,
337        colon_index,
338        |tokens, index| css_module_value_name_token_can_define(tokens[index]),
339    );
340    let reference_names = collect_css_module_value_definition_edge_names(
341        tokens,
342        colon_index + 1,
343        end,
344        css_module_value_reference_token_can_be_name,
345    );
346    if reference_names.is_empty() {
347        return;
348    }
349    let range_end = end
350        .checked_sub(1)
351        .and_then(|end| tokens.get(end))
352        .map(|token| token.range.end())
353        .unwrap_or_else(|| tokens[index].range.end());
354
355    for definition_name in definition_names {
356        edges.push(ParsedCssModuleValueDefinitionEdgeFact {
357            definition_name,
358            reference_names: reference_names.clone(),
359            range: TextRange::new(tokens[index].range.start(), range_end),
360        });
361    }
362}
363
364pub(crate) fn collect_css_module_value_definition_edge_names(
365    tokens: &[Token<'_>],
366    start: usize,
367    end: usize,
368    predicate: impl Fn(&[Token<'_>], usize) -> bool,
369) -> Vec<String> {
370    let mut names = Vec::new();
371    let mut index = start;
372    while index < end {
373        if predicate(tokens, index) && !names.iter().any(|name| name == tokens[index].text) {
374            names.push(tokens[index].text.to_string());
375        }
376        index += 1;
377    }
378    names
379}
380
381fn css_module_value_statement_tokens_from_cst<'text>(
382    text: &'text str,
383    parsed: &ParseResult,
384) -> Vec<Vec<Token<'text>>> {
385    parsed
386        .syntax()
387        .descendants()
388        .filter(|node| {
389            matches!(
390                node.kind(),
391                SyntaxKind::CssModuleExportBlock
392                    | SyntaxKind::CssModuleImportBlock
393                    | SyntaxKind::BogusCssModuleBlock
394            )
395        })
396        .map(|node| tokens_from_syntax_node(text, parsed, node))
397        .collect()
398}
399
400fn css_module_value_reference_declaration_tokens_from_cst<'text>(
401    text: &'text str,
402    parsed: &ParseResult,
403) -> Vec<Vec<Token<'text>>> {
404    parsed
405        .syntax()
406        .descendants()
407        .filter(|node| {
408            matches!(
409                node.kind(),
410                SyntaxKind::Declaration | SyntaxKind::CssModuleComposesDeclaration
411            )
412        })
413        .map(|node| tokens_from_syntax_node(text, parsed, node))
414        .collect()
415}
416
417fn css_module_composes_declaration_tokens_from_cst<'text>(
418    text: &'text str,
419    parsed: &ParseResult,
420) -> Vec<Vec<Token<'text>>> {
421    parsed
422        .syntax()
423        .descendants()
424        .filter(|node| node.kind() == SyntaxKind::CssModuleComposesDeclaration)
425        .map(|node| tokens_from_syntax_node(text, parsed, node))
426        .collect()
427}
428
429fn css_module_value_import_edge_source(
430    tokens: &[Token<'_>],
431    start: usize,
432    end: usize,
433    value_path_aliases: &BTreeMap<String, String>,
434) -> Option<(String, TextRange)> {
435    let source_index = next_non_trivia_token_index_until(tokens, start, end)?;
436    let token = tokens[source_index];
437    if matches!(token.kind, SyntaxKind::String | SyntaxKind::Url) {
438        return Some((css_module_value_source_name(token), token.range));
439    }
440    if css_module_value_name_token_can_define(token) {
441        return css_module_value_source_alias_target(token.text, token.range, value_path_aliases);
442    }
443    None
444}
445
446fn css_module_value_source_alias_target(
447    name: &str,
448    range: TextRange,
449    value_path_aliases: &BTreeMap<String, String>,
450) -> Option<(String, TextRange)> {
451    value_path_aliases
452        .get(name)
453        .map(|source| (source.clone(), range))
454}
455
456fn collect_css_module_value_import_edges(
457    tokens: &[Token<'_>],
458    start: usize,
459    end: usize,
460    import_source: String,
461    edges: &mut Vec<ParsedCssModuleValueImportEdgeFact>,
462) {
463    let mut index = start;
464    while index < end {
465        let token = tokens[index];
466        if !css_module_value_name_token_can_define(token) {
467            index += 1;
468            continue;
469        }
470        if previous_non_trivia_token_index(tokens, index, start)
471            .is_some_and(|previous| css_keyword(tokens[previous].text).equals("as"))
472        {
473            index += 1;
474            continue;
475        }
476        let remote_name = token.text.to_string();
477        let mut local_name = remote_name.clone();
478        let mut local_range = token.range;
479        if let Some(as_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
480            && css_keyword(tokens[as_index].text).equals("as")
481            && let Some(local_index) = next_non_trivia_token_index_until(tokens, as_index + 1, end)
482            && css_module_value_name_token_can_define(tokens[local_index])
483        {
484            local_name = tokens[local_index].text.to_string();
485            local_range = tokens[local_index].range;
486            index = local_index + 1;
487        } else {
488            index += 1;
489        }
490        edges.push(ParsedCssModuleValueImportEdgeFact {
491            remote_name,
492            local_name,
493            import_source: import_source.clone(),
494            local_range,
495            remote_range: token.range,
496            range: token.range,
497        });
498    }
499}
500
501fn collect_css_module_value_import_names(
502    tokens: &[Token<'_>],
503    start: usize,
504    end: usize,
505    values: &mut Vec<ParsedCssModuleValueFact>,
506    seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
507) {
508    let mut index = start;
509    while index < end {
510        let token = tokens[index];
511        if css_module_value_name_token_can_define(token) {
512            let previous = previous_non_trivia_token_index(tokens, index, start);
513            let next = next_non_trivia_token_index_until(tokens, index + 1, end);
514            let kind = if previous
515                .is_some_and(|previous| css_keyword(tokens[previous].text).equals("as"))
516            {
517                Some(ParsedCssModuleValueFactKind::Definition)
518            } else if next.is_some_and(|next| css_keyword(tokens[next].text).equals("as")) {
519                Some(ParsedCssModuleValueFactKind::Reference)
520            } else {
521                Some(ParsedCssModuleValueFactKind::Definition)
522            };
523            if let Some(kind) = kind {
524                push_css_module_value_fact(values, seen, kind, token.text.to_string(), token.range);
525            }
526        }
527        index += 1;
528    }
529}
530
531fn collect_css_module_value_definition_facts(
532    tokens: &[Token<'_>],
533    start: usize,
534    end: usize,
535    values: &mut Vec<ParsedCssModuleValueFact>,
536    seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
537) {
538    let mut index = start;
539    while index < end {
540        let token = tokens[index];
541        if css_module_value_name_token_can_define(token) {
542            push_css_module_value_fact(
543                values,
544                seen,
545                ParsedCssModuleValueFactKind::Definition,
546                token.text.to_string(),
547                token.range,
548            );
549        }
550        index += 1;
551    }
552}
553
554fn collect_css_module_value_reference_facts(
555    tokens: &[Token<'_>],
556    start: usize,
557    end: usize,
558    values: &mut Vec<ParsedCssModuleValueFact>,
559    seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
560) {
561    let mut index = start;
562    let mut paren_depth = 0usize;
563    let mut bracket_depth = 0usize;
564    while index < end {
565        match tokens[index].kind {
566            SyntaxKind::LeftParen => paren_depth += 1,
567            SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
568            SyntaxKind::LeftBracket => bracket_depth += 1,
569            SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
570            _ => {}
571        }
572        if paren_depth == 0
573            && bracket_depth == 0
574            && css_module_value_reference_token_can_be_name(tokens, index)
575        {
576            push_css_module_value_fact(
577                values,
578                seen,
579                ParsedCssModuleValueFactKind::Reference,
580                tokens[index].text.to_string(),
581                tokens[index].range,
582            );
583        }
584        index += 1;
585    }
586}
587
588fn collect_css_module_value_declaration_reference_facts_from_declaration_tokens(
589    tokens: &[Token<'_>],
590    local_value_names: &BTreeSet<String>,
591    values: &mut Vec<ParsedCssModuleValueFact>,
592    seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
593) {
594    if local_value_names.is_empty() {
595        return;
596    }
597    if let Some(colon_index) = declaration_colon_index(tokens, 0, tokens.len()) {
598        collect_known_css_module_value_reference_facts(
599            tokens,
600            colon_index + 1,
601            tokens.len(),
602            local_value_names,
603            values,
604            seen,
605        );
606    }
607}
608
609pub(crate) fn declaration_colon_index(
610    tokens: &[Token<'_>],
611    start: usize,
612    end: usize,
613) -> Option<usize> {
614    let colon_index = top_level_token_kind_index(tokens, start, end, SyntaxKind::Colon)?;
615    let property_index = previous_non_trivia_token_index(tokens, colon_index, start)?;
616    if !matches!(
617        tokens[property_index].kind,
618        SyntaxKind::Ident
619            | SyntaxKind::CustomPropertyName
620            | SyntaxKind::ScssVariable
621            | SyntaxKind::LessVariable
622            | SyntaxKind::LessPropertyVariableToken
623    ) {
624        return None;
625    }
626    let value_index = next_non_trivia_token_index_until(tokens, colon_index + 1, end)?;
627    if matches!(
628        tokens[value_index].kind,
629        SyntaxKind::LeftBrace | SyntaxKind::LeftParen | SyntaxKind::LeftBracket
630    ) {
631        return None;
632    }
633    Some(colon_index)
634}
635
636fn collect_known_css_module_value_reference_facts(
637    tokens: &[Token<'_>],
638    start: usize,
639    end: usize,
640    local_value_names: &BTreeSet<String>,
641    values: &mut Vec<ParsedCssModuleValueFact>,
642    seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
643) {
644    let mut index = start;
645    let mut paren_depth = 0usize;
646    let mut bracket_depth = 0usize;
647    while index < end {
648        match tokens[index].kind {
649            SyntaxKind::LeftParen => paren_depth += 1,
650            SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
651            SyntaxKind::LeftBracket => bracket_depth += 1,
652            SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
653            _ => {}
654        }
655        if paren_depth == 0
656            && bracket_depth == 0
657            && css_module_value_reference_token_can_be_name(tokens, index)
658            && local_value_names.contains(tokens[index].text)
659        {
660            push_css_module_value_fact(
661                values,
662                seen,
663                ParsedCssModuleValueFactKind::Reference,
664                tokens[index].text.to_string(),
665                tokens[index].range,
666            );
667        }
668        index += 1;
669    }
670}
671
672fn push_css_module_value_fact(
673    values: &mut Vec<ParsedCssModuleValueFact>,
674    seen: &mut BTreeSet<(ParsedCssModuleValueFactKind, String, u32, u32)>,
675    kind: ParsedCssModuleValueFactKind,
676    name: String,
677    range: TextRange,
678) {
679    if seen.insert((
680        kind,
681        name.clone(),
682        u32::from(range.start()),
683        u32::from(range.end()),
684    )) {
685        values.push(ParsedCssModuleValueFact { kind, name, range });
686    }
687}
688
689fn css_module_value_name_token_can_define(token: Token<'_>) -> bool {
690    matches!(
691        token.kind,
692        SyntaxKind::Ident | SyntaxKind::CustomPropertyName
693    ) && !css_keyword(token.text).equals("as")
694        && !css_keyword(token.text).equals("from")
695}
696
697pub(crate) fn css_module_value_reference_token_can_be_name(
698    tokens: &[Token<'_>],
699    index: usize,
700) -> bool {
701    let token = tokens[index];
702    if !matches!(
703        token.kind,
704        SyntaxKind::Ident | SyntaxKind::CustomPropertyName
705    ) {
706        return false;
707    }
708    if let Some(next_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
709        && tokens[next_index].kind == SyntaxKind::LeftParen
710    {
711        return false;
712    }
713    !css_module_value_literal_ident_is_not_reference(token.text)
714}
715
716fn css_module_value_literal_ident_is_not_reference(name: &str) -> bool {
717    matches_ignore_ascii_case(
718        name,
719        &[
720            "initial",
721            "inherit",
722            "unset",
723            "revert",
724            "revert-layer",
725            "none",
726            "auto",
727            "normal",
728            "transparent",
729            "currentcolor",
730            "black",
731            "white",
732            "red",
733            "green",
734            "blue",
735            "yellow",
736            "magenta",
737            "cyan",
738            "solid",
739            "dashed",
740            "block",
741            "inline",
742            "flex",
743            "grid",
744        ],
745    )
746}
747
748pub(crate) fn css_module_value_source_name(token: Token<'_>) -> String {
749    token
750        .text
751        .trim_matches(|character| character == '"' || character == '\'')
752        .to_string()
753}
754
755fn css_module_value_source_looks_like_style_request(source: &str) -> bool {
756    let lower = source.to_ascii_lowercase();
757    (lower.starts_with('/') || lower.starts_with("./") || lower.starts_with("../"))
758        && (lower.ends_with(".css")
759            || lower.ends_with(".scss")
760            || lower.ends_with(".sass")
761            || lower.ends_with(".less"))
762}
763
764#[derive(Debug, Clone, PartialEq, Eq)]
765pub struct ParsedCssModuleComposesFact {
766    pub kind: ParsedCssModuleComposesFactKind,
767    pub name: String,
768    pub range: TextRange,
769}
770
771#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
772pub enum ParsedCssModuleComposesFactKind {
773    Target,
774    ImportSource,
775}
776
777#[derive(Debug, Clone, PartialEq, Eq)]
778pub struct ParsedCssModuleComposesEdgeFact {
779    pub kind: ParsedCssModuleComposesEdgeKind,
780    pub owner_selector_names: Vec<String>,
781    pub target_names: Vec<String>,
782    pub import_source: Option<String>,
783    pub range: TextRange,
784}
785
786#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
787pub enum ParsedCssModuleComposesEdgeKind {
788    Local,
789    Global,
790    External,
791}
792
793pub(crate) fn collect_css_module_composes_facts_from_cst(
794    text: &str,
795    parsed: &ParseResult,
796) -> Vec<ParsedCssModuleComposesFact> {
797    css_module_composes_facts_from_cst_nodes(text, parsed)
798}
799
800fn css_module_composes_facts_from_cst_nodes(
801    text: &str,
802    parsed: &ParseResult,
803) -> Vec<ParsedCssModuleComposesFact> {
804    let mut composes = Vec::new();
805    let mut seen = BTreeSet::new();
806    for tokens in css_module_composes_declaration_tokens_from_cst(text, parsed) {
807        collect_css_module_composes_statement_facts(&tokens, &mut composes, &mut seen);
808    }
809    composes
810}
811
812fn collect_css_module_composes_statement_facts(
813    tokens: &[Token<'_>],
814    composes: &mut Vec<ParsedCssModuleComposesFact>,
815    seen: &mut BTreeSet<(ParsedCssModuleComposesFactKind, String, u32, u32)>,
816) {
817    let Some(index) = tokens.iter().position(|token| {
818        token.kind == SyntaxKind::Ident && matches_ignore_ascii_case(token.text, &["composes"])
819    }) else {
820        return;
821    };
822    let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
823    else {
824        return;
825    };
826    if tokens[colon_index].kind != SyntaxKind::Colon {
827        return;
828    }
829
830    let start = colon_index + 1;
831    let end = css_module_value_statement_end(tokens, start);
832    let from_index = top_level_token_text_index(tokens, start, end, "from");
833    let target_end = from_index.unwrap_or(end);
834    collect_css_module_composes_targets(tokens, start, target_end, composes, seen);
835    if let Some(from_index) = from_index {
836        collect_css_module_composes_import_source(tokens, from_index + 1, end, composes, seen);
837    }
838}
839
840pub(crate) fn collect_css_module_composes_edge_facts_from_cst(
841    text: &str,
842    parsed: &ParseResult,
843) -> Vec<ParsedCssModuleComposesEdgeFact> {
844    let mut edges = Vec::new();
845    for declaration in parsed
846        .syntax()
847        .descendants()
848        .filter(|node| node.kind() == SyntaxKind::CssModuleComposesDeclaration)
849    {
850        let owner_branches = css_module_composes_owner_branches_from_cst(text, parsed, declaration);
851        if owner_branches.is_empty() {
852            continue;
853        }
854        let tokens = tokens_from_syntax_node(text, parsed, declaration);
855        collect_immediate_css_module_composes_edge_facts(
856            &tokens,
857            0,
858            tokens.len(),
859            &owner_branches,
860            &mut edges,
861        );
862    }
863    edges
864}
865
866fn css_module_composes_owner_branches_from_cst(
867    text: &str,
868    parsed: &ParseResult,
869    declaration: &SyntaxNode<SyntaxKind>,
870) -> Vec<SelectorBranch> {
871    let mut branches = Vec::new();
872    let mut css_module_scope = None;
873    let mut ancestors = declaration.ancestors().collect::<Vec<_>>();
874    ancestors.reverse();
875    for ancestor in ancestors {
876        match ancestor.kind() {
877            SyntaxKind::Rule | SyntaxKind::NestRule => {
878                let tokens = tokens_from_syntax_node(text, parsed, ancestor);
879                let Some(open) = first_block_open_token_index(&tokens) else {
880                    continue;
881                };
882                let header_start = if ancestor.kind() == SyntaxKind::NestRule {
883                    tokens
884                        .iter()
885                        .position(|token| token.kind == SyntaxKind::AtKeyword)
886                        .map_or(0, |index| index + 1)
887                } else {
888                    0
889                };
890                let effective_scope = css_module_scope.or_else(|| {
891                    css_module_block_scope_marker_in_header(&tokens, header_start, open)
892                });
893                if effective_scope == Some("global") {
894                    branches.clear();
895                } else {
896                    branches = resolve_selector_header(&tokens, header_start, open, &branches);
897                }
898                css_module_scope = effective_scope;
899            }
900            SyntaxKind::CssModuleGlobalBlock => {
901                branches.clear();
902                css_module_scope = Some("global");
903            }
904            SyntaxKind::CssModuleLocalBlock if css_module_scope.is_none() => {
905                css_module_scope = Some("local");
906            }
907            _ => {}
908        }
909    }
910    if css_module_scope == Some("global") {
911        Vec::new()
912    } else {
913        branches
914    }
915}
916
917fn first_block_open_token_index(tokens: &[Token<'_>]) -> Option<usize> {
918    tokens
919        .iter()
920        .position(|token| matches!(token.kind, SyntaxKind::LeftBrace | SyntaxKind::SassIndent))
921}
922
923fn collect_immediate_css_module_composes_edge_facts(
924    tokens: &[Token<'_>],
925    start: usize,
926    end: usize,
927    owner_branches: &[SelectorBranch],
928    edges: &mut Vec<ParsedCssModuleComposesEdgeFact>,
929) {
930    let owner_selector_names = sorted_selector_branch_names(owner_branches);
931    let mut index = start;
932    let mut block_depth = 0usize;
933    while index < end {
934        match tokens[index].kind {
935            SyntaxKind::LeftBrace | SyntaxKind::SassIndent => {
936                block_depth += 1;
937                index += 1;
938                continue;
939            }
940            SyntaxKind::RightBrace | SyntaxKind::SassDedent => {
941                block_depth = block_depth.saturating_sub(1);
942                index += 1;
943                continue;
944            }
945            _ => {}
946        }
947        if block_depth > 0
948            || tokens[index].kind != SyntaxKind::Ident
949            || !matches_ignore_ascii_case(tokens[index].text, &["composes"])
950        {
951            index += 1;
952            continue;
953        }
954        let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end) else {
955            index += 1;
956            continue;
957        };
958        if tokens[colon_index].kind != SyntaxKind::Colon {
959            index += 1;
960            continue;
961        }
962
963        let value_start = colon_index + 1;
964        let value_end = css_module_value_statement_end(tokens, value_start).min(end);
965        let from_index = top_level_token_text_index(tokens, value_start, value_end, "from");
966        let target_end = from_index.unwrap_or(value_end);
967        let target_names =
968            collect_css_module_composes_target_names(tokens, value_start, target_end);
969        if target_names.is_empty() {
970            index = value_end;
971            continue;
972        }
973
974        let (kind, import_source) = from_index
975            .and_then(|from_index| {
976                css_module_composes_import_edge_source(tokens, from_index + 1, value_end)
977            })
978            .map(|source| {
979                if source == "global" {
980                    (ParsedCssModuleComposesEdgeKind::Global, Some(source))
981                } else {
982                    (ParsedCssModuleComposesEdgeKind::External, Some(source))
983                }
984            })
985            .unwrap_or((ParsedCssModuleComposesEdgeKind::Local, None));
986        let range_end = value_end
987            .checked_sub(1)
988            .and_then(|end| tokens.get(end))
989            .map(|token| token.range.end())
990            .unwrap_or_else(|| tokens[index].range.end());
991
992        edges.push(ParsedCssModuleComposesEdgeFact {
993            kind,
994            owner_selector_names: owner_selector_names.clone(),
995            target_names,
996            import_source,
997            range: TextRange::new(tokens[index].range.start(), range_end),
998        });
999        index = value_end;
1000    }
1001}
1002
1003fn sorted_selector_branch_names(branches: &[SelectorBranch]) -> Vec<String> {
1004    branches
1005        .iter()
1006        .map(|branch| branch.name.clone())
1007        .collect::<BTreeSet<_>>()
1008        .into_iter()
1009        .collect()
1010}
1011
1012fn collect_css_module_composes_target_names(
1013    tokens: &[Token<'_>],
1014    start: usize,
1015    end: usize,
1016) -> Vec<String> {
1017    let mut names = Vec::new();
1018    let mut index = start;
1019    while index < end {
1020        if let Some((open_index, close_index)) =
1021            css_module_global_composes_function_range(tokens, index, end)
1022        {
1023            collect_css_module_composes_target_names_into(
1024                tokens,
1025                open_index + 1,
1026                close_index,
1027                &mut names,
1028            );
1029            index = close_index + 1;
1030            continue;
1031        }
1032        push_css_module_composes_target_name(tokens[index], &mut names);
1033        index += 1;
1034    }
1035    names
1036}
1037
1038fn css_module_composes_import_edge_source(
1039    tokens: &[Token<'_>],
1040    start: usize,
1041    end: usize,
1042) -> Option<String> {
1043    let source_index = next_non_trivia_token_index_until(tokens, start, end)?;
1044    let token = tokens[source_index];
1045    matches!(
1046        token.kind,
1047        SyntaxKind::String | SyntaxKind::Url | SyntaxKind::Ident
1048    )
1049    .then(|| css_module_value_source_name(token))
1050}
1051
1052fn collect_css_module_composes_targets(
1053    tokens: &[Token<'_>],
1054    start: usize,
1055    end: usize,
1056    composes: &mut Vec<ParsedCssModuleComposesFact>,
1057    seen: &mut BTreeSet<(ParsedCssModuleComposesFactKind, String, u32, u32)>,
1058) {
1059    let mut index = start;
1060    while index < end {
1061        if let Some((open_index, close_index)) =
1062            css_module_global_composes_function_range(tokens, index, end)
1063        {
1064            collect_css_module_composes_target_facts_into(
1065                tokens,
1066                open_index + 1,
1067                close_index,
1068                composes,
1069                seen,
1070            );
1071            index = close_index + 1;
1072            continue;
1073        }
1074        push_css_module_composes_target_fact(tokens[index], composes, seen);
1075        index += 1;
1076    }
1077}
1078
1079fn collect_css_module_composes_target_names_into(
1080    tokens: &[Token<'_>],
1081    start: usize,
1082    end: usize,
1083    names: &mut Vec<String>,
1084) {
1085    let mut index = start;
1086    while index < end {
1087        push_css_module_composes_target_name(tokens[index], names);
1088        index += 1;
1089    }
1090}
1091
1092fn push_css_module_composes_target_name(token: Token<'_>, names: &mut Vec<String>) {
1093    if matches!(
1094        token.kind,
1095        SyntaxKind::Ident | SyntaxKind::CustomPropertyName
1096    ) && !matches_ignore_ascii_case(token.text, &["from"])
1097        && !names.iter().any(|name| name == token.text)
1098    {
1099        names.push(token.text.to_string());
1100    }
1101}
1102
1103fn collect_css_module_composes_target_facts_into(
1104    tokens: &[Token<'_>],
1105    start: usize,
1106    end: usize,
1107    composes: &mut Vec<ParsedCssModuleComposesFact>,
1108    seen: &mut BTreeSet<(ParsedCssModuleComposesFactKind, String, u32, u32)>,
1109) {
1110    let mut index = start;
1111    while index < end {
1112        push_css_module_composes_target_fact(tokens[index], composes, seen);
1113        index += 1;
1114    }
1115}
1116
1117fn push_css_module_composes_target_fact(
1118    token: Token<'_>,
1119    composes: &mut Vec<ParsedCssModuleComposesFact>,
1120    seen: &mut BTreeSet<(ParsedCssModuleComposesFactKind, String, u32, u32)>,
1121) {
1122    if matches!(
1123        token.kind,
1124        SyntaxKind::Ident | SyntaxKind::CustomPropertyName
1125    ) && !matches_ignore_ascii_case(token.text, &["from"])
1126    {
1127        push_css_module_composes_fact(
1128            composes,
1129            seen,
1130            ParsedCssModuleComposesFactKind::Target,
1131            token.text.to_string(),
1132            token.range,
1133        );
1134    }
1135}
1136
1137fn css_module_global_composes_function_range(
1138    tokens: &[Token<'_>],
1139    index: usize,
1140    end: usize,
1141) -> Option<(usize, usize)> {
1142    if tokens.get(index)?.kind != SyntaxKind::Ident
1143        || !matches_ignore_ascii_case(tokens[index].text, &["global"])
1144    {
1145        return None;
1146    }
1147    let open_index = next_non_trivia_token_index_until(tokens, index + 1, end)?;
1148    if tokens[open_index].kind != SyntaxKind::LeftParen {
1149        return None;
1150    }
1151    let mut depth = 0usize;
1152    for (close_index, token) in tokens.iter().enumerate().take(end).skip(open_index) {
1153        match token.kind {
1154            SyntaxKind::LeftParen => depth += 1,
1155            SyntaxKind::RightParen => {
1156                depth = depth.saturating_sub(1);
1157                if depth == 0 {
1158                    return Some((open_index, close_index));
1159                }
1160            }
1161            _ => {}
1162        }
1163    }
1164    None
1165}
1166
1167fn collect_css_module_composes_import_source(
1168    tokens: &[Token<'_>],
1169    start: usize,
1170    end: usize,
1171    composes: &mut Vec<ParsedCssModuleComposesFact>,
1172    seen: &mut BTreeSet<(ParsedCssModuleComposesFactKind, String, u32, u32)>,
1173) {
1174    if let Some(source_index) = next_non_trivia_token_index_until(tokens, start, end) {
1175        let token = tokens[source_index];
1176        if matches!(
1177            token.kind,
1178            SyntaxKind::String | SyntaxKind::Url | SyntaxKind::Ident
1179        ) {
1180            push_css_module_composes_fact(
1181                composes,
1182                seen,
1183                ParsedCssModuleComposesFactKind::ImportSource,
1184                css_module_value_source_name(token),
1185                token.range,
1186            );
1187        }
1188    }
1189}
1190
1191fn push_css_module_composes_fact(
1192    composes: &mut Vec<ParsedCssModuleComposesFact>,
1193    seen: &mut BTreeSet<(ParsedCssModuleComposesFactKind, String, u32, u32)>,
1194    kind: ParsedCssModuleComposesFactKind,
1195    name: String,
1196    range: TextRange,
1197) {
1198    if seen.insert((
1199        kind,
1200        name.clone(),
1201        u32::from(range.start()),
1202        u32::from(range.end()),
1203    )) {
1204        composes.push(ParsedCssModuleComposesFact { kind, name, range });
1205    }
1206}