1use cstree::text::{TextRange, TextSize};
2use omena_abstract_value::{
3 CssValueValidationClassV0, RegisteredPropertySyntaxV0, parse_registered_property_syntax_v0,
4 validate_registered_property_value_v0,
5};
6use omena_cascade::{
7 StaticSupportsAssumptionV0, StaticSupportsEvalVerdictV0, StaticSupportsEvalWitnessV0,
8 evaluate_static_supports_condition,
9};
10use omena_parser::{LexedToken, StyleDialect, lex};
11use omena_syntax::{
12 SyntaxKind,
13 ident::{AuthoredPropertyTextV0, CanonicalCustomPropertyNameV0, PropertyNameV0},
14};
15use omena_transform_cst::{IrNodeKindV0, IrNodeV0, TransformIrV0};
16use serde::Serialize;
17use std::collections::BTreeMap;
18
19use crate::control_flow::{analyze_scss_control_flow_values, build_scss_control_flow_graph};
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct OmenaScssEvalNativeCssFunctionSurfaceV0 {
24 pub schema_version: &'static str,
25 pub product: &'static str,
26 pub mode: &'static str,
27 pub dialect: &'static str,
28 pub function_count: usize,
29 pub parameter_count: usize,
30 pub typed_parameter_count: usize,
31 pub supported_parameter_syntax_count: usize,
32 pub unsupported_parameter_syntax_count: usize,
33 pub result_count: usize,
34 pub functions: Vec<OmenaScssEvalNativeCssFunctionV0>,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
38#[serde(rename_all = "camelCase")]
39pub struct OmenaScssEvalNativeCssFunctionV0 {
40 pub name: String,
41 pub source_span_start: usize,
42 pub source_span_end: usize,
43 pub name_span_start: usize,
44 pub name_span_end: usize,
45 pub return_syntax_source: Option<String>,
46 pub return_syntax: Option<RegisteredPropertySyntaxV0>,
47 pub parameter_count: usize,
48 pub typed_parameter_count: usize,
49 pub result_count: usize,
50 pub parameters: Vec<OmenaScssEvalNativeCssFunctionParameterV0>,
51 pub results: Vec<OmenaScssEvalNativeCssFunctionResultV0>,
52}
53
54#[derive(Debug, Clone, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct OmenaScssEvalNativeCssFunctionParameterV0 {
57 pub name: AuthoredPropertyTextV0,
58 pub property_key: CanonicalCustomPropertyNameV0,
59 pub source_span_start: usize,
60 pub source_span_end: usize,
61 pub name_span_start: usize,
62 pub name_span_end: usize,
63 pub syntax_source: Option<String>,
64 pub syntax: Option<RegisteredPropertySyntaxV0>,
65 pub default_value: Option<String>,
66}
67
68impl PartialEq for OmenaScssEvalNativeCssFunctionParameterV0 {
69 fn eq(&self, other: &Self) -> bool {
70 self.property_key == other.property_key
71 && self.source_span_start == other.source_span_start
72 && self.source_span_end == other.source_span_end
73 && self.name_span_start == other.name_span_start
74 && self.name_span_end == other.name_span_end
75 && self.syntax_source == other.syntax_source
76 && self.syntax == other.syntax
77 && self.default_value == other.default_value
78 }
79}
80
81impl Eq for OmenaScssEvalNativeCssFunctionParameterV0 {}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
84#[serde(rename_all = "camelCase")]
85pub struct OmenaScssEvalNativeCssFunctionResultV0 {
86 pub value: String,
87 pub source_span_start: usize,
88 pub source_span_end: usize,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct OmenaScssEvalNativeCssIfFunctionDecisionSurfaceV0 {
94 pub schema_version: &'static str,
95 pub product: &'static str,
96 pub mode: &'static str,
97 pub dialect: &'static str,
98 pub function_count: usize,
99 pub foldable_function_count: usize,
100 pub preserved_function_count: usize,
101 pub static_supports_branch_count: usize,
102 pub runtime_branch_count: usize,
103 pub functions: Vec<OmenaScssEvalNativeCssIfFunctionDecisionV0>,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
107#[serde(rename_all = "camelCase")]
108pub struct OmenaScssEvalNativeCssIfFunctionDecisionV0 {
109 pub source_span_start: usize,
110 pub source_span_end: usize,
111 pub branch_count: usize,
112 pub decision: &'static str,
113 pub reason: &'static str,
114 pub selected_branch_index: Option<usize>,
115 pub selected_value: Option<String>,
116 pub branches: Vec<OmenaScssEvalNativeCssIfFunctionBranchV0>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
120#[serde(rename_all = "camelCase")]
121pub struct OmenaScssEvalNativeCssIfFunctionBranchV0 {
122 pub branch_index: usize,
123 pub condition: String,
124 pub condition_kind: &'static str,
125 pub verdict: &'static str,
126 pub value: Option<String>,
127 pub source_span_start: usize,
128 pub source_span_end: usize,
129 pub supports_witness: Option<StaticSupportsEvalWitnessV0>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
133#[serde(rename_all = "camelCase")]
134pub struct OmenaScssEvalNativeCssFunctionCallEvaluationSurfaceV0 {
135 pub schema_version: &'static str,
136 pub product: &'static str,
137 pub mode: &'static str,
138 pub dialect: &'static str,
139 pub call_count: usize,
140 pub foldable_call_count: usize,
141 pub preserved_call_count: usize,
142 pub structural_error_count: usize,
143 pub runtime_dependent_call_count: usize,
144 pub missing_result_count: usize,
145 pub calls: Vec<OmenaScssEvalNativeCssFunctionCallEvaluationV0>,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
149#[serde(rename_all = "camelCase")]
150pub struct OmenaScssEvalNativeCssFunctionCallEvaluationV0 {
151 pub name: String,
152 pub source_span_start: usize,
153 pub source_span_end: usize,
154 pub argument_count: usize,
155 pub matched_function_count: usize,
156 pub matched_function_source_span_start: Option<usize>,
157 pub matched_function_source_span_end: Option<usize>,
158 pub decision: &'static str,
159 pub reason: &'static str,
160 pub evaluated_value: Option<String>,
161 pub arguments: Vec<OmenaScssEvalNativeCssFunctionCallArgumentV0>,
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
165#[serde(rename_all = "camelCase")]
166pub struct OmenaScssEvalNativeCssFunctionCallArgumentV0 {
167 pub argument_index: usize,
168 pub value: String,
169 pub source_span_start: usize,
170 pub source_span_end: usize,
171 pub static_value: bool,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
175#[serde(rename_all = "camelCase")]
176pub struct OmenaScssEvalNativeCssStaticEditPlanV0 {
177 pub schema_version: &'static str,
178 pub product: &'static str,
179 pub mode: &'static str,
180 pub dialect: &'static str,
181 pub edit_count: usize,
182 pub when_rule_edit_count: usize,
183 pub if_rule_edit_count: usize,
184 pub if_function_edit_count: usize,
185 pub function_call_edit_count: usize,
186 pub output_changed: bool,
187 pub edited_css: String,
188 pub edits: Vec<OmenaScssEvalNativeCssStaticEditV0>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
192#[serde(rename_all = "camelCase")]
193pub struct OmenaScssEvalNativeCssStaticEditV0 {
194 pub start: usize,
195 pub end: usize,
196 pub replacement: String,
197 pub edit_kind: &'static str,
198}
199
200pub fn summarize_native_css_function_surface(
201 source: &str,
202 dialect: StyleDialect,
203) -> Option<OmenaScssEvalNativeCssFunctionSurfaceV0> {
204 if dialect != StyleDialect::Css {
205 return None;
206 }
207 let lexed = lex(source, dialect);
208 let tokens = lexed.tokens();
209 let functions = collect_native_css_functions(source, tokens);
210 let parameter_count = functions
211 .iter()
212 .map(|function| function.parameter_count)
213 .sum();
214 let typed_parameter_count = functions
215 .iter()
216 .map(|function| function.typed_parameter_count)
217 .sum();
218 let supported_parameter_syntax_count = functions
219 .iter()
220 .flat_map(|function| function.parameters.iter())
221 .filter(|parameter| parameter_syntax_is_supported(parameter.syntax.as_ref()))
222 .count();
223 let unsupported_parameter_syntax_count = functions
224 .iter()
225 .flat_map(|function| function.parameters.iter())
226 .filter(|parameter| {
227 matches!(
228 parameter.syntax,
229 Some(RegisteredPropertySyntaxV0::Unsupported { .. })
230 )
231 })
232 .count();
233 let result_count = functions.iter().map(|function| function.result_count).sum();
234
235 Some(OmenaScssEvalNativeCssFunctionSurfaceV0 {
236 schema_version: "0",
237 product: "omena-scss-eval.native-css-function-surface",
238 mode: "recognitionOnly",
239 dialect: "css",
240 function_count: functions.len(),
241 parameter_count,
242 typed_parameter_count,
243 supported_parameter_syntax_count,
244 unsupported_parameter_syntax_count,
245 result_count,
246 functions,
247 })
248}
249
250pub fn summarize_native_css_function_call_evaluations(
251 source: &str,
252 dialect: StyleDialect,
253) -> Option<OmenaScssEvalNativeCssFunctionCallEvaluationSurfaceV0> {
254 if dialect != StyleDialect::Css {
255 return None;
256 }
257 let lexed = lex(source, dialect);
258 let tokens = lexed.tokens();
259 let functions = collect_native_css_functions(source, tokens);
260 let calls = collect_native_css_function_call_evaluations(source, tokens, &functions);
261 let call_count = calls.len();
262 let foldable_call_count = calls
263 .iter()
264 .filter(|call| call.decision == "foldToStaticValue")
265 .count();
266 let preserved_call_count = calls
267 .iter()
268 .filter(|call| call.decision == "preserveVerbatim")
269 .count();
270 let structural_error_count = calls
271 .iter()
272 .filter(|call| call.decision == "structuralError")
273 .count();
274 let runtime_dependent_call_count = calls
275 .iter()
276 .filter(|call| call.reason.contains("runtime") || call.reason.contains("cascade"))
277 .count();
278 let missing_result_count = calls
279 .iter()
280 .filter(|call| call.reason == "function has no result declaration")
281 .count();
282
283 Some(OmenaScssEvalNativeCssFunctionCallEvaluationSurfaceV0 {
284 schema_version: "0",
285 product: "omena-scss-eval.native-css-function-call-evaluations",
286 mode: "oracleOnlyPruneButKeep",
287 dialect: "css",
288 call_count,
289 foldable_call_count,
290 preserved_call_count,
291 structural_error_count,
292 runtime_dependent_call_count,
293 missing_result_count,
294 calls,
295 })
296}
297
298pub fn summarize_native_css_static_edit_plan(
299 source: &str,
300 dialect: StyleDialect,
301) -> Option<OmenaScssEvalNativeCssStaticEditPlanV0> {
302 if dialect != StyleDialect::Css {
303 return None;
304 }
305
306 let if_function_decisions = summarize_native_css_if_function_decisions(source, dialect)?;
307 let function_call_evaluations =
308 summarize_native_css_function_call_evaluations(source, dialect)?;
309 let mut edits = Vec::new();
310 let lexed = lex(source, dialect);
311 let tokens = lexed.tokens();
312 let functions = collect_native_css_functions(source, tokens);
313 let when_rule_truthiness_by_start = native_css_branch_truthiness_by_start(source, "@when");
314 let if_rule_truthiness_by_start = native_css_branch_truthiness_by_start(source, "@if");
315 let if_function_truthiness_by_start = native_css_branch_truthiness_by_start(source, "if()");
316 edits.extend(native_css_conditional_rule_static_edits(
317 source,
318 tokens,
319 "@when",
320 "whenRuleBranchFold",
321 &when_rule_truthiness_by_start,
322 ));
323 edits.extend(native_css_conditional_rule_static_edits(
324 source,
325 tokens,
326 "@if",
327 "ifRuleBranchFold",
328 &if_rule_truthiness_by_start,
329 ));
330 edits.extend(native_css_if_function_static_edits(
331 &if_function_decisions,
332 &if_function_truthiness_by_start,
333 &functions,
334 ));
335 edits.extend(native_css_function_call_static_edits(
336 &function_call_evaluations,
337 &functions,
338 ));
339 let edits = normalize_native_css_static_edits(source, edits)?;
340 let edited_css = apply_native_css_static_edits(source, &edits);
341 let edit_count = edits.len();
342 let when_rule_edit_count = edits
343 .iter()
344 .filter(|edit| edit.edit_kind == "whenRuleBranchFold")
345 .count();
346 let if_rule_edit_count = edits
347 .iter()
348 .filter(|edit| edit.edit_kind == "ifRuleBranchFold")
349 .count();
350 let if_function_edit_count = edits
351 .iter()
352 .filter(|edit| edit.edit_kind == "ifFunctionValueFold")
353 .count();
354 let function_call_edit_count = edits
355 .iter()
356 .filter(|edit| edit.edit_kind == "functionCallValueFold")
357 .count();
358 let output_changed = edited_css != source;
359
360 Some(OmenaScssEvalNativeCssStaticEditPlanV0 {
361 schema_version: "0",
362 product: "omena-scss-eval.native-css-static-edit-plan",
363 mode: "staticSubsetPruneButKeep",
364 dialect: "css",
365 edit_count,
366 when_rule_edit_count,
367 if_rule_edit_count,
368 if_function_edit_count,
369 function_call_edit_count,
370 output_changed,
371 edited_css,
372 edits,
373 })
374}
375
376pub fn summarize_native_css_static_edit_plan_from_transform_ir(
377 ir: &TransformIrV0,
378 dialect: StyleDialect,
379) -> Option<OmenaScssEvalNativeCssStaticEditPlanV0> {
380 if dialect != StyleDialect::Css {
381 return None;
382 }
383
384 let source = ir.source_text();
385 let functions = collect_native_css_functions_from_transform_ir(source, dialect, ir);
386 let if_function_decisions = native_css_if_function_decision_surface(
387 collect_native_css_if_function_decisions_from_transform_ir(source, dialect, ir),
388 );
389 let function_call_evaluations = native_css_function_call_evaluation_surface(
390 collect_native_css_function_call_evaluations_from_transform_ir(
391 source, dialect, ir, &functions,
392 ),
393 );
394 let when_rule_truthiness_by_start = native_css_branch_truthiness_by_start(source, "@when");
395 let if_rule_truthiness_by_start = native_css_branch_truthiness_by_start(source, "@if");
396 let if_function_truthiness_by_start = native_css_branch_truthiness_by_start(source, "if()");
397 let mut edits = native_css_conditional_rule_static_edits_from_transform_ir(
398 source,
399 dialect,
400 ir,
401 "@when",
402 "whenRuleBranchFold",
403 &when_rule_truthiness_by_start,
404 );
405 edits.extend(native_css_conditional_rule_static_edits_from_transform_ir(
406 source,
407 dialect,
408 ir,
409 "@if",
410 "ifRuleBranchFold",
411 &if_rule_truthiness_by_start,
412 ));
413 edits.extend(native_css_if_function_static_edits(
414 &if_function_decisions,
415 &if_function_truthiness_by_start,
416 &functions,
417 ));
418 edits.extend(native_css_function_call_static_edits(
419 &function_call_evaluations,
420 &functions,
421 ));
422 native_css_static_edit_plan_from_edits(source, edits)
423}
424
425fn native_css_if_function_decision_surface(
426 functions: Vec<OmenaScssEvalNativeCssIfFunctionDecisionV0>,
427) -> OmenaScssEvalNativeCssIfFunctionDecisionSurfaceV0 {
428 let function_count = functions.len();
429 let foldable_function_count = functions
430 .iter()
431 .filter(|function| function.decision == "foldToStaticValue")
432 .count();
433 let preserved_function_count = function_count.saturating_sub(foldable_function_count);
434 let static_supports_branch_count = functions
435 .iter()
436 .flat_map(|function| function.branches.iter())
437 .filter(|branch| branch.condition_kind == "supports")
438 .count();
439 let runtime_branch_count = functions
440 .iter()
441 .flat_map(|function| function.branches.iter())
442 .filter(|branch| matches!(branch.condition_kind, "media" | "style"))
443 .count();
444
445 OmenaScssEvalNativeCssIfFunctionDecisionSurfaceV0 {
446 schema_version: "0",
447 product: "omena-scss-eval.native-css-if-function-decisions",
448 mode: "oracleOnlyPruneButKeep",
449 dialect: "css",
450 function_count,
451 foldable_function_count,
452 preserved_function_count,
453 static_supports_branch_count,
454 runtime_branch_count,
455 functions,
456 }
457}
458
459fn native_css_function_call_evaluation_surface(
460 calls: Vec<OmenaScssEvalNativeCssFunctionCallEvaluationV0>,
461) -> OmenaScssEvalNativeCssFunctionCallEvaluationSurfaceV0 {
462 let call_count = calls.len();
463 let foldable_call_count = calls
464 .iter()
465 .filter(|call| call.decision == "foldToStaticValue")
466 .count();
467 let preserved_call_count = calls
468 .iter()
469 .filter(|call| call.decision == "preserveVerbatim")
470 .count();
471 let structural_error_count = calls
472 .iter()
473 .filter(|call| call.decision == "structuralError")
474 .count();
475 let runtime_dependent_call_count = calls
476 .iter()
477 .filter(|call| call.reason.contains("runtime") || call.reason.contains("cascade"))
478 .count();
479 let missing_result_count = calls
480 .iter()
481 .filter(|call| call.reason == "function has no result declaration")
482 .count();
483
484 OmenaScssEvalNativeCssFunctionCallEvaluationSurfaceV0 {
485 schema_version: "0",
486 product: "omena-scss-eval.native-css-function-call-evaluations",
487 mode: "oracleOnlyPruneButKeep",
488 dialect: "css",
489 call_count,
490 foldable_call_count,
491 preserved_call_count,
492 structural_error_count,
493 runtime_dependent_call_count,
494 missing_result_count,
495 calls,
496 }
497}
498
499fn native_css_static_edit_plan_from_edits(
500 source: &str,
501 edits: Vec<OmenaScssEvalNativeCssStaticEditV0>,
502) -> Option<OmenaScssEvalNativeCssStaticEditPlanV0> {
503 let edits = normalize_native_css_static_edits(source, edits)?;
504 let edited_css = apply_native_css_static_edits(source, &edits);
505 let edit_count = edits.len();
506 let when_rule_edit_count = edits
507 .iter()
508 .filter(|edit| edit.edit_kind == "whenRuleBranchFold")
509 .count();
510 let if_rule_edit_count = edits
511 .iter()
512 .filter(|edit| edit.edit_kind == "ifRuleBranchFold")
513 .count();
514 let if_function_edit_count = edits
515 .iter()
516 .filter(|edit| edit.edit_kind == "ifFunctionValueFold")
517 .count();
518 let function_call_edit_count = edits
519 .iter()
520 .filter(|edit| edit.edit_kind == "functionCallValueFold")
521 .count();
522 let output_changed = edited_css != source;
523
524 Some(OmenaScssEvalNativeCssStaticEditPlanV0 {
525 schema_version: "0",
526 product: "omena-scss-eval.native-css-static-edit-plan",
527 mode: "staticSubsetPruneButKeep",
528 dialect: "css",
529 edit_count,
530 when_rule_edit_count,
531 if_rule_edit_count,
532 if_function_edit_count,
533 function_call_edit_count,
534 output_changed,
535 edited_css,
536 edits,
537 })
538}
539
540pub fn summarize_native_css_if_function_decisions(
541 source: &str,
542 dialect: StyleDialect,
543) -> Option<OmenaScssEvalNativeCssIfFunctionDecisionSurfaceV0> {
544 if dialect != StyleDialect::Css {
545 return None;
546 }
547 let lexed = lex(source, dialect);
548 let tokens = lexed.tokens();
549 Some(native_css_if_function_decision_surface(
550 collect_native_css_if_function_decisions(source, tokens),
551 ))
552}
553
554fn collect_native_css_functions(
555 source: &str,
556 tokens: &[LexedToken],
557) -> Vec<OmenaScssEvalNativeCssFunctionV0> {
558 tokens
559 .iter()
560 .enumerate()
561 .filter_map(|(index, token)| {
562 (token.kind == SyntaxKind::AtKeyword && token.text.eq_ignore_ascii_case("@function"))
563 .then(|| collect_native_css_function(source, tokens, index))
564 .flatten()
565 })
566 .collect()
567}
568
569fn collect_native_css_if_function_decisions(
570 source: &str,
571 tokens: &[LexedToken],
572) -> Vec<OmenaScssEvalNativeCssIfFunctionDecisionV0> {
573 tokens
574 .iter()
575 .enumerate()
576 .filter_map(|(index, token)| {
577 (token.kind == SyntaxKind::Ident && token.text.eq_ignore_ascii_case("if"))
578 .then(|| collect_native_css_if_function_decision(source, tokens, index))
579 .flatten()
580 })
581 .collect()
582}
583
584fn collect_native_css_functions_from_transform_ir(
585 source: &str,
586 dialect: StyleDialect,
587 ir: &TransformIrV0,
588) -> Vec<OmenaScssEvalNativeCssFunctionV0> {
589 let mut functions_by_key = BTreeMap::new();
590 for node in active_transform_ir_nodes_by_kind(ir, &[IrNodeKindV0::AtRule]) {
591 let Some(tokens) = lex_native_css_transform_ir_node(source, dialect, node) else {
592 continue;
593 };
594 let Some(at_keyword_index) = next_non_trivia_token_index(&tokens, 0) else {
595 continue;
596 };
597 let Some(at_keyword) = tokens.get(at_keyword_index) else {
598 continue;
599 };
600 if at_keyword.kind != SyntaxKind::AtKeyword
601 || !at_keyword.text.eq_ignore_ascii_case("@function")
602 {
603 continue;
604 }
605 let Some(function) = collect_native_css_function(source, &tokens, at_keyword_index) else {
606 continue;
607 };
608 functions_by_key
609 .entry((
610 function.source_span_start,
611 function.source_span_end,
612 function.name.clone(),
613 ))
614 .or_insert(function);
615 }
616 functions_by_key.into_values().collect()
617}
618
619fn collect_native_css_if_function_decisions_from_transform_ir(
620 source: &str,
621 dialect: StyleDialect,
622 ir: &TransformIrV0,
623) -> Vec<OmenaScssEvalNativeCssIfFunctionDecisionV0> {
624 let mut functions_by_span = BTreeMap::new();
625 for node in active_transform_ir_nodes_by_kind(
626 ir,
627 &[
628 IrNodeKindV0::Value,
629 IrNodeKindV0::Declaration,
630 IrNodeKindV0::AtRule,
631 ],
632 ) {
633 let Some(tokens) = lex_native_css_transform_ir_node(source, dialect, node) else {
634 continue;
635 };
636 for (index, token) in tokens.iter().enumerate() {
637 if token.kind != SyntaxKind::Ident || !token.text.eq_ignore_ascii_case("if") {
638 continue;
639 }
640 let Some(function) = collect_native_css_if_function_decision(source, &tokens, index)
641 else {
642 continue;
643 };
644 functions_by_span
645 .entry((function.source_span_start, function.source_span_end))
646 .or_insert(function);
647 }
648 }
649 functions_by_span.into_values().collect()
650}
651
652fn collect_native_css_function_call_evaluations_from_transform_ir(
653 source: &str,
654 dialect: StyleDialect,
655 ir: &TransformIrV0,
656 functions: &[OmenaScssEvalNativeCssFunctionV0],
657) -> Vec<OmenaScssEvalNativeCssFunctionCallEvaluationV0> {
658 let mut calls_by_key = BTreeMap::new();
659 for node in active_transform_ir_nodes_by_kind(
660 ir,
661 &[
662 IrNodeKindV0::Value,
663 IrNodeKindV0::Declaration,
664 IrNodeKindV0::AtRule,
665 ],
666 ) {
667 if !ir_node_source(source, node).is_some_and(|node_source| node_source.contains("--")) {
668 continue;
669 }
670 let Some(tokens) = lex_native_css_transform_ir_node(source, dialect, node) else {
671 continue;
672 };
673 for (index, token) in tokens.iter().enumerate() {
674 if !matches!(
675 token.kind,
676 SyntaxKind::Ident | SyntaxKind::CustomPropertyName
677 ) || !token.text.starts_with("--")
678 || native_css_function_name_is_declaration(&tokens, index)
679 {
680 continue;
681 }
682 let Some(call) =
683 collect_native_css_function_call_evaluation(source, &tokens, functions, index)
684 else {
685 continue;
686 };
687 calls_by_key
688 .entry((
689 call.source_span_start,
690 call.source_span_end,
691 call.name.clone(),
692 ))
693 .or_insert(call);
694 }
695 }
696 calls_by_key.into_values().collect()
697}
698
699fn native_css_conditional_rule_static_edits_from_transform_ir(
700 source: &str,
701 dialect: StyleDialect,
702 ir: &TransformIrV0,
703 at_rule_name: &'static str,
704 edit_kind: &'static str,
705 truthiness_by_start: &BTreeMap<usize, bool>,
706) -> Vec<OmenaScssEvalNativeCssStaticEditV0> {
707 let mut edits_by_key = BTreeMap::new();
708 for node in active_transform_ir_nodes_by_kind(ir, &[IrNodeKindV0::AtRule]) {
709 let span_end =
710 native_css_conditional_rule_span_end(source, dialect, ir, node, at_rule_name)
711 .unwrap_or(node.source_span_end);
712 let Some(tokens) =
713 lex_native_css_transform_ir_span(source, dialect, node.source_span_start, span_end)
714 else {
715 continue;
716 };
717 let Some(at_rule_index) = next_non_trivia_token_index(&tokens, 0) else {
718 continue;
719 };
720 let Some(at_rule_token) = tokens.get(at_rule_index) else {
721 continue;
722 };
723 if at_rule_token.kind != SyntaxKind::AtKeyword
724 || !at_rule_token.text.eq_ignore_ascii_case(at_rule_name)
725 {
726 continue;
727 }
728 let Some((edit, _)) = collect_native_css_conditional_rule_static_edit(
729 source,
730 &tokens,
731 at_rule_index,
732 edit_kind,
733 truthiness_by_start,
734 ) else {
735 continue;
736 };
737 edits_by_key
738 .entry((edit.start, edit.end, edit.edit_kind))
739 .or_insert(edit);
740 }
741 edits_by_key.into_values().collect()
742}
743
744fn native_css_conditional_rule_span_end(
745 source: &str,
746 dialect: StyleDialect,
747 ir: &TransformIrV0,
748 node: &IrNodeV0,
749 at_rule_name: &str,
750) -> Option<usize> {
751 let tokens = lex_native_css_transform_ir_node(source, dialect, node)?;
752 let at_rule_index = next_non_trivia_token_index(&tokens, 0)?;
753 let at_rule_token = tokens.get(at_rule_index)?;
754 if at_rule_token.kind != SyntaxKind::AtKeyword
755 || !at_rule_token.text.eq_ignore_ascii_case(at_rule_name)
756 {
757 return None;
758 }
759 let else_node = immediate_native_css_else_at_rule_node(source, dialect, ir, node)?;
760 Some(else_node.source_span_end)
761}
762
763fn immediate_native_css_else_at_rule_node<'a>(
764 source: &str,
765 dialect: StyleDialect,
766 ir: &'a TransformIrV0,
767 node: &IrNodeV0,
768) -> Option<&'a IrNodeV0> {
769 let next = ir
770 .nodes
771 .iter()
772 .filter(|candidate| {
773 !candidate.deleted && candidate.source_span_start >= node.source_span_end
774 })
775 .min_by_key(|candidate| candidate.source_span_start)?;
776 if next.kind != IrNodeKindV0::AtRule
777 || !source_span_is_trivia_only(
778 source,
779 dialect,
780 node.source_span_end,
781 next.source_span_start,
782 )
783 {
784 return None;
785 }
786 let tokens = lex_native_css_transform_ir_node(source, dialect, next)?;
787 let at_keyword_index = next_non_trivia_token_index(&tokens, 0)?;
788 let at_keyword = tokens.get(at_keyword_index)?;
789 (at_keyword.kind == SyntaxKind::AtKeyword && at_keyword.text.eq_ignore_ascii_case("@else"))
790 .then_some(next)
791}
792
793fn active_transform_ir_nodes_by_kind<'a>(
794 ir: &'a TransformIrV0,
795 kinds: &[IrNodeKindV0],
796) -> Vec<&'a IrNodeV0> {
797 let mut nodes = ir
798 .nodes
799 .iter()
800 .filter(|node| !node.deleted && kinds.contains(&node.kind))
801 .collect::<Vec<_>>();
802 nodes.sort_by_key(|node| (node.source_span_start, node.source_span_end, node.node_id));
803 nodes
804}
805
806fn ir_node_source<'source>(source: &'source str, node: &IrNodeV0) -> Option<&'source str> {
807 source.get(node.source_span_start..node.source_span_end)
808}
809
810fn lex_native_css_transform_ir_node(
811 source: &str,
812 dialect: StyleDialect,
813 node: &IrNodeV0,
814) -> Option<Vec<LexedToken>> {
815 lex_native_css_transform_ir_span(
816 source,
817 dialect,
818 node.source_span_start,
819 node.source_span_end,
820 )
821}
822
823fn lex_native_css_transform_ir_span(
824 source: &str,
825 dialect: StyleDialect,
826 source_span_start: usize,
827 source_span_end: usize,
828) -> Option<Vec<LexedToken>> {
829 if source_span_start > source_span_end
830 || source_span_end > source.len()
831 || !source.is_char_boundary(source_span_start)
832 || !source.is_char_boundary(source_span_end)
833 {
834 return None;
835 }
836 let source_slice = source.get(source_span_start..source_span_end)?;
837 lex(source_slice, dialect)
838 .tokens()
839 .iter()
840 .map(|token| shift_lexed_token(token, source_span_start))
841 .collect()
842}
843
844fn shift_lexed_token(token: &LexedToken, source_span_start: usize) -> Option<LexedToken> {
845 let shifted_start = token_start(token).checked_add(source_span_start)?;
846 let shifted_end = token_end(token).checked_add(source_span_start)?;
847 Some(LexedToken {
848 kind: token.kind,
849 range: TextRange::new(
850 TextSize::from(u32::try_from(shifted_start).ok()?),
851 TextSize::from(u32::try_from(shifted_end).ok()?),
852 ),
853 text: token.text.clone(),
854 })
855}
856
857fn source_span_is_trivia_only(
858 source: &str,
859 dialect: StyleDialect,
860 source_span_start: usize,
861 source_span_end: usize,
862) -> bool {
863 lex_native_css_transform_ir_span(source, dialect, source_span_start, source_span_end)
864 .is_some_and(|tokens| tokens.iter().all(|token| token.kind.is_trivia()))
865}
866
867fn native_css_conditional_rule_static_edits(
868 source: &str,
869 tokens: &[LexedToken],
870 at_rule_name: &'static str,
871 edit_kind: &'static str,
872 truthiness_by_start: &BTreeMap<usize, bool>,
873) -> Vec<OmenaScssEvalNativeCssStaticEditV0> {
874 let mut edits = Vec::new();
875 let mut index = 0usize;
876 while index < tokens.len() {
877 let Some(token) = tokens.get(index) else {
878 break;
879 };
880 if token.kind == SyntaxKind::AtKeyword
881 && token.text.eq_ignore_ascii_case(at_rule_name)
882 && let Some((edit, next_index)) = collect_native_css_conditional_rule_static_edit(
883 source,
884 tokens,
885 index,
886 edit_kind,
887 truthiness_by_start,
888 )
889 {
890 edits.push(edit);
891 index = next_index.saturating_add(1);
892 continue;
893 }
894 index += 1;
895 }
896 edits
897}
898
899fn collect_native_css_conditional_rule_static_edit(
900 source: &str,
901 tokens: &[LexedToken],
902 at_rule_index: usize,
903 edit_kind: &'static str,
904 truthiness_by_start: &BTreeMap<usize, bool>,
905) -> Option<(OmenaScssEvalNativeCssStaticEditV0, usize)> {
906 let block_start_index = next_matching_token_index(tokens, at_rule_index + 1, |token| {
907 matches!(
908 token.kind,
909 SyntaxKind::LeftBrace | SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon
910 )
911 })?;
912 if tokens.get(block_start_index)?.kind != SyntaxKind::LeftBrace {
913 return None;
914 }
915 let block_end_index = matching_token_index(
916 tokens,
917 block_start_index,
918 SyntaxKind::LeftBrace,
919 SyntaxKind::RightBrace,
920 )?;
921 let else_rule = collect_immediate_native_css_else_rule(source, tokens, block_end_index + 1);
922
923 let source_span_start = token_start(tokens.get(at_rule_index)?);
924 match truthiness_by_start.get(&source_span_start).copied() {
925 Some(true) => {
926 let source_span_end = else_rule
927 .as_ref()
928 .map(|else_rule| else_rule.source_span_end)
929 .unwrap_or_else(|| token_end(&tokens[block_end_index]));
930 let replacement =
931 block_inner_source(source, tokens, block_start_index, block_end_index)?;
932 Some((
933 OmenaScssEvalNativeCssStaticEditV0 {
934 start: source_span_start,
935 end: source_span_end,
936 replacement,
937 edit_kind,
938 },
939 else_rule
940 .as_ref()
941 .map(|else_rule| else_rule.block_end_index)
942 .unwrap_or(block_end_index),
943 ))
944 }
945 Some(false) => {
946 let Some(else_rule) = else_rule else {
947 return Some((
948 OmenaScssEvalNativeCssStaticEditV0 {
949 start: source_span_start,
950 end: token_end(tokens.get(block_end_index)?),
951 replacement: String::new(),
952 edit_kind,
953 },
954 block_end_index,
955 ));
956 };
957 if !else_rule.header_text.trim().is_empty() {
958 return None;
959 }
960 let replacement = block_inner_source(
961 source,
962 tokens,
963 else_rule.block_start_index,
964 else_rule.block_end_index,
965 )?;
966 Some((
967 OmenaScssEvalNativeCssStaticEditV0 {
968 start: source_span_start,
969 end: else_rule.source_span_end,
970 replacement,
971 edit_kind,
972 },
973 else_rule.block_end_index,
974 ))
975 }
976 _ => None,
977 }
978}
979
980fn native_css_branch_truthiness_by_start(
981 source: &str,
982 at_rule_name: &str,
983) -> BTreeMap<usize, bool> {
984 let Some(graph) = build_scss_control_flow_graph(source, StyleDialect::Css) else {
985 return BTreeMap::new();
986 };
987 let Some(analysis) = analyze_scss_control_flow_values(source, StyleDialect::Css) else {
988 return BTreeMap::new();
989 };
990 let source_start_by_when_node_key = graph
991 .blocks
992 .iter()
993 .filter(|block| block.block.at_rule_name.eq_ignore_ascii_case(at_rule_name))
994 .map(|block| {
995 (
996 block.node_key.as_str().to_string(),
997 block.block.source_span_start,
998 )
999 })
1000 .collect::<BTreeMap<_, _>>();
1001
1002 analysis
1003 .blocks
1004 .iter()
1005 .filter_map(|block| {
1006 let truthy = match block.transfer_truthiness {
1007 Some("truthy") => true,
1008 Some("falsey") => false,
1009 _ => return None,
1010 };
1011 source_start_by_when_node_key
1012 .get(block.node_key.as_str())
1013 .copied()
1014 .map(|source_span_start| (source_span_start, truthy))
1015 })
1016 .collect()
1017}
1018
1019#[derive(Debug, Clone, PartialEq, Eq)]
1020struct NativeCssElseRuleV0 {
1021 header_text: String,
1022 block_start_index: usize,
1023 block_end_index: usize,
1024 source_span_end: usize,
1025}
1026
1027fn collect_immediate_native_css_else_rule(
1028 source: &str,
1029 tokens: &[LexedToken],
1030 start_index: usize,
1031) -> Option<NativeCssElseRuleV0> {
1032 let else_index = next_non_trivia_token_index(tokens, start_index)?;
1033 let else_token = tokens.get(else_index)?;
1034 if else_token.kind != SyntaxKind::AtKeyword || !else_token.text.eq_ignore_ascii_case("@else") {
1035 return None;
1036 }
1037 let block_start_index = next_matching_token_index(tokens, else_index + 1, |token| {
1038 matches!(
1039 token.kind,
1040 SyntaxKind::LeftBrace | SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon
1041 )
1042 })?;
1043 if tokens.get(block_start_index)?.kind != SyntaxKind::LeftBrace {
1044 return None;
1045 }
1046 let block_end_index = matching_token_index(
1047 tokens,
1048 block_start_index,
1049 SyntaxKind::LeftBrace,
1050 SyntaxKind::RightBrace,
1051 )?;
1052 Some(NativeCssElseRuleV0 {
1053 header_text: trimmed_source_between_tokens(
1054 source,
1055 tokens,
1056 else_index + 1,
1057 block_start_index,
1058 )
1059 .unwrap_or_default(),
1060 block_start_index,
1061 block_end_index,
1062 source_span_end: token_end(tokens.get(block_end_index)?),
1063 })
1064}
1065
1066fn block_inner_source(
1067 source: &str,
1068 tokens: &[LexedToken],
1069 block_start_index: usize,
1070 block_end_index: usize,
1071) -> Option<String> {
1072 let start = token_end(tokens.get(block_start_index)?);
1073 let end = token_start(tokens.get(block_end_index)?);
1074 source.get(start..end).map(ToString::to_string)
1075}
1076
1077fn native_css_if_function_static_edits(
1078 surface: &OmenaScssEvalNativeCssIfFunctionDecisionSurfaceV0,
1079 truthiness_by_start: &BTreeMap<usize, bool>,
1080 functions: &[OmenaScssEvalNativeCssFunctionV0],
1081) -> Vec<OmenaScssEvalNativeCssStaticEditV0> {
1082 surface
1083 .functions
1084 .iter()
1085 .filter(|function| {
1086 !native_css_if_function_is_inside_function_declaration(function, functions)
1087 })
1088 .filter_map(|function| {
1089 let replacement = function.selected_value.as_ref()?;
1090 (function.decision == "foldToStaticValue"
1091 && native_css_if_function_edge_ir_allows_fold(function, truthiness_by_start))
1092 .then(|| OmenaScssEvalNativeCssStaticEditV0 {
1093 start: function.source_span_start,
1094 end: function.source_span_end,
1095 replacement: replacement.clone(),
1096 edit_kind: "ifFunctionValueFold",
1097 })
1098 })
1099 .collect()
1100}
1101
1102fn native_css_if_function_is_inside_function_declaration(
1103 function: &OmenaScssEvalNativeCssIfFunctionDecisionV0,
1104 functions: &[OmenaScssEvalNativeCssFunctionV0],
1105) -> bool {
1106 functions.iter().any(|declaration| {
1107 declaration.source_span_start <= function.source_span_start
1108 && function.source_span_end <= declaration.source_span_end
1109 })
1110}
1111
1112fn native_css_if_function_edge_ir_allows_fold(
1113 function: &OmenaScssEvalNativeCssIfFunctionDecisionV0,
1114 truthiness_by_start: &BTreeMap<usize, bool>,
1115) -> bool {
1116 let Some(edge_ir_truthy) = truthiness_by_start
1117 .get(&function.source_span_start)
1118 .copied()
1119 else {
1120 return false;
1121 };
1122 match function.selected_branch_index {
1123 Some(0) => edge_ir_truthy,
1124 Some(selected_branch_index) => {
1125 !edge_ir_truthy
1126 && function
1127 .branches
1128 .get(selected_branch_index)
1129 .is_some_and(|branch| branch.verdict == "else")
1130 && function
1131 .branches
1132 .iter()
1133 .take(selected_branch_index)
1134 .all(|branch| branch.verdict == "alwaysFalse")
1135 }
1136 None => false,
1137 }
1138}
1139
1140fn native_css_function_call_static_edits(
1141 surface: &OmenaScssEvalNativeCssFunctionCallEvaluationSurfaceV0,
1142 functions: &[OmenaScssEvalNativeCssFunctionV0],
1143) -> Vec<OmenaScssEvalNativeCssStaticEditV0> {
1144 surface
1145 .calls
1146 .iter()
1147 .filter(|call| !native_css_function_call_is_inside_function_declaration(call, functions))
1148 .filter_map(|call| {
1149 let replacement = call.evaluated_value.as_ref()?;
1150 (call.decision == "foldToStaticValue").then(|| OmenaScssEvalNativeCssStaticEditV0 {
1151 start: call.source_span_start,
1152 end: call.source_span_end,
1153 replacement: replacement.clone(),
1154 edit_kind: "functionCallValueFold",
1155 })
1156 })
1157 .collect()
1158}
1159
1160fn native_css_function_call_is_inside_function_declaration(
1161 call: &OmenaScssEvalNativeCssFunctionCallEvaluationV0,
1162 functions: &[OmenaScssEvalNativeCssFunctionV0],
1163) -> bool {
1164 functions.iter().any(|function| {
1165 function.source_span_start <= call.source_span_start
1166 && call.source_span_end <= function.source_span_end
1167 })
1168}
1169
1170fn normalize_native_css_static_edits(
1171 source: &str,
1172 mut edits: Vec<OmenaScssEvalNativeCssStaticEditV0>,
1173) -> Option<Vec<OmenaScssEvalNativeCssStaticEditV0>> {
1174 edits.sort_by_key(|edit| edit.start);
1175 edits.dedup_by(|left, right| {
1176 left.start == right.start
1177 && left.end == right.end
1178 && left.replacement == right.replacement
1179 && left.edit_kind == right.edit_kind
1180 });
1181 let mut normalized: Vec<OmenaScssEvalNativeCssStaticEditV0> = Vec::new();
1182 let mut index = 0usize;
1183 while index < edits.len() {
1184 let mut edit = edits.get(index)?.clone();
1185 if edit.start > edit.end || edit.end > source.len() {
1186 return None;
1187 }
1188 let mut contained = Vec::new();
1189 let mut next_index = index + 1;
1190 while let Some(next_edit) = edits.get(next_index)
1191 && next_edit.start < edit.end
1192 {
1193 if next_edit.start > next_edit.end || next_edit.end > source.len() {
1194 return None;
1195 }
1196 if next_edit.end <= edit.end {
1197 contained.push(next_edit.clone());
1198 next_index += 1;
1199 continue;
1200 }
1201 return None;
1202 }
1203 compose_contained_native_css_static_edits(source, &mut edit, &contained)?;
1204 normalized.push(edit);
1205 index = next_index;
1206 }
1207 Some(normalized)
1208}
1209
1210fn compose_contained_native_css_static_edits(
1211 source: &str,
1212 outer: &mut OmenaScssEvalNativeCssStaticEditV0,
1213 contained: &[OmenaScssEvalNativeCssStaticEditV0],
1214) -> Option<()> {
1215 if !matches!(outer.edit_kind, "whenRuleBranchFold" | "ifRuleBranchFold") || contained.is_empty()
1216 {
1217 return Some(());
1218 }
1219 let Some((replacement_source_start, replacement_source_end)) =
1220 native_css_static_edit_replacement_source_span(source, outer)
1221 else {
1222 return Some(());
1223 };
1224 for inner in contained.iter().rev().filter(|inner| {
1225 replacement_source_start <= inner.start && inner.end <= replacement_source_end
1226 }) {
1227 let relative_start = inner.start - replacement_source_start;
1228 let relative_end = inner.end - replacement_source_start;
1229 if !outer.replacement.is_char_boundary(relative_start)
1230 || !outer.replacement.is_char_boundary(relative_end)
1231 {
1232 return None;
1233 }
1234 outer
1235 .replacement
1236 .replace_range(relative_start..relative_end, inner.replacement.as_str());
1237 }
1238 Some(())
1239}
1240
1241fn native_css_static_edit_replacement_source_span(
1242 source: &str,
1243 edit: &OmenaScssEvalNativeCssStaticEditV0,
1244) -> Option<(usize, usize)> {
1245 if edit.replacement.is_empty() {
1246 return None;
1247 }
1248 let edited_source = source.get(edit.start..edit.end)?;
1249 let mut matches = edited_source.match_indices(edit.replacement.as_str());
1250 let (relative_start, _) = matches.next()?;
1251 if matches.next().is_some() {
1252 return None;
1253 }
1254 let start = edit.start + relative_start;
1255 Some((start, start + edit.replacement.len()))
1256}
1257
1258fn apply_native_css_static_edits(
1259 source: &str,
1260 edits: &[OmenaScssEvalNativeCssStaticEditV0],
1261) -> String {
1262 let mut output = source.to_string();
1263 for edit in edits.iter().rev() {
1264 output.replace_range(edit.start..edit.end, edit.replacement.as_str());
1265 }
1266 output
1267}
1268
1269fn collect_native_css_function_call_evaluations(
1270 source: &str,
1271 tokens: &[LexedToken],
1272 functions: &[OmenaScssEvalNativeCssFunctionV0],
1273) -> Vec<OmenaScssEvalNativeCssFunctionCallEvaluationV0> {
1274 tokens
1275 .iter()
1276 .enumerate()
1277 .filter_map(|(index, token)| {
1278 (matches!(
1279 token.kind,
1280 SyntaxKind::Ident | SyntaxKind::CustomPropertyName
1281 ) && token.text.starts_with("--")
1282 && !native_css_function_name_is_declaration(tokens, index))
1283 .then(|| collect_native_css_function_call_evaluation(source, tokens, functions, index))
1284 .flatten()
1285 })
1286 .collect()
1287}
1288
1289fn collect_native_css_function_call_evaluation(
1290 source: &str,
1291 tokens: &[LexedToken],
1292 functions: &[OmenaScssEvalNativeCssFunctionV0],
1293 name_index: usize,
1294) -> Option<OmenaScssEvalNativeCssFunctionCallEvaluationV0> {
1295 let name = tokens.get(name_index)?;
1296 let left_paren_index = next_non_trivia_token_index(tokens, name_index + 1)?;
1297 if tokens.get(left_paren_index)?.kind != SyntaxKind::LeftParen {
1298 return None;
1299 }
1300 let right_paren_index = matching_token_index(
1301 tokens,
1302 left_paren_index,
1303 SyntaxKind::LeftParen,
1304 SyntaxKind::RightParen,
1305 )?;
1306 let arguments = collect_native_css_function_call_arguments(
1307 source,
1308 tokens,
1309 left_paren_index + 1,
1310 right_paren_index,
1311 );
1312 let matches = functions
1313 .iter()
1314 .filter(|function| function.name == name.text)
1315 .collect::<Vec<_>>();
1316 let matched_function_count = matches.len();
1317 let (
1318 decision,
1319 reason,
1320 evaluated_value,
1321 matched_function_source_span_start,
1322 matched_function_source_span_end,
1323 ) = decide_native_css_function_call(&arguments, &matches, functions);
1324
1325 Some(OmenaScssEvalNativeCssFunctionCallEvaluationV0 {
1326 name: name.text.clone(),
1327 source_span_start: token_start(name),
1328 source_span_end: token_end(tokens.get(right_paren_index)?),
1329 argument_count: arguments.len(),
1330 matched_function_count,
1331 matched_function_source_span_start,
1332 matched_function_source_span_end,
1333 decision,
1334 reason,
1335 evaluated_value,
1336 arguments,
1337 })
1338}
1339
1340fn collect_native_css_function_call_arguments(
1341 source: &str,
1342 tokens: &[LexedToken],
1343 start_index: usize,
1344 end_index: usize,
1345) -> Vec<OmenaScssEvalNativeCssFunctionCallArgumentV0> {
1346 split_top_level_ranges(tokens, start_index, end_index, SyntaxKind::Comma)
1347 .into_iter()
1348 .enumerate()
1349 .filter_map(|(argument_index, (start, end))| {
1350 let value = trimmed_source_between_tokens(source, tokens, start, end)?;
1351 let source_span_start = first_non_trivia_token_index_until(tokens, start, end)
1352 .and_then(|index| tokens.get(index))
1353 .map(token_start)?;
1354 let source_span_end = previous_non_trivia_token_index_until(tokens, start, end)
1355 .and_then(|index| tokens.get(index))
1356 .map(token_end)
1357 .unwrap_or(source_span_start);
1358 Some(OmenaScssEvalNativeCssFunctionCallArgumentV0 {
1359 argument_index,
1360 static_value: native_css_if_value_is_fully_static(&value),
1361 value,
1362 source_span_start,
1363 source_span_end,
1364 })
1365 })
1366 .collect()
1367}
1368
1369fn decide_native_css_function_call(
1370 arguments: &[OmenaScssEvalNativeCssFunctionCallArgumentV0],
1371 matches: &[&OmenaScssEvalNativeCssFunctionV0],
1372 functions: &[OmenaScssEvalNativeCssFunctionV0],
1373) -> (
1374 &'static str,
1375 &'static str,
1376 Option<String>,
1377 Option<usize>,
1378 Option<usize>,
1379) {
1380 let Some(function) = matches.first().copied() else {
1381 return (
1382 "preserveVerbatim",
1383 "function resolution is unavailable",
1384 None,
1385 None,
1386 None,
1387 );
1388 };
1389 if matches.len() != 1 {
1390 return (
1391 "preserveVerbatim",
1392 "function resolution is ambiguous",
1393 None,
1394 None,
1395 None,
1396 );
1397 }
1398 if arguments.len() > function.parameters.len() {
1399 return (
1400 "structuralError",
1401 "call has more arguments than declared parameters",
1402 None,
1403 Some(function.source_span_start),
1404 Some(function.source_span_end),
1405 );
1406 }
1407 if native_css_function_required_argument_is_missing(arguments, function) {
1408 return (
1409 "structuralError",
1410 "required argument is missing",
1411 None,
1412 Some(function.source_span_start),
1413 Some(function.source_span_end),
1414 );
1415 }
1416 if function.results.is_empty() {
1417 return (
1418 "structuralError",
1419 "function has no result declaration",
1420 None,
1421 Some(function.source_span_start),
1422 Some(function.source_span_end),
1423 );
1424 }
1425 if function.results.len() != 1 {
1426 return (
1427 "preserveVerbatim",
1428 "function has multiple result declarations",
1429 None,
1430 Some(function.source_span_start),
1431 Some(function.source_span_end),
1432 );
1433 }
1434 if native_css_function_has_guaranteed_result_cycle(function, functions) {
1435 return (
1436 "structuralError",
1437 "function result call graph contains a guaranteed cycle",
1438 None,
1439 Some(function.source_span_start),
1440 Some(function.source_span_end),
1441 );
1442 }
1443
1444 let Some(bindings) = bind_native_css_function_arguments(arguments, function) else {
1445 return (
1446 "preserveVerbatim",
1447 "argument or default value depends on runtime or cascade state",
1448 None,
1449 Some(function.source_span_start),
1450 Some(function.source_span_end),
1451 );
1452 };
1453 match native_css_function_parameter_syntax_gate(&bindings, function) {
1454 NativeCssFunctionSyntaxGateV0::Accepts => {}
1455 NativeCssFunctionSyntaxGateV0::Rejects { reason } => {
1456 return (
1457 "structuralError",
1458 reason,
1459 None,
1460 Some(function.source_span_start),
1461 Some(function.source_span_end),
1462 );
1463 }
1464 NativeCssFunctionSyntaxGateV0::Unknown { reason } => {
1465 return (
1466 "preserveVerbatim",
1467 reason,
1468 None,
1469 Some(function.source_span_start),
1470 Some(function.source_span_end),
1471 );
1472 }
1473 }
1474 let result = &function.results[0].value;
1475 let Some(evaluated_value) = evaluate_native_css_function_result_value(result, &bindings) else {
1476 return (
1477 "preserveVerbatim",
1478 "result value depends on runtime or cascade state",
1479 None,
1480 Some(function.source_span_start),
1481 Some(function.source_span_end),
1482 );
1483 };
1484 match native_css_function_return_syntax_gate(evaluated_value.as_str(), function) {
1485 NativeCssFunctionSyntaxGateV0::Accepts => {}
1486 NativeCssFunctionSyntaxGateV0::Rejects { reason } => {
1487 return (
1488 "structuralError",
1489 reason,
1490 None,
1491 Some(function.source_span_start),
1492 Some(function.source_span_end),
1493 );
1494 }
1495 NativeCssFunctionSyntaxGateV0::Unknown { reason } => {
1496 return (
1497 "preserveVerbatim",
1498 reason,
1499 None,
1500 Some(function.source_span_start),
1501 Some(function.source_span_end),
1502 );
1503 }
1504 }
1505
1506 (
1507 "foldToStaticValue",
1508 "unique function call resolved to a static result value",
1509 Some(evaluated_value),
1510 Some(function.source_span_start),
1511 Some(function.source_span_end),
1512 )
1513}
1514
1515fn native_css_function_has_guaranteed_result_cycle(
1516 function: &OmenaScssEvalNativeCssFunctionV0,
1517 functions: &[OmenaScssEvalNativeCssFunctionV0],
1518) -> bool {
1519 native_css_result_reaches_function(
1520 function.name.as_str(),
1521 function.name.as_str(),
1522 functions,
1523 &mut Vec::new(),
1524 )
1525}
1526
1527fn native_css_result_reaches_function(
1528 target_name: &str,
1529 current_name: &str,
1530 functions: &[OmenaScssEvalNativeCssFunctionV0],
1531 visited: &mut Vec<String>,
1532) -> bool {
1533 if visited.iter().any(|name| name == current_name) {
1534 return false;
1535 }
1536 visited.push(current_name.to_string());
1537 let Some(function) = native_css_unique_function_by_name(functions, current_name) else {
1538 return false;
1539 };
1540 for result in &function.results {
1541 let Some(next_name) = native_css_exact_function_call_name(result.value.as_str()) else {
1542 continue;
1543 };
1544 if next_name == target_name {
1545 return true;
1546 }
1547 if native_css_result_reaches_function(target_name, next_name, functions, visited) {
1548 return true;
1549 }
1550 }
1551 false
1552}
1553
1554fn native_css_unique_function_by_name<'a>(
1555 functions: &'a [OmenaScssEvalNativeCssFunctionV0],
1556 name: &str,
1557) -> Option<&'a OmenaScssEvalNativeCssFunctionV0> {
1558 let mut matches = functions.iter().filter(|function| function.name == name);
1559 let first = matches.next()?;
1560 matches.next().is_none().then_some(first)
1561}
1562
1563fn native_css_exact_function_call_name(value: &str) -> Option<&str> {
1564 let trimmed = value.trim();
1565 let open_index = trimmed.find('(')?;
1566 let name = trimmed[..open_index].trim();
1567 if !name.starts_with("--") {
1568 return None;
1569 }
1570 let call_tail = &trimmed[open_index..];
1571 let close_index = matching_closing_paren_byte_index(call_tail)?;
1572 call_tail[close_index + 1..]
1573 .trim()
1574 .is_empty()
1575 .then_some(name)
1576}
1577
1578#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1579enum NativeCssFunctionSyntaxGateV0 {
1580 Accepts,
1581 Rejects { reason: &'static str },
1582 Unknown { reason: &'static str },
1583}
1584
1585type NativeCssFunctionBindingV0 = (CanonicalCustomPropertyNameV0, String);
1586
1587fn native_css_function_parameter_syntax_gate(
1588 bindings: &[NativeCssFunctionBindingV0],
1589 function: &OmenaScssEvalNativeCssFunctionV0,
1590) -> NativeCssFunctionSyntaxGateV0 {
1591 for parameter in &function.parameters {
1592 let Some(syntax) = parameter.syntax_source.as_deref() else {
1593 continue;
1594 };
1595 let Some((_, value)) = bindings
1596 .iter()
1597 .find(|(name, _)| name == ¶meter.property_key)
1598 else {
1599 continue;
1600 };
1601 match validate_registered_property_value_v0(syntax, value).class {
1602 CssValueValidationClassV0::Valid => {}
1603 CssValueValidationClassV0::Invalid => {
1604 return NativeCssFunctionSyntaxGateV0::Rejects {
1605 reason: "argument does not match parameter syntax",
1606 };
1607 }
1608 CssValueValidationClassV0::NotValidatable => {
1609 return NativeCssFunctionSyntaxGateV0::Unknown {
1610 reason: "argument syntax match is unknown",
1611 };
1612 }
1613 }
1614 }
1615 NativeCssFunctionSyntaxGateV0::Accepts
1616}
1617
1618fn native_css_function_return_syntax_gate(
1619 evaluated_value: &str,
1620 function: &OmenaScssEvalNativeCssFunctionV0,
1621) -> NativeCssFunctionSyntaxGateV0 {
1622 let Some(syntax) = function.return_syntax_source.as_deref() else {
1623 return NativeCssFunctionSyntaxGateV0::Accepts;
1624 };
1625 match validate_registered_property_value_v0(syntax, evaluated_value).class {
1626 CssValueValidationClassV0::Valid => NativeCssFunctionSyntaxGateV0::Accepts,
1627 CssValueValidationClassV0::Invalid => NativeCssFunctionSyntaxGateV0::Rejects {
1628 reason: "result value does not match return syntax",
1629 },
1630 CssValueValidationClassV0::NotValidatable => NativeCssFunctionSyntaxGateV0::Unknown {
1631 reason: "result return syntax match is unknown",
1632 },
1633 }
1634}
1635
1636fn bind_native_css_function_arguments(
1637 arguments: &[OmenaScssEvalNativeCssFunctionCallArgumentV0],
1638 function: &OmenaScssEvalNativeCssFunctionV0,
1639) -> Option<Vec<NativeCssFunctionBindingV0>> {
1640 function
1641 .parameters
1642 .iter()
1643 .enumerate()
1644 .map(|(index, parameter)| {
1645 if let Some(argument) = arguments.get(index) {
1646 return argument
1647 .static_value
1648 .then(|| (parameter.property_key.clone(), argument.value.clone()));
1649 }
1650 let default_value = parameter.default_value.as_deref()?;
1651 native_css_if_value_is_fully_static(default_value)
1652 .then(|| (parameter.property_key.clone(), default_value.to_string()))
1653 })
1654 .collect()
1655}
1656
1657fn native_css_function_required_argument_is_missing(
1658 arguments: &[OmenaScssEvalNativeCssFunctionCallArgumentV0],
1659 function: &OmenaScssEvalNativeCssFunctionV0,
1660) -> bool {
1661 function
1662 .parameters
1663 .iter()
1664 .enumerate()
1665 .any(|(index, parameter)| index >= arguments.len() && parameter.default_value.is_none())
1666}
1667
1668fn evaluate_native_css_function_result_value(
1669 result: &str,
1670 bindings: &[NativeCssFunctionBindingV0],
1671) -> Option<String> {
1672 if let Some(parameter_name) = extract_exact_var_reference(result) {
1673 let parameter_key =
1674 omena_syntax::ident::PropertyNameV0::canonical_custom_key(parameter_name);
1675 return bindings
1676 .iter()
1677 .find_map(|(name, value)| (name == ¶meter_key).then(|| value.clone()));
1678 }
1679 if let Some(value) = evaluate_native_css_function_result_if_value(result) {
1680 return Some(value);
1681 }
1682 if native_css_if_value_is_fully_static(result) {
1683 return Some(result.trim().to_string());
1684 }
1685 None
1686}
1687
1688fn evaluate_native_css_function_result_if_value(result: &str) -> Option<String> {
1689 let trimmed = result.trim();
1690 if !trimmed
1691 .get(..2)
1692 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("if"))
1693 {
1694 return None;
1695 }
1696 let source = format!(".native-result {{ value: {trimmed}; }}");
1697 let value_start = source.find(trimmed)?;
1698 let decisions = summarize_native_css_if_function_decisions(&source, StyleDialect::Css)?;
1699 let decision = decisions.functions.first()?;
1700 if decisions.functions.len() != 1
1701 || decision.decision != "foldToStaticValue"
1702 || decision.source_span_start != value_start
1703 || decision.source_span_end != value_start + trimmed.len()
1704 {
1705 return None;
1706 }
1707 let truthiness_by_start = native_css_branch_truthiness_by_start(&source, "if()");
1708 native_css_if_function_edge_ir_allows_fold(decision, &truthiness_by_start)
1709 .then(|| decision.selected_value.clone())
1710 .flatten()
1711}
1712
1713fn collect_native_css_if_function_decision(
1714 source: &str,
1715 tokens: &[LexedToken],
1716 if_index: usize,
1717) -> Option<OmenaScssEvalNativeCssIfFunctionDecisionV0> {
1718 let left_paren_index = next_non_trivia_token_index(tokens, if_index + 1)?;
1719 if tokens.get(left_paren_index)?.kind != SyntaxKind::LeftParen {
1720 return None;
1721 }
1722 let right_paren_index = matching_token_index(
1723 tokens,
1724 left_paren_index,
1725 SyntaxKind::LeftParen,
1726 SyntaxKind::RightParen,
1727 )?;
1728 let branches = collect_native_css_if_function_branches(
1729 source,
1730 tokens,
1731 left_paren_index + 1,
1732 right_paren_index,
1733 );
1734 if branches.is_empty() {
1735 return None;
1736 }
1737 let (decision, reason, selected_branch_index, selected_value) =
1738 decide_native_css_if_function(&branches);
1739
1740 Some(OmenaScssEvalNativeCssIfFunctionDecisionV0 {
1741 source_span_start: token_start(tokens.get(if_index)?),
1742 source_span_end: token_end(tokens.get(right_paren_index)?),
1743 branch_count: branches.len(),
1744 decision,
1745 reason,
1746 selected_branch_index,
1747 selected_value,
1748 branches,
1749 })
1750}
1751
1752fn collect_native_css_if_function_branches(
1753 source: &str,
1754 tokens: &[LexedToken],
1755 start_index: usize,
1756 end_index: usize,
1757) -> Vec<OmenaScssEvalNativeCssIfFunctionBranchV0> {
1758 split_top_level_ranges(tokens, start_index, end_index, SyntaxKind::Semicolon)
1759 .into_iter()
1760 .enumerate()
1761 .filter_map(|(branch_index, (start, end))| {
1762 collect_native_css_if_function_branch(source, tokens, branch_index, start, end)
1763 })
1764 .collect()
1765}
1766
1767fn collect_native_css_if_function_branch(
1768 source: &str,
1769 tokens: &[LexedToken],
1770 branch_index: usize,
1771 start_index: usize,
1772 end_index: usize,
1773) -> Option<OmenaScssEvalNativeCssIfFunctionBranchV0> {
1774 let condition_end = first_top_level_token_index(tokens, start_index, end_index, |token| {
1775 token.kind == SyntaxKind::Colon
1776 })?;
1777 let condition = trimmed_source_between_tokens(source, tokens, start_index, condition_end)?;
1778 let value = trimmed_source_between_tokens(source, tokens, condition_end + 1, end_index);
1779 let (condition_kind, verdict, supports_witness) =
1780 classify_native_css_if_function_condition(&condition);
1781 let source_span_start = first_non_trivia_token_index_until(tokens, start_index, end_index)
1782 .and_then(|index| tokens.get(index))
1783 .map(token_start)?;
1784 let source_span_end = previous_non_trivia_token_index_until(tokens, start_index, end_index)
1785 .and_then(|index| tokens.get(index))
1786 .map(token_end)
1787 .unwrap_or(source_span_start);
1788
1789 Some(OmenaScssEvalNativeCssIfFunctionBranchV0 {
1790 branch_index,
1791 condition,
1792 condition_kind,
1793 verdict,
1794 value,
1795 source_span_start,
1796 source_span_end,
1797 supports_witness,
1798 })
1799}
1800
1801fn classify_native_css_if_function_condition(
1802 condition: &str,
1803) -> (
1804 &'static str,
1805 &'static str,
1806 Option<StaticSupportsEvalWitnessV0>,
1807) {
1808 if condition.trim().eq_ignore_ascii_case("else") {
1809 return ("else", "else", None);
1810 }
1811 if let Some(inner) = extract_named_function_inner(condition, "supports") {
1812 let normalized_condition = normalize_supports_condition_for_if(inner);
1813 let witness = evaluate_static_supports_condition(
1814 &normalized_condition,
1815 StaticSupportsAssumptionV0::ModernBrowser,
1816 );
1817 let verdict = static_supports_verdict_label(witness.verdict);
1818 return ("supports", verdict, Some(witness));
1819 }
1820 if extract_named_function_inner(condition, "media").is_some() {
1821 return ("media", "runtime", None);
1822 }
1823 if extract_named_function_inner(condition, "style").is_some() {
1824 return ("style", "runtime", None);
1825 }
1826
1827 ("unknown", "unknown", None)
1828}
1829
1830fn decide_native_css_if_function(
1831 branches: &[OmenaScssEvalNativeCssIfFunctionBranchV0],
1832) -> (&'static str, &'static str, Option<usize>, Option<String>) {
1833 for branch in branches {
1834 match branch.verdict {
1835 "alwaysFalse" => continue,
1836 "alwaysTrue" | "else" => {
1837 let Some(value) = branch.value.as_deref() else {
1838 return (
1839 "preserveVerbatim",
1840 "selected branch has an empty token stream",
1841 None,
1842 None,
1843 );
1844 };
1845 if native_css_if_value_is_fully_static(value) {
1846 return (
1847 "foldToStaticValue",
1848 "all earlier branches are statically false and the selected value is static",
1849 Some(branch.branch_index),
1850 Some(value.to_string()),
1851 );
1852 }
1853 return (
1854 "preserveVerbatim",
1855 "selected branch value depends on runtime or cascade state",
1856 None,
1857 None,
1858 );
1859 }
1860 "runtime" => {
1861 return (
1862 "preserveVerbatim",
1863 "encountered runtime condition before a static winner",
1864 None,
1865 None,
1866 );
1867 }
1868 _ => {
1869 return (
1870 "preserveVerbatim",
1871 "encountered unknown condition before a static winner",
1872 None,
1873 None,
1874 );
1875 }
1876 }
1877 }
1878
1879 (
1880 "preserveVerbatim",
1881 "no statically selected branch",
1882 None,
1883 None,
1884 )
1885}
1886
1887fn collect_native_css_function(
1888 source: &str,
1889 tokens: &[LexedToken],
1890 at_keyword_index: usize,
1891) -> Option<OmenaScssEvalNativeCssFunctionV0> {
1892 let name_index = next_non_trivia_token_index(tokens, at_keyword_index + 1)?;
1893 let name = tokens.get(name_index)?;
1894 if !name.text.starts_with("--") {
1895 return None;
1896 }
1897 let left_paren_index = next_non_trivia_token_index(tokens, name_index + 1)?;
1898 if tokens.get(left_paren_index)?.kind != SyntaxKind::LeftParen {
1899 return None;
1900 }
1901 let right_paren_index = matching_token_index(
1902 tokens,
1903 left_paren_index,
1904 SyntaxKind::LeftParen,
1905 SyntaxKind::RightParen,
1906 )?;
1907 let block_start_index = next_matching_token_index(tokens, right_paren_index + 1, |token| {
1908 matches!(
1909 token.kind,
1910 SyntaxKind::LeftBrace | SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon
1911 )
1912 })?;
1913 if tokens.get(block_start_index)?.kind != SyntaxKind::LeftBrace {
1914 return None;
1915 }
1916 let block_end_index = matching_token_index(
1917 tokens,
1918 block_start_index,
1919 SyntaxKind::LeftBrace,
1920 SyntaxKind::RightBrace,
1921 )
1922 .unwrap_or(block_start_index);
1923 let parameters = collect_native_css_function_parameters(
1924 source,
1925 tokens,
1926 left_paren_index + 1,
1927 right_paren_index,
1928 );
1929 let (return_syntax_source, return_syntax) = collect_native_css_function_return_syntax(
1930 source,
1931 tokens,
1932 right_paren_index,
1933 block_start_index,
1934 );
1935 let results =
1936 collect_native_css_function_results(source, tokens, block_start_index, block_end_index);
1937 let source_span_start = token_start(tokens.get(at_keyword_index)?);
1938 let source_span_end = token_end(tokens.get(block_end_index)?);
1939 let typed_parameter_count = parameters
1940 .iter()
1941 .filter(|parameter| parameter.syntax.is_some())
1942 .count();
1943
1944 Some(OmenaScssEvalNativeCssFunctionV0 {
1945 name: name.text.clone(),
1946 source_span_start,
1947 source_span_end,
1948 name_span_start: token_start(name),
1949 name_span_end: token_end(name),
1950 return_syntax_source,
1951 return_syntax,
1952 parameter_count: parameters.len(),
1953 typed_parameter_count,
1954 result_count: results.len(),
1955 parameters,
1956 results,
1957 })
1958}
1959
1960fn collect_native_css_function_parameters(
1961 source: &str,
1962 tokens: &[LexedToken],
1963 start_index: usize,
1964 end_index: usize,
1965) -> Vec<OmenaScssEvalNativeCssFunctionParameterV0> {
1966 split_top_level_ranges(tokens, start_index, end_index, SyntaxKind::Comma)
1967 .into_iter()
1968 .filter_map(|(start, end)| {
1969 collect_native_css_function_parameter(source, tokens, start, end)
1970 })
1971 .collect()
1972}
1973
1974fn collect_native_css_function_parameter(
1975 source: &str,
1976 tokens: &[LexedToken],
1977 start_index: usize,
1978 end_index: usize,
1979) -> Option<OmenaScssEvalNativeCssFunctionParameterV0> {
1980 let name_index = next_non_trivia_token_index_until(tokens, start_index, end_index)?;
1981 let name = tokens.get(name_index)?;
1982 if !name.text.starts_with("--") {
1983 return None;
1984 }
1985 let separator_index = first_top_level_token_index(tokens, name_index + 1, end_index, |token| {
1986 matches!(token.kind, SyntaxKind::Colon)
1987 });
1988 let syntax_end_index = separator_index.unwrap_or(end_index);
1989 let syntax_source =
1990 trimmed_source_between_tokens(source, tokens, name_index + 1, syntax_end_index);
1991 let syntax = syntax_source
1992 .as_deref()
1993 .map(parse_registered_property_syntax_v0);
1994 let default_value = separator_index
1995 .and_then(|index| trimmed_source_between_tokens(source, tokens, index + 1, end_index));
1996 let source_span_start = token_start(name);
1997 let source_span_end = previous_non_trivia_token_index_until(tokens, start_index, end_index)
1998 .and_then(|index| tokens.get(index))
1999 .map(token_end)
2000 .unwrap_or_else(|| token_end(name));
2001
2002 Some(OmenaScssEvalNativeCssFunctionParameterV0 {
2003 name: AuthoredPropertyTextV0::new(name.text.clone()),
2004 property_key: PropertyNameV0::canonical_custom_key(name.text.clone()),
2005 source_span_start,
2006 source_span_end,
2007 name_span_start: token_start(name),
2008 name_span_end: token_end(name),
2009 syntax_source,
2010 syntax,
2011 default_value,
2012 })
2013}
2014
2015fn collect_native_css_function_return_syntax(
2016 source: &str,
2017 tokens: &[LexedToken],
2018 right_paren_index: usize,
2019 block_start_index: usize,
2020) -> (Option<String>, Option<RegisteredPropertySyntaxV0>) {
2021 let Some(returns_index) =
2022 first_top_level_token_index(tokens, right_paren_index + 1, block_start_index, |token| {
2023 token.kind == SyntaxKind::Ident && token.text.eq_ignore_ascii_case("returns")
2024 })
2025 else {
2026 return (None, None);
2027 };
2028 let source =
2029 trimmed_source_between_tokens(source, tokens, returns_index + 1, block_start_index);
2030 let syntax = source.as_deref().map(parse_registered_property_syntax_v0);
2031 (source, syntax)
2032}
2033
2034fn collect_native_css_function_results(
2035 source: &str,
2036 tokens: &[LexedToken],
2037 block_start_index: usize,
2038 block_end_index: usize,
2039) -> Vec<OmenaScssEvalNativeCssFunctionResultV0> {
2040 let mut results = Vec::new();
2041 let mut index = block_start_index + 1;
2042 while index < block_end_index {
2043 let Some(property_index) =
2044 next_non_trivia_token_index_until(tokens, index, block_end_index)
2045 else {
2046 break;
2047 };
2048 let property = &tokens[property_index];
2049 if property.kind != SyntaxKind::Ident || !property.text.eq_ignore_ascii_case("result") {
2050 index = property_index.saturating_add(1);
2051 continue;
2052 }
2053 let Some(colon_index) =
2054 next_non_trivia_token_index_until(tokens, property_index + 1, block_end_index)
2055 else {
2056 index = property_index.saturating_add(1);
2057 continue;
2058 };
2059 if tokens[colon_index].kind != SyntaxKind::Colon {
2060 index = property_index.saturating_add(1);
2061 continue;
2062 }
2063 let value_end =
2064 first_top_level_token_index(tokens, colon_index + 1, block_end_index, |token| {
2065 matches!(
2066 token.kind,
2067 SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon
2068 )
2069 })
2070 .unwrap_or(block_end_index);
2071 if let Some(value) =
2072 trimmed_source_between_tokens(source, tokens, colon_index + 1, value_end)
2073 {
2074 let value_span_start =
2075 first_non_trivia_token_index_until(tokens, colon_index + 1, value_end)
2076 .and_then(|index| tokens.get(index))
2077 .map(token_start)
2078 .unwrap_or_else(|| token_end(&tokens[colon_index]));
2079 let value_span_end =
2080 previous_non_trivia_token_index_until(tokens, colon_index + 1, value_end)
2081 .and_then(|index| tokens.get(index))
2082 .map(token_end)
2083 .unwrap_or(value_span_start);
2084 results.push(OmenaScssEvalNativeCssFunctionResultV0 {
2085 value,
2086 source_span_start: value_span_start,
2087 source_span_end: value_span_end,
2088 });
2089 }
2090 index = value_end.saturating_add(1);
2091 }
2092 results
2093}
2094
2095fn split_top_level_ranges(
2096 tokens: &[LexedToken],
2097 start_index: usize,
2098 end_index: usize,
2099 separator: SyntaxKind,
2100) -> Vec<(usize, usize)> {
2101 let mut ranges = Vec::new();
2102 let mut range_start = start_index;
2103 let mut paren_depth = 0usize;
2104 let mut brace_depth = 0usize;
2105 let mut bracket_depth = 0usize;
2106
2107 for index in start_index..end_index {
2108 let Some(token) = tokens.get(index) else {
2109 break;
2110 };
2111 match token.kind {
2112 SyntaxKind::LeftParen => paren_depth += 1,
2113 SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
2114 SyntaxKind::LeftBrace => brace_depth += 1,
2115 SyntaxKind::RightBrace => brace_depth = brace_depth.saturating_sub(1),
2116 SyntaxKind::LeftBracket => bracket_depth += 1,
2117 SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
2118 kind if kind == separator
2119 && paren_depth == 0
2120 && brace_depth == 0
2121 && bracket_depth == 0 =>
2122 {
2123 ranges.push((range_start, index));
2124 range_start = index + 1;
2125 }
2126 _ => {}
2127 }
2128 }
2129 if range_start < end_index {
2130 ranges.push((range_start, end_index));
2131 }
2132 ranges
2133}
2134
2135fn first_top_level_token_index(
2136 tokens: &[LexedToken],
2137 start_index: usize,
2138 end_index: usize,
2139 predicate: impl Fn(&LexedToken) -> bool,
2140) -> Option<usize> {
2141 let mut paren_depth = 0usize;
2142 let mut brace_depth = 0usize;
2143 let mut bracket_depth = 0usize;
2144
2145 for index in start_index..end_index {
2146 let token = tokens.get(index)?;
2147 match token.kind {
2148 SyntaxKind::LeftParen => paren_depth += 1,
2149 SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
2150 SyntaxKind::LeftBrace => brace_depth += 1,
2151 SyntaxKind::RightBrace => brace_depth = brace_depth.saturating_sub(1),
2152 SyntaxKind::LeftBracket => bracket_depth += 1,
2153 SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
2154 _ => {}
2155 }
2156 if paren_depth == 0 && brace_depth == 0 && bracket_depth == 0 && predicate(token) {
2157 return Some(index);
2158 }
2159 }
2160 None
2161}
2162
2163fn matching_token_index(
2164 tokens: &[LexedToken],
2165 open_index: usize,
2166 open_kind: SyntaxKind,
2167 close_kind: SyntaxKind,
2168) -> Option<usize> {
2169 let mut depth = 0usize;
2170 for index in open_index..tokens.len() {
2171 match tokens.get(index)?.kind {
2172 kind if kind == open_kind => depth += 1,
2173 kind if kind == close_kind => {
2174 depth = depth.checked_sub(1)?;
2175 if depth == 0 {
2176 return Some(index);
2177 }
2178 }
2179 _ => {}
2180 }
2181 }
2182 None
2183}
2184
2185fn next_matching_token_index(
2186 tokens: &[LexedToken],
2187 start_index: usize,
2188 predicate: impl Fn(&LexedToken) -> bool,
2189) -> Option<usize> {
2190 tokens
2191 .iter()
2192 .enumerate()
2193 .skip(start_index)
2194 .find_map(|(index, token)| predicate(token).then_some(index))
2195}
2196
2197fn next_non_trivia_token_index(tokens: &[LexedToken], start_index: usize) -> Option<usize> {
2198 tokens
2199 .iter()
2200 .enumerate()
2201 .skip(start_index)
2202 .find_map(|(index, token)| (!token.kind.is_trivia()).then_some(index))
2203}
2204
2205fn next_non_trivia_token_index_until(
2206 tokens: &[LexedToken],
2207 start_index: usize,
2208 end_index: usize,
2209) -> Option<usize> {
2210 tokens
2211 .iter()
2212 .enumerate()
2213 .take(end_index)
2214 .skip(start_index)
2215 .find_map(|(index, token)| (!token.kind.is_trivia()).then_some(index))
2216}
2217
2218fn first_non_trivia_token_index_until(
2219 tokens: &[LexedToken],
2220 start_index: usize,
2221 end_index: usize,
2222) -> Option<usize> {
2223 next_non_trivia_token_index_until(tokens, start_index, end_index)
2224}
2225
2226fn previous_non_trivia_token_index_until(
2227 tokens: &[LexedToken],
2228 start_index: usize,
2229 end_index: usize,
2230) -> Option<usize> {
2231 tokens
2232 .iter()
2233 .enumerate()
2234 .take(end_index)
2235 .skip(start_index)
2236 .rev()
2237 .find_map(|(index, token)| (!token.kind.is_trivia()).then_some(index))
2238}
2239
2240fn previous_non_trivia_token_index(tokens: &[LexedToken], before_index: usize) -> Option<usize> {
2241 tokens
2242 .iter()
2243 .enumerate()
2244 .take(before_index)
2245 .rev()
2246 .find_map(|(index, token)| (!token.kind.is_trivia()).then_some(index))
2247}
2248
2249fn trimmed_source_between_tokens(
2250 source: &str,
2251 tokens: &[LexedToken],
2252 start_index: usize,
2253 end_index: usize,
2254) -> Option<String> {
2255 let start = first_non_trivia_token_index_until(tokens, start_index, end_index)
2256 .and_then(|index| tokens.get(index))
2257 .map(token_start)?;
2258 let end = previous_non_trivia_token_index_until(tokens, start_index, end_index)
2259 .and_then(|index| tokens.get(index))
2260 .map(token_end)?;
2261 source
2262 .get(start..end)
2263 .map(str::trim)
2264 .and_then(|value| (!value.is_empty()).then(|| value.to_string()))
2265}
2266
2267fn token_start(token: &LexedToken) -> usize {
2268 u32::from(token.range.start()) as usize
2269}
2270
2271fn token_end(token: &LexedToken) -> usize {
2272 u32::from(token.range.end()) as usize
2273}
2274
2275fn parameter_syntax_is_supported(syntax: Option<&RegisteredPropertySyntaxV0>) -> bool {
2276 matches!(
2277 syntax,
2278 Some(RegisteredPropertySyntaxV0::Universal | RegisteredPropertySyntaxV0::Supported { .. })
2279 )
2280}
2281
2282fn extract_named_function_inner<'a>(condition: &'a str, name: &str) -> Option<&'a str> {
2283 let trimmed = condition.trim();
2284 let prefix = trimmed.get(..name.len())?;
2285 if !prefix.eq_ignore_ascii_case(name) {
2286 return None;
2287 }
2288 let rest = trimmed[name.len()..].trim_start();
2289 if !rest.starts_with('(') {
2290 return None;
2291 }
2292 let close_index = matching_closing_paren_byte_index(rest)?;
2293 rest[close_index + 1..]
2294 .trim()
2295 .is_empty()
2296 .then_some(&rest[1..close_index])
2297}
2298
2299fn extract_exact_var_reference(value: &str) -> Option<&str> {
2300 let inner = extract_named_function_inner(value, "var")?;
2301 let name = inner.trim();
2302 (name.starts_with("--") && !name.contains(',')).then_some(name)
2303}
2304
2305fn native_css_function_name_is_declaration(tokens: &[LexedToken], name_index: usize) -> bool {
2306 previous_non_trivia_token_index(tokens, name_index)
2307 .and_then(|index| tokens.get(index))
2308 .is_some_and(|token| {
2309 token.kind == SyntaxKind::AtKeyword && token.text.eq_ignore_ascii_case("@function")
2310 })
2311}
2312
2313fn matching_closing_paren_byte_index(value: &str) -> Option<usize> {
2314 let mut depth = 0usize;
2315 for (index, ch) in value.char_indices() {
2316 match ch {
2317 '(' => depth += 1,
2318 ')' => {
2319 depth = depth.checked_sub(1)?;
2320 if depth == 0 {
2321 return Some(index);
2322 }
2323 }
2324 _ => {}
2325 }
2326 }
2327 None
2328}
2329
2330fn normalize_supports_condition_for_if(condition: &str) -> String {
2331 let trimmed = condition.trim();
2332 if trimmed.starts_with('(') {
2333 trimmed.to_string()
2334 } else {
2335 format!("({trimmed})")
2336 }
2337}
2338
2339fn static_supports_verdict_label(verdict: StaticSupportsEvalVerdictV0) -> &'static str {
2340 match verdict {
2341 StaticSupportsEvalVerdictV0::AlwaysTrue => "alwaysTrue",
2342 StaticSupportsEvalVerdictV0::AlwaysFalse => "alwaysFalse",
2343 StaticSupportsEvalVerdictV0::Unknown => "unknown",
2344 }
2345}
2346
2347fn native_css_if_value_is_fully_static(value: &str) -> bool {
2348 let trimmed = value.trim();
2349 if trimmed.is_empty() {
2350 return false;
2351 }
2352 let lower = trimmed.to_ascii_lowercase();
2353 ![
2354 "var(",
2355 "attr(",
2356 "env(",
2357 "style(",
2358 "media(",
2359 "supports(",
2360 "if(",
2361 "--",
2362 ]
2363 .iter()
2364 .any(|marker| lower.contains(marker))
2365}
2366
2367#[cfg(test)]
2368mod tests {
2369 use omena_abstract_value::RegisteredPropertySyntaxV0;
2370 use omena_parser::StyleDialect;
2371 use omena_transform_cst::lower_transform_ir_from_source;
2372
2373 use super::{
2374 native_css_branch_truthiness_by_start, summarize_native_css_function_call_evaluations,
2375 summarize_native_css_function_surface, summarize_native_css_if_function_decisions,
2376 summarize_native_css_static_edit_plan,
2377 summarize_native_css_static_edit_plan_from_transform_ir,
2378 };
2379
2380 #[test]
2381 fn native_css_function_surface_reuses_registered_property_syntax() {
2382 let source = "@function --gap(--size <length>: 1rem, --tone <color>) returns <length> { result: var(--size); }";
2383 let report = summarize_native_css_function_surface(source, StyleDialect::Css);
2384 assert!(report.is_some());
2385 let Some(report) = report else {
2386 return;
2387 };
2388
2389 assert_eq!(report.mode, "recognitionOnly");
2390 assert_eq!(report.function_count, 1);
2391 assert_eq!(report.parameter_count, 2);
2392 assert_eq!(report.typed_parameter_count, 2);
2393 assert_eq!(report.supported_parameter_syntax_count, 2);
2394 assert_eq!(report.result_count, 1);
2395 assert_eq!(report.functions[0].name, "--gap");
2396 assert_eq!(
2397 report.functions[0].return_syntax,
2398 Some(RegisteredPropertySyntaxV0::Supported {
2399 alternatives: vec![
2400 omena_abstract_value::RegisteredPropertySyntaxAlternativeV0::Sequence {
2401 components: vec![
2402 omena_abstract_value::RegisteredPropertySyntaxComponentV0 {
2403 base: omena_abstract_value::RegisteredPropertySyntaxBaseV0::Length,
2404 multiplier:
2405 omena_abstract_value::RegisteredPropertySyntaxMultiplierV0::One,
2406 },
2407 ],
2408 },
2409 ]
2410 })
2411 );
2412 let mut parameter_name = String::new();
2413 let write_result = report.functions[0].parameters[0]
2414 .name
2415 .write_into(&mut parameter_name);
2416 assert!(write_result.is_ok(), "writing into a String must succeed");
2417 assert_eq!(parameter_name, "--size");
2418 assert_eq!(
2419 report.functions[0].parameters[0].syntax_source.as_deref(),
2420 Some("<length>")
2421 );
2422 assert_eq!(
2423 report.functions[0].parameters[0].default_value.as_deref(),
2424 Some("1rem")
2425 );
2426 assert_eq!(report.functions[0].results[0].value, "var(--size)");
2427 }
2428
2429 #[test]
2430 fn native_css_function_surface_stays_css_dialect_only() {
2431 let source = "@function gap($size) { @return $size; }";
2432
2433 assert!(summarize_native_css_function_surface(source, StyleDialect::Scss).is_none());
2434 }
2435
2436 #[test]
2437 fn native_css_function_call_evaluation_folds_static_result_binding() {
2438 let source = "@function --gap(--size <length>: 1rem) returns <length> { result: var(--size); } .card { gap: --gap(2rem); }";
2439 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2440 assert!(report.is_some());
2441 let Some(report) = report else {
2442 return;
2443 };
2444
2445 assert_eq!(report.mode, "oracleOnlyPruneButKeep");
2446 assert_eq!(report.call_count, 1);
2447 assert_eq!(report.foldable_call_count, 1);
2448 assert_eq!(report.preserved_call_count, 0);
2449 assert_eq!(report.structural_error_count, 0);
2450 assert_eq!(report.calls[0].name, "--gap");
2451 assert_eq!(report.calls[0].argument_count, 1);
2452 assert_eq!(report.calls[0].matched_function_count, 1);
2453 assert_eq!(report.calls[0].decision, "foldToStaticValue");
2454 assert_eq!(report.calls[0].evaluated_value.as_deref(), Some("2rem"));
2455 assert_eq!(report.calls[0].arguments[0].value, "2rem");
2456 assert!(report.calls[0].arguments[0].static_value);
2457 }
2458
2459 #[test]
2460 fn native_css_function_parameter_binding_decodes_before_case_sensitive_join() {
2461 let source = r#"@function --echo(--P\61 ram <length>, --param <color>) returns <length> { result: var(--Param); } .card { width: --echo(2rem, red); }"#;
2462 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2463 assert!(report.is_some());
2464 let Some(report) = report else {
2465 return;
2466 };
2467
2468 assert_eq!(report.call_count, 1);
2469 assert_eq!(report.foldable_call_count, 1);
2470 assert_eq!(report.preserved_call_count, 0);
2471 assert_eq!(report.calls[0].decision, "foldToStaticValue");
2472 assert_eq!(report.calls[0].evaluated_value.as_deref(), Some("2rem"));
2473 }
2474
2475 #[test]
2476 fn native_css_function_parameter_binding_joins_escape_equivalent_identity() {
2477 let source = r#"@function --echo(--P\61 ram <length>) returns <length> { result: var(--Param); } .card { width: --echo(2rem); }"#;
2478 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2479 assert!(report.is_some());
2480 let Some(report) = report else {
2481 return;
2482 };
2483
2484 assert_eq!(report.call_count, 1);
2485 assert_eq!(report.foldable_call_count, 1);
2486 assert_eq!(report.preserved_call_count, 0);
2487 assert_eq!(report.calls[0].decision, "foldToStaticValue");
2488 assert_eq!(report.calls[0].evaluated_value.as_deref(), Some("2rem"));
2489 }
2490
2491 #[test]
2492 fn native_css_function_call_evaluation_folds_negative_dimensions() {
2493 let source = "@function --offset(--value <length>) returns <length> { result: var(--value); } .card { margin: --offset(-10px); }";
2494 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2495 assert!(report.is_some());
2496 let Some(report) = report else {
2497 return;
2498 };
2499
2500 assert_eq!(report.call_count, 1);
2501 assert_eq!(report.foldable_call_count, 1);
2502 assert_eq!(report.preserved_call_count, 0);
2503 assert_eq!(report.structural_error_count, 0);
2504 assert_eq!(report.calls[0].decision, "foldToStaticValue");
2505 assert_eq!(report.calls[0].evaluated_value.as_deref(), Some("-10px"));
2506 }
2507
2508 #[test]
2509 fn native_css_function_call_evaluation_folds_static_if_result_through_edge_ir() {
2510 let source = "@function --gap() returns <length> { result: if(supports(display: grid): 2rem; else: 1rem); } .card { gap: --gap(); }";
2511 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2512 assert!(report.is_some());
2513 let Some(report) = report else {
2514 return;
2515 };
2516
2517 assert_eq!(report.call_count, 1);
2518 assert_eq!(report.foldable_call_count, 1);
2519 assert_eq!(report.preserved_call_count, 0);
2520 assert_eq!(report.calls[0].decision, "foldToStaticValue");
2521 assert_eq!(report.calls[0].evaluated_value.as_deref(), Some("2rem"));
2522 }
2523
2524 #[test]
2525 fn native_css_function_call_evaluation_preserves_runtime_argument() {
2526 let source = "@function --gap(--size <length>: 1rem) returns <length> { result: var(--size); } .card { gap: --gap(var(--space)); }";
2527 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2528 assert!(report.is_some());
2529 let Some(report) = report else {
2530 return;
2531 };
2532
2533 assert_eq!(report.call_count, 1);
2534 assert_eq!(report.foldable_call_count, 0);
2535 assert_eq!(report.preserved_call_count, 1);
2536 assert_eq!(report.runtime_dependent_call_count, 1);
2537 assert_eq!(report.calls[0].decision, "preserveVerbatim");
2538 assert_eq!(
2539 report.calls[0].reason,
2540 "argument or default value depends on runtime or cascade state"
2541 );
2542 assert!(!report.calls[0].arguments[0].static_value);
2543 }
2544
2545 #[test]
2546 fn native_css_function_call_evaluation_rejects_parameter_syntax_mismatch() {
2547 let source = "@function --gap(--size <length>) returns <length> { result: var(--size); } .card { gap: --gap(red); }";
2548 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2549 assert!(report.is_some());
2550 let Some(report) = report else {
2551 return;
2552 };
2553
2554 assert_eq!(report.call_count, 1);
2555 assert_eq!(report.foldable_call_count, 0);
2556 assert_eq!(report.structural_error_count, 1);
2557 assert_eq!(report.calls[0].decision, "structuralError");
2558 assert_eq!(
2559 report.calls[0].reason,
2560 "argument does not match parameter syntax"
2561 );
2562 assert_eq!(report.calls[0].evaluated_value, None);
2563 }
2564
2565 #[test]
2566 fn native_css_function_call_evaluation_rejects_return_syntax_mismatch() {
2567 let source = "@function --tone(--size <length>) returns <color> { result: var(--size); } .card { color: --tone(2rem); }";
2568 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2569 assert!(report.is_some());
2570 let Some(report) = report else {
2571 return;
2572 };
2573
2574 assert_eq!(report.call_count, 1);
2575 assert_eq!(report.foldable_call_count, 0);
2576 assert_eq!(report.structural_error_count, 1);
2577 assert_eq!(report.calls[0].decision, "structuralError");
2578 assert_eq!(
2579 report.calls[0].reason,
2580 "result value does not match return syntax"
2581 );
2582 assert_eq!(report.calls[0].evaluated_value, None);
2583 }
2584
2585 #[test]
2586 fn native_css_function_call_evaluation_preserves_unknown_return_syntax_match() {
2587 let source = "@function --tone(--value) returns <future-value> { result: var(--value); } .card { color: --tone(customvalue); }";
2588 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2589 assert!(report.is_some());
2590 let Some(report) = report else {
2591 return;
2592 };
2593
2594 assert_eq!(report.call_count, 1);
2595 assert_eq!(report.foldable_call_count, 0);
2596 assert_eq!(report.preserved_call_count, 1);
2597 assert_eq!(report.structural_error_count, 0);
2598 assert_eq!(report.calls[0].decision, "preserveVerbatim");
2599 assert_eq!(
2600 report.calls[0].reason,
2601 "result return syntax match is unknown"
2602 );
2603 assert_eq!(report.calls[0].evaluated_value, None);
2604 }
2605
2606 #[test]
2607 fn native_css_function_call_evaluation_surfaces_direct_result_cycle() {
2608 let source =
2609 "@function --loop() returns <length> { result: --loop(); } .card { width: --loop(); }";
2610 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2611 assert!(report.is_some());
2612 let Some(report) = report else {
2613 return;
2614 };
2615
2616 assert_eq!(report.foldable_call_count, 0);
2617 assert!(report.structural_error_count >= 1);
2618 assert!(report.calls.iter().any(|call| {
2619 call.name == "--loop"
2620 && call.decision == "structuralError"
2621 && call.reason == "function result call graph contains a guaranteed cycle"
2622 }));
2623 }
2624
2625 #[test]
2626 fn native_css_function_call_evaluation_surfaces_mutual_result_cycle() {
2627 let source = "@function --a() returns <length> { result: --b(); } @function --b() returns <length> { result: --a(); } .card { width: --a(); height: --b(); }";
2628 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2629 assert!(report.is_some());
2630 let Some(report) = report else {
2631 return;
2632 };
2633
2634 assert_eq!(report.foldable_call_count, 0);
2635 assert!(report.structural_error_count >= 2);
2636 assert!(report.calls.iter().any(|call| {
2637 call.name == "--a"
2638 && call.decision == "structuralError"
2639 && call.reason == "function result call graph contains a guaranteed cycle"
2640 }));
2641 assert!(report.calls.iter().any(|call| {
2642 call.name == "--b"
2643 && call.decision == "structuralError"
2644 && call.reason == "function result call graph contains a guaranteed cycle"
2645 }));
2646 }
2647
2648 #[test]
2649 fn native_css_function_call_evaluation_preserves_ambiguous_result_cycle() {
2650 let source = "@function --a() returns <length> { result: --b(); } @function --b() returns <length> { result: --a(); } @function --b() returns <length> { result: 1px; } .card { width: --a(); }";
2651 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2652 assert!(report.is_some());
2653 let Some(report) = report else {
2654 return;
2655 };
2656
2657 assert_eq!(report.foldable_call_count, 0);
2658 assert_eq!(report.structural_error_count, 0);
2659 assert!(report.calls.iter().any(|call| {
2660 call.name == "--a"
2661 && call.decision == "preserveVerbatim"
2662 && call.reason == "result value depends on runtime or cascade state"
2663 }));
2664 }
2665
2666 #[test]
2667 fn native_css_function_call_evaluation_surfaces_missing_result() {
2668 let source = "@function --gap(--size <length>) returns <length> { color: red; } .card { gap: --gap(2rem); }";
2669 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2670 assert!(report.is_some());
2671 let Some(report) = report else {
2672 return;
2673 };
2674
2675 assert_eq!(report.call_count, 1);
2676 assert_eq!(report.foldable_call_count, 0);
2677 assert_eq!(report.structural_error_count, 1);
2678 assert_eq!(report.missing_result_count, 1);
2679 assert_eq!(report.calls[0].decision, "structuralError");
2680 assert_eq!(report.calls[0].reason, "function has no result declaration");
2681 }
2682
2683 #[test]
2684 fn native_css_function_call_evaluation_surfaces_missing_required_argument() {
2685 let source = "@function --gap(--size <length>) returns <length> { result: var(--size); } .card { gap: --gap(); }";
2686 let report = summarize_native_css_function_call_evaluations(source, StyleDialect::Css);
2687 assert!(report.is_some());
2688 let Some(report) = report else {
2689 return;
2690 };
2691
2692 assert_eq!(report.call_count, 1);
2693 assert_eq!(report.foldable_call_count, 0);
2694 assert_eq!(report.structural_error_count, 1);
2695 assert_eq!(report.calls[0].decision, "structuralError");
2696 assert_eq!(report.calls[0].reason, "required argument is missing");
2697 }
2698
2699 #[test]
2700 fn native_css_function_call_evaluation_stays_css_dialect_only() {
2701 let source = "@function --gap(--size <length>) returns <length> { result: var(--size); } .card { gap: --gap(2rem); }";
2702
2703 assert!(
2704 summarize_native_css_function_call_evaluations(source, StyleDialect::Scss).is_none()
2705 );
2706 }
2707
2708 #[test]
2709 fn native_css_static_edit_plan_folds_static_if_and_function_call_values() {
2710 let source = "@function --gap(--size <length>: 1rem) returns <length> { result: var(--size); } .card { gap: --gap(2rem); display: if(supports(display: grid): grid; else: block); }";
2711 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2712 assert!(report.is_some());
2713 let Some(report) = report else {
2714 return;
2715 };
2716
2717 assert_eq!(report.mode, "staticSubsetPruneButKeep");
2718 assert_eq!(report.edit_count, 2);
2719 assert_eq!(report.if_function_edit_count, 1);
2720 assert_eq!(report.function_call_edit_count, 1);
2721 assert!(report.output_changed);
2722 assert!(report.edited_css.contains("gap: 2rem"));
2723 assert!(report.edited_css.contains("display: grid"));
2724 assert!(!report.edited_css.contains("--gap(2rem)"));
2725 assert!(!report.edited_css.contains("if(supports"));
2726 assert_eq!(report.edits[0].edit_kind, "functionCallValueFold");
2727 assert_eq!(report.edits[1].edit_kind, "ifFunctionValueFold");
2728 }
2729
2730 #[test]
2731 fn native_css_static_edit_plan_from_transform_ir_matches_source_plan() {
2732 let source = "@function --gap(--size <length>: 1rem) returns <length> { result: var(--size); } .card { gap: --gap(2rem); } @when supports(display: grid) { .grid { display: if(supports(display: grid): grid; else: block); } } @else { .fallback { display: block; } } @if supports(display: grid) { .native-if { display: grid; } } @else { .native-if { display: block; } }";
2733 let ir = lower_transform_ir_from_source(source, StyleDialect::Css, "native-css-static");
2734 let source_plan = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2735 let ir_plan =
2736 summarize_native_css_static_edit_plan_from_transform_ir(&ir, StyleDialect::Css);
2737
2738 assert!(source_plan.is_some());
2739 assert!(ir_plan.is_some());
2740 let Some(source_plan) = source_plan else {
2741 return;
2742 };
2743 let Some(ir_plan) = ir_plan else {
2744 return;
2745 };
2746
2747 assert_eq!(ir_plan.edits, source_plan.edits);
2748 assert_eq!(ir_plan.edit_count, source_plan.edit_count);
2749 assert_eq!(
2750 ir_plan.when_rule_edit_count,
2751 source_plan.when_rule_edit_count
2752 );
2753 assert_eq!(ir_plan.if_rule_edit_count, source_plan.if_rule_edit_count);
2754 assert_eq!(
2755 ir_plan.if_function_edit_count,
2756 source_plan.if_function_edit_count
2757 );
2758 assert_eq!(
2759 ir_plan.function_call_edit_count,
2760 source_plan.function_call_edit_count
2761 );
2762 assert_eq!(ir_plan.output_changed, source_plan.output_changed);
2763 assert_eq!(ir_plan.edited_css, source_plan.edited_css);
2764 }
2765
2766 #[test]
2767 fn native_css_static_edit_plan_from_transform_ir_folds_if_rule_else_branch() {
2768 let source = "@if supports(display: -ms-grid) { .grid { display: grid; } } @else { .fallback { display: block; } }";
2769 let ir = lower_transform_ir_from_source(source, StyleDialect::Css, "native-css-static");
2770 let report =
2771 summarize_native_css_static_edit_plan_from_transform_ir(&ir, StyleDialect::Css);
2772 assert!(report.is_some());
2773 let Some(report) = report else {
2774 return;
2775 };
2776
2777 assert_eq!(report.edit_count, 1);
2778 assert_eq!(report.if_rule_edit_count, 1);
2779 assert!(report.output_changed);
2780 assert!(report.edited_css.contains(".fallback { display: block; }"));
2781 assert!(!report.edited_css.contains("@if"));
2782 assert!(!report.edited_css.contains(".grid"));
2783 }
2784
2785 #[test]
2786 fn native_css_static_edit_plan_preserves_function_body_calls() {
2787 let source = "@function --inner() returns <length> { result: 1px; } @function --outer() returns <length> { result: --inner(); } .card { width: --inner(); }";
2788 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2789 assert!(report.is_some());
2790 let Some(report) = report else {
2791 return;
2792 };
2793
2794 assert_eq!(report.edit_count, 1);
2795 assert_eq!(report.function_call_edit_count, 1);
2796 assert!(report.output_changed);
2797 assert!(report.edited_css.contains("result: --inner();"));
2798 assert!(report.edited_css.contains("width: 1px"));
2799 }
2800
2801 #[test]
2802 fn native_css_static_edit_plan_folds_function_result_if_without_rewriting_function_body() {
2803 let source = "@function --gap() returns <length> { result: if(supports(display: grid): 2rem; else: 1rem); } .card { gap: --gap(); }";
2804 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2805 assert!(report.is_some());
2806 let Some(report) = report else {
2807 return;
2808 };
2809
2810 assert_eq!(report.edit_count, 1);
2811 assert_eq!(report.if_function_edit_count, 0);
2812 assert_eq!(report.function_call_edit_count, 1);
2813 assert!(report.output_changed);
2814 assert!(
2815 report
2816 .edited_css
2817 .contains("result: if(supports(display: grid): 2rem; else: 1rem);")
2818 );
2819 assert!(report.edited_css.contains("gap: 2rem"));
2820 assert!(!report.edited_css.contains("--gap();"));
2821 }
2822
2823 #[test]
2824 fn native_css_static_edit_plan_folds_static_when_rule_true_branch() {
2825 let source = "@when supports(display: grid) { .grid { display: grid; } } @else { .fallback { display: block; } }";
2826 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2827 assert!(report.is_some());
2828 let Some(report) = report else {
2829 return;
2830 };
2831
2832 assert_eq!(report.edit_count, 1);
2833 assert_eq!(report.when_rule_edit_count, 1);
2834 assert_eq!(report.if_function_edit_count, 0);
2835 assert_eq!(report.function_call_edit_count, 0);
2836 assert!(report.output_changed);
2837 assert!(report.edited_css.contains(".grid { display: grid; }"));
2838 assert!(!report.edited_css.contains("@when"));
2839 assert!(!report.edited_css.contains(".fallback"));
2840 assert_eq!(report.edits[0].edit_kind, "whenRuleBranchFold");
2841 }
2842
2843 #[test]
2844 fn native_css_static_edit_plan_folds_static_if_rule_true_branch() {
2845 let source = "@if supports(display: grid) { .grid { display: grid; } } @else { .fallback { display: block; } }";
2846 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2847 assert!(report.is_some());
2848 let Some(report) = report else {
2849 return;
2850 };
2851
2852 assert_eq!(report.edit_count, 1);
2853 assert_eq!(report.when_rule_edit_count, 0);
2854 assert_eq!(report.if_rule_edit_count, 1);
2855 assert_eq!(report.if_function_edit_count, 0);
2856 assert_eq!(report.function_call_edit_count, 0);
2857 assert!(report.output_changed);
2858 assert!(report.edited_css.contains(".grid { display: grid; }"));
2859 assert!(!report.edited_css.contains("@if"));
2860 assert!(!report.edited_css.contains(".fallback"));
2861 assert_eq!(report.edits[0].edit_kind, "ifRuleBranchFold");
2862 }
2863
2864 #[test]
2865 fn native_css_static_edit_plan_consumes_edge_ir_when_truthiness() {
2866 let source = "@when supports(display: grid) { .grid { display: grid; } } @else { .fallback { display: block; } }";
2867 let truthiness_by_start = native_css_branch_truthiness_by_start(source, "@when");
2868
2869 assert_eq!(truthiness_by_start.get(&0), Some(&true));
2870
2871 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2872 assert!(report.is_some());
2873 let Some(report) = report else {
2874 return;
2875 };
2876
2877 assert_eq!(report.when_rule_edit_count, 1);
2878 assert!(report.edited_css.contains(".grid { display: grid; }"));
2879 assert!(!report.edited_css.contains(".fallback"));
2880 }
2881
2882 #[test]
2883 fn native_css_static_edit_plan_consumes_edge_ir_if_rule_truthiness() {
2884 let source = "@if supports(display: grid) { .grid { display: grid; } } @else { .fallback { display: block; } }";
2885 let truthiness_by_start = native_css_branch_truthiness_by_start(source, "@if");
2886
2887 assert_eq!(truthiness_by_start.get(&0), Some(&true));
2888
2889 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2890 assert!(report.is_some());
2891 let Some(report) = report else {
2892 return;
2893 };
2894
2895 assert_eq!(report.if_rule_edit_count, 1);
2896 assert!(report.edited_css.contains(".grid { display: grid; }"));
2897 assert!(!report.edited_css.contains(".fallback"));
2898 }
2899
2900 #[test]
2901 fn native_css_static_edit_plan_consumes_edge_ir_if_function_truthiness() {
2902 let source = ".card { display: if(supports(display: grid): grid; else: block); }";
2903 let truthiness_by_start = native_css_branch_truthiness_by_start(source, "if()");
2904
2905 assert_eq!(truthiness_by_start.len(), 1);
2906 assert!(truthiness_by_start.values().all(|truthy| *truthy));
2907
2908 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2909 assert!(report.is_some());
2910 let Some(report) = report else {
2911 return;
2912 };
2913
2914 assert_eq!(report.if_function_edit_count, 1);
2915 assert!(report.edited_css.contains("display: grid"));
2916 assert!(!report.edited_css.contains("if(supports"));
2917 }
2918
2919 #[test]
2920 fn native_css_static_edit_plan_composes_nested_static_when_body_edits() {
2921 let source = "@when supports(display: grid) { .grid { display: if(supports(display: grid): grid; else: block); } } @else { .fallback { display: block; } }";
2922 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2923 assert!(report.is_some());
2924 let Some(report) = report else {
2925 return;
2926 };
2927
2928 assert_eq!(report.edit_count, 1);
2929 assert_eq!(report.when_rule_edit_count, 1);
2930 assert!(report.output_changed);
2931 assert!(report.edited_css.contains(".grid { display: grid; }"));
2932 assert!(!report.edited_css.contains("@when"));
2933 assert!(!report.edited_css.contains("if(supports"));
2934 assert!(!report.edited_css.contains(".fallback"));
2935 }
2936
2937 #[test]
2938 fn native_css_static_edit_plan_composes_nested_static_if_rule_edits() {
2939 let source = "@if supports(display: grid) { .grid { display: if(supports(display: grid): grid; else: block); } } @else { .fallback { display: block; } }";
2940 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2941 assert!(report.is_some());
2942 let Some(report) = report else {
2943 return;
2944 };
2945
2946 assert_eq!(report.edit_count, 1);
2947 assert_eq!(report.if_rule_edit_count, 1);
2948 assert!(report.output_changed);
2949 assert!(report.edited_css.contains(".grid { display: grid; }"));
2950 assert!(!report.edited_css.contains("@if"));
2951 assert!(!report.edited_css.contains("if(supports"));
2952 assert!(!report.edited_css.contains(".fallback"));
2953 }
2954
2955 #[test]
2956 fn native_css_static_edit_plan_folds_static_when_rule_else_branch() {
2957 let source = "@when supports(display: -ms-grid) { .grid { display: grid; } } @else { .fallback { display: block; } }";
2958 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2959 assert!(report.is_some());
2960 let Some(report) = report else {
2961 return;
2962 };
2963
2964 assert_eq!(report.edit_count, 1);
2965 assert_eq!(report.when_rule_edit_count, 1);
2966 assert!(report.edited_css.contains(".fallback { display: block; }"));
2967 assert!(!report.edited_css.contains("@when"));
2968 assert!(!report.edited_css.contains(".grid"));
2969 }
2970
2971 #[test]
2972 fn native_css_static_edit_plan_folds_static_if_rule_else_branch() {
2973 let source = "@if supports(display: -ms-grid) { .grid { display: grid; } } @else { .fallback { display: block; } }";
2974 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2975 assert!(report.is_some());
2976 let Some(report) = report else {
2977 return;
2978 };
2979
2980 assert_eq!(report.edit_count, 1);
2981 assert_eq!(report.if_rule_edit_count, 1);
2982 assert!(report.edited_css.contains(".fallback { display: block; }"));
2983 assert!(!report.edited_css.contains("@if"));
2984 assert!(!report.edited_css.contains(".grid"));
2985 }
2986
2987 #[test]
2988 fn native_css_static_edit_plan_preserves_runtime_when_rule() {
2989 let source = "@when media(width >= 1px) { .grid { display: grid; } } @else { .fallback { display: block; } }";
2990 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
2991 assert!(report.is_some());
2992 let Some(report) = report else {
2993 return;
2994 };
2995
2996 assert_eq!(report.edit_count, 0);
2997 assert_eq!(report.when_rule_edit_count, 0);
2998 assert!(!report.output_changed);
2999 assert_eq!(report.edited_css, source);
3000 }
3001
3002 #[test]
3003 fn native_css_static_edit_plan_preserves_runtime_if_rule() {
3004 let source = "@if media(width >= 1px) { .grid { display: grid; } } @else { .fallback { display: block; } }";
3005 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
3006 assert!(report.is_some());
3007 let Some(report) = report else {
3008 return;
3009 };
3010
3011 assert_eq!(report.edit_count, 0);
3012 assert_eq!(report.if_rule_edit_count, 0);
3013 assert!(!report.output_changed);
3014 assert_eq!(report.edited_css, source);
3015 }
3016
3017 #[test]
3018 fn native_css_static_edit_plan_preserves_runtime_native_values() {
3019 let source = "@function --gap(--size <length>: 1rem) returns <length> { result: var(--size); } .card { gap: --gap(var(--space)); margin: if(media(width >= 1px): 1rem; else: 2rem); }";
3020 let report = summarize_native_css_static_edit_plan(source, StyleDialect::Css);
3021 assert!(report.is_some());
3022 let Some(report) = report else {
3023 return;
3024 };
3025
3026 assert_eq!(report.edit_count, 0);
3027 assert_eq!(report.if_function_edit_count, 0);
3028 assert_eq!(report.function_call_edit_count, 0);
3029 assert!(!report.output_changed);
3030 assert_eq!(report.edited_css, source);
3031 }
3032
3033 #[test]
3034 fn native_css_static_edit_plan_stays_css_dialect_only() {
3035 let source = ".card { display: if(supports(display: grid): grid; else: block); }";
3036
3037 assert!(summarize_native_css_static_edit_plan(source, StyleDialect::Scss).is_none());
3038 }
3039
3040 #[test]
3041 fn native_css_if_function_decision_folds_static_supports_branch() {
3042 let source = ".card { display: if(supports(display: grid): grid; else: block); }";
3043 let report = summarize_native_css_if_function_decisions(source, StyleDialect::Css);
3044 assert!(report.is_some());
3045 let Some(report) = report else {
3046 return;
3047 };
3048
3049 assert_eq!(report.mode, "oracleOnlyPruneButKeep");
3050 assert_eq!(report.function_count, 1);
3051 assert_eq!(report.foldable_function_count, 1);
3052 assert_eq!(report.preserved_function_count, 0);
3053 assert_eq!(report.static_supports_branch_count, 1);
3054 assert_eq!(report.runtime_branch_count, 0);
3055 assert_eq!(report.functions[0].decision, "foldToStaticValue");
3056 assert_eq!(report.functions[0].selected_branch_index, Some(0));
3057 assert_eq!(report.functions[0].selected_value.as_deref(), Some("grid"));
3058 assert_eq!(report.functions[0].branches[0].condition_kind, "supports");
3059 assert_eq!(report.functions[0].branches[0].verdict, "alwaysTrue");
3060 assert!(
3061 report.functions[0].branches[0]
3062 .supports_witness
3063 .as_ref()
3064 .is_some_and(|witness| witness.product == "omena-cascade.supports-static-eval")
3065 );
3066 }
3067
3068 #[test]
3069 fn native_css_if_function_decision_preserves_runtime_media_branch() {
3070 let source = ".card { margin: if(media(width >= 1px): 1rem; else: 2rem); }";
3071 let report = summarize_native_css_if_function_decisions(source, StyleDialect::Css);
3072 assert!(report.is_some());
3073 let Some(report) = report else {
3074 return;
3075 };
3076
3077 assert_eq!(report.function_count, 1);
3078 assert_eq!(report.foldable_function_count, 0);
3079 assert_eq!(report.preserved_function_count, 1);
3080 assert_eq!(report.static_supports_branch_count, 0);
3081 assert_eq!(report.runtime_branch_count, 1);
3082 assert_eq!(report.functions[0].decision, "preserveVerbatim");
3083 assert_eq!(
3084 report.functions[0].reason,
3085 "encountered runtime condition before a static winner"
3086 );
3087 assert_eq!(report.functions[0].selected_branch_index, None);
3088 assert_eq!(report.functions[0].branches[0].condition_kind, "media");
3089 assert_eq!(report.functions[0].branches[0].verdict, "runtime");
3090 }
3091
3092 #[test]
3093 fn native_css_if_function_decision_preserves_runtime_value() {
3094 let source = ".card { color: if(supports(color: red): var(--accent); else: blue); }";
3095 let report = summarize_native_css_if_function_decisions(source, StyleDialect::Css);
3096 assert!(report.is_some());
3097 let Some(report) = report else {
3098 return;
3099 };
3100
3101 assert_eq!(report.function_count, 1);
3102 assert_eq!(report.functions[0].decision, "preserveVerbatim");
3103 assert_eq!(
3104 report.functions[0].reason,
3105 "selected branch value depends on runtime or cascade state"
3106 );
3107 assert_eq!(report.functions[0].selected_value, None);
3108 }
3109
3110 #[test]
3111 fn native_css_if_function_decision_stays_css_dialect_only() {
3112 let source = ".card { width: if(supports(width: 1px): 1px; else: 2px); }";
3113
3114 assert!(summarize_native_css_if_function_decisions(source, StyleDialect::Scss).is_none());
3115 }
3116}