Skip to main content

omena_bridge/
source_cfg.rs

1use oxc_ast::ast::{
2    AssignmentOperator, AssignmentTarget, BinaryOperator, BindingIdentifier, BindingPattern,
3    Declaration, ExportDefaultDeclarationKind, Expression, FunctionBody, IdentifierReference,
4    IfStatement, LabeledStatement, LogicalExpression, Program, Statement, SwitchCase,
5    VariableDeclaration,
6};
7use oxc_ast_visit::Visit;
8use oxc_semantic::{Scoping, SymbolId};
9use oxc_span::{GetSpan, Span};
10use serde::Serialize;
11use std::collections::{BTreeMap, BTreeSet};
12
13use engine_input_producers::{
14    StringTypeFactsV2, TypeFactControlFlowBlockV2, TypeFactControlFlowGraphV2,
15};
16use omena_abstract_value::{
17    ClassBoundaryEffectV0, DomClassTokenizationV0, OrderedTokenWordV0,
18    external_string_type_facts_from_abstract_class_value, prefix_suffix_class_value,
19};
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct SourceControlFlowGraphCaptureV0 {
24    pub schema_version: &'static str,
25    pub product: &'static str,
26    pub binding: SourceFlowBindingRefV0,
27    pub variable_name: String,
28    pub reference_byte_offset: usize,
29    pub snapshot: SourceFlowBlockGraphSnapshotV0,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct SourceFlowBindingRefV0 {
35    pub symbol_ordinal: usize,
36    pub name: String,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
40#[serde(rename_all = "camelCase")]
41pub struct SourceFlowBlockGraphSnapshotV0 {
42    pub entry_block_id: String,
43    pub blocks: Vec<SourceFlowBlockSnapshotV0>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct SourceFlowBlockSnapshotV0 {
49    pub id: String,
50    pub kind: &'static str,
51    pub transfer_kind: &'static str,
52    pub successor_block_ids: Vec<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub binding: Option<SourceFlowBindingRefV0>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub symbol_ordinal: Option<usize>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub variable_name: Option<String>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub expression_kind: Option<&'static str>,
61    pub boundary_effect: ClassBoundaryEffectV0,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub ordered_word: Option<OrderedTokenWordV0>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub facts: Option<StringTypeFactsV2>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub(crate) struct SourceClassSiteExpressionValueV0 {
70    pub facts: Option<StringTypeFactsV2>,
71    pub boundary_effect: ClassBoundaryEffectV0,
72    pub ordered_word: Option<OrderedTokenWordV0>,
73}
74
75enum SourceFlowNode<'a> {
76    Assignment {
77        binding: Option<SourceFlowBindingRefV0>,
78        expression: Option<&'a Expression<'a>>,
79    },
80    Branch {
81        then_nodes: Vec<SourceFlowNode<'a>>,
82        else_nodes: Vec<SourceFlowNode<'a>>,
83    },
84    Loop {
85        body_nodes: Vec<SourceFlowNode<'a>>,
86    },
87    Break,
88    Terminate,
89}
90
91type SourceFlowBlockMetadataV0 = (
92    Option<SourceFlowBindingRefV0>,
93    Option<&'static str>,
94    Option<StringTypeFactsV2>,
95    ClassBoundaryEffectV0,
96);
97
98pub fn summarize_omena_bridge_source_control_flow_graph_for_source_language(
99    source_path: &str,
100    source: &str,
101    source_language: Option<&str>,
102    variable_name: &str,
103    reference_byte_offset: usize,
104) -> Option<SourceControlFlowGraphCaptureV0> {
105    crate::source_syntax::summarize_source_control_flow_graph_with_semantic(
106        source_path,
107        source,
108        source_language,
109        variable_name,
110        reference_byte_offset,
111    )
112}
113
114pub(crate) fn summarize_source_control_flow_graph_from_program<'a>(
115    program: &'a Program<'a>,
116    scoping: &'a Scoping,
117    variable_name: &str,
118    reference_byte_offset: usize,
119) -> Option<SourceControlFlowGraphCaptureV0> {
120    if variable_name.contains('.') {
121        return None;
122    }
123
124    let reference_binding =
125        reference_binding_for_offset(program, scoping, variable_name, reference_byte_offset)?;
126
127    let container = statement_container_for_reference(&program.body, reference_byte_offset);
128    let nodes = build_flow_nodes(container, scoping, reference_byte_offset);
129    Some(SourceControlFlowGraphCaptureV0 {
130        schema_version: "0",
131        product: "omena-bridge.source-control-flow-graph",
132        variable_name: reference_binding.name.clone(),
133        binding: reference_binding,
134        reference_byte_offset,
135        snapshot: SourceFlowBlockGraphSnapshotBuilder::new(&program.body, scoping)
136            .build(nodes.as_slice()),
137    })
138}
139
140pub(crate) fn summarize_source_class_site_expression_value_from_program<'a>(
141    program: &'a Program<'a>,
142    scoping: &'a Scoping,
143    expression: &'a Expression<'a>,
144) -> SourceClassSiteExpressionValueV0 {
145    let reference_byte_offset = expression.span().start as usize;
146    let container = statement_container_for_reference(&program.body, reference_byte_offset);
147    let nodes = build_flow_nodes(container, scoping, reference_byte_offset);
148    let snapshot =
149        SourceFlowBlockGraphSnapshotBuilder::new(&program.body, scoping).build(nodes.as_slice());
150    let facts = expression_type_facts(expression, &program.body, scoping, &snapshot.blocks);
151    SourceClassSiteExpressionValueV0 {
152        ordered_word: facts.as_ref().and_then(ordered_word_for_type_facts),
153        boundary_effect: class_boundary_effect_for_expression(expression),
154        facts,
155    }
156}
157
158pub(crate) fn summarize_source_class_site_literal_value(
159    value: &str,
160) -> SourceClassSiteExpressionValueV0 {
161    let facts = exact_type_facts(value);
162    SourceClassSiteExpressionValueV0 {
163        ordered_word: ordered_word_for_type_facts(&facts),
164        boundary_effect: ClassBoundaryEffectV0::UnknownBoundary,
165        facts: Some(facts),
166    }
167}
168
169pub fn summarize_omena_bridge_source_type_fact_control_flow_graph_for_source_language(
170    source_path: &str,
171    source: &str,
172    source_language: Option<&str>,
173    variable_name: &str,
174    reference_byte_offset: usize,
175) -> Option<TypeFactControlFlowGraphV2> {
176    summarize_omena_bridge_source_control_flow_graph_for_source_language(
177        source_path,
178        source,
179        source_language,
180        variable_name,
181        reference_byte_offset,
182    )
183    .map(|capture| source_type_fact_control_flow_graph_from_snapshot(&capture.snapshot))
184}
185
186pub fn source_type_fact_control_flow_graph_from_snapshot(
187    snapshot: &SourceFlowBlockGraphSnapshotV0,
188) -> TypeFactControlFlowGraphV2 {
189    TypeFactControlFlowGraphV2 {
190        entry_block_id: snapshot.entry_block_id.clone(),
191        blocks: snapshot
192            .blocks
193            .iter()
194            .map(source_type_fact_control_flow_block_from_snapshot)
195            .collect(),
196    }
197}
198
199fn source_type_fact_control_flow_block_from_snapshot(
200    block: &SourceFlowBlockSnapshotV0,
201) -> TypeFactControlFlowBlockV2 {
202    TypeFactControlFlowBlockV2 {
203        id: block.id.clone(),
204        kind: block.kind.to_string(),
205        transfer_kind: block.transfer_kind.to_string(),
206        successor_block_ids: block.successor_block_ids.clone(),
207        symbol_ordinal: block.symbol_ordinal,
208        variable_name: block.variable_name.clone(),
209        expression_kind: block.expression_kind.map(str::to_string),
210        boundary_effect: boundary_effect_wire_value(block.boundary_effect).to_string(),
211        facts: block.facts.clone(),
212    }
213}
214
215fn statement_container_for_reference<'a>(
216    statements: &'a oxc_allocator::Vec<'a, Statement<'a>>,
217    reference_byte_offset: usize,
218) -> &'a oxc_allocator::Vec<'a, Statement<'a>> {
219    find_function_body_statements_containing_reference(statements, reference_byte_offset)
220        .unwrap_or(statements)
221}
222
223fn find_function_body_statements_containing_reference<'a>(
224    statements: &'a oxc_allocator::Vec<'a, Statement<'a>>,
225    reference_byte_offset: usize,
226) -> Option<&'a oxc_allocator::Vec<'a, Statement<'a>>> {
227    for statement in statements {
228        if let Some(body) = function_body_for_statement(statement)
229            && span_contains(body.span, reference_byte_offset)
230        {
231            return find_function_body_statements_containing_reference(
232                &body.statements,
233                reference_byte_offset,
234            )
235            .or(Some(&body.statements));
236        }
237    }
238    None
239}
240
241fn function_body_for_statement<'a>(statement: &'a Statement<'a>) -> Option<&'a FunctionBody<'a>> {
242    match statement {
243        Statement::FunctionDeclaration(function) => function.body.as_deref(),
244        Statement::ExportDeclaration(export) => {
245            if let Declaration::FunctionDeclaration(function) = &export.declaration {
246                function.body.as_deref()
247            } else {
248                None
249            }
250        }
251        Statement::ExportDefaultDeclaration(export) => {
252            if let ExportDefaultDeclarationKind::FunctionDeclaration(function) = &export.declaration
253            {
254                function.body.as_deref()
255            } else {
256                None
257            }
258        }
259        _ => None,
260    }
261}
262
263fn build_flow_nodes<'a>(
264    statements: &'a oxc_allocator::Vec<'a, Statement<'a>>,
265    scoping: &Scoping,
266    reference_byte_offset: usize,
267) -> Vec<SourceFlowNode<'a>> {
268    let mut nodes = Vec::new();
269
270    for statement in statements {
271        if span_start(statement.span()) >= reference_byte_offset {
272            break;
273        }
274        if matches!(statement, Statement::FunctionDeclaration(_)) {
275            continue;
276        }
277
278        match statement {
279            Statement::IfStatement(if_statement) => {
280                let reference_location =
281                    locate_reference_in_if(if_statement, reference_byte_offset);
282                nodes.push(SourceFlowNode::Branch {
283                    then_nodes: build_flow_nodes_for_statement(
284                        &if_statement.consequent,
285                        scoping,
286                        branch_reference_offset(reference_location, "then", reference_byte_offset),
287                    ),
288                    else_nodes: if_statement
289                        .alternate
290                        .as_ref()
291                        .map(|alternate| {
292                            build_flow_nodes_for_statement(
293                                alternate,
294                                scoping,
295                                branch_reference_offset(
296                                    reference_location,
297                                    "else",
298                                    reference_byte_offset,
299                                ),
300                            )
301                        })
302                        .unwrap_or_default(),
303                });
304                if reference_location != "after" {
305                    break;
306                }
307            }
308            Statement::WhileStatement(while_statement) => {
309                nodes.push(SourceFlowNode::Loop {
310                    body_nodes: build_loop_body_nodes(
311                        &while_statement.body,
312                        scoping,
313                        reference_byte_offset,
314                    ),
315                });
316                if span_contains(while_statement.body.span(), reference_byte_offset) {
317                    break;
318                }
319            }
320            Statement::ForStatement(for_statement) => {
321                nodes.push(SourceFlowNode::Loop {
322                    body_nodes: build_loop_body_nodes(
323                        &for_statement.body,
324                        scoping,
325                        reference_byte_offset,
326                    ),
327                });
328                if span_contains(for_statement.body.span(), reference_byte_offset) {
329                    break;
330                }
331            }
332            Statement::DoWhileStatement(do_statement) => {
333                nodes.push(SourceFlowNode::Loop {
334                    body_nodes: build_loop_body_nodes(
335                        &do_statement.body,
336                        scoping,
337                        reference_byte_offset,
338                    ),
339                });
340                if span_contains(do_statement.body.span(), reference_byte_offset) {
341                    break;
342                }
343            }
344            Statement::LabeledStatement(labeled) => {
345                nodes.extend(build_flow_nodes_for_labeled(
346                    labeled,
347                    scoping,
348                    reference_byte_offset,
349                ));
350                if span_contains(labeled.body.span(), reference_byte_offset) {
351                    break;
352                }
353            }
354            _ if span_contains(statement.span(), reference_byte_offset) => break,
355            Statement::BreakStatement(_) => {
356                nodes.push(SourceFlowNode::Break);
357                break;
358            }
359            Statement::ReturnStatement(_) | Statement::ThrowStatement(_) => {
360                nodes.push(SourceFlowNode::Terminate);
361                break;
362            }
363            _ => nodes.extend(assignment_nodes_for_statement(statement, scoping)),
364        }
365    }
366
367    nodes
368}
369
370fn build_flow_nodes_for_statement<'a>(
371    statement: &'a Statement<'a>,
372    scoping: &Scoping,
373    reference_byte_offset: usize,
374) -> Vec<SourceFlowNode<'a>> {
375    match statement {
376        Statement::BlockStatement(block) => {
377            build_flow_nodes(&block.body, scoping, reference_byte_offset)
378        }
379        _ => build_flow_nodes_from_slice(
380            std::slice::from_ref(statement),
381            scoping,
382            reference_byte_offset,
383        ),
384    }
385}
386
387fn build_flow_nodes_from_slice<'a>(
388    statements: &'a [Statement<'a>],
389    scoping: &Scoping,
390    reference_byte_offset: usize,
391) -> Vec<SourceFlowNode<'a>> {
392    let mut nodes = Vec::new();
393    for statement in statements {
394        if span_start(statement.span()) >= reference_byte_offset {
395            break;
396        }
397        if span_contains(statement.span(), reference_byte_offset) {
398            break;
399        }
400        nodes.extend(assignment_nodes_for_statement(statement, scoping));
401    }
402    nodes
403}
404
405fn build_loop_body_nodes<'a>(
406    body: &'a Statement<'a>,
407    scoping: &Scoping,
408    reference_byte_offset: usize,
409) -> Vec<SourceFlowNode<'a>> {
410    if span_contains(body.span(), reference_byte_offset) {
411        build_flow_nodes_for_statement(body, scoping, reference_byte_offset)
412    } else {
413        build_flow_nodes_for_statement(body, scoping, usize::MAX)
414    }
415}
416
417fn build_flow_nodes_for_labeled<'a>(
418    labeled: &'a LabeledStatement<'a>,
419    scoping: &Scoping,
420    reference_byte_offset: usize,
421) -> Vec<SourceFlowNode<'a>> {
422    build_flow_nodes_for_statement(&labeled.body, scoping, reference_byte_offset)
423}
424
425fn locate_reference_in_if(
426    statement: &IfStatement<'_>,
427    reference_byte_offset: usize,
428) -> &'static str {
429    if span_contains(statement.consequent.span(), reference_byte_offset) {
430        return "then";
431    }
432    if statement
433        .alternate
434        .as_ref()
435        .is_some_and(|alternate| span_contains(alternate.span(), reference_byte_offset))
436    {
437        return "else";
438    }
439    "after"
440}
441
442fn branch_reference_offset(
443    reference_location: &'static str,
444    branch: &'static str,
445    reference_byte_offset: usize,
446) -> usize {
447    if reference_location == branch {
448        reference_byte_offset
449    } else {
450        usize::MAX
451    }
452}
453
454fn assignment_nodes_for_statement<'a>(
455    statement: &'a Statement<'a>,
456    scoping: &Scoping,
457) -> Vec<SourceFlowNode<'a>> {
458    match statement {
459        Statement::VariableDeclaration(declaration) => {
460            assignment_nodes_for_variable_declaration(declaration)
461        }
462        Statement::ExpressionStatement(statement) => {
463            if let Expression::AssignmentExpression(assignment) = &statement.expression
464                && assignment.operator == AssignmentOperator::Assign
465                && let AssignmentTarget::AssignmentTargetIdentifier(identifier) = &assignment.left
466            {
467                return vec![SourceFlowNode::Assignment {
468                    binding: binding_ref_from_reference(scoping, identifier),
469                    expression: Some(&assignment.right),
470                }];
471            }
472            Vec::new()
473        }
474        Statement::BlockStatement(block) => build_flow_nodes(&block.body, scoping, usize::MAX)
475            .into_iter()
476            .filter(|node| matches!(node, SourceFlowNode::Assignment { .. }))
477            .collect(),
478        _ => Vec::new(),
479    }
480}
481
482fn assignment_nodes_for_variable_declaration<'a>(
483    declaration: &'a VariableDeclaration<'a>,
484) -> Vec<SourceFlowNode<'a>> {
485    declaration
486        .declarations
487        .iter()
488        .filter_map(|declarator| {
489            binding_pattern_identifier(&declarator.id).map(|identifier| {
490                SourceFlowNode::Assignment {
491                    binding: binding_ref_from_binding_identifier(identifier),
492                    expression: declarator.init.as_ref(),
493                }
494            })
495        })
496        .collect()
497}
498
499fn reference_binding_for_offset(
500    program: &Program<'_>,
501    scoping: &Scoping,
502    variable_name: &str,
503    reference_byte_offset: usize,
504) -> Option<SourceFlowBindingRefV0> {
505    let mut visitor = SourceReferenceBindingFinder {
506        scoping,
507        variable_name,
508        reference_byte_offset,
509        binding: None,
510    };
511    visitor.visit_program(program);
512    visitor.binding
513}
514
515struct SourceReferenceBindingFinder<'a> {
516    scoping: &'a Scoping,
517    variable_name: &'a str,
518    reference_byte_offset: usize,
519    binding: Option<SourceFlowBindingRefV0>,
520}
521
522impl<'a, 'ast> Visit<'ast> for SourceReferenceBindingFinder<'a> {
523    fn visit_identifier_reference(&mut self, identifier: &IdentifierReference<'ast>) {
524        if self.binding.is_none()
525            && identifier.name.as_str() == self.variable_name
526            && span_contains(identifier.span, self.reference_byte_offset)
527        {
528            self.binding = binding_ref_from_reference(self.scoping, identifier);
529        }
530    }
531}
532
533fn binding_ref_from_reference(
534    scoping: &Scoping,
535    identifier: &IdentifierReference<'_>,
536) -> Option<SourceFlowBindingRefV0> {
537    identifier
538        .reference_id
539        .get()
540        .and_then(|reference_id| scoping.get_reference(reference_id).symbol_id())
541        .map(|symbol_id| binding_ref_from_symbol(symbol_id, identifier.name.as_str()))
542}
543
544fn binding_ref_from_binding_identifier(
545    identifier: &BindingIdentifier<'_>,
546) -> Option<SourceFlowBindingRefV0> {
547    binding_identifier_symbol_id(identifier)
548        .map(|symbol_id| binding_ref_from_symbol(symbol_id, identifier.name.as_str()))
549}
550
551fn binding_identifier_symbol_id(identifier: &BindingIdentifier<'_>) -> Option<SymbolId> {
552    identifier.symbol_id.get()
553}
554
555fn binding_ref_from_symbol(symbol_id: SymbolId, name: &str) -> SourceFlowBindingRefV0 {
556    SourceFlowBindingRefV0 {
557        symbol_ordinal: symbol_id.index(),
558        name: name.to_string(),
559    }
560}
561
562fn binding_pattern_identifier<'a>(
563    pattern: &'a BindingPattern<'a>,
564) -> Option<&'a BindingIdentifier<'a>> {
565    match pattern {
566        BindingPattern::BindingIdentifier(identifier) => Some(identifier),
567        _ => None,
568    }
569}
570
571struct SourceFlowBlockGraphSnapshotBuilder<'a> {
572    blocks: Vec<SourceFlowBlockSnapshotV0>,
573    counters: BTreeMap<&'static str, usize>,
574    root_statements: &'a oxc_allocator::Vec<'a, Statement<'a>>,
575    scoping: &'a Scoping,
576}
577
578impl<'a> SourceFlowBlockGraphSnapshotBuilder<'a> {
579    fn new(
580        root_statements: &'a oxc_allocator::Vec<'a, Statement<'a>>,
581        scoping: &'a Scoping,
582    ) -> Self {
583        Self {
584            blocks: Vec::new(),
585            counters: BTreeMap::new(),
586            root_statements,
587            scoping,
588        }
589    }
590
591    fn build(mut self, nodes: &[SourceFlowNode<'_>]) -> SourceFlowBlockGraphSnapshotV0 {
592        let entry_block_id = self.add_block("entry", Some("entry"), None, None);
593        let tails = self.append_nodes(nodes, vec![entry_block_id], None);
594        let exit_block_id = self.add_block("exit", Some("exit"), None, None);
595        self.connect(tails.as_slice(), exit_block_id.as_str());
596        SourceFlowBlockGraphSnapshotV0 {
597            entry_block_id: "entry".to_string(),
598            blocks: self.blocks,
599        }
600    }
601
602    fn append_nodes(
603        &mut self,
604        nodes: &[SourceFlowNode<'_>],
605        incoming_block_ids: Vec<String>,
606        break_target_block_id: Option<&str>,
607    ) -> Vec<String> {
608        let mut tails = incoming_block_ids;
609        for node in nodes {
610            if tails.is_empty() {
611                return Vec::new();
612            }
613            tails = self.append_node(node, tails, break_target_block_id);
614        }
615        tails
616    }
617
618    fn append_node(
619        &mut self,
620        node: &SourceFlowNode<'_>,
621        incoming_block_ids: Vec<String>,
622        break_target_block_id: Option<&str>,
623    ) -> Vec<String> {
624        match node {
625            SourceFlowNode::Assignment {
626                binding,
627                expression,
628            } => self.append_assignment(binding.as_ref(), *expression, incoming_block_ids),
629            SourceFlowNode::Branch {
630                then_nodes,
631                else_nodes,
632            } => self.append_branch(
633                then_nodes,
634                else_nodes,
635                incoming_block_ids,
636                break_target_block_id,
637            ),
638            SourceFlowNode::Loop { body_nodes } => self.append_loop(body_nodes, incoming_block_ids),
639            SourceFlowNode::Break => {
640                let break_block_id = self.add_block("break", None, None, None);
641                self.connect(incoming_block_ids.as_slice(), break_block_id.as_str());
642                if let Some(target) = break_target_block_id {
643                    self.connect(std::slice::from_ref(&break_block_id), target);
644                }
645                Vec::new()
646            }
647            SourceFlowNode::Terminate => {
648                let terminate_block_id = self.add_block("terminate", None, None, None);
649                self.connect(incoming_block_ids.as_slice(), terminate_block_id.as_str());
650                Vec::new()
651            }
652        }
653    }
654
655    fn append_assignment(
656        &mut self,
657        binding: Option<&SourceFlowBindingRefV0>,
658        expression: Option<&Expression<'_>>,
659        incoming_block_ids: Vec<String>,
660    ) -> Vec<String> {
661        let boundary_effect = expression
662            .map(class_boundary_effect_for_expression)
663            .unwrap_or_default();
664        let transfer_kind = if expression.is_some_and(is_concat_expression) {
665            "concatFacts"
666        } else {
667            "assignFacts"
668        };
669        let facts = expression.and_then(|expression| {
670            expression_type_facts(expression, self.root_statements, self.scoping, &self.blocks)
671        });
672        let assignment_block_id = self.add_block(
673            "assignment",
674            None,
675            Some(transfer_kind),
676            Some((binding.cloned(), None, facts, boundary_effect)),
677        );
678        self.connect(incoming_block_ids.as_slice(), assignment_block_id.as_str());
679
680        if let Some(Expression::LogicalExpression(expression)) = expression {
681            return self.append_short_circuit_expression(expression, vec![assignment_block_id]);
682        }
683
684        vec![assignment_block_id]
685    }
686
687    fn append_short_circuit_expression(
688        &mut self,
689        expression: &LogicalExpression<'_>,
690        incoming_block_ids: Vec<String>,
691    ) -> Vec<String> {
692        let expression_kind = logical_expression_kind(expression);
693        let operand_block_id = self.add_block(
694            "logicalOperand",
695            None,
696            None,
697            Some((
698                None,
699                expression_kind,
700                None,
701                ClassBoundaryEffectV0::UnknownBoundary,
702            )),
703        );
704        let rhs_block_id = self.add_block(
705            "logicalRhs",
706            None,
707            None,
708            Some((
709                None,
710                expression_kind,
711                None,
712                ClassBoundaryEffectV0::UnknownBoundary,
713            )),
714        );
715        let join_block_id = self.add_block(
716            "logicalJoin",
717            None,
718            None,
719            Some((
720                None,
721                expression_kind,
722                None,
723                ClassBoundaryEffectV0::UnknownBoundary,
724            )),
725        );
726        self.connect(incoming_block_ids.as_slice(), operand_block_id.as_str());
727        self.connect(
728            std::slice::from_ref(&operand_block_id),
729            join_block_id.as_str(),
730        );
731        self.connect(
732            std::slice::from_ref(&operand_block_id),
733            rhs_block_id.as_str(),
734        );
735        self.connect(std::slice::from_ref(&rhs_block_id), join_block_id.as_str());
736        vec![join_block_id]
737    }
738
739    fn append_branch(
740        &mut self,
741        then_nodes: &[SourceFlowNode<'_>],
742        else_nodes: &[SourceFlowNode<'_>],
743        incoming_block_ids: Vec<String>,
744        break_target_block_id: Option<&str>,
745    ) -> Vec<String> {
746        let branch_block_id = self.add_block("branch", None, None, None);
747        let join_block_id = self.add_block("join", None, None, None);
748        self.connect(incoming_block_ids.as_slice(), branch_block_id.as_str());
749        let then_tails = self.append_nodes(
750            then_nodes,
751            vec![branch_block_id.clone()],
752            break_target_block_id,
753        );
754        let else_tails = if else_nodes.is_empty() {
755            vec![branch_block_id]
756        } else {
757            self.append_nodes(else_nodes, vec![branch_block_id], break_target_block_id)
758        };
759        self.connect(then_tails.as_slice(), join_block_id.as_str());
760        self.connect(else_tails.as_slice(), join_block_id.as_str());
761        vec![join_block_id]
762    }
763
764    fn append_loop(
765        &mut self,
766        body_nodes: &[SourceFlowNode<'_>],
767        incoming_block_ids: Vec<String>,
768    ) -> Vec<String> {
769        let loop_index = self.next_index("loop");
770        let header_block_id = format!("loop:{loop_index}:header");
771        let body_block_id = format!("loop:{loop_index}:body");
772        let exit_block_id = format!("loop:{loop_index}:exit");
773        self.add_block("loopHeader", Some(header_block_id.as_str()), None, None);
774        self.add_block("loopBody", Some(body_block_id.as_str()), None, None);
775        self.add_block("loopExit", Some(exit_block_id.as_str()), None, None);
776        self.connect(incoming_block_ids.as_slice(), header_block_id.as_str());
777        self.connect(
778            std::slice::from_ref(&header_block_id),
779            body_block_id.as_str(),
780        );
781        self.connect(
782            std::slice::from_ref(&header_block_id),
783            exit_block_id.as_str(),
784        );
785        let body_tails = self.append_nodes(
786            body_nodes,
787            vec![body_block_id],
788            Some(exit_block_id.as_str()),
789        );
790        self.connect(body_tails.as_slice(), header_block_id.as_str());
791        vec![exit_block_id]
792    }
793
794    fn add_block(
795        &mut self,
796        kind: &'static str,
797        explicit_id: Option<&str>,
798        transfer_kind: Option<&'static str>,
799        metadata: Option<SourceFlowBlockMetadataV0>,
800    ) -> String {
801        let id = explicit_id
802            .map(str::to_string)
803            .unwrap_or_else(|| format!("{kind}:{}", self.next_index(kind)));
804        let (binding, expression_kind, facts, boundary_effect) = metadata.unwrap_or_default();
805        let ordered_word = facts.as_ref().and_then(ordered_word_for_type_facts);
806        self.blocks.push(SourceFlowBlockSnapshotV0 {
807            id: id.clone(),
808            kind,
809            transfer_kind: transfer_kind.unwrap_or_else(|| transfer_kind_for_block_kind(kind)),
810            successor_block_ids: Vec::new(),
811            symbol_ordinal: binding.as_ref().map(|binding| binding.symbol_ordinal),
812            variable_name: binding.as_ref().map(|binding| binding.name.clone()),
813            binding,
814            expression_kind,
815            boundary_effect,
816            ordered_word,
817            facts,
818        });
819        id
820    }
821
822    fn connect(&mut self, from_block_ids: &[String], to_block_id: &str) {
823        for from_block_id in from_block_ids {
824            if let Some(block) = self
825                .blocks
826                .iter_mut()
827                .find(|candidate| candidate.id == *from_block_id)
828                && !block
829                    .successor_block_ids
830                    .iter()
831                    .any(|candidate| candidate == to_block_id)
832            {
833                block.successor_block_ids.push(to_block_id.to_string());
834            }
835        }
836    }
837
838    fn next_index(&mut self, kind: &'static str) -> usize {
839        let next = self.counters.get(kind).copied().unwrap_or_default();
840        self.counters.insert(kind, next + 1);
841        next
842    }
843}
844
845fn is_concat_expression(expression: &Expression<'_>) -> bool {
846    match transparent_expression(expression) {
847        Expression::BinaryExpression(expression) => expression.operator == BinaryOperator::Addition,
848        Expression::TemplateLiteral(template) => !template.expressions.is_empty(),
849        Expression::CallExpression(call) => {
850            class_boundary_effect_for_call(call) != ClassBoundaryEffectV0::UnknownBoundary
851        }
852        _ => false,
853    }
854}
855
856fn class_boundary_effect_for_expression(expression: &Expression<'_>) -> ClassBoundaryEffectV0 {
857    match transparent_expression(expression) {
858        Expression::BinaryExpression(expression)
859            if expression.operator == BinaryOperator::Addition =>
860        {
861            boundary_effect_for_binary_concat(&expression.left, &expression.right)
862        }
863        Expression::TemplateLiteral(template) if !template.expressions.is_empty() => {
864            let mut saw_literal_delimiter = false;
865            for (index, interpolation) in template.expressions.iter().enumerate() {
866                let left = template
867                    .quasis
868                    .get(index)
869                    .and_then(|quasi| quasi.value.cooked.as_ref())
870                    .map(|value| value.as_str())
871                    .unwrap_or_default();
872                let right = template
873                    .quasis
874                    .get(index + 1)
875                    .and_then(|quasi| quasi.value.cooked.as_ref())
876                    .map(|value| value.as_str())
877                    .unwrap_or_default();
878                if left
879                    .chars()
880                    .next_back()
881                    .is_some_and(omena_abstract_value::is_dom_class_ascii_whitespace_v0)
882                    || right
883                        .chars()
884                        .next()
885                        .is_some_and(omena_abstract_value::is_dom_class_ascii_whitespace_v0)
886                {
887                    return ClassBoundaryEffectV0::ConcatAtTokenBoundary;
888                }
889                saw_literal_delimiter |= !left.is_empty() || !right.is_empty();
890                if class_boundary_effect_for_expression(interpolation)
891                    == ClassBoundaryEffectV0::ConcatAtTokenBoundary
892                {
893                    return ClassBoundaryEffectV0::ConcatAtTokenBoundary;
894                }
895            }
896            if saw_literal_delimiter {
897                ClassBoundaryEffectV0::ConcatInsideToken
898            } else {
899                ClassBoundaryEffectV0::UnknownBoundary
900            }
901        }
902        Expression::CallExpression(call) => class_boundary_effect_for_call(call),
903        _ => ClassBoundaryEffectV0::UnknownBoundary,
904    }
905}
906
907fn boundary_effect_for_binary_concat(
908    left: &Expression<'_>,
909    right: &Expression<'_>,
910) -> ClassBoundaryEffectV0 {
911    let left_edge = expression_literal_edge(left, LiteralEdgeSide::Trailing);
912    let right_edge = expression_literal_edge(right, LiteralEdgeSide::Leading);
913    if matches!(left_edge, LiteralEdgeV0::Whitespace)
914        || matches!(right_edge, LiteralEdgeV0::Whitespace)
915    {
916        ClassBoundaryEffectV0::ConcatAtTokenBoundary
917    } else if matches!(left_edge, LiteralEdgeV0::NonWhitespace)
918        || matches!(right_edge, LiteralEdgeV0::NonWhitespace)
919    {
920        ClassBoundaryEffectV0::ConcatInsideToken
921    } else {
922        ClassBoundaryEffectV0::UnknownBoundary
923    }
924}
925
926fn class_boundary_effect_for_call(
927    call: &oxc_ast::ast::CallExpression<'_>,
928) -> ClassBoundaryEffectV0 {
929    if matches!(
930        transparent_expression(&call.callee),
931        Expression::Identifier(identifier)
932            if matches!(identifier.name.as_str(), "clsx" | "classnames" | "classNames")
933    ) {
934        return if call.arguments.len() >= 2 {
935            ClassBoundaryEffectV0::ConcatAtTokenBoundary
936        } else {
937            ClassBoundaryEffectV0::UnknownBoundary
938        };
939    }
940    let Expression::StaticMemberExpression(member) = transparent_expression(&call.callee) else {
941        return ClassBoundaryEffectV0::UnknownBoundary;
942    };
943    if member.property.name.as_str() != "join" {
944        return ClassBoundaryEffectV0::UnknownBoundary;
945    }
946    let Some(separator) = call
947        .arguments
948        .first()
949        .and_then(argument_expression)
950        .and_then(static_string_value)
951    else {
952        return ClassBoundaryEffectV0::UnknownBoundary;
953    };
954    if separator
955        .chars()
956        .any(omena_abstract_value::is_dom_class_ascii_whitespace_v0)
957    {
958        ClassBoundaryEffectV0::ConcatAtTokenBoundary
959    } else {
960        ClassBoundaryEffectV0::ConcatInsideToken
961    }
962}
963
964#[derive(Clone, Copy)]
965enum LiteralEdgeSide {
966    Leading,
967    Trailing,
968}
969
970#[derive(Clone, Copy)]
971enum LiteralEdgeV0 {
972    Empty,
973    Whitespace,
974    NonWhitespace,
975    Unknown,
976}
977
978fn expression_literal_edge(expression: &Expression<'_>, side: LiteralEdgeSide) -> LiteralEdgeV0 {
979    match transparent_expression(expression) {
980        Expression::StringLiteral(literal) => literal_edge(literal.value.as_str(), side),
981        Expression::TemplateLiteral(template) => {
982            let value = match side {
983                LiteralEdgeSide::Leading => template.quasis.first(),
984                LiteralEdgeSide::Trailing => template.quasis.last(),
985            }
986            .and_then(|quasi| quasi.value.cooked.as_ref())
987            .map(|value| value.as_str())
988            .unwrap_or_default();
989            literal_edge(value, side)
990        }
991        Expression::BinaryExpression(binary) if binary.operator == BinaryOperator::Addition => {
992            let primary = match side {
993                LiteralEdgeSide::Leading => &binary.left,
994                LiteralEdgeSide::Trailing => &binary.right,
995            };
996            let fallback = match side {
997                LiteralEdgeSide::Leading => &binary.right,
998                LiteralEdgeSide::Trailing => &binary.left,
999            };
1000            match expression_literal_edge(primary, side) {
1001                LiteralEdgeV0::Empty => expression_literal_edge(fallback, side),
1002                edge => edge,
1003            }
1004        }
1005        _ => LiteralEdgeV0::Unknown,
1006    }
1007}
1008
1009fn literal_edge(value: &str, side: LiteralEdgeSide) -> LiteralEdgeV0 {
1010    let character = match side {
1011        LiteralEdgeSide::Leading => value.chars().next(),
1012        LiteralEdgeSide::Trailing => value.chars().next_back(),
1013    };
1014    match character {
1015        None => LiteralEdgeV0::Empty,
1016        Some(character) if omena_abstract_value::is_dom_class_ascii_whitespace_v0(character) => {
1017            LiteralEdgeV0::Whitespace
1018        }
1019        Some(_) => LiteralEdgeV0::NonWhitespace,
1020    }
1021}
1022
1023fn transparent_expression<'a>(expression: &'a Expression<'a>) -> &'a Expression<'a> {
1024    match expression {
1025        Expression::ParenthesizedExpression(expression) => {
1026            transparent_expression(&expression.expression)
1027        }
1028        Expression::TSAsExpression(expression) => transparent_expression(&expression.expression),
1029        Expression::TSSatisfiesExpression(expression) => {
1030            transparent_expression(&expression.expression)
1031        }
1032        Expression::TSTypeAssertion(expression) => transparent_expression(&expression.expression),
1033        Expression::TSNonNullExpression(expression) => {
1034            transparent_expression(&expression.expression)
1035        }
1036        Expression::TSInstantiationExpression(expression) => {
1037            transparent_expression(&expression.expression)
1038        }
1039        _ => expression,
1040    }
1041}
1042
1043fn static_string_value<'a>(expression: &'a Expression<'a>) -> Option<&'a str> {
1044    match transparent_expression(expression) {
1045        Expression::StringLiteral(literal) => Some(literal.value.as_str()),
1046        Expression::TemplateLiteral(template) if template.expressions.is_empty() => template
1047            .quasis
1048            .first()
1049            .and_then(|quasi| quasi.value.cooked.as_ref())
1050            .map(|value| value.as_str()),
1051        _ => None,
1052    }
1053}
1054
1055fn argument_expression<'a>(argument: &'a oxc_ast::ast::Argument<'a>) -> Option<&'a Expression<'a>> {
1056    match argument {
1057        oxc_ast::ast::Argument::SpreadElement(spread) => Some(&spread.argument),
1058        _ => argument.as_expression(),
1059    }
1060}
1061
1062fn ordered_word_for_type_facts(facts: &StringTypeFactsV2) -> Option<OrderedTokenWordV0> {
1063    let mut values = facts.values.as_ref()?.iter();
1064    let first = values.next()?;
1065    let DomClassTokenizationV0::Known { word, .. } =
1066        omena_abstract_value::tokenize_dom_class_attribute_v0(Some(first))
1067    else {
1068        return None;
1069    };
1070    values
1071        .all(|value| {
1072            matches!(
1073                omena_abstract_value::tokenize_dom_class_attribute_v0(Some(value)),
1074                DomClassTokenizationV0::Known { word: candidate, .. } if candidate == word
1075            )
1076        })
1077        .then_some(word)
1078}
1079
1080fn boundary_effect_wire_value(effect: ClassBoundaryEffectV0) -> &'static str {
1081    match effect {
1082        ClassBoundaryEffectV0::ConcatInsideToken => "concatInsideToken",
1083        ClassBoundaryEffectV0::ConcatAtTokenBoundary => "concatAtTokenBoundary",
1084        ClassBoundaryEffectV0::UnknownBoundary => "unknownBoundary",
1085    }
1086}
1087
1088fn logical_expression_kind(expression: &LogicalExpression<'_>) -> Option<&'static str> {
1089    if expression.operator.is_and() {
1090        Some("logicalAnd")
1091    } else if expression.operator.is_or() {
1092        Some("logicalOr")
1093    } else if expression.operator.is_coalesce() {
1094        Some("nullishCoalesce")
1095    } else {
1096        None
1097    }
1098}
1099
1100struct ExpressionFactContext<'ast, 'context> {
1101    root_statements: &'context oxc_allocator::Vec<'ast, Statement<'ast>>,
1102    scoping: &'context Scoping,
1103    binding_facts: &'context BTreeMap<usize, Vec<StringTypeFactsV2>>,
1104}
1105
1106fn expression_type_facts<'ast>(
1107    expression: &Expression<'ast>,
1108    root_statements: &oxc_allocator::Vec<'ast, Statement<'ast>>,
1109    scoping: &Scoping,
1110    prior_blocks: &[SourceFlowBlockSnapshotV0],
1111) -> Option<StringTypeFactsV2> {
1112    let mut binding_facts = BTreeMap::<usize, Vec<StringTypeFactsV2>>::new();
1113    for block in prior_blocks {
1114        if let (Some(symbol_ordinal), Some(facts)) = (block.symbol_ordinal, block.facts.clone()) {
1115            binding_facts.entry(symbol_ordinal).or_default().push(facts);
1116        }
1117    }
1118    let context = ExpressionFactContext {
1119        root_statements,
1120        scoping,
1121        binding_facts: &binding_facts,
1122    };
1123    expression_type_facts_inner(
1124        expression,
1125        &context,
1126        &mut BTreeSet::new(),
1127        &mut BTreeSet::new(),
1128    )
1129}
1130
1131fn expression_type_facts_inner(
1132    expression: &Expression<'_>,
1133    context: &ExpressionFactContext<'_, '_>,
1134    seen_functions: &mut BTreeSet<String>,
1135    seen_bindings: &mut BTreeSet<usize>,
1136) -> Option<StringTypeFactsV2> {
1137    match expression {
1138        Expression::StringLiteral(literal) => Some(exact_type_facts(literal.value.as_str())),
1139        Expression::Identifier(identifier) => {
1140            let symbol_ordinal =
1141                binding_ref_from_reference(context.scoping, identifier)?.symbol_ordinal;
1142            if !seen_bindings.insert(symbol_ordinal) {
1143                return None;
1144            }
1145            let facts = merge_type_facts(
1146                context
1147                    .binding_facts
1148                    .get(&symbol_ordinal)?
1149                    .iter()
1150                    .cloned()
1151                    .map(Some),
1152            );
1153            seen_bindings.remove(&symbol_ordinal);
1154            facts
1155        }
1156        Expression::TemplateLiteral(template) => {
1157            let first = template.quasis.first()?.value.cooked.as_ref()?.as_str();
1158            let mut facts = Some(exact_type_facts(first));
1159            for (index, expression) in template.expressions.iter().enumerate() {
1160                facts = concatenate_type_facts(
1161                    facts,
1162                    expression_type_facts_inner(expression, context, seen_functions, seen_bindings),
1163                );
1164                let suffix = template
1165                    .quasis
1166                    .get(index + 1)?
1167                    .value
1168                    .cooked
1169                    .as_ref()?
1170                    .as_str();
1171                facts = concatenate_type_facts(facts, Some(exact_type_facts(suffix)));
1172            }
1173            facts
1174        }
1175        Expression::ParenthesizedExpression(expression) => expression_type_facts_inner(
1176            &expression.expression,
1177            context,
1178            seen_functions,
1179            seen_bindings,
1180        ),
1181        Expression::TSAsExpression(expression) => expression_type_facts_inner(
1182            &expression.expression,
1183            context,
1184            seen_functions,
1185            seen_bindings,
1186        ),
1187        Expression::TSSatisfiesExpression(expression) => expression_type_facts_inner(
1188            &expression.expression,
1189            context,
1190            seen_functions,
1191            seen_bindings,
1192        ),
1193        Expression::TSTypeAssertion(expression) => expression_type_facts_inner(
1194            &expression.expression,
1195            context,
1196            seen_functions,
1197            seen_bindings,
1198        ),
1199        Expression::TSNonNullExpression(expression) => expression_type_facts_inner(
1200            &expression.expression,
1201            context,
1202            seen_functions,
1203            seen_bindings,
1204        ),
1205        Expression::TSInstantiationExpression(expression) => expression_type_facts_inner(
1206            &expression.expression,
1207            context,
1208            seen_functions,
1209            seen_bindings,
1210        ),
1211        Expression::ConditionalExpression(expression) => merge_type_facts([
1212            expression_type_facts_inner(
1213                &expression.consequent,
1214                context,
1215                seen_functions,
1216                seen_bindings,
1217            ),
1218            expression_type_facts_inner(
1219                &expression.alternate,
1220                context,
1221                seen_functions,
1222                seen_bindings,
1223            ),
1224        ]),
1225        Expression::LogicalExpression(expression) => {
1226            if expression.operator.is_and() {
1227                return merge_type_facts([
1228                    Some(exact_type_facts("")),
1229                    expression_type_facts_inner(
1230                        &expression.right,
1231                        context,
1232                        seen_functions,
1233                        seen_bindings,
1234                    ),
1235                ]);
1236            }
1237            merge_type_facts([
1238                expression_type_facts_inner(
1239                    &expression.left,
1240                    context,
1241                    seen_functions,
1242                    seen_bindings,
1243                ),
1244                expression_type_facts_inner(
1245                    &expression.right,
1246                    context,
1247                    seen_functions,
1248                    seen_bindings,
1249                ),
1250            ])
1251        }
1252        Expression::BinaryExpression(expression)
1253            if expression.operator == BinaryOperator::Addition =>
1254        {
1255            concatenate_type_facts(
1256                expression_type_facts_inner(
1257                    &expression.left,
1258                    context,
1259                    seen_functions,
1260                    seen_bindings,
1261                ),
1262                expression_type_facts_inner(
1263                    &expression.right,
1264                    context,
1265                    seen_functions,
1266                    seen_bindings,
1267                ),
1268            )
1269        }
1270        Expression::CallExpression(call) => {
1271            if let Some(facts) =
1272                class_boundary_call_type_facts(call, context, seen_functions, seen_bindings)
1273            {
1274                return Some(facts);
1275            }
1276            let Expression::Identifier(callee) = &call.callee else {
1277                return None;
1278            };
1279            function_return_type_facts(callee.name.as_str(), context, seen_functions, seen_bindings)
1280        }
1281        _ => None,
1282    }
1283}
1284
1285fn class_boundary_call_type_facts(
1286    call: &oxc_ast::ast::CallExpression<'_>,
1287    context: &ExpressionFactContext<'_, '_>,
1288    seen_functions: &mut BTreeSet<String>,
1289    seen_bindings: &mut BTreeSet<usize>,
1290) -> Option<StringTypeFactsV2> {
1291    if matches!(
1292        transparent_expression(&call.callee),
1293        Expression::Identifier(identifier)
1294            if matches!(identifier.name.as_str(), "clsx" | "classnames" | "classNames")
1295    ) {
1296        let facts = call
1297            .arguments
1298            .iter()
1299            .map(|argument| {
1300                argument_expression(argument).and_then(|expression| {
1301                    class_list_argument_type_facts(
1302                        expression,
1303                        context,
1304                        seen_functions,
1305                        seen_bindings,
1306                    )
1307                })
1308            })
1309            .collect::<Option<Vec<_>>>()?;
1310        return concatenate_type_fact_sequence(facts, " ");
1311    }
1312
1313    let Expression::StaticMemberExpression(member) = transparent_expression(&call.callee) else {
1314        return None;
1315    };
1316    if member.property.name.as_str() != "join" {
1317        return None;
1318    }
1319    let Expression::ArrayExpression(array) = transparent_expression(&member.object) else {
1320        return None;
1321    };
1322    let separator = call
1323        .arguments
1324        .first()
1325        .and_then(argument_expression)
1326        .and_then(static_string_value)
1327        .unwrap_or(",");
1328    let facts = array
1329        .elements
1330        .iter()
1331        .map(|element| {
1332            let expression = match element {
1333                oxc_ast::ast::ArrayExpressionElement::SpreadElement(spread) => &spread.argument,
1334                oxc_ast::ast::ArrayExpressionElement::Elision(_) => return None,
1335                _ => element.as_expression()?,
1336            };
1337            expression_type_facts_inner(expression, context, seen_functions, seen_bindings)
1338        })
1339        .collect::<Option<Vec<_>>>()?;
1340    concatenate_type_fact_sequence(facts, separator)
1341}
1342
1343fn class_list_argument_type_facts(
1344    expression: &Expression<'_>,
1345    context: &ExpressionFactContext<'_, '_>,
1346    seen_functions: &mut BTreeSet<String>,
1347    seen_bindings: &mut BTreeSet<usize>,
1348) -> Option<StringTypeFactsV2> {
1349    match transparent_expression(expression) {
1350        Expression::BooleanLiteral(_) | Expression::NullLiteral(_) => Some(exact_type_facts("")),
1351        Expression::Identifier(identifier) if identifier.name.as_str() == "undefined" => {
1352            Some(exact_type_facts(""))
1353        }
1354        Expression::LogicalExpression(expression) if expression.operator.is_and() => {
1355            merge_type_facts([
1356                Some(exact_type_facts("")),
1357                class_list_argument_type_facts(
1358                    &expression.right,
1359                    context,
1360                    seen_functions,
1361                    seen_bindings,
1362                ),
1363            ])
1364        }
1365        Expression::ConditionalExpression(expression) => merge_type_facts([
1366            class_list_argument_type_facts(
1367                &expression.consequent,
1368                context,
1369                seen_functions,
1370                seen_bindings,
1371            ),
1372            class_list_argument_type_facts(
1373                &expression.alternate,
1374                context,
1375                seen_functions,
1376                seen_bindings,
1377            ),
1378        ]),
1379        expression => {
1380            expression_type_facts_inner(expression, context, seen_functions, seen_bindings)
1381        }
1382    }
1383}
1384
1385fn concatenate_type_fact_sequence(
1386    facts: Vec<StringTypeFactsV2>,
1387    separator: &str,
1388) -> Option<StringTypeFactsV2> {
1389    let mut facts = facts.into_iter();
1390    let mut combined = Some(facts.next()?);
1391    for fact in facts {
1392        combined = concatenate_type_facts(combined, Some(exact_type_facts(separator)));
1393        combined = concatenate_type_facts(combined, Some(fact));
1394    }
1395    combined
1396}
1397
1398fn function_return_type_facts(
1399    function_name: &str,
1400    context: &ExpressionFactContext<'_, '_>,
1401    seen_functions: &mut BTreeSet<String>,
1402    seen_bindings: &mut BTreeSet<usize>,
1403) -> Option<StringTypeFactsV2> {
1404    if !seen_functions.insert(function_name.to_string()) {
1405        return None;
1406    }
1407    let body = context
1408        .root_statements
1409        .iter()
1410        .find_map(|statement| function_body_for_named_statement(statement, function_name))?;
1411    let facts = merge_type_facts(
1412        body.statements
1413            .iter()
1414            .flat_map(|statement| {
1415                return_type_facts_for_statement(statement, context, seen_functions, seen_bindings)
1416            })
1417            .map(Some),
1418    );
1419    seen_functions.remove(function_name);
1420    facts
1421}
1422
1423fn function_body_for_named_statement<'a>(
1424    statement: &'a Statement<'a>,
1425    function_name: &str,
1426) -> Option<&'a FunctionBody<'a>> {
1427    match statement {
1428        Statement::FunctionDeclaration(function)
1429            if function
1430                .id
1431                .as_ref()
1432                .is_some_and(|id| id.name.as_str() == function_name) =>
1433        {
1434            function.body.as_deref()
1435        }
1436        Statement::ExportDeclaration(export) => {
1437            if let Declaration::FunctionDeclaration(function) = &export.declaration
1438                && function
1439                    .id
1440                    .as_ref()
1441                    .is_some_and(|id| id.name.as_str() == function_name)
1442            {
1443                return function.body.as_deref();
1444            }
1445            None
1446        }
1447        _ => None,
1448    }
1449}
1450
1451fn return_type_facts_for_statement(
1452    statement: &Statement<'_>,
1453    context: &ExpressionFactContext<'_, '_>,
1454    seen_functions: &mut BTreeSet<String>,
1455    seen_bindings: &mut BTreeSet<usize>,
1456) -> Vec<StringTypeFactsV2> {
1457    match statement {
1458        Statement::ReturnStatement(statement) => statement
1459            .argument
1460            .as_ref()
1461            .and_then(|expression| {
1462                expression_type_facts_inner(expression, context, seen_functions, seen_bindings)
1463            })
1464            .into_iter()
1465            .collect(),
1466        Statement::BlockStatement(block) => block
1467            .body
1468            .iter()
1469            .flat_map(|statement| {
1470                return_type_facts_for_statement(statement, context, seen_functions, seen_bindings)
1471            })
1472            .collect(),
1473        Statement::IfStatement(statement) => {
1474            let mut facts = return_type_facts_for_statement(
1475                &statement.consequent,
1476                context,
1477                seen_functions,
1478                seen_bindings,
1479            );
1480            if let Some(alternate) = &statement.alternate {
1481                facts.extend(return_type_facts_for_statement(
1482                    alternate,
1483                    context,
1484                    seen_functions,
1485                    seen_bindings,
1486                ));
1487            }
1488            facts
1489        }
1490        Statement::SwitchStatement(statement) => statement
1491            .cases
1492            .iter()
1493            .flat_map(|case| {
1494                return_type_facts_for_switch_case(case, context, seen_functions, seen_bindings)
1495            })
1496            .collect(),
1497        _ => Vec::new(),
1498    }
1499}
1500
1501fn return_type_facts_for_switch_case(
1502    case: &SwitchCase<'_>,
1503    context: &ExpressionFactContext<'_, '_>,
1504    seen_functions: &mut BTreeSet<String>,
1505    seen_bindings: &mut BTreeSet<usize>,
1506) -> Vec<StringTypeFactsV2> {
1507    case.consequent
1508        .iter()
1509        .flat_map(|statement| {
1510            return_type_facts_for_statement(statement, context, seen_functions, seen_bindings)
1511        })
1512        .collect()
1513}
1514
1515fn merge_type_facts(
1516    facts: impl IntoIterator<Item = Option<StringTypeFactsV2>>,
1517) -> Option<StringTypeFactsV2> {
1518    let mut values = BTreeSet::new();
1519    for fact in facts {
1520        let fact = fact?;
1521        for value in finite_values_for_type_facts(&fact)? {
1522            values.insert(value);
1523        }
1524    }
1525    finite_type_facts(values)
1526}
1527
1528fn concatenate_type_facts(
1529    left: Option<StringTypeFactsV2>,
1530    right: Option<StringTypeFactsV2>,
1531) -> Option<StringTypeFactsV2> {
1532    if left.as_ref().and_then(single_finite_value).as_deref() == Some("") {
1533        return right;
1534    }
1535    if right.as_ref().and_then(single_finite_value).as_deref() == Some("") {
1536        return left;
1537    }
1538    match (left, right) {
1539        (Some(left), Some(right)) => {
1540            if let (Some(left_values), Some(right_values)) = (
1541                finite_values_for_type_facts(&left),
1542                finite_values_for_type_facts(&right),
1543            ) {
1544                return finite_type_facts(left_values.iter().flat_map(|left| {
1545                    right_values
1546                        .iter()
1547                        .map(move |right| format!("{left}{right}"))
1548                }));
1549            }
1550            if left.constraint_kind.as_deref() == Some("prefix")
1551                && let Some(suffix) = single_finite_value(&right)
1552            {
1553                return Some(prefix_suffix_type_facts(
1554                    left.prefix.as_deref().unwrap_or_default(),
1555                    suffix.as_str(),
1556                ));
1557            }
1558            if right.constraint_kind.as_deref() == Some("suffix")
1559                && let Some(prefix) = single_finite_value(&left)
1560            {
1561                return Some(prefix_suffix_type_facts(
1562                    prefix.as_str(),
1563                    right.suffix.as_deref().unwrap_or_default(),
1564                ));
1565            }
1566            None
1567        }
1568        (Some(left), None) => finite_values_for_type_facts(&left)
1569            .and_then(|values| longest_common_prefix(values.as_slice()))
1570            .map(|prefix| {
1571                constrained_type_facts("prefix", Some(prefix), None, "concatUnknownRight")
1572            }),
1573        (None, Some(right)) => finite_values_for_type_facts(&right)
1574            .and_then(|values| longest_common_suffix(values.as_slice()))
1575            .map(|suffix| {
1576                constrained_type_facts("suffix", None, Some(suffix), "concatUnknownLeft")
1577            }),
1578        (None, None) => None,
1579    }
1580}
1581
1582fn exact_type_facts(value: &str) -> StringTypeFactsV2 {
1583    let mut facts = empty_type_facts("exact");
1584    facts.values = Some(vec![value.to_string()]);
1585    facts
1586}
1587
1588fn finite_type_facts(values: impl IntoIterator<Item = String>) -> Option<StringTypeFactsV2> {
1589    let values = values.into_iter().collect::<BTreeSet<_>>();
1590    if values.is_empty() {
1591        return None;
1592    }
1593    if values.len() == 1 {
1594        return values.iter().next().map(|value| exact_type_facts(value));
1595    }
1596    let mut facts = empty_type_facts("finiteSet");
1597    facts.values = Some(values.into_iter().collect());
1598    Some(facts)
1599}
1600
1601fn prefix_suffix_type_facts(prefix: &str, suffix: &str) -> StringTypeFactsV2 {
1602    let mut facts = constrained_type_facts(
1603        "prefixSuffix",
1604        Some(prefix.to_string()),
1605        Some(suffix.to_string()),
1606        "concatKnownEdges",
1607    );
1608    let value = prefix_suffix_class_value(
1609        prefix,
1610        suffix,
1611        Some(prefix.len().saturating_add(suffix.len())),
1612        None,
1613    );
1614    facts.min_len = external_string_type_facts_from_abstract_class_value(&value).min_len;
1615    facts
1616}
1617
1618fn constrained_type_facts(
1619    constraint_kind: &str,
1620    prefix: Option<String>,
1621    suffix: Option<String>,
1622    provenance: &str,
1623) -> StringTypeFactsV2 {
1624    let mut facts = empty_type_facts("constrained");
1625    facts.constraint_kind = Some(constraint_kind.to_string());
1626    facts.prefix = prefix;
1627    facts.suffix = suffix;
1628    facts.provenance = Some(provenance.to_string());
1629    facts
1630}
1631
1632fn empty_type_facts(kind: &str) -> StringTypeFactsV2 {
1633    StringTypeFactsV2 {
1634        kind: kind.to_string(),
1635        values: None,
1636        constraint_kind: None,
1637        prefix: None,
1638        suffix: None,
1639        min_len: None,
1640        max_len: None,
1641        char_must: None,
1642        char_may: None,
1643        may_include_other_chars: None,
1644        provenance: None,
1645    }
1646}
1647
1648fn finite_values_for_type_facts(facts: &StringTypeFactsV2) -> Option<Vec<String>> {
1649    match facts.kind.as_str() {
1650        "exact" | "finiteSet" => facts.values.clone(),
1651        _ => None,
1652    }
1653}
1654
1655fn single_finite_value(facts: &StringTypeFactsV2) -> Option<String> {
1656    let values = finite_values_for_type_facts(facts)?;
1657    (values.len() == 1).then(|| values[0].clone())
1658}
1659
1660fn longest_common_prefix(values: &[String]) -> Option<String> {
1661    let first = values.first()?;
1662    let mut prefix = first.clone();
1663    for value in values.iter().skip(1) {
1664        while !value.starts_with(prefix.as_str()) {
1665            prefix.pop()?;
1666        }
1667    }
1668    (!prefix.is_empty()).then_some(prefix)
1669}
1670
1671fn longest_common_suffix(values: &[String]) -> Option<String> {
1672    let first = values.first()?;
1673    let mut suffix = first.clone();
1674    for value in values.iter().skip(1) {
1675        while !value.ends_with(suffix.as_str()) {
1676            let mut chars = suffix.chars();
1677            chars.next()?;
1678            suffix = chars.collect();
1679        }
1680    }
1681    (!suffix.is_empty()).then_some(suffix)
1682}
1683
1684fn transfer_kind_for_block_kind(kind: &str) -> &'static str {
1685    match kind {
1686        "entry" => "entry",
1687        "assignment" => "assignFacts",
1688        "branch" | "logicalOperand" => "branch",
1689        "join" | "logicalJoin" => "join",
1690        "loopHeader" | "loopBody" | "loopExit" => "loop",
1691        "break" => "break",
1692        "terminate" => "terminate",
1693        "logicalRhs" => "assignFacts",
1694        "exit" => "exit",
1695        _ => "exit",
1696    }
1697}
1698
1699fn span_contains(span: Span, byte_offset: usize) -> bool {
1700    span_start(span) <= byte_offset && byte_offset < span_end(span)
1701}
1702
1703fn span_start(span: Span) -> usize {
1704    span.start as usize
1705}
1706
1707fn span_end(span: Span) -> usize {
1708    span.end as usize
1709}
1710
1711#[cfg(test)]
1712mod tests {
1713    use super::{
1714        ClassBoundaryEffectV0, boundary_effect_wire_value, prefix_suffix_type_facts,
1715        source_type_fact_control_flow_graph_from_snapshot,
1716        summarize_omena_bridge_source_control_flow_graph_for_source_language,
1717    };
1718
1719    #[test]
1720    fn captures_branchy_css_module_source_cfg_shape() -> Result<(), String> {
1721        let source = [
1722            "export function Card({ enabled }: { enabled: boolean }) {",
1723            "  let size = \"card\";",
1724            "  if (enabled) {",
1725            "    size = \"card--active\";",
1726            "  }",
1727            "  return <div className={size} />;",
1728            "}",
1729            "",
1730        ]
1731        .join("\n");
1732        let Some(reference) = source.rfind("size") else {
1733            return Err("fixture contains size reference".to_string());
1734        };
1735        let Some(graph) = summarize_omena_bridge_source_control_flow_graph_for_source_language(
1736            "/fake/ws/src/Card.tsx",
1737            source.as_str(),
1738            Some("typescriptreact"),
1739            "size",
1740            reference,
1741        ) else {
1742            return Err("fixture should produce CFG".to_string());
1743        };
1744
1745        assert_eq!(graph.product, "omena-bridge.source-control-flow-graph");
1746        assert_eq!(graph.snapshot.entry_block_id, "entry");
1747        assert_eq!(
1748            graph
1749                .snapshot
1750                .blocks
1751                .iter()
1752                .map(|block| block.kind)
1753                .collect::<Vec<_>>(),
1754            vec![
1755                "entry",
1756                "assignment",
1757                "branch",
1758                "join",
1759                "assignment",
1760                "exit"
1761            ]
1762        );
1763        assert!(
1764            graph
1765                .snapshot
1766                .blocks
1767                .iter()
1768                .any(|block| block.variable_name.as_deref() == Some("size"))
1769        );
1770        let symbol_ordinals = graph
1771            .snapshot
1772            .blocks
1773            .iter()
1774            .filter(|block| block.variable_name.as_deref() == Some("size"))
1775            .map(|block| block.symbol_ordinal)
1776            .collect::<Vec<_>>();
1777        assert!(symbol_ordinals.iter().all(Option::is_some));
1778        assert!(symbol_ordinals.contains(&Some(graph.binding.symbol_ordinal)));
1779        let type_fact_graph = source_type_fact_control_flow_graph_from_snapshot(&graph.snapshot);
1780        assert_eq!(
1781            type_fact_graph.entry_block_id,
1782            graph.snapshot.entry_block_id
1783        );
1784        assert_eq!(
1785            type_fact_graph
1786                .blocks
1787                .iter()
1788                .map(|block| block.kind.as_str())
1789                .collect::<Vec<_>>(),
1790            graph
1791                .snapshot
1792                .blocks
1793                .iter()
1794                .map(|block| block.kind)
1795                .collect::<Vec<_>>()
1796        );
1797        assert!(type_fact_graph.blocks.iter().any(|block| {
1798            block.symbol_ordinal == Some(graph.binding.symbol_ordinal)
1799                && block.variable_name.as_deref() == Some("size")
1800        }));
1801        Ok(())
1802    }
1803
1804    #[test]
1805    fn captures_assignment_value_facts_for_concatenated_source_cfg() -> Result<(), String> {
1806        let source = [
1807            "export function Card(variant: string) {",
1808            "  const size = \"btn-\" + variant + \"-chip\";",
1809            "  return cx(size);",
1810            "}",
1811            "",
1812        ]
1813        .join("\n");
1814        let Some(reference) = source.rfind("size") else {
1815            return Err("fixture contains size reference".to_string());
1816        };
1817        let Some(graph) = summarize_omena_bridge_source_control_flow_graph_for_source_language(
1818            "/fake/ws/src/Card.tsx",
1819            source.as_str(),
1820            Some("typescriptreact"),
1821            "size",
1822            reference,
1823        ) else {
1824            return Err("fixture should produce CFG".to_string());
1825        };
1826
1827        let Some(block) = graph
1828            .snapshot
1829            .blocks
1830            .iter()
1831            .find(|block| block.variable_name.as_deref() == Some("size"))
1832        else {
1833            return Err("size assignment block should be present".to_string());
1834        };
1835        let Some(facts) = &block.facts else {
1836            return Err("size assignment should carry value facts".to_string());
1837        };
1838        assert_eq!(facts.kind, "constrained");
1839        assert_eq!(facts.constraint_kind.as_deref(), Some("prefixSuffix"));
1840        assert_eq!(facts.prefix.as_deref(), Some("btn-"));
1841        assert_eq!(facts.suffix.as_deref(), Some("-chip"));
1842        assert_eq!(facts.min_len, Some(9));
1843
1844        let type_fact_graph = source_type_fact_control_flow_graph_from_snapshot(&graph.snapshot);
1845        assert!(type_fact_graph.blocks.iter().any(|block| {
1846            block.variable_name.as_deref() == Some("size")
1847                && block
1848                    .facts
1849                    .as_ref()
1850                    .is_some_and(|facts| facts.constraint_kind.as_deref() == Some("prefixSuffix"))
1851        }));
1852        Ok(())
1853    }
1854
1855    #[test]
1856    fn delimiter_content_drives_boundary_effects_and_ordered_words() -> Result<(), String> {
1857        let source = [
1858            "export function Card(flag: boolean) {",
1859            "  const token = flag ? \"large\" : \"small\";",
1860            "  const binary = \"btn-\" + token;",
1861            "  const template = `btn-${token}`;",
1862            "  const explicitBoundary = \"btn-\" + \" \" + token;",
1863            "  const joinedInside = [\"a\", \"b\"].join(\"\");",
1864            "  const joinedBoundary = [\"a\", \"b\"].join(\" \" );",
1865            "  const listed = clsx(\"a\", \"b\");",
1866            "  const guarded = clsx({ active: flag });",
1867            "  const opaque = left() + right();",
1868            "  return opaque;",
1869            "}",
1870            "",
1871        ]
1872        .join("\n");
1873        let reference = source
1874            .rfind("opaque")
1875            .ok_or_else(|| "fixture contains opaque reference".to_string())?;
1876        let graph = summarize_omena_bridge_source_control_flow_graph_for_source_language(
1877            "/fake/ws/src/Card.tsx",
1878            source.as_str(),
1879            Some("typescriptreact"),
1880            "opaque",
1881            reference,
1882        )
1883        .ok_or_else(|| "fixture should produce CFG".to_string())?;
1884        let block = |name: &str| {
1885            graph
1886                .snapshot
1887                .blocks
1888                .iter()
1889                .find(|block| block.variable_name.as_deref() == Some(name))
1890                .ok_or_else(|| format!("missing {name} assignment"))
1891        };
1892
1893        let binary = block("binary")?;
1894        let template = block("template")?;
1895        assert_eq!(
1896            binary.boundary_effect,
1897            ClassBoundaryEffectV0::ConcatInsideToken
1898        );
1899        assert_eq!(template.boundary_effect, binary.boundary_effect);
1900        assert_eq!(template.facts, binary.facts);
1901        assert_eq!(
1902            binary.facts.as_ref().and_then(|facts| facts.values.clone()),
1903            Some(vec!["btn-large".to_string(), "btn-small".to_string()])
1904        );
1905
1906        assert_eq!(
1907            block("explicitBoundary")?.boundary_effect,
1908            ClassBoundaryEffectV0::ConcatAtTokenBoundary
1909        );
1910        assert_eq!(
1911            block("joinedInside")?.boundary_effect,
1912            ClassBoundaryEffectV0::ConcatInsideToken
1913        );
1914        assert_eq!(
1915            block("joinedBoundary")?.boundary_effect,
1916            ClassBoundaryEffectV0::ConcatAtTokenBoundary
1917        );
1918        assert_eq!(
1919            block("listed")?.boundary_effect,
1920            ClassBoundaryEffectV0::ConcatAtTokenBoundary
1921        );
1922        assert_eq!(
1923            block("listed")?.ordered_word.as_ref().map(|word| word
1924                .tokens()
1925                .iter()
1926                .map(|token| token.as_str())
1927                .collect::<Vec<_>>()),
1928            Some(vec!["a", "b"])
1929        );
1930        assert_eq!(
1931            block("guarded")?.boundary_effect,
1932            ClassBoundaryEffectV0::UnknownBoundary
1933        );
1934        assert!(block("guarded")?.ordered_word.is_none());
1935        assert_eq!(
1936            block("opaque")?.boundary_effect,
1937            ClassBoundaryEffectV0::UnknownBoundary
1938        );
1939        assert!(block("opaque")?.ordered_word.is_none());
1940        Ok(())
1941    }
1942
1943    #[test]
1944    fn class_boundary_transfer_table_names_each_delimiter_authority()
1945    -> Result<(), Box<dyn std::error::Error>> {
1946        let rows: serde_json::Value =
1947            serde_json::from_str(include_str!("../data/class-boundary-transfer-v0.json"))?;
1948        let rows = rows
1949            .as_array()
1950            .ok_or_else(|| std::io::Error::other("boundary transfer table must be an array"))?;
1951        assert_eq!(rows.len(), 6);
1952        assert!(rows.iter().all(|row| row["delimiterFact"].is_string()));
1953        assert_eq!(rows[0]["construct"], "binaryAddition");
1954        assert_eq!(rows[1]["construct"], "templateInterpolation");
1955        assert_eq!(rows[2]["construct"], "clsxOrClassnamesArguments");
1956        assert_eq!(rows[3]["construct"], "arrayJoin");
1957        assert_eq!(rows[4]["construct"], "objectMapEntry");
1958        assert_eq!(rows[5]["construct"], "opaqueOperands");
1959
1960        let mut source = vec![
1961            "declare function clsx(...values: unknown[]): string;".to_string(),
1962            "declare function left(): string;".to_string(),
1963            "declare function right(): string;".to_string(),
1964            "export function Probe(flag: boolean) {".to_string(),
1965        ];
1966        for (index, row) in rows.iter().enumerate() {
1967            let expression = row["probeExpression"]
1968                .as_str()
1969                .ok_or_else(|| std::io::Error::other("transfer row needs probeExpression"))?;
1970            source.push(format!("  const probe_{index} = {expression};"));
1971            if let (Some(control), Some(_)) = (
1972                row["controlExpression"].as_str(),
1973                row["controlBoundaryEffect"].as_str(),
1974            ) {
1975                source.push(format!("  const control_{index} = {control};"));
1976            }
1977        }
1978        source.push("  return probe_5;".to_string());
1979        source.push("}".to_string());
1980        let source = source.join("\n");
1981        let reference = source
1982            .rfind("probe_5")
1983            .ok_or_else(|| std::io::Error::other("transfer probe reference"))?;
1984        let graph = summarize_omena_bridge_source_control_flow_graph_for_source_language(
1985            "/fake/ws/src/BoundaryProbe.ts",
1986            source.as_str(),
1987            Some("typescript"),
1988            "probe_5",
1989            reference,
1990        )
1991        .ok_or_else(|| std::io::Error::other("transfer probes should produce CFG"))?;
1992        for (index, row) in rows.iter().enumerate() {
1993            let expected = row["expectedBoundaryEffect"]
1994                .as_str()
1995                .ok_or_else(|| std::io::Error::other("transfer row needs expected effect"))?;
1996            let probe = graph
1997                .snapshot
1998                .blocks
1999                .iter()
2000                .find(|block| block.variable_name.as_deref() == Some(&format!("probe_{index}")))
2001                .ok_or_else(|| std::io::Error::other("missing transfer probe block"))?;
2002            assert_eq!(boundary_effect_wire_value(probe.boundary_effect), expected);
2003            if let Some(expected_control) = row["controlBoundaryEffect"].as_str() {
2004                let control = graph
2005                    .snapshot
2006                    .blocks
2007                    .iter()
2008                    .find(|block| {
2009                        block.variable_name.as_deref() == Some(&format!("control_{index}"))
2010                    })
2011                    .ok_or_else(|| std::io::Error::other("missing transfer control block"))?;
2012                assert_eq!(
2013                    boundary_effect_wire_value(control.boundary_effect),
2014                    expected_control
2015                );
2016            }
2017        }
2018        Ok(())
2019    }
2020
2021    #[test]
2022    fn source_cfg_prefix_suffix_fact_preserves_explicit_concat_utf16_length() {
2023        let facts = prefix_suffix_type_facts("카드-", "-활성");
2024
2025        assert_eq!(facts.min_len, Some(6));
2026        assert_eq!(facts.prefix.as_deref(), Some("카드-"));
2027        assert_eq!(facts.suffix.as_deref(), Some("-활성"));
2028    }
2029
2030    #[test]
2031    fn captures_same_file_helper_return_facts_for_source_cfg() -> Result<(), String> {
2032        let source = [
2033            "type Status = \"idle\" | \"busy\" | \"error\";",
2034            "function resolveStatusClass(status: Status): string {",
2035            "  switch (status) {",
2036            "    case \"idle\": return \"state-idle\";",
2037            "    case \"busy\": return \"state-busy\";",
2038            "    case \"error\": return \"state-error\";",
2039            "    default: return \"state-idle\";",
2040            "  }",
2041            "}",
2042            "export function Card(status: Status) {",
2043            "  const size = resolveStatusClass(status);",
2044            "  return cx(size);",
2045            "}",
2046            "",
2047        ]
2048        .join("\n");
2049        let Some(reference) = source.rfind("size") else {
2050            return Err("fixture contains size reference".to_string());
2051        };
2052        let Some(graph) = summarize_omena_bridge_source_control_flow_graph_for_source_language(
2053            "/fake/ws/src/Card.tsx",
2054            source.as_str(),
2055            Some("typescriptreact"),
2056            "size",
2057            reference,
2058        ) else {
2059            return Err("fixture should produce CFG".to_string());
2060        };
2061
2062        let values = graph
2063            .snapshot
2064            .blocks
2065            .iter()
2066            .find(|block| block.variable_name.as_deref() == Some("size"))
2067            .and_then(|block| block.facts.as_ref())
2068            .and_then(|facts| facts.values.clone())
2069            .unwrap_or_default();
2070        assert_eq!(values, vec!["state-busy", "state-error", "state-idle"]);
2071        Ok(())
2072    }
2073
2074    #[test]
2075    fn source_cfg_serializes_symbol_ordinals_without_raw_symbol_ids() -> Result<(), String> {
2076        let source = [
2077            "export function Card() {",
2078            "  const size = \"card\";",
2079            "  return cx(size);",
2080            "}",
2081            "",
2082        ]
2083        .join("\n");
2084        let Some(reference) = source.rfind("size") else {
2085            return Err("fixture contains size reference".to_string());
2086        };
2087        let Some(graph) = summarize_omena_bridge_source_control_flow_graph_for_source_language(
2088            "/fake/ws/src/Card.tsx",
2089            source.as_str(),
2090            Some("typescriptreact"),
2091            "size",
2092            reference,
2093        ) else {
2094            return Err("fixture should produce CFG".to_string());
2095        };
2096
2097        let value = serde_json::to_value(&graph).map_err(|error| error.to_string())?;
2098        assert!(value.pointer("/binding/symbolOrdinal").is_some());
2099        assert!(value.pointer("/snapshot/blocks/1/symbolOrdinal").is_some());
2100        let serialized = serde_json::to_string(&value).map_err(|error| error.to_string())?;
2101        assert!(!serialized.contains("SymbolId"));
2102        Ok(())
2103    }
2104}