Skip to main content

omena_parser/facts/
icss.rs

1//! Parser facts for ICSS import/export blocks.
2//!
3//! ICSS facts preserve the raw local names and specifier spans needed by
4//! resolver and CSS Modules consumers.
5
6use cstree::text::TextRange;
7use omena_syntax::SyntaxKind;
8use std::collections::BTreeSet;
9
10use crate::{
11    ParseResult, Token, collect_css_module_value_definition_edge_names,
12    css_module_value_reference_token_can_be_name, css_module_value_source_name,
13    css_module_value_statement_end, find_block_after_header, matches_ignore_ascii_case,
14    next_non_trivia_token_index_until,
15};
16
17use super::tokens_from_syntax_node;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct ParsedIcssFact {
21    pub kind: ParsedIcssFactKind,
22    pub name: String,
23    pub range: TextRange,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27pub enum ParsedIcssFactKind {
28    ExportName,
29    ImportLocalName,
30    ImportRemoteName,
31    ImportSource,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ParsedIcssImportEdgeFact {
36    pub local_name: String,
37    pub remote_name: String,
38    pub import_source: String,
39    pub range: TextRange,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ParsedIcssExportEdgeFact {
44    pub export_name: String,
45    pub reference_names: Vec<String>,
46    pub range: TextRange,
47}
48
49pub fn collect_icss_export_values_from_cst(
50    text: &str,
51    parsed: &ParseResult,
52) -> Vec<(String, String)> {
53    let mut values = Vec::new();
54    for tokens in icss_block_tokens_from_cst(text, parsed) {
55        collect_icss_export_values_from_block_tokens(&tokens, &mut values);
56    }
57    values
58}
59
60pub(crate) fn collect_icss_facts_from_cst(text: &str, parsed: &ParseResult) -> Vec<ParsedIcssFact> {
61    let mut icss = Vec::new();
62    let mut seen = BTreeSet::new();
63    for tokens in icss_block_tokens_from_cst(text, parsed) {
64        collect_icss_facts_from_block_tokens(&tokens, &mut icss, &mut seen);
65    }
66    icss
67}
68
69fn collect_icss_facts_from_block_tokens(
70    tokens: &[Token<'_>],
71    icss: &mut Vec<ParsedIcssFact>,
72    seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
73) {
74    for (index, token) in tokens.iter().enumerate() {
75        if token.kind != SyntaxKind::Colon {
76            continue;
77        }
78        let Some(name_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
79        else {
80            continue;
81        };
82        let name = tokens[name_index].text;
83        if !matches!(tokens[name_index].kind, SyntaxKind::Ident) {
84            continue;
85        }
86        if matches_ignore_ascii_case(name, &["export"]) {
87            if let Some((open, close)) =
88                find_block_after_header(tokens, name_index + 1, tokens.len())
89            {
90                collect_icss_export_names(tokens, open + 1, close, icss, seen);
91            }
92            continue;
93        }
94        if matches_ignore_ascii_case(name, &["import"]) {
95            collect_icss_import_source(tokens, name_index + 1, icss, seen);
96            if let Some((open, close)) =
97                find_block_after_header(tokens, name_index + 1, tokens.len())
98            {
99                collect_icss_import_names(tokens, open + 1, close, icss, seen);
100            }
101        }
102    }
103}
104
105pub(crate) fn collect_icss_import_edge_facts_from_cst(
106    text: &str,
107    parsed: &ParseResult,
108) -> Vec<ParsedIcssImportEdgeFact> {
109    let mut edges = Vec::new();
110    for tokens in icss_block_tokens_from_cst(text, parsed) {
111        collect_icss_import_edge_facts_from_block_tokens(&tokens, &mut edges);
112    }
113    edges
114}
115
116fn collect_icss_import_edge_facts_from_block_tokens(
117    tokens: &[Token<'_>],
118    edges: &mut Vec<ParsedIcssImportEdgeFact>,
119) {
120    for (index, token) in tokens.iter().enumerate() {
121        if token.kind != SyntaxKind::Colon {
122            continue;
123        }
124        let Some(name_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
125        else {
126            continue;
127        };
128        if tokens[name_index].kind != SyntaxKind::Ident
129            || !matches_ignore_ascii_case(tokens[name_index].text, &["import"])
130        {
131            continue;
132        }
133        let Some(import_source) = icss_import_edge_source(tokens, name_index + 1) else {
134            continue;
135        };
136        if let Some((open, close)) = find_block_after_header(tokens, name_index + 1, tokens.len()) {
137            collect_icss_import_edges(tokens, open + 1, close, import_source, edges);
138        }
139    }
140}
141
142pub(crate) fn collect_icss_export_edge_facts_from_cst(
143    text: &str,
144    parsed: &ParseResult,
145) -> Vec<ParsedIcssExportEdgeFact> {
146    let mut edges = Vec::new();
147    for tokens in icss_block_tokens_from_cst(text, parsed) {
148        collect_icss_export_edge_facts_from_block_tokens(&tokens, &mut edges);
149    }
150    edges
151}
152
153fn collect_icss_export_edge_facts_from_block_tokens(
154    tokens: &[Token<'_>],
155    edges: &mut Vec<ParsedIcssExportEdgeFact>,
156) {
157    for (index, token) in tokens.iter().enumerate() {
158        if token.kind != SyntaxKind::Colon {
159            continue;
160        }
161        let Some(name_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
162        else {
163            continue;
164        };
165        if tokens[name_index].kind != SyntaxKind::Ident
166            || !matches_ignore_ascii_case(tokens[name_index].text, &["export"])
167        {
168            continue;
169        }
170        if let Some((open, close)) = find_block_after_header(tokens, name_index + 1, tokens.len()) {
171            collect_icss_export_edges(tokens, open + 1, close, edges);
172        }
173    }
174}
175
176fn icss_block_tokens_from_cst<'text>(
177    text: &'text str,
178    parsed: &ParseResult,
179) -> Vec<Vec<Token<'text>>> {
180    parsed
181        .syntax()
182        .descendants()
183        .filter(|node| {
184            matches!(
185                node.kind(),
186                SyntaxKind::CssModuleExportBlock | SyntaxKind::CssModuleImportBlock
187            )
188        })
189        .map(|node| tokens_from_syntax_node(text, parsed, node))
190        .collect()
191}
192
193fn collect_icss_export_edges(
194    tokens: &[Token<'_>],
195    start: usize,
196    end: usize,
197    edges: &mut Vec<ParsedIcssExportEdgeFact>,
198) {
199    let mut index = start;
200    while index < end {
201        let token = tokens[index];
202        if matches!(
203            token.kind,
204            SyntaxKind::Ident | SyntaxKind::CustomPropertyName
205        ) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
206            && tokens[colon_index].kind == SyntaxKind::Colon
207        {
208            let value_end = css_module_value_statement_end(tokens, colon_index + 1).min(end);
209            let reference_names = collect_css_module_value_definition_edge_names(
210                tokens,
211                colon_index + 1,
212                value_end,
213                css_module_value_reference_token_can_be_name,
214            );
215            if !reference_names.is_empty() {
216                let range_end = value_end
217                    .checked_sub(1)
218                    .and_then(|end| tokens.get(end))
219                    .map(|token| token.range.end())
220                    .unwrap_or_else(|| token.range.end());
221                edges.push(ParsedIcssExportEdgeFact {
222                    export_name: token.text.to_string(),
223                    reference_names,
224                    range: TextRange::new(token.range.start(), range_end),
225                });
226            }
227            index = value_end;
228            continue;
229        }
230        index += 1;
231    }
232}
233
234fn collect_icss_export_values_from_block_tokens(
235    tokens: &[Token<'_>],
236    values: &mut Vec<(String, String)>,
237) {
238    for (index, token) in tokens.iter().enumerate() {
239        if token.kind != SyntaxKind::Colon {
240            continue;
241        }
242        let Some(name_index) = next_non_trivia_token_index_until(tokens, index + 1, tokens.len())
243        else {
244            continue;
245        };
246        if tokens[name_index].kind != SyntaxKind::Ident
247            || !matches_ignore_ascii_case(tokens[name_index].text, &["export"])
248        {
249            continue;
250        }
251        let Some((open, close)) = find_block_after_header(tokens, name_index + 1, tokens.len())
252        else {
253            continue;
254        };
255        let mut declaration_index = open + 1;
256        while declaration_index < close {
257            let declaration = tokens[declaration_index];
258            if matches!(
259                declaration.kind,
260                SyntaxKind::Ident | SyntaxKind::CustomPropertyName
261            ) && let Some(colon_index) =
262                next_non_trivia_token_index_until(tokens, declaration_index + 1, close)
263                && tokens[colon_index].kind == SyntaxKind::Colon
264            {
265                let value_end = css_module_value_statement_end(tokens, colon_index + 1).min(close);
266                let value = tokens[colon_index + 1..value_end]
267                    .iter()
268                    .map(|token| token.text)
269                    .collect::<String>()
270                    .trim()
271                    .to_string();
272                values.push((declaration.text.to_string(), value));
273                declaration_index = value_end;
274                continue;
275            }
276            declaration_index += 1;
277        }
278    }
279}
280
281fn icss_import_edge_source(tokens: &[Token<'_>], start: usize) -> Option<String> {
282    let open_index = next_non_trivia_token_index_until(tokens, start, tokens.len())?;
283    if tokens[open_index].kind != SyntaxKind::LeftParen {
284        return None;
285    }
286    let source_index = next_non_trivia_token_index_until(tokens, open_index + 1, tokens.len())?;
287    let token = tokens[source_index];
288    matches!(
289        token.kind,
290        SyntaxKind::String | SyntaxKind::Url | SyntaxKind::Ident
291    )
292    .then(|| css_module_value_source_name(token))
293}
294
295fn collect_icss_import_edges(
296    tokens: &[Token<'_>],
297    start: usize,
298    end: usize,
299    import_source: String,
300    edges: &mut Vec<ParsedIcssImportEdgeFact>,
301) {
302    let mut index = start;
303    while index < end {
304        let token = tokens[index];
305        if matches!(
306            token.kind,
307            SyntaxKind::Ident | SyntaxKind::CustomPropertyName
308        ) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
309            && tokens[colon_index].kind == SyntaxKind::Colon
310            && let Some(remote_index) =
311                next_non_trivia_token_index_until(tokens, colon_index + 1, end)
312            && matches!(
313                tokens[remote_index].kind,
314                SyntaxKind::Ident | SyntaxKind::CustomPropertyName
315            )
316        {
317            edges.push(ParsedIcssImportEdgeFact {
318                local_name: token.text.to_string(),
319                remote_name: tokens[remote_index].text.to_string(),
320                import_source: import_source.clone(),
321                range: token.range,
322            });
323            index = css_module_value_statement_end(tokens, colon_index + 1);
324            continue;
325        }
326        index += 1;
327    }
328}
329
330fn collect_icss_export_names(
331    tokens: &[Token<'_>],
332    start: usize,
333    end: usize,
334    icss: &mut Vec<ParsedIcssFact>,
335    seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
336) {
337    let mut index = start;
338    while index < end {
339        let token = tokens[index];
340        if matches!(
341            token.kind,
342            SyntaxKind::Ident | SyntaxKind::CustomPropertyName
343        ) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
344            && tokens[colon_index].kind == SyntaxKind::Colon
345        {
346            push_icss_fact(
347                icss,
348                seen,
349                ParsedIcssFactKind::ExportName,
350                token.text.to_string(),
351                token.range,
352            );
353            index = css_module_value_statement_end(tokens, colon_index + 1);
354            continue;
355        }
356        index += 1;
357    }
358}
359
360fn collect_icss_import_source(
361    tokens: &[Token<'_>],
362    start: usize,
363    icss: &mut Vec<ParsedIcssFact>,
364    seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
365) {
366    let Some(open_index) = next_non_trivia_token_index_until(tokens, start, tokens.len()) else {
367        return;
368    };
369    if tokens[open_index].kind != SyntaxKind::LeftParen {
370        return;
371    }
372    let Some(source_index) =
373        next_non_trivia_token_index_until(tokens, open_index + 1, tokens.len())
374    else {
375        return;
376    };
377    let token = tokens[source_index];
378    if matches!(
379        token.kind,
380        SyntaxKind::String | SyntaxKind::Url | SyntaxKind::Ident
381    ) {
382        push_icss_fact(
383            icss,
384            seen,
385            ParsedIcssFactKind::ImportSource,
386            css_module_value_source_name(token),
387            token.range,
388        );
389    }
390}
391
392fn collect_icss_import_names(
393    tokens: &[Token<'_>],
394    start: usize,
395    end: usize,
396    icss: &mut Vec<ParsedIcssFact>,
397    seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
398) {
399    let mut index = start;
400    while index < end {
401        let token = tokens[index];
402        if matches!(
403            token.kind,
404            SyntaxKind::Ident | SyntaxKind::CustomPropertyName
405        ) && let Some(colon_index) = next_non_trivia_token_index_until(tokens, index + 1, end)
406            && tokens[colon_index].kind == SyntaxKind::Colon
407        {
408            push_icss_fact(
409                icss,
410                seen,
411                ParsedIcssFactKind::ImportLocalName,
412                token.text.to_string(),
413                token.range,
414            );
415            if let Some(remote_index) =
416                next_non_trivia_token_index_until(tokens, colon_index + 1, end)
417                && matches!(
418                    tokens[remote_index].kind,
419                    SyntaxKind::Ident | SyntaxKind::CustomPropertyName
420                )
421            {
422                push_icss_fact(
423                    icss,
424                    seen,
425                    ParsedIcssFactKind::ImportRemoteName,
426                    tokens[remote_index].text.to_string(),
427                    tokens[remote_index].range,
428                );
429            }
430            index = css_module_value_statement_end(tokens, colon_index + 1);
431            continue;
432        }
433        index += 1;
434    }
435}
436
437fn push_icss_fact(
438    icss: &mut Vec<ParsedIcssFact>,
439    seen: &mut BTreeSet<(ParsedIcssFactKind, String, u32, u32)>,
440    kind: ParsedIcssFactKind,
441    name: String,
442    range: TextRange,
443) {
444    if seen.insert((
445        kind,
446        name.clone(),
447        u32::from(range.start()),
448        u32::from(range.end()),
449    )) {
450        icss.push(ParsedIcssFact { kind, name, range });
451    }
452}