1use 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}