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