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};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct SourceControlFlowGraphCaptureV0 {
20    pub schema_version: &'static str,
21    pub product: &'static str,
22    pub binding: SourceFlowBindingRefV0,
23    pub variable_name: String,
24    pub reference_byte_offset: usize,
25    pub snapshot: SourceFlowBlockGraphSnapshotV0,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct SourceFlowBindingRefV0 {
31    pub symbol_ordinal: usize,
32    pub name: String,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
36#[serde(rename_all = "camelCase")]
37pub struct SourceFlowBlockGraphSnapshotV0 {
38    pub entry_block_id: String,
39    pub blocks: Vec<SourceFlowBlockSnapshotV0>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct SourceFlowBlockSnapshotV0 {
45    pub id: String,
46    pub kind: &'static str,
47    pub transfer_kind: &'static str,
48    pub successor_block_ids: Vec<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub binding: Option<SourceFlowBindingRefV0>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub symbol_ordinal: Option<usize>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub variable_name: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub expression_kind: Option<&'static str>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub facts: Option<StringTypeFactsV2>,
59}
60
61enum SourceFlowNode<'a> {
62    Assignment {
63        binding: Option<SourceFlowBindingRefV0>,
64        expression: Option<&'a Expression<'a>>,
65    },
66    Branch {
67        then_nodes: Vec<SourceFlowNode<'a>>,
68        else_nodes: Vec<SourceFlowNode<'a>>,
69    },
70    Loop {
71        body_nodes: Vec<SourceFlowNode<'a>>,
72    },
73    Break,
74    Terminate,
75}
76
77pub fn summarize_omena_bridge_source_control_flow_graph_for_source_language(
78    source_path: &str,
79    source: &str,
80    source_language: Option<&str>,
81    variable_name: &str,
82    reference_byte_offset: usize,
83) -> Option<SourceControlFlowGraphCaptureV0> {
84    crate::source_syntax::summarize_source_control_flow_graph_with_semantic(
85        source_path,
86        source,
87        source_language,
88        variable_name,
89        reference_byte_offset,
90    )
91}
92
93pub(crate) fn summarize_source_control_flow_graph_from_program(
94    program: &Program<'_>,
95    scoping: &Scoping,
96    variable_name: &str,
97    reference_byte_offset: usize,
98) -> Option<SourceControlFlowGraphCaptureV0> {
99    if variable_name.contains('.') {
100        return None;
101    }
102
103    let reference_binding =
104        reference_binding_for_offset(program, scoping, variable_name, reference_byte_offset)?;
105
106    let container = statement_container_for_reference(&program.body, reference_byte_offset);
107    let nodes = build_flow_nodes(container, scoping, reference_byte_offset);
108    Some(SourceControlFlowGraphCaptureV0 {
109        schema_version: "0",
110        product: "omena-bridge.source-control-flow-graph",
111        variable_name: reference_binding.name.clone(),
112        binding: reference_binding,
113        reference_byte_offset,
114        snapshot: SourceFlowBlockGraphSnapshotBuilder::new(&program.body).build(nodes.as_slice()),
115    })
116}
117
118pub fn summarize_omena_bridge_source_type_fact_control_flow_graph_for_source_language(
119    source_path: &str,
120    source: &str,
121    source_language: Option<&str>,
122    variable_name: &str,
123    reference_byte_offset: usize,
124) -> Option<TypeFactControlFlowGraphV2> {
125    summarize_omena_bridge_source_control_flow_graph_for_source_language(
126        source_path,
127        source,
128        source_language,
129        variable_name,
130        reference_byte_offset,
131    )
132    .map(|capture| source_type_fact_control_flow_graph_from_snapshot(&capture.snapshot))
133}
134
135pub fn source_type_fact_control_flow_graph_from_snapshot(
136    snapshot: &SourceFlowBlockGraphSnapshotV0,
137) -> TypeFactControlFlowGraphV2 {
138    TypeFactControlFlowGraphV2 {
139        entry_block_id: snapshot.entry_block_id.clone(),
140        blocks: snapshot
141            .blocks
142            .iter()
143            .map(source_type_fact_control_flow_block_from_snapshot)
144            .collect(),
145    }
146}
147
148fn source_type_fact_control_flow_block_from_snapshot(
149    block: &SourceFlowBlockSnapshotV0,
150) -> TypeFactControlFlowBlockV2 {
151    TypeFactControlFlowBlockV2 {
152        id: block.id.clone(),
153        kind: block.kind.to_string(),
154        transfer_kind: block.transfer_kind.to_string(),
155        successor_block_ids: block.successor_block_ids.clone(),
156        symbol_ordinal: block.symbol_ordinal,
157        variable_name: block.variable_name.clone(),
158        expression_kind: block.expression_kind.map(str::to_string),
159        facts: block.facts.clone(),
160    }
161}
162
163fn statement_container_for_reference<'a>(
164    statements: &'a oxc_allocator::Vec<'a, Statement<'a>>,
165    reference_byte_offset: usize,
166) -> &'a oxc_allocator::Vec<'a, Statement<'a>> {
167    find_function_body_statements_containing_reference(statements, reference_byte_offset)
168        .unwrap_or(statements)
169}
170
171fn find_function_body_statements_containing_reference<'a>(
172    statements: &'a oxc_allocator::Vec<'a, Statement<'a>>,
173    reference_byte_offset: usize,
174) -> Option<&'a oxc_allocator::Vec<'a, Statement<'a>>> {
175    for statement in statements {
176        if let Some(body) = function_body_for_statement(statement)
177            && span_contains(body.span, reference_byte_offset)
178        {
179            return find_function_body_statements_containing_reference(
180                &body.statements,
181                reference_byte_offset,
182            )
183            .or(Some(&body.statements));
184        }
185    }
186    None
187}
188
189fn function_body_for_statement<'a>(statement: &'a Statement<'a>) -> Option<&'a FunctionBody<'a>> {
190    match statement {
191        Statement::FunctionDeclaration(function) => function.body.as_deref(),
192        Statement::ExportNamedDeclaration(export) => {
193            if let Some(Declaration::FunctionDeclaration(function)) = &export.declaration {
194                function.body.as_deref()
195            } else {
196                None
197            }
198        }
199        Statement::ExportDefaultDeclaration(export) => {
200            if let ExportDefaultDeclarationKind::FunctionDeclaration(function) = &export.declaration
201            {
202                function.body.as_deref()
203            } else {
204                None
205            }
206        }
207        _ => None,
208    }
209}
210
211fn build_flow_nodes<'a>(
212    statements: &'a oxc_allocator::Vec<'a, Statement<'a>>,
213    scoping: &Scoping,
214    reference_byte_offset: usize,
215) -> Vec<SourceFlowNode<'a>> {
216    let mut nodes = Vec::new();
217
218    for statement in statements {
219        if span_start(statement.span()) >= reference_byte_offset {
220            break;
221        }
222        if matches!(statement, Statement::FunctionDeclaration(_)) {
223            continue;
224        }
225
226        match statement {
227            Statement::IfStatement(if_statement) => {
228                let reference_location =
229                    locate_reference_in_if(if_statement, reference_byte_offset);
230                nodes.push(SourceFlowNode::Branch {
231                    then_nodes: build_flow_nodes_for_statement(
232                        &if_statement.consequent,
233                        scoping,
234                        branch_reference_offset(reference_location, "then", reference_byte_offset),
235                    ),
236                    else_nodes: if_statement
237                        .alternate
238                        .as_ref()
239                        .map(|alternate| {
240                            build_flow_nodes_for_statement(
241                                alternate,
242                                scoping,
243                                branch_reference_offset(
244                                    reference_location,
245                                    "else",
246                                    reference_byte_offset,
247                                ),
248                            )
249                        })
250                        .unwrap_or_default(),
251                });
252                if reference_location != "after" {
253                    break;
254                }
255            }
256            Statement::WhileStatement(while_statement) => {
257                nodes.push(SourceFlowNode::Loop {
258                    body_nodes: build_loop_body_nodes(
259                        &while_statement.body,
260                        scoping,
261                        reference_byte_offset,
262                    ),
263                });
264                if span_contains(while_statement.body.span(), reference_byte_offset) {
265                    break;
266                }
267            }
268            Statement::ForStatement(for_statement) => {
269                nodes.push(SourceFlowNode::Loop {
270                    body_nodes: build_loop_body_nodes(
271                        &for_statement.body,
272                        scoping,
273                        reference_byte_offset,
274                    ),
275                });
276                if span_contains(for_statement.body.span(), reference_byte_offset) {
277                    break;
278                }
279            }
280            Statement::DoWhileStatement(do_statement) => {
281                nodes.push(SourceFlowNode::Loop {
282                    body_nodes: build_loop_body_nodes(
283                        &do_statement.body,
284                        scoping,
285                        reference_byte_offset,
286                    ),
287                });
288                if span_contains(do_statement.body.span(), reference_byte_offset) {
289                    break;
290                }
291            }
292            Statement::LabeledStatement(labeled) => {
293                nodes.extend(build_flow_nodes_for_labeled(
294                    labeled,
295                    scoping,
296                    reference_byte_offset,
297                ));
298                if span_contains(labeled.body.span(), reference_byte_offset) {
299                    break;
300                }
301            }
302            _ if span_contains(statement.span(), reference_byte_offset) => break,
303            Statement::BreakStatement(_) => {
304                nodes.push(SourceFlowNode::Break);
305                break;
306            }
307            Statement::ReturnStatement(_) | Statement::ThrowStatement(_) => {
308                nodes.push(SourceFlowNode::Terminate);
309                break;
310            }
311            _ => nodes.extend(assignment_nodes_for_statement(statement, scoping)),
312        }
313    }
314
315    nodes
316}
317
318fn build_flow_nodes_for_statement<'a>(
319    statement: &'a Statement<'a>,
320    scoping: &Scoping,
321    reference_byte_offset: usize,
322) -> Vec<SourceFlowNode<'a>> {
323    match statement {
324        Statement::BlockStatement(block) => {
325            build_flow_nodes(&block.body, scoping, reference_byte_offset)
326        }
327        _ => build_flow_nodes_from_slice(
328            std::slice::from_ref(statement),
329            scoping,
330            reference_byte_offset,
331        ),
332    }
333}
334
335fn build_flow_nodes_from_slice<'a>(
336    statements: &'a [Statement<'a>],
337    scoping: &Scoping,
338    reference_byte_offset: usize,
339) -> Vec<SourceFlowNode<'a>> {
340    let mut nodes = Vec::new();
341    for statement in statements {
342        if span_start(statement.span()) >= reference_byte_offset {
343            break;
344        }
345        if span_contains(statement.span(), reference_byte_offset) {
346            break;
347        }
348        nodes.extend(assignment_nodes_for_statement(statement, scoping));
349    }
350    nodes
351}
352
353fn build_loop_body_nodes<'a>(
354    body: &'a Statement<'a>,
355    scoping: &Scoping,
356    reference_byte_offset: usize,
357) -> Vec<SourceFlowNode<'a>> {
358    if span_contains(body.span(), reference_byte_offset) {
359        build_flow_nodes_for_statement(body, scoping, reference_byte_offset)
360    } else {
361        build_flow_nodes_for_statement(body, scoping, usize::MAX)
362    }
363}
364
365fn build_flow_nodes_for_labeled<'a>(
366    labeled: &'a LabeledStatement<'a>,
367    scoping: &Scoping,
368    reference_byte_offset: usize,
369) -> Vec<SourceFlowNode<'a>> {
370    build_flow_nodes_for_statement(&labeled.body, scoping, reference_byte_offset)
371}
372
373fn locate_reference_in_if(
374    statement: &IfStatement<'_>,
375    reference_byte_offset: usize,
376) -> &'static str {
377    if span_contains(statement.consequent.span(), reference_byte_offset) {
378        return "then";
379    }
380    if statement
381        .alternate
382        .as_ref()
383        .is_some_and(|alternate| span_contains(alternate.span(), reference_byte_offset))
384    {
385        return "else";
386    }
387    "after"
388}
389
390fn branch_reference_offset(
391    reference_location: &'static str,
392    branch: &'static str,
393    reference_byte_offset: usize,
394) -> usize {
395    if reference_location == branch {
396        reference_byte_offset
397    } else {
398        usize::MAX
399    }
400}
401
402fn assignment_nodes_for_statement<'a>(
403    statement: &'a Statement<'a>,
404    scoping: &Scoping,
405) -> Vec<SourceFlowNode<'a>> {
406    match statement {
407        Statement::VariableDeclaration(declaration) => {
408            assignment_nodes_for_variable_declaration(declaration)
409        }
410        Statement::ExpressionStatement(statement) => {
411            if let Expression::AssignmentExpression(assignment) = &statement.expression
412                && assignment.operator == AssignmentOperator::Assign
413                && let AssignmentTarget::AssignmentTargetIdentifier(identifier) = &assignment.left
414            {
415                return vec![SourceFlowNode::Assignment {
416                    binding: binding_ref_from_reference(scoping, identifier),
417                    expression: Some(&assignment.right),
418                }];
419            }
420            Vec::new()
421        }
422        Statement::BlockStatement(block) => build_flow_nodes(&block.body, scoping, usize::MAX)
423            .into_iter()
424            .filter(|node| matches!(node, SourceFlowNode::Assignment { .. }))
425            .collect(),
426        _ => Vec::new(),
427    }
428}
429
430fn assignment_nodes_for_variable_declaration<'a>(
431    declaration: &'a VariableDeclaration<'a>,
432) -> Vec<SourceFlowNode<'a>> {
433    declaration
434        .declarations
435        .iter()
436        .filter_map(|declarator| {
437            binding_pattern_identifier(&declarator.id).map(|identifier| {
438                SourceFlowNode::Assignment {
439                    binding: binding_ref_from_binding_identifier(identifier),
440                    expression: declarator.init.as_ref(),
441                }
442            })
443        })
444        .collect()
445}
446
447fn reference_binding_for_offset(
448    program: &Program<'_>,
449    scoping: &Scoping,
450    variable_name: &str,
451    reference_byte_offset: usize,
452) -> Option<SourceFlowBindingRefV0> {
453    let mut visitor = SourceReferenceBindingFinder {
454        scoping,
455        variable_name,
456        reference_byte_offset,
457        binding: None,
458    };
459    visitor.visit_program(program);
460    visitor.binding
461}
462
463struct SourceReferenceBindingFinder<'a> {
464    scoping: &'a Scoping,
465    variable_name: &'a str,
466    reference_byte_offset: usize,
467    binding: Option<SourceFlowBindingRefV0>,
468}
469
470impl<'a, 'ast> Visit<'ast> for SourceReferenceBindingFinder<'a> {
471    fn visit_identifier_reference(&mut self, identifier: &IdentifierReference<'ast>) {
472        if self.binding.is_none()
473            && identifier.name.as_str() == self.variable_name
474            && span_contains(identifier.span, self.reference_byte_offset)
475        {
476            self.binding = binding_ref_from_reference(self.scoping, identifier);
477        }
478    }
479}
480
481fn binding_ref_from_reference(
482    scoping: &Scoping,
483    identifier: &IdentifierReference<'_>,
484) -> Option<SourceFlowBindingRefV0> {
485    identifier
486        .reference_id
487        .get()
488        .and_then(|reference_id| scoping.get_reference(reference_id).symbol_id())
489        .map(|symbol_id| binding_ref_from_symbol(symbol_id, identifier.name.as_str()))
490}
491
492fn binding_ref_from_binding_identifier(
493    identifier: &BindingIdentifier<'_>,
494) -> Option<SourceFlowBindingRefV0> {
495    binding_identifier_symbol_id(identifier)
496        .map(|symbol_id| binding_ref_from_symbol(symbol_id, identifier.name.as_str()))
497}
498
499fn binding_identifier_symbol_id(identifier: &BindingIdentifier<'_>) -> Option<SymbolId> {
500    identifier.symbol_id.get()
501}
502
503fn binding_ref_from_symbol(symbol_id: SymbolId, name: &str) -> SourceFlowBindingRefV0 {
504    SourceFlowBindingRefV0 {
505        symbol_ordinal: symbol_id.index(),
506        name: name.to_string(),
507    }
508}
509
510fn binding_pattern_identifier<'a>(
511    pattern: &'a BindingPattern<'a>,
512) -> Option<&'a BindingIdentifier<'a>> {
513    match pattern {
514        BindingPattern::BindingIdentifier(identifier) => Some(identifier),
515        _ => None,
516    }
517}
518
519struct SourceFlowBlockGraphSnapshotBuilder<'a> {
520    blocks: Vec<SourceFlowBlockSnapshotV0>,
521    counters: BTreeMap<&'static str, usize>,
522    root_statements: &'a oxc_allocator::Vec<'a, Statement<'a>>,
523}
524
525impl<'a> SourceFlowBlockGraphSnapshotBuilder<'a> {
526    fn new(root_statements: &'a oxc_allocator::Vec<'a, Statement<'a>>) -> Self {
527        Self {
528            blocks: Vec::new(),
529            counters: BTreeMap::new(),
530            root_statements,
531        }
532    }
533
534    fn build(mut self, nodes: &[SourceFlowNode<'_>]) -> SourceFlowBlockGraphSnapshotV0 {
535        let entry_block_id = self.add_block("entry", Some("entry"), None, None);
536        let tails = self.append_nodes(nodes, vec![entry_block_id], None);
537        let exit_block_id = self.add_block("exit", Some("exit"), None, None);
538        self.connect(tails.as_slice(), exit_block_id.as_str());
539        SourceFlowBlockGraphSnapshotV0 {
540            entry_block_id: "entry".to_string(),
541            blocks: self.blocks,
542        }
543    }
544
545    fn append_nodes(
546        &mut self,
547        nodes: &[SourceFlowNode<'_>],
548        incoming_block_ids: Vec<String>,
549        break_target_block_id: Option<&str>,
550    ) -> Vec<String> {
551        let mut tails = incoming_block_ids;
552        for node in nodes {
553            if tails.is_empty() {
554                return Vec::new();
555            }
556            tails = self.append_node(node, tails, break_target_block_id);
557        }
558        tails
559    }
560
561    fn append_node(
562        &mut self,
563        node: &SourceFlowNode<'_>,
564        incoming_block_ids: Vec<String>,
565        break_target_block_id: Option<&str>,
566    ) -> Vec<String> {
567        match node {
568            SourceFlowNode::Assignment {
569                binding,
570                expression,
571            } => self.append_assignment(binding.as_ref(), *expression, incoming_block_ids),
572            SourceFlowNode::Branch {
573                then_nodes,
574                else_nodes,
575            } => self.append_branch(
576                then_nodes,
577                else_nodes,
578                incoming_block_ids,
579                break_target_block_id,
580            ),
581            SourceFlowNode::Loop { body_nodes } => self.append_loop(body_nodes, incoming_block_ids),
582            SourceFlowNode::Break => {
583                let break_block_id = self.add_block("break", None, None, None);
584                self.connect(incoming_block_ids.as_slice(), break_block_id.as_str());
585                if let Some(target) = break_target_block_id {
586                    self.connect(std::slice::from_ref(&break_block_id), target);
587                }
588                Vec::new()
589            }
590            SourceFlowNode::Terminate => {
591                let terminate_block_id = self.add_block("terminate", None, None, None);
592                self.connect(incoming_block_ids.as_slice(), terminate_block_id.as_str());
593                Vec::new()
594            }
595        }
596    }
597
598    fn append_assignment(
599        &mut self,
600        binding: Option<&SourceFlowBindingRefV0>,
601        expression: Option<&Expression<'_>>,
602        incoming_block_ids: Vec<String>,
603    ) -> Vec<String> {
604        let transfer_kind = if expression.is_some_and(is_concat_expression) {
605            "concatFacts"
606        } else {
607            "assignFacts"
608        };
609        let assignment_block_id = self.add_block(
610            "assignment",
611            None,
612            Some(transfer_kind),
613            Some((
614                binding.cloned(),
615                None,
616                expression
617                    .and_then(|expression| expression_type_facts(expression, self.root_statements)),
618            )),
619        );
620        self.connect(incoming_block_ids.as_slice(), assignment_block_id.as_str());
621
622        if let Some(Expression::LogicalExpression(expression)) = expression {
623            return self.append_short_circuit_expression(expression, vec![assignment_block_id]);
624        }
625
626        vec![assignment_block_id]
627    }
628
629    fn append_short_circuit_expression(
630        &mut self,
631        expression: &LogicalExpression<'_>,
632        incoming_block_ids: Vec<String>,
633    ) -> Vec<String> {
634        let expression_kind = logical_expression_kind(expression);
635        let operand_block_id = self.add_block(
636            "logicalOperand",
637            None,
638            None,
639            Some((None, expression_kind, None)),
640        );
641        let rhs_block_id = self.add_block(
642            "logicalRhs",
643            None,
644            None,
645            Some((None, expression_kind, None)),
646        );
647        let join_block_id = self.add_block(
648            "logicalJoin",
649            None,
650            None,
651            Some((None, expression_kind, None)),
652        );
653        self.connect(incoming_block_ids.as_slice(), operand_block_id.as_str());
654        self.connect(
655            std::slice::from_ref(&operand_block_id),
656            join_block_id.as_str(),
657        );
658        self.connect(
659            std::slice::from_ref(&operand_block_id),
660            rhs_block_id.as_str(),
661        );
662        self.connect(std::slice::from_ref(&rhs_block_id), join_block_id.as_str());
663        vec![join_block_id]
664    }
665
666    fn append_branch(
667        &mut self,
668        then_nodes: &[SourceFlowNode<'_>],
669        else_nodes: &[SourceFlowNode<'_>],
670        incoming_block_ids: Vec<String>,
671        break_target_block_id: Option<&str>,
672    ) -> Vec<String> {
673        let branch_block_id = self.add_block("branch", None, None, None);
674        let join_block_id = self.add_block("join", None, None, None);
675        self.connect(incoming_block_ids.as_slice(), branch_block_id.as_str());
676        let then_tails = self.append_nodes(
677            then_nodes,
678            vec![branch_block_id.clone()],
679            break_target_block_id,
680        );
681        let else_tails = if else_nodes.is_empty() {
682            vec![branch_block_id]
683        } else {
684            self.append_nodes(else_nodes, vec![branch_block_id], break_target_block_id)
685        };
686        self.connect(then_tails.as_slice(), join_block_id.as_str());
687        self.connect(else_tails.as_slice(), join_block_id.as_str());
688        vec![join_block_id]
689    }
690
691    fn append_loop(
692        &mut self,
693        body_nodes: &[SourceFlowNode<'_>],
694        incoming_block_ids: Vec<String>,
695    ) -> Vec<String> {
696        let loop_index = self.next_index("loop");
697        let header_block_id = format!("loop:{loop_index}:header");
698        let body_block_id = format!("loop:{loop_index}:body");
699        let exit_block_id = format!("loop:{loop_index}:exit");
700        self.add_block("loopHeader", Some(header_block_id.as_str()), None, None);
701        self.add_block("loopBody", Some(body_block_id.as_str()), None, None);
702        self.add_block("loopExit", Some(exit_block_id.as_str()), None, None);
703        self.connect(incoming_block_ids.as_slice(), header_block_id.as_str());
704        self.connect(
705            std::slice::from_ref(&header_block_id),
706            body_block_id.as_str(),
707        );
708        self.connect(
709            std::slice::from_ref(&header_block_id),
710            exit_block_id.as_str(),
711        );
712        let body_tails = self.append_nodes(
713            body_nodes,
714            vec![body_block_id],
715            Some(exit_block_id.as_str()),
716        );
717        self.connect(body_tails.as_slice(), header_block_id.as_str());
718        vec![exit_block_id]
719    }
720
721    fn add_block(
722        &mut self,
723        kind: &'static str,
724        explicit_id: Option<&str>,
725        transfer_kind: Option<&'static str>,
726        metadata: Option<(
727            Option<SourceFlowBindingRefV0>,
728            Option<&'static str>,
729            Option<StringTypeFactsV2>,
730        )>,
731    ) -> String {
732        let id = explicit_id
733            .map(str::to_string)
734            .unwrap_or_else(|| format!("{kind}:{}", self.next_index(kind)));
735        let (binding, expression_kind, facts) = metadata.unwrap_or_default();
736        self.blocks.push(SourceFlowBlockSnapshotV0 {
737            id: id.clone(),
738            kind,
739            transfer_kind: transfer_kind.unwrap_or_else(|| transfer_kind_for_block_kind(kind)),
740            successor_block_ids: Vec::new(),
741            symbol_ordinal: binding.as_ref().map(|binding| binding.symbol_ordinal),
742            variable_name: binding.as_ref().map(|binding| binding.name.clone()),
743            binding,
744            expression_kind,
745            facts,
746        });
747        id
748    }
749
750    fn connect(&mut self, from_block_ids: &[String], to_block_id: &str) {
751        for from_block_id in from_block_ids {
752            if let Some(block) = self
753                .blocks
754                .iter_mut()
755                .find(|candidate| candidate.id == *from_block_id)
756                && !block
757                    .successor_block_ids
758                    .iter()
759                    .any(|candidate| candidate == to_block_id)
760            {
761                block.successor_block_ids.push(to_block_id.to_string());
762            }
763        }
764    }
765
766    fn next_index(&mut self, kind: &'static str) -> usize {
767        let next = self.counters.get(kind).copied().unwrap_or_default();
768        self.counters.insert(kind, next + 1);
769        next
770    }
771}
772
773fn is_concat_expression(expression: &Expression<'_>) -> bool {
774    matches!(
775        expression,
776        Expression::BinaryExpression(expression) if expression.operator == BinaryOperator::Addition
777    )
778}
779
780fn logical_expression_kind(expression: &LogicalExpression<'_>) -> Option<&'static str> {
781    if expression.operator.is_and() {
782        Some("logicalAnd")
783    } else if expression.operator.is_or() {
784        Some("logicalOr")
785    } else if expression.operator.is_coalesce() {
786        Some("nullishCoalesce")
787    } else {
788        None
789    }
790}
791
792fn expression_type_facts(
793    expression: &Expression<'_>,
794    root_statements: &oxc_allocator::Vec<'_, Statement<'_>>,
795) -> Option<StringTypeFactsV2> {
796    expression_type_facts_inner(expression, root_statements, &mut BTreeSet::new())
797}
798
799fn expression_type_facts_inner(
800    expression: &Expression<'_>,
801    root_statements: &oxc_allocator::Vec<'_, Statement<'_>>,
802    seen_functions: &mut BTreeSet<String>,
803) -> Option<StringTypeFactsV2> {
804    match expression {
805        Expression::StringLiteral(literal) => Some(exact_type_facts(literal.value.as_str())),
806        Expression::TemplateLiteral(template) if template.expressions.is_empty() => {
807            let value = template.quasis.first()?.value.cooked.as_ref()?.as_str();
808            Some(exact_type_facts(value))
809        }
810        Expression::ParenthesizedExpression(expression) => {
811            expression_type_facts_inner(&expression.expression, root_statements, seen_functions)
812        }
813        Expression::TSAsExpression(expression) => {
814            expression_type_facts_inner(&expression.expression, root_statements, seen_functions)
815        }
816        Expression::TSSatisfiesExpression(expression) => {
817            expression_type_facts_inner(&expression.expression, root_statements, seen_functions)
818        }
819        Expression::TSTypeAssertion(expression) => {
820            expression_type_facts_inner(&expression.expression, root_statements, seen_functions)
821        }
822        Expression::TSNonNullExpression(expression) => {
823            expression_type_facts_inner(&expression.expression, root_statements, seen_functions)
824        }
825        Expression::TSInstantiationExpression(expression) => {
826            expression_type_facts_inner(&expression.expression, root_statements, seen_functions)
827        }
828        Expression::ConditionalExpression(expression) => merge_type_facts([
829            expression_type_facts_inner(&expression.consequent, root_statements, seen_functions),
830            expression_type_facts_inner(&expression.alternate, root_statements, seen_functions),
831        ]),
832        Expression::LogicalExpression(expression) => {
833            if expression.operator.is_and() {
834                return expression_type_facts_inner(
835                    &expression.right,
836                    root_statements,
837                    seen_functions,
838                );
839            }
840            merge_type_facts([
841                expression_type_facts_inner(&expression.left, root_statements, seen_functions),
842                expression_type_facts_inner(&expression.right, root_statements, seen_functions),
843            ])
844        }
845        Expression::BinaryExpression(expression)
846            if expression.operator == BinaryOperator::Addition =>
847        {
848            concatenate_type_facts(
849                expression_type_facts_inner(&expression.left, root_statements, seen_functions),
850                expression_type_facts_inner(&expression.right, root_statements, seen_functions),
851            )
852        }
853        Expression::CallExpression(call) => {
854            let Expression::Identifier(callee) = &call.callee else {
855                return None;
856            };
857            function_return_type_facts(callee.name.as_str(), root_statements, seen_functions)
858        }
859        _ => None,
860    }
861}
862
863fn function_return_type_facts(
864    function_name: &str,
865    root_statements: &oxc_allocator::Vec<'_, Statement<'_>>,
866    seen_functions: &mut BTreeSet<String>,
867) -> Option<StringTypeFactsV2> {
868    if !seen_functions.insert(function_name.to_string()) {
869        return None;
870    }
871    let body = root_statements
872        .iter()
873        .find_map(|statement| function_body_for_named_statement(statement, function_name))?;
874    let facts = merge_type_facts(
875        body.statements
876            .iter()
877            .flat_map(|statement| {
878                return_type_facts_for_statement(statement, root_statements, seen_functions)
879            })
880            .map(Some),
881    );
882    seen_functions.remove(function_name);
883    facts
884}
885
886fn function_body_for_named_statement<'a>(
887    statement: &'a Statement<'a>,
888    function_name: &str,
889) -> Option<&'a FunctionBody<'a>> {
890    match statement {
891        Statement::FunctionDeclaration(function)
892            if function
893                .id
894                .as_ref()
895                .is_some_and(|id| id.name.as_str() == function_name) =>
896        {
897            function.body.as_deref()
898        }
899        Statement::ExportNamedDeclaration(export) => {
900            if let Some(Declaration::FunctionDeclaration(function)) = &export.declaration
901                && function
902                    .id
903                    .as_ref()
904                    .is_some_and(|id| id.name.as_str() == function_name)
905            {
906                return function.body.as_deref();
907            }
908            None
909        }
910        _ => None,
911    }
912}
913
914fn return_type_facts_for_statement(
915    statement: &Statement<'_>,
916    root_statements: &oxc_allocator::Vec<'_, Statement<'_>>,
917    seen_functions: &mut BTreeSet<String>,
918) -> Vec<StringTypeFactsV2> {
919    match statement {
920        Statement::ReturnStatement(statement) => statement
921            .argument
922            .as_ref()
923            .and_then(|expression| {
924                expression_type_facts_inner(expression, root_statements, seen_functions)
925            })
926            .into_iter()
927            .collect(),
928        Statement::BlockStatement(block) => block
929            .body
930            .iter()
931            .flat_map(|statement| {
932                return_type_facts_for_statement(statement, root_statements, seen_functions)
933            })
934            .collect(),
935        Statement::IfStatement(statement) => {
936            let mut facts = return_type_facts_for_statement(
937                &statement.consequent,
938                root_statements,
939                seen_functions,
940            );
941            if let Some(alternate) = &statement.alternate {
942                facts.extend(return_type_facts_for_statement(
943                    alternate,
944                    root_statements,
945                    seen_functions,
946                ));
947            }
948            facts
949        }
950        Statement::SwitchStatement(statement) => statement
951            .cases
952            .iter()
953            .flat_map(|case| {
954                return_type_facts_for_switch_case(case, root_statements, seen_functions)
955            })
956            .collect(),
957        _ => Vec::new(),
958    }
959}
960
961fn return_type_facts_for_switch_case(
962    case: &SwitchCase<'_>,
963    root_statements: &oxc_allocator::Vec<'_, Statement<'_>>,
964    seen_functions: &mut BTreeSet<String>,
965) -> Vec<StringTypeFactsV2> {
966    case.consequent
967        .iter()
968        .flat_map(|statement| {
969            return_type_facts_for_statement(statement, root_statements, seen_functions)
970        })
971        .collect()
972}
973
974fn merge_type_facts(
975    facts: impl IntoIterator<Item = Option<StringTypeFactsV2>>,
976) -> Option<StringTypeFactsV2> {
977    let mut values = BTreeSet::new();
978    for fact in facts {
979        let fact = fact?;
980        for value in finite_values_for_type_facts(&fact)? {
981            values.insert(value);
982        }
983    }
984    finite_type_facts(values)
985}
986
987fn concatenate_type_facts(
988    left: Option<StringTypeFactsV2>,
989    right: Option<StringTypeFactsV2>,
990) -> Option<StringTypeFactsV2> {
991    match (left, right) {
992        (Some(left), Some(right)) => {
993            if let (Some(left_values), Some(right_values)) = (
994                finite_values_for_type_facts(&left),
995                finite_values_for_type_facts(&right),
996            ) {
997                return finite_type_facts(left_values.iter().flat_map(|left| {
998                    right_values
999                        .iter()
1000                        .map(move |right| format!("{left}{right}"))
1001                }));
1002            }
1003            if left.constraint_kind.as_deref() == Some("prefix")
1004                && let Some(suffix) = single_finite_value(&right)
1005            {
1006                return Some(prefix_suffix_type_facts(
1007                    left.prefix.as_deref().unwrap_or_default(),
1008                    suffix.as_str(),
1009                ));
1010            }
1011            if right.constraint_kind.as_deref() == Some("suffix")
1012                && let Some(prefix) = single_finite_value(&left)
1013            {
1014                return Some(prefix_suffix_type_facts(
1015                    prefix.as_str(),
1016                    right.suffix.as_deref().unwrap_or_default(),
1017                ));
1018            }
1019            None
1020        }
1021        (Some(left), None) => finite_values_for_type_facts(&left)
1022            .and_then(|values| longest_common_prefix(values.as_slice()))
1023            .map(|prefix| {
1024                constrained_type_facts("prefix", Some(prefix), None, "concatUnknownRight")
1025            }),
1026        (None, Some(right)) => finite_values_for_type_facts(&right)
1027            .and_then(|values| longest_common_suffix(values.as_slice()))
1028            .map(|suffix| {
1029                constrained_type_facts("suffix", None, Some(suffix), "concatUnknownLeft")
1030            }),
1031        (None, None) => None,
1032    }
1033}
1034
1035fn exact_type_facts(value: &str) -> StringTypeFactsV2 {
1036    let mut facts = empty_type_facts("exact");
1037    facts.values = Some(vec![value.to_string()]);
1038    facts
1039}
1040
1041fn finite_type_facts(values: impl IntoIterator<Item = String>) -> Option<StringTypeFactsV2> {
1042    let values = values.into_iter().collect::<BTreeSet<_>>();
1043    if values.is_empty() {
1044        return None;
1045    }
1046    if values.len() == 1 {
1047        return values.iter().next().map(|value| exact_type_facts(value));
1048    }
1049    let mut facts = empty_type_facts("finiteSet");
1050    facts.values = Some(values.into_iter().collect());
1051    Some(facts)
1052}
1053
1054fn prefix_suffix_type_facts(prefix: &str, suffix: &str) -> StringTypeFactsV2 {
1055    let mut facts = constrained_type_facts(
1056        "prefixSuffix",
1057        Some(prefix.to_string()),
1058        Some(suffix.to_string()),
1059        "concatKnownEdges",
1060    );
1061    facts.min_len = Some(prefix.len() + suffix.len());
1062    facts
1063}
1064
1065fn constrained_type_facts(
1066    constraint_kind: &str,
1067    prefix: Option<String>,
1068    suffix: Option<String>,
1069    provenance: &str,
1070) -> StringTypeFactsV2 {
1071    let mut facts = empty_type_facts("constrained");
1072    facts.constraint_kind = Some(constraint_kind.to_string());
1073    facts.prefix = prefix;
1074    facts.suffix = suffix;
1075    facts.provenance = Some(provenance.to_string());
1076    facts
1077}
1078
1079fn empty_type_facts(kind: &str) -> StringTypeFactsV2 {
1080    StringTypeFactsV2 {
1081        kind: kind.to_string(),
1082        values: None,
1083        constraint_kind: None,
1084        prefix: None,
1085        suffix: None,
1086        min_len: None,
1087        max_len: None,
1088        char_must: None,
1089        char_may: None,
1090        may_include_other_chars: None,
1091        provenance: None,
1092    }
1093}
1094
1095fn finite_values_for_type_facts(facts: &StringTypeFactsV2) -> Option<Vec<String>> {
1096    match facts.kind.as_str() {
1097        "exact" | "finiteSet" => facts.values.clone(),
1098        _ => None,
1099    }
1100}
1101
1102fn single_finite_value(facts: &StringTypeFactsV2) -> Option<String> {
1103    let values = finite_values_for_type_facts(facts)?;
1104    (values.len() == 1).then(|| values[0].clone())
1105}
1106
1107fn longest_common_prefix(values: &[String]) -> Option<String> {
1108    let first = values.first()?;
1109    let mut prefix = first.clone();
1110    for value in values.iter().skip(1) {
1111        while !value.starts_with(prefix.as_str()) {
1112            prefix.pop()?;
1113        }
1114    }
1115    (!prefix.is_empty()).then_some(prefix)
1116}
1117
1118fn longest_common_suffix(values: &[String]) -> Option<String> {
1119    let first = values.first()?;
1120    let mut suffix = first.clone();
1121    for value in values.iter().skip(1) {
1122        while !value.ends_with(suffix.as_str()) {
1123            let mut chars = suffix.chars();
1124            chars.next()?;
1125            suffix = chars.collect();
1126        }
1127    }
1128    (!suffix.is_empty()).then_some(suffix)
1129}
1130
1131fn transfer_kind_for_block_kind(kind: &str) -> &'static str {
1132    match kind {
1133        "entry" => "entry",
1134        "assignment" => "assignFacts",
1135        "branch" | "logicalOperand" => "branch",
1136        "join" | "logicalJoin" => "join",
1137        "loopHeader" | "loopBody" | "loopExit" => "loop",
1138        "break" => "break",
1139        "terminate" => "terminate",
1140        "logicalRhs" => "assignFacts",
1141        "exit" => "exit",
1142        _ => "exit",
1143    }
1144}
1145
1146fn span_contains(span: Span, byte_offset: usize) -> bool {
1147    span_start(span) <= byte_offset && byte_offset < span_end(span)
1148}
1149
1150fn span_start(span: Span) -> usize {
1151    span.start as usize
1152}
1153
1154fn span_end(span: Span) -> usize {
1155    span.end as usize
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160    use super::{
1161        source_type_fact_control_flow_graph_from_snapshot,
1162        summarize_omena_bridge_source_control_flow_graph_for_source_language,
1163    };
1164
1165    #[test]
1166    fn captures_branchy_css_module_source_cfg_shape() -> Result<(), String> {
1167        let source = [
1168            "export function Card({ enabled }: { enabled: boolean }) {",
1169            "  let size = \"card\";",
1170            "  if (enabled) {",
1171            "    size = \"card--active\";",
1172            "  }",
1173            "  return <div className={size} />;",
1174            "}",
1175            "",
1176        ]
1177        .join("\n");
1178        let Some(reference) = source.rfind("size") else {
1179            return Err("fixture contains size reference".to_string());
1180        };
1181        let Some(graph) = summarize_omena_bridge_source_control_flow_graph_for_source_language(
1182            "/fake/ws/src/Card.tsx",
1183            source.as_str(),
1184            Some("typescriptreact"),
1185            "size",
1186            reference,
1187        ) else {
1188            return Err("fixture should produce CFG".to_string());
1189        };
1190
1191        assert_eq!(graph.product, "omena-bridge.source-control-flow-graph");
1192        assert_eq!(graph.snapshot.entry_block_id, "entry");
1193        assert_eq!(
1194            graph
1195                .snapshot
1196                .blocks
1197                .iter()
1198                .map(|block| block.kind)
1199                .collect::<Vec<_>>(),
1200            vec![
1201                "entry",
1202                "assignment",
1203                "branch",
1204                "join",
1205                "assignment",
1206                "exit"
1207            ]
1208        );
1209        assert!(
1210            graph
1211                .snapshot
1212                .blocks
1213                .iter()
1214                .any(|block| block.variable_name.as_deref() == Some("size"))
1215        );
1216        let symbol_ordinals = graph
1217            .snapshot
1218            .blocks
1219            .iter()
1220            .filter(|block| block.variable_name.as_deref() == Some("size"))
1221            .map(|block| block.symbol_ordinal)
1222            .collect::<Vec<_>>();
1223        assert!(symbol_ordinals.iter().all(Option::is_some));
1224        assert!(symbol_ordinals.contains(&Some(graph.binding.symbol_ordinal)));
1225        let type_fact_graph = source_type_fact_control_flow_graph_from_snapshot(&graph.snapshot);
1226        assert_eq!(
1227            type_fact_graph.entry_block_id,
1228            graph.snapshot.entry_block_id
1229        );
1230        assert_eq!(
1231            type_fact_graph
1232                .blocks
1233                .iter()
1234                .map(|block| block.kind.as_str())
1235                .collect::<Vec<_>>(),
1236            graph
1237                .snapshot
1238                .blocks
1239                .iter()
1240                .map(|block| block.kind)
1241                .collect::<Vec<_>>()
1242        );
1243        assert!(type_fact_graph.blocks.iter().any(|block| {
1244            block.symbol_ordinal == Some(graph.binding.symbol_ordinal)
1245                && block.variable_name.as_deref() == Some("size")
1246        }));
1247        Ok(())
1248    }
1249
1250    #[test]
1251    fn captures_assignment_value_facts_for_concatenated_source_cfg() -> Result<(), String> {
1252        let source = [
1253            "export function Card(variant: string) {",
1254            "  const size = \"btn-\" + variant + \"-chip\";",
1255            "  return cx(size);",
1256            "}",
1257            "",
1258        ]
1259        .join("\n");
1260        let Some(reference) = source.rfind("size") else {
1261            return Err("fixture contains size reference".to_string());
1262        };
1263        let Some(graph) = summarize_omena_bridge_source_control_flow_graph_for_source_language(
1264            "/fake/ws/src/Card.tsx",
1265            source.as_str(),
1266            Some("typescriptreact"),
1267            "size",
1268            reference,
1269        ) else {
1270            return Err("fixture should produce CFG".to_string());
1271        };
1272
1273        let Some(block) = graph
1274            .snapshot
1275            .blocks
1276            .iter()
1277            .find(|block| block.variable_name.as_deref() == Some("size"))
1278        else {
1279            return Err("size assignment block should be present".to_string());
1280        };
1281        let Some(facts) = &block.facts else {
1282            return Err("size assignment should carry value facts".to_string());
1283        };
1284        assert_eq!(facts.kind, "constrained");
1285        assert_eq!(facts.constraint_kind.as_deref(), Some("prefixSuffix"));
1286        assert_eq!(facts.prefix.as_deref(), Some("btn-"));
1287        assert_eq!(facts.suffix.as_deref(), Some("-chip"));
1288        assert_eq!(facts.min_len, Some("btn-".len() + "-chip".len()));
1289
1290        let type_fact_graph = source_type_fact_control_flow_graph_from_snapshot(&graph.snapshot);
1291        assert!(type_fact_graph.blocks.iter().any(|block| {
1292            block.variable_name.as_deref() == Some("size")
1293                && block
1294                    .facts
1295                    .as_ref()
1296                    .is_some_and(|facts| facts.constraint_kind.as_deref() == Some("prefixSuffix"))
1297        }));
1298        Ok(())
1299    }
1300
1301    #[test]
1302    fn captures_same_file_helper_return_facts_for_source_cfg() -> Result<(), String> {
1303        let source = [
1304            "type Status = \"idle\" | \"busy\" | \"error\";",
1305            "function resolveStatusClass(status: Status): string {",
1306            "  switch (status) {",
1307            "    case \"idle\": return \"state-idle\";",
1308            "    case \"busy\": return \"state-busy\";",
1309            "    case \"error\": return \"state-error\";",
1310            "    default: return \"state-idle\";",
1311            "  }",
1312            "}",
1313            "export function Card(status: Status) {",
1314            "  const size = resolveStatusClass(status);",
1315            "  return cx(size);",
1316            "}",
1317            "",
1318        ]
1319        .join("\n");
1320        let Some(reference) = source.rfind("size") else {
1321            return Err("fixture contains size reference".to_string());
1322        };
1323        let Some(graph) = summarize_omena_bridge_source_control_flow_graph_for_source_language(
1324            "/fake/ws/src/Card.tsx",
1325            source.as_str(),
1326            Some("typescriptreact"),
1327            "size",
1328            reference,
1329        ) else {
1330            return Err("fixture should produce CFG".to_string());
1331        };
1332
1333        let values = graph
1334            .snapshot
1335            .blocks
1336            .iter()
1337            .find(|block| block.variable_name.as_deref() == Some("size"))
1338            .and_then(|block| block.facts.as_ref())
1339            .and_then(|facts| facts.values.clone())
1340            .unwrap_or_default();
1341        assert_eq!(values, vec!["state-busy", "state-error", "state-idle"]);
1342        Ok(())
1343    }
1344
1345    #[test]
1346    fn source_cfg_serializes_symbol_ordinals_without_raw_symbol_ids() -> Result<(), String> {
1347        let source = [
1348            "export function Card() {",
1349            "  const size = \"card\";",
1350            "  return cx(size);",
1351            "}",
1352            "",
1353        ]
1354        .join("\n");
1355        let Some(reference) = source.rfind("size") else {
1356            return Err("fixture contains size reference".to_string());
1357        };
1358        let Some(graph) = summarize_omena_bridge_source_control_flow_graph_for_source_language(
1359            "/fake/ws/src/Card.tsx",
1360            source.as_str(),
1361            Some("typescriptreact"),
1362            "size",
1363            reference,
1364        ) else {
1365            return Err("fixture should produce CFG".to_string());
1366        };
1367
1368        let value = serde_json::to_value(&graph).map_err(|error| error.to_string())?;
1369        assert!(value.pointer("/binding/symbolOrdinal").is_some());
1370        assert!(value.pointer("/snapshot/blocks/1/symbolOrdinal").is_some());
1371        let serialized = serde_json::to_string(&value).map_err(|error| error.to_string())?;
1372        assert!(!serialized.contains("SymbolId"));
1373        Ok(())
1374    }
1375}