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}