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