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