1use std::collections::{BTreeMap, BTreeSet};
18
19use ruby_prism::{
20 AndNode, BeginNode, CallNode, CaseMatchNode, CaseNode, DefNode, ForNode, IfNode, Location,
21 Node, OrNode, RescueModifierNode, RescueNode, StatementsNode, UnlessNode, UntilNode, Visit,
22 WhileNode,
23};
24use serde::{Deserialize, Serialize};
25use serde_json::json;
26use sha2::{Digest, Sha256};
27
28use crate::{
29 coverage_analysis::PointKind,
30 coverage_report::{
31 BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
32 },
33};
34
35pub const RUBY_PROBE_PLAN_VERSION: u32 = 1;
36pub const RUBY_PROBE_RECEIVER: &str = "$__supercov";
39
40const ITERATORS: &[&[u8]] = &[
44 b"each",
45 b"each_with_index",
46 b"each_with_object",
47 b"each_pair",
48 b"each_key",
49 b"each_value",
50 b"each_char",
51 b"each_byte",
52 b"each_line",
53 b"each_slice",
54 b"each_cons",
55 b"each_entry",
56 b"each_index",
57 b"reverse_each",
58 b"map",
59 b"collect",
60 b"flat_map",
61 b"collect_concat",
62 b"filter_map",
63 b"select",
64 b"filter",
65 b"reject",
66 b"find",
67 b"detect",
68 b"find_index",
69 b"find_all",
70 b"all?",
71 b"any?",
72 b"none?",
73 b"one?",
74 b"count",
75 b"sum",
76 b"min_by",
77 b"max_by",
78 b"sort_by",
79 b"group_by",
80 b"partition",
81 b"inject",
82 b"reduce",
83 b"take_while",
84 b"drop_while",
85 b"times",
86 b"upto",
87 b"downto",
88 b"step",
89];
90pub const BEGIN_BODY_LIMITATION: &str = "ruby-begin-completion-unmeasured";
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum RubyInstrumenterError {
94 Parse(String),
95 InvalidRange,
96}
97
98impl std::fmt::Display for RubyInstrumenterError {
99 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 Self::Parse(error) => write!(formatter, "Ruby parse failed: {error}"),
102 Self::InvalidRange => write!(formatter, "Ruby parser returned an invalid range"),
103 }
104 }
105}
106
107impl std::error::Error for RubyInstrumenterError {}
108
109#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
113#[serde(rename_all = "camelCase")]
114pub enum KeyKind {
115 List,
119 Node,
121 Point,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
127#[serde(from = "[[usize; 2]; 2]", into = "[[usize; 2]; 2]")]
128pub struct PlanSpan {
129 pub start: [usize; 2],
130 pub end: [usize; 2],
131}
132
133impl From<[[usize; 2]; 2]> for PlanSpan {
134 fn from(value: [[usize; 2]; 2]) -> Self {
135 Self {
136 start: value[0],
137 end: value[1],
138 }
139 }
140}
141
142impl From<PlanSpan> for [[usize; 2]; 2] {
143 fn from(value: PlanSpan) -> Self {
144 [value.start, value.end]
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase", deny_unknown_fields)]
153pub struct Edit {
154 pub offset: usize,
155 pub text: String,
156 pub rank: String,
160 pub scope: usize,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase", deny_unknown_fields)]
171pub struct StdlibKey {
172 pub group: String,
173 pub branch: String,
174 pub kind: KeyKind,
177 pub span: PlanSpan,
179 pub unshifted: PlanSpan,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase", deny_unknown_fields)]
186pub struct StdlibDecision {
187 pub id: String,
188 pub value: bool,
189 pub outcome: String,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase", deny_unknown_fields)]
194pub struct BranchKeyPlan {
195 pub key: StdlibKey,
196 pub hits: Vec<String>,
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub decision: Option<StdlibDecision>,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase", deny_unknown_fields)]
206pub struct MethodKeyPlan {
207 pub span: PlanSpan,
208 pub unshifted: PlanSpan,
209 pub id: String,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213#[serde(rename_all = "camelCase", deny_unknown_fields)]
214pub struct CaseClausePlan {
215 pub key: StdlibKey,
216 pub missed: String,
217 pub selected: String,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase", deny_unknown_fields)]
222pub struct CaseNoMatchPlan {
223 pub key: StdlibKey,
224 pub matched: String,
225 pub unmatched: String,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase", deny_unknown_fields)]
233pub struct CasePlan {
234 pub clauses: Vec<CaseClausePlan>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub no_match: Option<CaseNoMatchPlan>,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241#[serde(untagged)]
242pub enum ConditionTree {
243 Leaf(usize),
244 Node {
245 op: String,
246 items: Vec<ConditionTree>,
247 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
248 negate: bool,
249 },
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase", deny_unknown_fields)]
254pub struct DerivedLogical {
255 pub previous_leaves: Vec<usize>,
256 pub operand_leaves: Vec<usize>,
257 pub short_circuit: String,
258 pub evaluated: String,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase", deny_unknown_fields)]
263pub struct LoopTarget {
264 pub id: String,
265 pub zero: String,
266 pub entered: String,
267 pub until: bool,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase", deny_unknown_fields)]
273pub struct HandlerTarget {
274 pub id: String,
275 pub missed: String,
276 pub selected: String,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
283pub enum ProbeTarget {
284 #[serde(rename_all = "camelCase")]
286 Statement { id: String },
287 #[serde(rename_all = "camelCase")]
289 Decision {
290 id: String,
291 width: usize,
292 not: Vec<bool>,
293 tree: ConditionTree,
294 outcome_true: String,
295 outcome_false: String,
296 logical: Vec<DerivedLogical>,
297 #[serde(rename = "loop", skip_serializing_if = "Option::is_none")]
298 loop_: Option<LoopTarget>,
299 },
300 #[serde(rename_all = "camelCase")]
302 For {
303 id: String,
304 zero: String,
305 entered: String,
306 },
307 #[serde(rename_all = "camelCase")]
309 Logical {
310 op: String,
311 short_circuit: String,
312 evaluated: String,
313 },
314 #[serde(rename_all = "camelCase")]
318 Arrival {
319 short_circuit: String,
320 evaluated: String,
321 },
322 #[serde(rename_all = "camelCase")]
325 Try {
326 id: String,
327 success: String,
328 raised: String,
329 handlers: Vec<HandlerTarget>,
330 },
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
334#[serde(rename_all = "camelCase", deny_unknown_fields)]
335pub struct RubyFilePlan {
336 pub edits: Vec<Edit>,
337 pub lines: BTreeMap<usize, String>,
339 pub statement_offsets: BTreeMap<String, [usize; 2]>,
343 pub branches: Vec<BranchKeyPlan>,
344 pub methods: Vec<MethodKeyPlan>,
345 pub cases: Vec<CasePlan>,
346 #[serde(default)]
349 pub probe_obligations: Vec<String>,
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353#[serde(rename_all = "camelCase", deny_unknown_fields)]
354pub struct RubyProbePlan {
355 pub version: u32,
356 pub root: String,
357 pub receiver: String,
358 pub files: BTreeMap<String, RubyFilePlan>,
359 pub probes: BTreeMap<u64, ProbeTarget>,
360 #[serde(default)]
363 pub probe_obligations: Vec<String>,
364}
365
366impl RubyProbePlan {
367 pub fn probe_obligations(&self) -> Vec<String> {
371 probe_obligations_of(&self.probes)
372 }
373}
374
375fn probe_obligations_of(probes: &BTreeMap<u64, ProbeTarget>) -> Vec<String> {
378 {
379 let mut ids = BTreeSet::new();
380 for target in probes.values() {
381 match target {
382 ProbeTarget::Statement { id } => {
383 ids.insert(id.clone());
384 }
385 ProbeTarget::Decision {
386 id,
387 outcome_true,
388 logical,
389 loop_,
390 ..
391 } => {
392 ids.insert(id.clone());
393 ids.insert(branch_of(outcome_true));
394 for derived in logical {
395 ids.insert(branch_of(&derived.short_circuit));
396 }
397 if let Some(loop_) = loop_ {
398 ids.insert(loop_.id.clone());
399 }
400 }
401 ProbeTarget::For { id, .. } => {
402 ids.insert(id.clone());
403 }
404 ProbeTarget::Logical { short_circuit, .. } => {
405 ids.insert(branch_of(short_circuit));
406 }
407 ProbeTarget::Arrival { short_circuit, .. } => {
408 ids.insert(branch_of(short_circuit));
409 }
410 ProbeTarget::Try { id, handlers, .. } => {
411 ids.insert(id.clone());
412 for handler in handlers {
413 ids.insert(handler.id.clone());
414 }
415 }
416 }
417 }
418 ids.into_iter().collect()
419 }
420}
421
422fn branch_of(alternative: &str) -> String {
425 alternative
426 .rsplit_once(':')
427 .map(|(branch, _)| branch.to_owned())
428 .unwrap_or_else(|| alternative.to_owned())
429}
430
431#[derive(Debug, Clone, PartialEq)]
432pub struct RubyFileObligations {
433 pub manifest: CoverageManifest,
434 pub plan: RubyFilePlan,
435 pub probes: BTreeMap<u64, ProbeTarget>,
436}
437
438fn stable_id(file: &str, kind: &str, start: usize, end: usize, suffix: &str) -> String {
439 let mut hash = Sha256::new();
440 for value in [file, kind, &start.to_string(), &end.to_string(), suffix] {
441 hash.update(value.as_bytes());
442 hash.update([0]);
443 }
444 let digest = hash.finalize();
445 let mut encoded = String::with_capacity(24);
446 for byte in &digest[..12] {
447 use std::fmt::Write as _;
448 write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
449 }
450 format!("rb:{kind}:{encoded}")
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
454enum EditRank {
455 Clause,
456 StatementProbe,
457 Opener,
458 Closer,
459}
460
461#[derive(Debug, Clone)]
462struct PendingEdit {
463 offset: usize,
464 rank: EditRank,
465 order: i64,
467 sequence: usize,
468 text: String,
469 scope: usize,
470}
471
472struct Collector<'a> {
473 file: &'a str,
474 source: &'a [u8],
475 line_starts: Vec<usize>,
476 manifest: CoverageManifest,
477 lines: BTreeMap<usize, String>,
478 statement_offsets: BTreeMap<String, [usize; 2]>,
479 branches: Vec<BranchKeyPlan>,
480 methods: Vec<MethodKeyPlan>,
481 cases: Vec<CasePlan>,
482 probes: BTreeMap<u64, ProbeTarget>,
483 edits: Vec<PendingEdit>,
484 next_probe: &'a mut u64,
485 point_ids: BTreeSet<String>,
486 decision_ids: BTreeSet<String>,
487 branch_ids: BTreeSet<String>,
488 claimed_lines: BTreeSet<usize>,
489 key_statements: BTreeMap<usize, usize>,
492 endless_bodies: std::collections::BTreeSet<usize>,
496 tree_logicals: BTreeSet<usize>,
498 expression_lists: BTreeSet<usize>,
501 guard_nodes: BTreeSet<usize>,
503 elsif_nodes: BTreeSet<usize>,
505 depth: i64,
506 begin_unmeasured: Vec<(String, usize)>,
507 error: Option<RubyInstrumenterError>,
508}
509
510impl<'a> Collector<'a> {
511 fn new(file: &'a str, source: &'a [u8], next_probe: &'a mut u64) -> Self {
512 let mut line_starts = vec![0];
513 line_starts.extend(
514 source
515 .iter()
516 .enumerate()
517 .filter_map(|(index, byte)| (*byte == b'\n').then_some(index + 1)),
518 );
519 Self {
520 file,
521 source,
522 line_starts,
523 manifest: CoverageManifest {
524 unmeasured: Vec::new(),
525 decisions: Vec::new(),
526 points: Vec::new(),
527 branches: Vec::new(),
528 limitations: Vec::new(),
529 scope: None,
530 },
531 lines: BTreeMap::new(),
532 statement_offsets: BTreeMap::new(),
533 branches: Vec::new(),
534 methods: Vec::new(),
535 cases: Vec::new(),
536 probes: BTreeMap::new(),
537 edits: Vec::new(),
538 next_probe,
539 point_ids: BTreeSet::new(),
540 decision_ids: BTreeSet::new(),
541 branch_ids: BTreeSet::new(),
542 claimed_lines: BTreeSet::new(),
543 key_statements: BTreeMap::new(),
544 endless_bodies: std::collections::BTreeSet::new(),
545 tree_logicals: BTreeSet::new(),
546 expression_lists: BTreeSet::new(),
547 guard_nodes: BTreeSet::new(),
548 elsif_nodes: BTreeSet::new(),
549 depth: 0,
550 begin_unmeasured: Vec::new(),
551 error: None,
552 }
553 }
554
555 fn line_column(&self, offset: usize) -> (usize, usize) {
558 let line_index = self.line_starts.partition_point(|start| *start <= offset) - 1;
559 (line_index + 1, offset - self.line_starts[line_index])
560 }
561
562 fn span(&self, start: usize, end: usize) -> PlanSpan {
563 let (start_line, start_column) = self.line_column(start);
564 let (end_line, end_column) = self.line_column(end);
565 PlanSpan {
566 start: [start_line, start_column],
567 end: [end_line, end_column],
568 }
569 }
570
571 fn point_span(&self, offset: usize) -> PlanSpan {
572 let (line, column) = self.line_column(offset);
573 PlanSpan {
574 start: [line, column],
575 end: [line, column],
576 }
577 }
578
579 fn location_span(&self, location: &Location<'_>) -> PlanSpan {
580 self.span(location.start_offset(), location.end_offset())
581 }
582
583 fn node_span(&self, node: &Node<'_>) -> PlanSpan {
584 self.location_span(&node.location())
585 }
586
587 fn text(&self, start: usize, end: usize) -> String {
588 String::from_utf8_lossy(&self.source[start.min(end)..end.min(self.source.len())])
589 .trim()
590 .to_owned()
591 }
592
593 fn statements_span(&self, statements: &Option<StatementsNode<'_>>) -> Option<PlanSpan> {
594 statements
595 .as_ref()
596 .map(|statements| self.location_span(&statements.location()))
597 }
598
599 fn edit(&mut self, offset: usize, rank: EditRank, text: String, scope: usize) {
602 debug_assert!(!text.contains('\n'));
603 let order = match rank {
604 EditRank::Opener => self.depth,
605 EditRank::Closer => -self.depth,
606 _ => 0,
607 };
608 let sequence = self.edits.len();
609 self.edits.push(PendingEdit {
610 offset,
611 rank,
612 order,
613 sequence,
614 text,
615 scope,
616 });
617 }
618
619 fn probe_key(&mut self, target: ProbeTarget) -> u64 {
620 let key = *self.next_probe;
621 *self.next_probe += 1;
622 self.probes.insert(key, target);
623 key
624 }
625
626 fn wrap(&mut self, start: usize, end: usize, opener: String) {
627 self.edit(start, EditRank::Opener, opener, end);
628 self.edit(end, EditRank::Closer, "))".into(), start);
629 }
630
631 fn push_point(
634 &mut self,
635 id: &str,
636 start: usize,
637 end: usize,
638 kind: PointKind,
639 label: Option<String>,
640 ) {
641 let (line, column) = self.line_column(start);
642 self.manifest.points.push(PointMeta {
643 id: id.into(),
644 kind,
645 file: self.file.into(),
646 line,
647 column,
648 source: self.text(start, end),
649 label,
650 });
651 }
652
653 fn branch<const N: usize>(
654 &mut self,
655 start: usize,
656 end: usize,
657 kind: &str,
658 alternatives: [(&str, &str); N],
659 ) -> Option<String> {
660 let id = stable_id(self.file, "branch", start, end, kind);
661 let source = self.text(start, end);
662 self.branch_with_id(id, start, end, kind, source, alternatives)
663 }
664
665 fn branch_with_id<const N: usize>(
666 &mut self,
667 id: String,
668 start: usize,
669 _end: usize,
670 kind: &str,
671 source: String,
672 alternatives: [(&str, &str); N],
673 ) -> Option<String> {
674 if !self.branch_ids.insert(id.clone()) {
675 return None;
676 }
677 let (line, column) = self.line_column(start);
678 self.manifest.branches.push(BranchMeta {
679 id: id.clone(),
680 kind: kind.into(),
681 file: self.file.into(),
682 line,
683 column,
684 source,
685 alternatives: alternatives
686 .into_iter()
687 .map(|(suffix, label)| BranchAlternativeMeta {
688 id: format!("{id}:{suffix}"),
689 label: label.into(),
690 })
691 .collect(),
692 });
693 Some(id)
694 }
695
696 fn stdlib(
697 &mut self,
698 group: &str,
699 branch: &str,
700 span: PlanSpan,
701 kind: KeyKind,
702 hits: Vec<String>,
703 ) -> usize {
704 self.branches.push(BranchKeyPlan {
705 key: StdlibKey {
706 group: group.into(),
707 branch: branch.into(),
708 kind,
709 span,
710 unshifted: span,
711 },
712 hits,
713 decision: None,
714 });
715 self.branches.len() - 1
716 }
717
718 fn statements(&mut self, statements: &StatementsNode<'_>) {
721 for statement in statements.body().iter() {
722 self.statement(&statement);
723 }
724 }
725
726 fn statement(&mut self, node: &Node<'_>) {
727 let location = node.location();
728 let (start, end) = (location.start_offset(), location.end_offset());
729 let id = stable_id(self.file, "statement", start, end, "");
730 if !self.point_ids.insert(id.clone()) {
731 return;
732 }
733 self.push_point(&id, start, end, PointKind::Statement, None);
734 let (line, _) = self.line_column(start);
735 if let Some(index) = self.key_statements.get(&start).copied() {
736 self.branches[index].hits.push(id);
739 } else if self.needs_probe(node) {
740 self.claimed_lines.insert(line);
743 let key = self.probe_key(ProbeTarget::Statement { id });
744 self.statement_probe(start, end, key);
745 } else if self.claimed_lines.insert(line) {
746 self.lines.insert(line, id.clone());
750 self.statement_offsets.insert(id, [start, end]);
751 } else {
752 let key = self.probe_key(ProbeTarget::Statement { id });
753 self.statement_probe(start, end, key);
754 }
755 }
756
757 fn statement_probe(&mut self, start: usize, end: usize, key: u64) {
760 if self.endless_bodies.contains(&start) {
761 self.depth += 1;
762 self.edit(
763 start,
764 EditRank::Opener,
765 format!("({RUBY_PROBE_RECEIVER}.s({key}); "),
766 end,
767 );
768 self.edit(end, EditRank::Closer, ")".into(), start);
769 self.depth -= 1;
770 } else {
771 self.edit(
772 start,
773 EditRank::StatementProbe,
774 format!("{RUBY_PROBE_RECEIVER}.s({key}); "),
775 end,
776 );
777 }
778 }
779
780 fn needs_probe(&self, node: &Node<'_>) -> bool {
781 let value = if let Some(write) = node.as_local_variable_write_node() {
782 Some(write.value())
783 } else if let Some(write) = node.as_instance_variable_write_node() {
784 Some(write.value())
785 } else if let Some(write) = node.as_class_variable_write_node() {
786 Some(write.value())
787 } else if let Some(write) = node.as_global_variable_write_node() {
788 Some(write.value())
789 } else if let Some(write) = node.as_constant_write_node() {
790 Some(write.value())
791 } else {
792 node.as_multi_write_node().map(|write| write.value())
793 };
794 value.is_some_and(|value| {
797 value
798 .as_begin_node()
799 .is_some_and(|begin| begin.begin_keyword_loc().is_some())
800 || value.as_parentheses_node().is_some()
801 })
802 }
803
804 fn key_body(&mut self, statements: Option<StatementsNode<'_>>, index: usize) {
806 if let Some(first) = statements.and_then(|statements| statements.body().iter().next()) {
807 self.key_statements
808 .insert(first.location().start_offset(), index);
809 }
810 }
811
812 fn strip<'n>(&self, node: Node<'n>) -> (Node<'n>, usize) {
816 let mut current = node;
817 let mut not = 0;
818 loop {
819 if let Some(parens) = current.as_parentheses_node() {
820 if !parens.is_multiple_statements()
821 && let Some(body) = parens.body()
822 && let Some(statements) = body.as_statements_node()
823 && statements.body().iter().count() == 1
824 && let Some(inner) = statements.body().iter().next()
825 {
826 current = inner;
827 continue;
828 }
829 break;
830 }
831 if let Some(call) = current.as_call_node()
832 && call.name().as_slice() == b"!"
833 && call.arguments().is_none()
834 && call.block().is_none()
835 && let Some(receiver) = call.receiver()
836 {
837 not += 1;
838 current = receiver;
839 continue;
840 }
841 break;
842 }
843 (current, not)
844 }
845
846 fn tree(
847 &mut self,
848 node: Node<'_>,
849 leaves: &mut Vec<(usize, usize, usize)>,
850 logicals: &mut Vec<(String, Vec<Vec<usize>>)>,
851 ) -> ConditionTree {
852 let (operand, not) = self.strip(node);
853 let logical: Option<(&str, Node<'_>, Node<'_>, usize)> =
854 if let Some(and) = operand.as_and_node() {
855 Some((
856 "and",
857 and.left(),
858 and.right(),
859 operand.location().start_offset(),
860 ))
861 } else if let Some(or) = operand.as_or_node() {
862 Some((
863 "or",
864 or.left(),
865 or.right(),
866 operand.location().start_offset(),
867 ))
868 } else {
869 None
870 };
871 if let Some((op, left, right, offset)) = logical {
872 self.tree_logicals.insert(offset);
873 let first = leaves.len();
874 let left_tree = self.tree(left, leaves, logicals);
875 let middle = leaves.len();
876 let right_tree = self.tree(right, leaves, logicals);
877 logicals.push((
878 op.into(),
879 vec![(first..middle).collect(), (middle..leaves.len()).collect()],
880 ));
881 return ConditionTree::Node {
882 op: op.into(),
883 items: vec![left_tree, right_tree],
884 negate: not % 2 == 1,
885 };
886 }
887 let location = operand.location();
888 leaves.push((location.start_offset(), location.end_offset(), not));
889 ConditionTree::Leaf(leaves.len() - 1)
890 }
891
892 fn probe_decision(
896 &mut self,
897 predicate: Node<'_>,
898 kind: &str,
899 loop_: Option<LoopTarget>,
900 wrapper: &str,
901 ) -> Option<u64> {
902 let location = predicate.location();
903 let (start, end) = (location.start_offset(), location.end_offset());
904 let id = stable_id(self.file, "decision", start, end, kind);
905 if !self.decision_ids.insert(id.clone()) {
906 return None;
907 }
908 let mut leaves = Vec::new();
909 let mut logicals = Vec::new();
910 let tree = self.tree(predicate, &mut leaves, &mut logicals);
911 let conditions = leaves
912 .iter()
913 .map(|(leaf_start, leaf_end, not)| {
914 let text = self.text(*leaf_start, *leaf_end);
915 if not % 2 == 1 {
916 format!("!{text}")
917 } else {
918 text
919 }
920 })
921 .collect::<Vec<_>>();
922 let (line, column) = self.line_column(start);
923 let source = self.text(start, end);
924 self.manifest.decisions.push(DecisionMeta {
925 id: id.clone(),
926 file: self.file.into(),
927 line,
928 column,
929 source: source.clone(),
930 conditions,
931 kind: kind.into(),
932 });
933 let outcome_id = format!("{id}:outcome");
934 self.branch_with_id(
935 outcome_id.clone(),
936 start,
937 end,
938 kind,
939 source,
940 [("true", "true"), ("false", "false")],
941 );
942 let mut derived = Vec::new();
943 for (op, groups) in logicals {
944 let right_leaf = groups[1].first().copied().unwrap_or(0);
946 let (right_start, right_end, _) = leaves[right_leaf];
947 if let Some(branch_id) = self.branch(
948 right_start,
949 right_end,
950 &format!("logical-{op}"),
951 [
952 ("short-circuit", "short-circuited"),
953 ("evaluated", "right operand evaluated"),
954 ],
955 ) {
956 derived.push(DerivedLogical {
957 previous_leaves: groups[0].clone(),
958 operand_leaves: groups[1].clone(),
959 short_circuit: format!("{branch_id}:short-circuit"),
960 evaluated: format!("{branch_id}:evaluated"),
961 });
962 }
963 }
964 let key = self.probe_key(ProbeTarget::Decision {
965 id,
966 width: leaves.len(),
967 not: leaves.iter().map(|(_, _, not)| not % 2 == 1).collect(),
968 tree,
969 outcome_true: format!("{outcome_id}:true"),
970 outcome_false: format!("{outcome_id}:false"),
971 logical: derived,
972 loop_,
973 });
974 self.depth += 1;
975 self.wrap(
976 start,
977 end,
978 format!("{RUBY_PROBE_RECEIVER}.{wrapper}({key}, ("),
979 );
980 self.depth += 1;
981 for (index, (leaf_start, leaf_end, _)) in leaves.iter().enumerate() {
982 self.wrap(
983 *leaf_start,
984 *leaf_end,
985 format!("{RUBY_PROBE_RECEIVER}.c({key}, {index}, ("),
986 );
987 }
988 self.depth -= 2;
989 Some(key)
990 }
991
992 fn stdlib_decision(
995 &mut self,
996 predicate: Node<'_>,
997 kind: &str,
998 then_index: usize,
999 else_index: usize,
1000 then_is_true: bool,
1001 ) {
1002 let location = predicate.location();
1003 let (start, end) = (location.start_offset(), location.end_offset());
1004 let id = stable_id(self.file, "decision", start, end, kind);
1005 if !self.decision_ids.insert(id.clone()) {
1006 return;
1007 }
1008 let (operand, not) = self.strip(predicate);
1009 let operand_location = operand.location();
1010 let mut condition = self.text(
1011 operand_location.start_offset(),
1012 operand_location.end_offset(),
1013 );
1014 if not % 2 == 1 {
1015 condition = format!("!{condition}");
1016 }
1017 let (line, column) = self.line_column(start);
1018 let source = self.text(start, end);
1019 self.manifest.decisions.push(DecisionMeta {
1020 id: id.clone(),
1021 file: self.file.into(),
1022 line,
1023 column,
1024 source: source.clone(),
1025 conditions: vec![condition],
1026 kind: kind.into(),
1027 });
1028 let outcome_id = format!("{id}:outcome");
1029 self.branch_with_id(
1030 outcome_id.clone(),
1031 start,
1032 end,
1033 kind,
1034 source,
1035 [("true", "true"), ("false", "false")],
1036 );
1037 let (true_index, false_index) = if then_is_true {
1038 (then_index, else_index)
1039 } else {
1040 (else_index, then_index)
1041 };
1042 self.branches[true_index].decision = Some(StdlibDecision {
1043 id: id.clone(),
1044 value: true,
1045 outcome: format!("{outcome_id}:true"),
1046 });
1047 self.branches[false_index].decision = Some(StdlibDecision {
1048 id,
1049 value: false,
1050 outcome: format!("{outcome_id}:false"),
1051 });
1052 }
1053
1054 fn literal_truth(&self, predicate: Node<'_>) -> Option<bool> {
1059 let mut node = predicate;
1060 loop {
1061 let inner = node.as_parentheses_node().and_then(|parens| {
1062 let body = parens.body()?;
1063 let statements = body.as_statements_node()?;
1064 let mut iter = statements.body().iter();
1065 let only = iter.next()?;
1066 iter.next().is_none().then_some(only)
1067 });
1068 match inner {
1069 Some(inner) => node = inner,
1070 None => break,
1071 }
1072 }
1073 if node.as_true_node().is_some()
1074 || node.as_integer_node().is_some()
1075 || node.as_float_node().is_some()
1076 || node.as_rational_node().is_some()
1077 || node.as_imaginary_node().is_some()
1078 || node.as_string_node().is_some()
1079 || node.as_symbol_node().is_some()
1080 {
1081 Some(true)
1082 } else if node.as_false_node().is_some() || node.as_nil_node().is_some() {
1083 Some(false)
1084 } else {
1085 None
1086 }
1087 }
1088
1089 fn is_compound(&self, predicate: Node<'_>) -> bool {
1090 let (operand, _) = self.strip(predicate);
1091 operand.as_and_node().is_some() || operand.as_or_node().is_some()
1092 }
1093
1094 fn predicate_decision<'n>(
1097 &mut self,
1098 predicate: impl Fn() -> Node<'n>,
1099 kind: &str,
1100 then_index: usize,
1101 else_index: usize,
1102 then_is_true: bool,
1103 ) {
1104 if self.is_compound(predicate()) {
1105 self.probe_decision(predicate(), kind, None, "d");
1106 } else {
1107 self.stdlib_decision(predicate(), kind, then_index, else_index, then_is_true);
1108 }
1109 }
1110
1111 fn if_node(&mut self, node: &IfNode<'_>, kind: &str) {
1114 let location = node.location();
1115 let node_span = self.location_span(&location);
1116 let then_statements = self.statements_span(&node.statements());
1117 let (then_span, then_kind) = match then_statements {
1119 Some(span) => (span, KeyKind::List),
1120 None => (
1121 self.point_span(node.predicate().location().end_offset()),
1122 KeyKind::Point,
1123 ),
1124 };
1125 let (else_span, else_kind) = match node.subsequent() {
1126 Some(subsequent) => match subsequent.as_else_node() {
1127 Some(else_node) => match self.statements_span(&else_node.statements()) {
1128 Some(span) => (span, KeyKind::List),
1129 None => (self.node_span(&subsequent), KeyKind::Node),
1130 },
1131 None => (self.node_span(&subsequent), KeyKind::Node),
1132 },
1133 None => (node_span, KeyKind::Node),
1134 };
1135 let then_index = self.stdlib("if", "then", then_span, then_kind, Vec::new());
1136 let else_index = self.stdlib("if", "else", else_span, else_kind, Vec::new());
1137 if kind == "ternary" {
1138 if let Some(statements) = node.statements() {
1140 self.expression_lists
1141 .insert(statements.location().start_offset());
1142 }
1143 if let Some(subsequent) = node.subsequent()
1144 && let Some(else_node) = subsequent.as_else_node()
1145 && let Some(statements) = else_node.statements()
1146 {
1147 self.expression_lists
1148 .insert(statements.location().start_offset());
1149 }
1150 } else {
1151 self.key_body(node.statements(), then_index);
1152 if let Some(subsequent) = node.subsequent()
1153 && let Some(else_node) = subsequent.as_else_node()
1154 {
1155 self.key_body(else_node.statements(), else_index);
1156 }
1157 }
1158 self.predicate_decision(|| node.predicate(), kind, then_index, else_index, true);
1159 }
1160
1161 fn unless_node(&mut self, node: &UnlessNode<'_>) {
1162 let node_span = self.location_span(&node.location());
1163 let then_statements = self.statements_span(&node.statements());
1164 let (then_span, then_kind) = match then_statements {
1165 Some(span) => (span, KeyKind::List),
1166 None => (
1167 self.point_span(node.predicate().location().end_offset()),
1168 KeyKind::Point,
1169 ),
1170 };
1171 let (else_span, else_kind) = match node.else_clause() {
1172 Some(else_node) => match self.statements_span(&else_node.statements()) {
1173 Some(span) => (span, KeyKind::List),
1174 None => (self.location_span(&else_node.location()), KeyKind::Node),
1175 },
1176 None => (node_span, KeyKind::Node),
1177 };
1178 let then_index = self.stdlib("unless", "then", then_span, then_kind, Vec::new());
1179 let else_index = self.stdlib("unless", "else", else_span, else_kind, Vec::new());
1180 self.key_body(node.statements(), then_index);
1181 if let Some(else_node) = node.else_clause() {
1182 self.key_body(else_node.statements(), else_index);
1183 }
1184 self.predicate_decision(|| node.predicate(), "unless", then_index, else_index, false);
1186 }
1187
1188 fn loop_node(
1189 &mut self,
1190 location: &Location<'_>,
1191 predicate: Node<'_>,
1192 statements: Option<StatementsNode<'_>>,
1193 begin_modifier: bool,
1194 until: bool,
1195 ) {
1196 let kind = if until { "until" } else { "while" };
1197 let (start, end) = (location.start_offset(), location.end_offset());
1198 if let Some(body_span) = self.statements_span(&statements) {
1200 let index = self.stdlib(kind, "body", body_span, KeyKind::List, Vec::new());
1201 self.key_body(statements, index);
1202 }
1203 let loop_target = if begin_modifier {
1204 None
1206 } else {
1207 self.branch(
1208 start,
1209 end,
1210 kind,
1211 [("zero", "zero iterations"), ("entered", "entered")],
1212 )
1213 .map(|id| LoopTarget {
1214 zero: format!("{id}:zero"),
1215 entered: format!("{id}:entered"),
1216 id,
1217 until,
1218 })
1219 };
1220 self.probe_decision(predicate, kind, loop_target, "w");
1221 }
1222
1223 fn for_node(&mut self, node: &ForNode<'_>) {
1224 let location = node.location();
1225 let Some(id) = self.branch(
1226 location.start_offset(),
1227 location.end_offset(),
1228 "for",
1229 [("zero", "zero iterations"), ("entered", "entered")],
1230 ) else {
1231 return;
1232 };
1233 let key = self.probe_key(ProbeTarget::For {
1234 zero: format!("{id}:zero"),
1235 entered: format!("{id}:entered"),
1236 id: id.clone(),
1237 });
1238 let collection = node.collection().location();
1239 self.depth += 1;
1240 self.wrap(
1241 collection.start_offset(),
1242 collection.end_offset(),
1243 format!("{RUBY_PROBE_RECEIVER}.f({key}, ("),
1244 );
1245 self.depth -= 1;
1246 match node
1247 .statements()
1248 .and_then(|statements| statements.body().iter().next())
1249 {
1250 Some(first) => self.edit(
1251 first.location().start_offset(),
1252 EditRank::StatementProbe,
1253 format!("{RUBY_PROBE_RECEIVER}.fb({key}); "),
1254 first.location().end_offset(),
1255 ),
1256 None => {
1257 self.manifest.unmeasured.push(format!("{id}:entered"));
1258 self.manifest.unmeasured.push(format!("{id}:zero"));
1259 }
1260 }
1261 }
1262
1263 fn iterator_loop(
1268 &mut self,
1269 node: &CallNode<'_>,
1270 receiver: &Node<'_>,
1271 block: &ruby_prism::BlockNode<'_>,
1272 ) {
1273 let location = node.location();
1274 let name = String::from_utf8_lossy(node.name().as_slice()).into_owned();
1275 let Some(id) = self.branch(
1276 location.start_offset(),
1277 location.end_offset(),
1278 &format!("iterator-{}", name.trim_end_matches(['?', '!'])),
1279 [("zero", "zero iterations"), ("entered", "entered")],
1280 ) else {
1281 return;
1282 };
1283 let first = block.body().and_then(|body| {
1284 if let Some(statements) = body.as_statements_node() {
1285 statements.body().iter().next()
1286 } else if let Some(begin) = body.as_begin_node() {
1287 begin
1288 .statements()
1289 .and_then(|statements| statements.body().iter().next())
1290 } else {
1291 None
1292 }
1293 });
1294 let Some(first) = first else {
1295 self.manifest.unmeasured.push(format!("{id}:entered"));
1298 self.manifest.unmeasured.push(format!("{id}:zero"));
1299 return;
1300 };
1301 let key = self.probe_key(ProbeTarget::For {
1302 zero: format!("{id}:zero"),
1303 entered: format!("{id}:entered"),
1304 id,
1305 });
1306 let receiver_location = receiver.location();
1307 self.depth += 1;
1308 self.wrap(
1309 receiver_location.start_offset(),
1310 receiver_location.end_offset(),
1311 format!("{RUBY_PROBE_RECEIVER}.f({key}, ("),
1312 );
1313 self.depth -= 1;
1314 self.edit(
1315 first.location().start_offset(),
1316 EditRank::StatementProbe,
1317 format!("{RUBY_PROBE_RECEIVER}.fb({key}); "),
1318 first.location().end_offset(),
1319 );
1320 }
1321
1322 fn case_node(&mut self, node: &CaseNode<'_>) {
1323 let node_span = self.location_span(&node.location());
1324 let (start, end) = (node.location().start_offset(), node.location().end_offset());
1325 let mut clauses = Vec::new();
1326 for (index, condition) in node.conditions().iter().enumerate() {
1327 let Some(when) = condition.as_when_node() else {
1328 continue;
1329 };
1330 let Some(id) = self.branch(
1331 when.location().start_offset(),
1332 when.location().end_offset(),
1333 &format!("case-when-{index}"),
1334 [("missed", "not selected"), ("selected", "selected")],
1335 ) else {
1336 return;
1337 };
1338 let statements = self.statements_span(&when.statements());
1339 let span = statements.unwrap_or_else(|| self.location_span(&when.location()));
1340 let key_index = self.stdlib(
1341 "case",
1342 "when",
1343 span,
1344 if statements.is_some() {
1345 KeyKind::List
1346 } else {
1347 KeyKind::Node
1348 },
1349 vec![format!("{id}:selected")],
1350 );
1351 self.key_body(when.statements(), key_index);
1352 clauses.push(CaseClausePlan {
1353 key: self.branches[key_index].key.clone(),
1354 missed: format!("{id}:missed"),
1355 selected: format!("{id}:selected"),
1356 });
1357 }
1358 let no_match = match node.else_clause() {
1359 Some(else_node) => {
1360 let Some(id) = self.branch(
1361 else_node.location().start_offset(),
1362 else_node.location().end_offset(),
1363 "case-else",
1364 [("missed", "not selected"), ("selected", "selected")],
1365 ) else {
1366 return;
1367 };
1368 let statements = self.statements_span(&else_node.statements());
1369 let span = statements.unwrap_or_else(|| self.location_span(&else_node.location()));
1370 let key_index = self.stdlib(
1371 "case",
1372 "else",
1373 span,
1374 if statements.is_some() {
1375 KeyKind::List
1376 } else {
1377 KeyKind::Node
1378 },
1379 vec![format!("{id}:selected")],
1380 );
1381 self.key_body(else_node.statements(), key_index);
1382 clauses.push(CaseClausePlan {
1383 key: self.branches[key_index].key.clone(),
1384 missed: format!("{id}:missed"),
1385 selected: format!("{id}:selected"),
1386 });
1387 None
1388 }
1389 None => self
1390 .branch(
1391 start,
1392 end,
1393 "case-no-match",
1394 [
1395 ("matched", "some clause matched"),
1396 ("unmatched", "no clause matched"),
1397 ],
1398 )
1399 .map(|id| {
1400 let key_index = self.stdlib(
1401 "case",
1402 "else",
1403 node_span,
1404 KeyKind::Node,
1405 vec![format!("{id}:unmatched")],
1406 );
1407 CaseNoMatchPlan {
1408 key: self.branches[key_index].key.clone(),
1409 matched: format!("{id}:matched"),
1410 unmatched: format!("{id}:unmatched"),
1411 }
1412 }),
1413 };
1414 self.cases.push(CasePlan { clauses, no_match });
1415 }
1416
1417 fn case_match_node(&mut self, node: &CaseMatchNode<'_>) {
1418 let node_span = self.location_span(&node.location());
1419 let (start, end) = (node.location().start_offset(), node.location().end_offset());
1420 let mut clauses = Vec::new();
1421 for (index, condition) in node.conditions().iter().enumerate() {
1422 let Some(in_node) = condition.as_in_node() else {
1423 continue;
1424 };
1425 let Some(id) = self.branch(
1426 in_node.location().start_offset(),
1427 in_node.location().end_offset(),
1428 &format!("case-in-{index}"),
1429 [("missed", "not selected"), ("selected", "selected")],
1430 ) else {
1431 return;
1432 };
1433 let statements = self.statements_span(&in_node.statements());
1434 let span = statements.unwrap_or_else(|| self.location_span(&in_node.location()));
1435 let key_index = self.stdlib(
1436 "case",
1437 "in",
1438 span,
1439 if statements.is_some() {
1440 KeyKind::List
1441 } else {
1442 KeyKind::Node
1443 },
1444 vec![format!("{id}:selected")],
1445 );
1446 self.key_body(in_node.statements(), key_index);
1447 clauses.push(CaseClausePlan {
1448 key: self.branches[key_index].key.clone(),
1449 missed: format!("{id}:missed"),
1450 selected: format!("{id}:selected"),
1451 });
1452 let pattern = in_node.pattern();
1455 if let Some(guard) = pattern.as_if_node() {
1456 self.guard_nodes.insert(pattern.location().start_offset());
1457 self.probe_decision(guard.predicate(), "in-guard", None, "d");
1458 } else if let Some(guard) = pattern.as_unless_node() {
1459 self.guard_nodes.insert(pattern.location().start_offset());
1460 self.probe_decision(guard.predicate(), "in-guard-unless", None, "d");
1461 }
1462 }
1463 let no_match = match node.else_clause() {
1464 Some(else_node) => {
1465 let Some(id) = self.branch(
1466 else_node.location().start_offset(),
1467 else_node.location().end_offset(),
1468 "case-else",
1469 [("missed", "not selected"), ("selected", "selected")],
1470 ) else {
1471 return;
1472 };
1473 let statements = self.statements_span(&else_node.statements());
1474 let span = statements.unwrap_or_else(|| self.location_span(&else_node.location()));
1475 let key_index = self.stdlib(
1476 "case",
1477 "else",
1478 span,
1479 if statements.is_some() {
1480 KeyKind::List
1481 } else {
1482 KeyKind::Node
1483 },
1484 vec![format!("{id}:selected")],
1485 );
1486 self.key_body(else_node.statements(), key_index);
1487 clauses.push(CaseClausePlan {
1488 key: self.branches[key_index].key.clone(),
1489 missed: format!("{id}:missed"),
1490 selected: format!("{id}:selected"),
1491 });
1492 None
1493 }
1494 None => self
1495 .branch(
1496 start,
1497 end,
1498 "case-no-match",
1499 [
1500 ("matched", "some pattern matched"),
1501 ("unmatched", "no pattern matched"),
1502 ],
1503 )
1504 .map(|id| {
1505 let key_index = self.stdlib(
1506 "case",
1507 "else",
1508 node_span,
1509 KeyKind::Node,
1510 vec![format!("{id}:unmatched")],
1511 );
1512 CaseNoMatchPlan {
1513 key: self.branches[key_index].key.clone(),
1514 matched: format!("{id}:matched"),
1515 unmatched: format!("{id}:unmatched"),
1516 }
1517 }),
1518 };
1519 self.cases.push(CasePlan { clauses, no_match });
1520 }
1521
1522 fn safe_navigation(&mut self, node: &CallNode<'_>) {
1523 let location = node.location();
1524 let Some(id) = self.branch(
1525 location.start_offset(),
1526 location.end_offset(),
1527 "safe-navigation",
1528 [("nil", "receiver nil"), ("called", "method called")],
1529 ) else {
1530 return;
1531 };
1532 let end = match node.arguments() {
1536 Some(arguments) => node
1537 .closing_loc()
1538 .map(|closing| closing.end_offset())
1539 .unwrap_or_else(|| arguments.location().end_offset()),
1540 None => node
1541 .message_loc()
1542 .map(|message| message.end_offset())
1543 .unwrap_or_else(|| location.end_offset()),
1544 };
1545 let (start_line, start_column) = self.line_column(location.start_offset());
1546 let (end_line, end_column) = self.line_column(end);
1547 let span = PlanSpan {
1548 start: [start_line, start_column],
1549 end: [end_line, end_column],
1550 };
1551 self.stdlib(
1552 "&.",
1553 "then",
1554 span,
1555 KeyKind::Node,
1556 vec![format!("{id}:called")],
1557 );
1558 self.stdlib("&.", "else", span, KeyKind::Node, vec![format!("{id}:nil")]);
1559 }
1560
1561 fn value_logical(&mut self, op: &str, left: &Node<'_>, node_start: usize, node_end: usize) {
1562 let Some(id) = self.branch(
1563 node_start,
1564 node_end,
1565 &format!("logical-{op}"),
1566 [
1567 ("short-circuit", "short-circuited"),
1568 ("evaluated", "right operand evaluated"),
1569 ],
1570 ) else {
1571 return;
1572 };
1573 let key = self.probe_key(ProbeTarget::Logical {
1574 op: op.into(),
1575 short_circuit: format!("{id}:short-circuit"),
1576 evaluated: format!("{id}:evaluated"),
1577 });
1578 let left = left.location();
1579 self.depth += 1;
1580 self.wrap(
1581 left.start_offset(),
1582 left.end_offset(),
1583 format!("{RUBY_PROBE_RECEIVER}.l({key}, ("),
1584 );
1585 self.depth -= 1;
1586 }
1587
1588 fn op_assign(
1592 &mut self,
1593 op: &str,
1594 node_start: usize,
1595 node_end: usize,
1596 name: Option<&[u8]>,
1597 value: &Node<'_>,
1598 ) {
1599 let Some(id) = self.branch(
1600 node_start,
1601 node_end,
1602 &format!("{op}-assign"),
1603 [
1604 ("short-circuit", "assignment skipped"),
1605 ("evaluated", "value evaluated and assigned"),
1606 ],
1607 ) else {
1608 return;
1609 };
1610 match name {
1611 Some(name) => {
1612 let key = self.probe_key(ProbeTarget::Logical {
1613 op: op.into(),
1614 short_circuit: format!("{id}:short-circuit"),
1615 evaluated: format!("{id}:evaluated"),
1616 });
1617 let name = String::from_utf8_lossy(name);
1618 self.depth += 1;
1619 self.edit(
1620 node_start,
1621 EditRank::Opener,
1622 format!("({RUBY_PROBE_RECEIVER}.l({key}, {name}); "),
1623 node_end,
1624 );
1625 self.edit(node_end, EditRank::Closer, ")".into(), node_start);
1626 self.depth -= 1;
1627 }
1628 None => {
1629 let key = self.probe_key(ProbeTarget::Arrival {
1633 short_circuit: format!("{id}:short-circuit"),
1634 evaluated: format!("{id}:evaluated"),
1635 });
1636 let value_location = value.location();
1637 self.depth += 1;
1638 self.edit(
1639 node_start,
1640 EditRank::Opener,
1641 format!("({RUBY_PROBE_RECEIVER}.pre({key}); "),
1642 node_end,
1643 );
1644 self.edit(node_end, EditRank::Closer, ")".into(), node_start);
1645 self.depth += 1;
1646 self.edit(
1647 value_location.start_offset(),
1648 EditRank::Opener,
1649 format!("({RUBY_PROBE_RECEIVER}.es({key}); "),
1650 value_location.end_offset(),
1651 );
1652 self.edit(
1653 value_location.end_offset(),
1654 EditRank::Closer,
1655 ")".into(),
1656 value_location.start_offset(),
1657 );
1658 self.depth -= 2;
1659 }
1660 }
1661 }
1662
1663 fn begin_node(&mut self, node: &BeginNode<'_>, close: Option<usize>) {
1670 let has_rescue = node.rescue_clause().is_some();
1671 let has_ensure = node.ensure_clause().is_some();
1672 if !has_rescue && !has_ensure {
1673 return;
1674 }
1675 let location = node.location();
1676 let (start, end) = (location.start_offset(), location.end_offset());
1677 let Some(id) = self.branch(
1678 start,
1679 end,
1680 "begin",
1681 [
1682 ("success", "body completed"),
1683 ("raised", "exception raised"),
1684 ],
1685 ) else {
1686 return;
1687 };
1688 let mut handlers = Vec::new();
1689 let mut handler_edits = Vec::new();
1690 let mut rescue = node.rescue_clause();
1691 let mut index = 0;
1692 while let Some(clause) = rescue {
1693 let clause_location = clause.location();
1694 let Some(handler_id) = self.branch(
1695 clause_location.start_offset(),
1696 clause_location.end_offset(),
1697 &format!("rescue-{index}"),
1698 [("missed", "not selected"), ("selected", "selected")],
1699 ) else {
1700 return;
1701 };
1702 handlers.push(HandlerTarget {
1703 missed: format!("{handler_id}:missed"),
1704 selected: format!("{handler_id}:selected"),
1705 id: handler_id,
1706 });
1707 handler_edits.push(self.handler_probe_position(&clause));
1708 rescue = clause.subsequent();
1709 index += 1;
1710 }
1711 let key = self.probe_key(ProbeTarget::Try {
1712 success: format!("{id}:success"),
1713 raised: format!("{id}:raised"),
1714 id: id.clone(),
1715 handlers,
1716 });
1717 for (index, (offset, leading, scope_end)) in handler_edits.into_iter().enumerate() {
1718 let text = if leading {
1719 format!("; {RUBY_PROBE_RECEIVER}.h({key}, {index})")
1720 } else {
1721 format!("{RUBY_PROBE_RECEIVER}.h({key}, {index}); ")
1722 };
1723 self.edit(offset, EditRank::StatementProbe, text, scope_end);
1724 }
1725 let clause_offset = node
1727 .else_clause()
1728 .map(|clause| clause.else_keyword_loc().start_offset())
1729 .or_else(|| {
1730 node.ensure_clause()
1731 .map(|clause| clause.ensure_keyword_loc().start_offset())
1732 })
1733 .or_else(|| node.end_keyword_loc().map(|loc| loc.start_offset()))
1734 .or(close);
1735 match clause_offset {
1736 Some(offset) => self.edit(
1737 offset,
1738 EditRank::Clause,
1739 format!(
1740 "rescue Exception => __supercov_e; {RUBY_PROBE_RECEIVER}.p({key}); raise; "
1741 ),
1742 offset,
1743 ),
1744 None => {
1745 self.manifest.unmeasured.push(format!("{id}:raised"));
1746 let (line, _) = self.line_column(start);
1747 self.begin_unmeasured.push((id.clone(), line));
1748 }
1749 }
1750 if let Some(else_clause) = node.else_clause() {
1752 match else_clause
1753 .statements()
1754 .and_then(|statements| statements.body().iter().next())
1755 {
1756 Some(first) => self.edit(
1757 first.location().start_offset(),
1758 EditRank::StatementProbe,
1759 format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
1760 first.location().end_offset(),
1761 ),
1762 None => self.edit(
1763 else_clause.else_keyword_loc().end_offset(),
1764 EditRank::StatementProbe,
1765 format!(" {RUBY_PROBE_RECEIVER}.ok0({key});"),
1766 else_clause.else_keyword_loc().end_offset(),
1767 ),
1768 }
1769 return;
1770 }
1771 let last = node
1772 .statements()
1773 .and_then(|statements| statements.body().iter().last());
1774 match last {
1775 Some(last) => {
1776 if !self.completion_probe(last, key) {
1777 self.manifest.unmeasured.push(format!("{id}:success"));
1778 let (line, _) = self.line_column(start);
1779 self.begin_unmeasured.push((id, line));
1780 }
1781 }
1782 None => {
1783 self.manifest.unmeasured.push(format!("{id}:success"));
1784 let (line, _) = self.line_column(start);
1785 self.begin_unmeasured.push((id, line));
1786 }
1787 }
1788 }
1789
1790 fn handler_probe_position(&self, clause: &RescueNode<'_>) -> (usize, bool, usize) {
1793 if let Some(first) = clause
1794 .statements()
1795 .and_then(|statements| statements.body().iter().next())
1796 {
1797 return (
1798 first.location().start_offset(),
1799 false,
1800 first.location().end_offset(),
1801 );
1802 }
1803 let offset = if let Some(then_keyword) = clause.then_keyword_loc() {
1804 then_keyword.end_offset()
1805 } else if let Some(reference) = clause.reference() {
1806 reference.location().end_offset()
1807 } else if let Some(last) = clause.exceptions().iter().last() {
1808 last.location().end_offset()
1809 } else {
1810 clause.keyword_loc().end_offset()
1811 };
1812 (offset, true, offset)
1813 }
1814
1815 fn jump_exposed(&self, node: &Node<'_>) -> bool {
1826 if Self::is_jump(node) {
1827 return true;
1828 }
1829 if let Some(if_node) = node.as_if_node() {
1830 let else_exposed = match if_node.subsequent() {
1831 Some(subsequent) => match subsequent.as_else_node() {
1832 Some(else_node) => self.arm_jump_exposed(else_node.statements()),
1833 None => self.jump_exposed(&subsequent),
1834 },
1835 None => false,
1836 };
1837 return else_exposed || self.arm_jump_exposed(if_node.statements());
1838 }
1839 if let Some(unless_node) = node.as_unless_node() {
1840 let else_exposed = match unless_node.else_clause() {
1841 Some(else_node) => self.arm_jump_exposed(else_node.statements()),
1842 None => false,
1843 };
1844 return else_exposed || self.arm_jump_exposed(unless_node.statements());
1845 }
1846 if let Some(begin) = node.as_begin_node() {
1847 if self.arm_jump_exposed(begin.statements())
1848 || begin
1849 .else_clause()
1850 .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()))
1851 {
1852 return true;
1853 }
1854 let mut rescue = begin.rescue_clause();
1855 while let Some(clause) = rescue {
1856 if self.arm_jump_exposed(clause.statements()) {
1857 return true;
1858 }
1859 rescue = clause.subsequent();
1860 }
1861 return false;
1862 }
1863 if let Some(case_node) = node.as_case_node() {
1864 return case_node
1865 .conditions()
1866 .iter()
1867 .any(|condition| match condition.as_when_node() {
1868 Some(when_node) => self.arm_jump_exposed(when_node.statements()),
1869 None => false,
1870 })
1871 || case_node
1872 .else_clause()
1873 .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()));
1874 }
1875 if let Some(case_node) = node.as_case_match_node() {
1876 return case_node
1877 .conditions()
1878 .iter()
1879 .any(|condition| match condition.as_in_node() {
1880 Some(in_node) => self.arm_jump_exposed(in_node.statements()),
1881 None => false,
1882 })
1883 || case_node
1884 .else_clause()
1885 .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()));
1886 }
1887 if let Some(parentheses) = node.as_parentheses_node() {
1888 return match parentheses.body() {
1889 Some(body) => match body.as_statements_node() {
1890 Some(statements) => self.arm_jump_exposed(Some(statements)),
1891 None => self.jump_exposed(&body),
1892 },
1893 None => false,
1894 };
1895 }
1896 false
1897 }
1898
1899 fn arm_jump_exposed(&self, statements: Option<StatementsNode<'_>>) -> bool {
1900 match statements.and_then(|statements| statements.body().iter().last()) {
1901 Some(last) => self.jump_exposed(&last),
1902 None => false,
1903 }
1904 }
1905
1906 fn probe_targets<'n>(&self, node: Node<'n>) -> Option<Vec<Node<'n>>> {
1912 if Self::is_jump(&node) {
1913 return Some(vec![node]);
1914 }
1915 if node.as_multi_write_node().is_some()
1916 || node.as_alias_method_node().is_some()
1917 || node.as_alias_global_variable_node().is_some()
1918 || node.as_undef_node().is_some()
1919 {
1920 return None;
1921 }
1922 if !self.jump_exposed(&node) {
1923 return Some(vec![node]);
1924 }
1925 if let Some(if_node) = node.as_if_node() {
1926 let mut targets = self.arm_targets(if_node.statements())?;
1927 match if_node.subsequent() {
1928 Some(subsequent) => match subsequent.as_else_node() {
1929 Some(else_node) => targets.extend(self.arm_targets(else_node.statements())?),
1930 None => targets.extend(self.probe_targets(subsequent)?),
1931 },
1932 None => return None,
1933 }
1934 return Some(targets);
1935 }
1936 if let Some(unless_node) = node.as_unless_node() {
1937 let mut targets = self.arm_targets(unless_node.statements())?;
1938 let else_node = unless_node.else_clause()?;
1939 targets.extend(self.arm_targets(else_node.statements())?);
1940 return Some(targets);
1941 }
1942 if let Some(case_node) = node.as_case_node() {
1943 let mut targets = Vec::new();
1944 for condition in case_node.conditions().iter() {
1945 let when_node = condition.as_when_node()?;
1946 targets.extend(self.arm_targets(when_node.statements())?);
1947 }
1948 targets.extend(self.arm_targets(case_node.else_clause()?.statements())?);
1949 return Some(targets);
1950 }
1951 if let Some(case_node) = node.as_case_match_node() {
1952 let mut targets = Vec::new();
1953 for condition in case_node.conditions().iter() {
1954 let in_node = condition.as_in_node()?;
1955 targets.extend(self.arm_targets(in_node.statements())?);
1956 }
1957 targets.extend(self.arm_targets(case_node.else_clause()?.statements())?);
1958 return Some(targets);
1959 }
1960 if let Some(begin) = node.as_begin_node() {
1961 let mut targets = match begin.else_clause() {
1962 Some(else_node) => self.arm_targets(else_node.statements())?,
1963 None => self.arm_targets(begin.statements())?,
1964 };
1965 let mut rescue = begin.rescue_clause();
1966 while let Some(clause) = rescue {
1967 targets.extend(self.arm_targets(clause.statements())?);
1968 rescue = clause.subsequent();
1969 }
1970 return Some(targets);
1971 }
1972 if let Some(parentheses) = node.as_parentheses_node() {
1973 let body = parentheses.body()?;
1974 return match body.as_statements_node() {
1975 Some(statements) => self.arm_targets(Some(statements)),
1976 None => self.probe_targets(body),
1977 };
1978 }
1979 None
1980 }
1981
1982 fn arm_targets<'n>(&self, statements: Option<StatementsNode<'n>>) -> Option<Vec<Node<'n>>> {
1983 let last = statements.and_then(|statements| statements.body().iter().last())?;
1984 self.probe_targets(last)
1985 }
1986
1987 fn completion_probe(&mut self, last: Node<'_>, key: u64) -> bool {
1992 let Some(targets) = self.probe_targets(last) else {
1993 return false;
1994 };
1995 for target in &targets {
1996 self.wrap_completion(target, key);
1997 }
1998 true
1999 }
2000
2001 fn wrap_completion(&mut self, last: &Node<'_>, key: u64) {
2003 let location = last.location();
2004 let (start, end) = (location.start_offset(), location.end_offset());
2005 let arguments = if let Some(node) = last.as_return_node() {
2006 Some((node.keyword_loc(), node.arguments()))
2007 } else if let Some(node) = last.as_break_node() {
2008 Some((node.keyword_loc(), node.arguments()))
2009 } else {
2010 last.as_next_node()
2011 .map(|node| (node.keyword_loc(), node.arguments()))
2012 };
2013 if let Some((keyword, arguments)) = arguments {
2014 match arguments {
2015 Some(arguments) => {
2016 let arguments_location = arguments.location();
2017 let multiple = arguments.arguments().iter().count() > 1
2018 || arguments
2019 .arguments()
2020 .iter()
2021 .any(|argument| argument.as_splat_node().is_some());
2022 let (open, close) = if multiple {
2023 (format!("{RUBY_PROBE_RECEIVER}.ok({key}, ["), "])")
2025 } else {
2026 (format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("), "))")
2027 };
2028 self.depth += 1;
2029 self.edit(
2030 arguments_location.start_offset(),
2031 EditRank::Opener,
2032 open,
2033 arguments_location.end_offset(),
2034 );
2035 self.edit(
2036 arguments_location.end_offset(),
2037 EditRank::Closer,
2038 close.into(),
2039 arguments_location.start_offset(),
2040 );
2041 self.depth -= 1;
2042 }
2043 None => self.edit(
2044 keyword.start_offset(),
2045 EditRank::StatementProbe,
2046 format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2047 end,
2048 ),
2049 }
2050 return;
2051 }
2052 if last.as_redo_node().is_some() || last.as_retry_node().is_some() {
2053 self.edit(
2054 start,
2055 EditRank::StatementProbe,
2056 format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2057 end,
2058 );
2059 return;
2060 }
2061 self.depth += 1;
2062 self.wrap(start, end, format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("));
2063 self.depth -= 1;
2064 }
2065
2066 fn rescue_modifier(&mut self, node: &RescueModifierNode<'_>) {
2067 let location = node.location();
2068 let (start, end) = (location.start_offset(), location.end_offset());
2069 let Some(id) = self.branch(
2070 start,
2071 end,
2072 "rescue-modifier",
2073 [
2074 ("success", "expression completed"),
2075 ("raised", "fallback used"),
2076 ],
2077 ) else {
2078 return;
2079 };
2080 let key = self.probe_key(ProbeTarget::Try {
2081 success: format!("{id}:success"),
2082 raised: format!("{id}:raised"),
2083 id,
2084 handlers: Vec::new(),
2085 });
2086 let expression = node.expression();
2087 let fallback_node = node.rescue_expression();
2088 let fallback = fallback_node.location();
2089 self.depth += 1;
2090 if Self::is_jump(&expression) {
2091 self.completion_probe(expression, key);
2094 } else {
2095 let expression = expression.location();
2096 self.wrap(
2097 expression.start_offset(),
2098 expression.end_offset(),
2099 format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("),
2100 );
2101 }
2102 if Self::is_jump(&fallback_node) {
2103 self.edit(
2105 fallback.start_offset(),
2106 EditRank::Opener,
2107 format!("({RUBY_PROBE_RECEIVER}.hm0({key}); "),
2108 fallback.end_offset(),
2109 );
2110 self.edit(
2111 fallback.end_offset(),
2112 EditRank::Closer,
2113 ")".into(),
2114 fallback.start_offset(),
2115 );
2116 } else {
2117 self.wrap(
2118 fallback.start_offset(),
2119 fallback.end_offset(),
2120 format!("{RUBY_PROBE_RECEIVER}.hm({key}, ("),
2121 );
2122 }
2123 self.depth -= 1;
2124 }
2125
2126 fn is_jump(node: &Node<'_>) -> bool {
2128 node.as_return_node().is_some()
2129 || node.as_break_node().is_some()
2130 || node.as_next_node().is_some()
2131 || node.as_redo_node().is_some()
2132 || node.as_retry_node().is_some()
2133 }
2134
2135 fn def_node(&mut self, node: &DefNode<'_>) {
2136 let location = node.location();
2137 let (start, end) = (location.start_offset(), location.end_offset());
2138 let name = String::from_utf8_lossy(node.name().as_slice()).into_owned();
2139 let id = stable_id(self.file, "function", start, end, &name);
2140 if !self.point_ids.insert(id.clone()) {
2141 return;
2142 }
2143 self.push_point(&id, start, end, PointKind::Function, Some(name));
2144 let span = self.location_span(&location);
2145 self.methods.push(MethodKeyPlan {
2146 span,
2147 unshifted: span,
2148 id,
2149 });
2150 if let Some(body) = node.body()
2151 && let Some(begin) = body.as_begin_node()
2152 {
2153 self.begin_node(&begin, node.end_keyword_loc().map(|loc| loc.start_offset()));
2154 }
2155 if node.equal_loc().is_some()
2156 && let Some(body) = node.body()
2157 && let Some(statements) = body.as_statements_node()
2158 {
2159 for statement in statements.body().iter() {
2160 self.endless_bodies
2161 .insert(statement.location().start_offset());
2162 }
2163 }
2164 }
2165
2166 fn shifted(&self, span: PlanSpan, kind: KeyKind, edits: &[PendingEdit]) -> PlanSpan {
2182 let start_offset = self.line_starts[span.start[0] - 1] + span.start[1];
2183 let end_offset = self.line_starts[span.end[0] - 1] + span.end[1];
2184 let mut start_shift = 0;
2185 let mut end_shift = 0;
2186 for edit in edits {
2187 let (line, _) = self.line_column(edit.offset);
2188 let moves_start = edit.offset < start_offset
2189 || (edit.offset == start_offset
2190 && match (edit.rank, kind) {
2191 (_, KeyKind::Point) => true,
2192 (EditRank::Closer, _) => false,
2193 (EditRank::Opener, KeyKind::List) => end_offset < edit.scope,
2194 (EditRank::Opener, KeyKind::Node) => end_offset <= edit.scope,
2195 (_, KeyKind::List) => end_offset < edit.scope,
2196 (_, KeyKind::Node) => true,
2197 });
2198 let moves_end = edit.offset < end_offset
2199 || (edit.offset == end_offset
2200 && match (edit.rank, kind) {
2201 (_, KeyKind::Point) => true,
2202 (EditRank::Closer, KeyKind::List) => edit.scope >= start_offset,
2203 (EditRank::Closer, KeyKind::Node) => edit.scope > start_offset,
2204 _ => false,
2205 });
2206 if line == span.start[0] && moves_start {
2207 start_shift += edit.text.len();
2208 }
2209 if line == span.end[0] && moves_end {
2210 end_shift += edit.text.len();
2211 }
2212 }
2213 PlanSpan {
2214 start: [span.start[0], span.start[1] + start_shift],
2215 end: [span.end[0], span.end[1] + end_shift],
2216 }
2217 }
2218
2219 fn finish(mut self) -> RubyFileObligations {
2220 let mut pending = std::mem::take(&mut self.edits);
2221 pending.sort_by(|left, right| {
2222 left.offset
2223 .cmp(&right.offset)
2224 .then(left.rank.cmp(&right.rank))
2225 .then(left.order.cmp(&right.order))
2226 .then(left.sequence.cmp(&right.sequence))
2227 });
2228 let branches = std::mem::take(&mut self.branches)
2229 .into_iter()
2230 .map(|mut branch| {
2231 branch.key.span = self.shifted(branch.key.span, branch.key.kind, &pending);
2232 branch
2233 })
2234 .collect::<Vec<_>>();
2235 let cases = std::mem::take(&mut self.cases)
2236 .into_iter()
2237 .map(|mut case| {
2238 for clause in &mut case.clauses {
2239 clause.key.span = self.shifted(clause.key.span, clause.key.kind, &pending);
2240 }
2241 if let Some(no_match) = &mut case.no_match {
2242 no_match.key.span =
2243 self.shifted(no_match.key.span, no_match.key.kind, &pending);
2244 }
2245 case
2246 })
2247 .collect();
2248 let methods = std::mem::take(&mut self.methods)
2249 .into_iter()
2250 .map(|mut method| {
2251 method.span = self.shifted(method.span, KeyKind::Node, &pending);
2252 method
2253 })
2254 .collect();
2255 let edits = pending
2256 .into_iter()
2257 .map(|edit| Edit {
2258 offset: edit.offset,
2259 text: edit.text,
2260 rank: match edit.rank {
2261 EditRank::Clause => "clause",
2262 EditRank::StatementProbe => "statement",
2263 EditRank::Opener => "opener",
2264 EditRank::Closer => "closer",
2265 }
2266 .into(),
2267 scope: edit.scope,
2268 })
2269 .collect::<Vec<_>>();
2270 if let Some((id, line)) = self.begin_unmeasured.first() {
2271 let source = self
2272 .manifest
2273 .branches
2274 .iter()
2275 .find(|branch| &branch.id == id)
2276 .map(|branch| branch.source.lines().next().unwrap_or_default().to_owned())
2277 .unwrap_or_default();
2278 self.manifest.limitations.push(limitation(
2279 BEGIN_BODY_LIMITATION,
2280 self.file,
2281 *line,
2282 &source,
2283 "a begin body that is empty, or ends in a statement with no expression form, cannot have its completion observed",
2284 ));
2285 }
2286 self.manifest.unmeasured.sort();
2287 self.manifest.unmeasured.dedup();
2288 RubyFileObligations {
2289 manifest: self.manifest,
2290 plan: RubyFilePlan {
2291 probe_obligations: probe_obligations_of(&self.probes),
2292 edits,
2293 lines: self.lines,
2294 statement_offsets: self.statement_offsets,
2295 branches,
2296 methods,
2297 cases,
2298 },
2299 probes: self.probes,
2300 }
2301 }
2302}
2303
2304fn limitation(id: &str, file: &str, line: usize, source: &str, reason: &str) -> serde_json::Value {
2305 json!({
2306 "id": id,
2307 "kind": "semantic-safety",
2308 "file": file,
2309 "line": line,
2310 "column": 0,
2311 "source": source,
2312 "reason": reason
2313 })
2314}
2315
2316impl<'pr> Visit<'pr> for Collector<'_> {
2317 fn visit_statements_node(&mut self, node: &StatementsNode<'pr>) {
2318 if !self
2319 .expression_lists
2320 .contains(&node.location().start_offset())
2321 {
2322 self.statements(node);
2323 }
2324 ruby_prism::visit_statements_node(self, node);
2325 }
2326
2327 fn visit_parentheses_node(&mut self, node: &ruby_prism::ParenthesesNode<'pr>) {
2328 if let Some(body) = node.body() {
2329 self.expression_lists.insert(body.location().start_offset());
2330 }
2331 ruby_prism::visit_parentheses_node(self, node);
2332 }
2333
2334 fn visit_embedded_statements_node(&mut self, node: &ruby_prism::EmbeddedStatementsNode<'pr>) {
2335 if let Some(statements) = node.statements() {
2336 self.expression_lists
2337 .insert(statements.location().start_offset());
2338 }
2339 ruby_prism::visit_embedded_statements_node(self, node);
2340 }
2341
2342 fn visit_if_node(&mut self, node: &IfNode<'pr>) {
2343 let offset = node.location().start_offset();
2344 if self.guard_nodes.contains(&offset) || self.elsif_nodes.contains(&offset) {
2345 self.depth += 1;
2346 ruby_prism::visit_if_node(self, node);
2347 self.depth -= 1;
2348 return;
2349 }
2350 if let Some(truthy) = self.literal_truth(node.predicate()) {
2351 self.depth += 1;
2354 if truthy {
2355 if let Some(statements) = node.statements() {
2356 self.visit_statements_node(&statements);
2357 }
2358 } else if let Some(subsequent) = node.subsequent() {
2359 match subsequent.as_if_node() {
2360 Some(elsif) => self.visit_if_node(&elsif),
2361 None => {
2362 if let Some(else_node) = subsequent.as_else_node()
2363 && let Some(statements) = else_node.statements()
2364 {
2365 self.visit_statements_node(&statements);
2366 }
2367 }
2368 }
2369 }
2370 self.depth -= 1;
2371 return;
2372 }
2373 let kind = if node.if_keyword_loc().is_none() {
2376 "ternary"
2377 } else {
2378 "if"
2379 };
2380 self.if_node(node, kind);
2381 let mut subsequent = node.subsequent();
2382 while let Some(next) = subsequent {
2383 match next.as_if_node() {
2384 Some(elsif) => {
2385 self.elsif_nodes.insert(elsif.location().start_offset());
2386 self.if_node(&elsif, "elsif");
2387 subsequent = elsif.subsequent();
2388 }
2389 None => break,
2390 }
2391 }
2392 self.depth += 1;
2393 ruby_prism::visit_if_node(self, node);
2397 self.depth -= 1;
2398 }
2399
2400 fn visit_unless_node(&mut self, node: &ruby_prism::UnlessNode<'pr>) {
2401 if self.guard_nodes.contains(&node.location().start_offset()) {
2402 self.depth += 1;
2403 ruby_prism::visit_unless_node(self, node);
2404 self.depth -= 1;
2405 return;
2406 }
2407 if let Some(truthy) = self.literal_truth(node.predicate()) {
2408 self.depth += 1;
2409 if truthy {
2410 if let Some(else_node) = node.else_clause()
2411 && let Some(statements) = else_node.statements()
2412 {
2413 self.visit_statements_node(&statements);
2414 }
2415 } else if let Some(statements) = node.statements() {
2416 self.visit_statements_node(&statements);
2417 }
2418 self.depth -= 1;
2419 return;
2420 }
2421 self.unless_node(node);
2422 self.depth += 1;
2423 ruby_prism::visit_unless_node(self, node);
2424 self.depth -= 1;
2425 }
2426
2427 fn visit_while_node(&mut self, node: &WhileNode<'pr>) {
2428 self.loop_node(
2429 &node.location(),
2430 node.predicate(),
2431 node.statements(),
2432 node.is_begin_modifier(),
2433 false,
2434 );
2435 self.depth += 1;
2436 ruby_prism::visit_while_node(self, node);
2437 self.depth -= 1;
2438 }
2439
2440 fn visit_until_node(&mut self, node: &UntilNode<'pr>) {
2441 self.loop_node(
2442 &node.location(),
2443 node.predicate(),
2444 node.statements(),
2445 node.is_begin_modifier(),
2446 true,
2447 );
2448 self.depth += 1;
2449 ruby_prism::visit_until_node(self, node);
2450 self.depth -= 1;
2451 }
2452
2453 fn visit_for_node(&mut self, node: &ForNode<'pr>) {
2454 self.for_node(node);
2455 self.depth += 1;
2456 ruby_prism::visit_for_node(self, node);
2457 self.depth -= 1;
2458 }
2459
2460 fn visit_case_node(&mut self, node: &CaseNode<'pr>) {
2461 self.case_node(node);
2462 self.depth += 1;
2463 ruby_prism::visit_case_node(self, node);
2464 self.depth -= 1;
2465 }
2466
2467 fn visit_case_match_node(&mut self, node: &CaseMatchNode<'pr>) {
2468 self.case_match_node(node);
2469 self.depth += 1;
2470 ruby_prism::visit_case_match_node(self, node);
2471 self.depth -= 1;
2472 }
2473
2474 fn visit_and_node(&mut self, node: &AndNode<'pr>) {
2475 let location = node.location();
2476 if !self.tree_logicals.contains(&location.start_offset()) {
2477 let left = node.left();
2478 self.value_logical("and", &left, location.start_offset(), location.end_offset());
2479 }
2480 self.depth += 1;
2481 ruby_prism::visit_and_node(self, node);
2482 self.depth -= 1;
2483 }
2484
2485 fn visit_or_node(&mut self, node: &OrNode<'pr>) {
2486 let location = node.location();
2487 if !self.tree_logicals.contains(&location.start_offset()) {
2488 let left = node.left();
2489 self.value_logical("or", &left, location.start_offset(), location.end_offset());
2490 }
2491 self.depth += 1;
2492 ruby_prism::visit_or_node(self, node);
2493 self.depth -= 1;
2494 }
2495
2496 fn visit_call_node(&mut self, node: &CallNode<'pr>) {
2497 if node.is_safe_navigation() {
2498 self.safe_navigation(node);
2499 }
2500 if let Some(block) = node.block()
2501 && let Some(block) = block.as_block_node()
2502 && let Some(receiver) = node.receiver()
2503 && !node.is_safe_navigation()
2504 && ITERATORS.contains(&node.name().as_slice())
2505 {
2506 self.iterator_loop(node, &receiver, &block);
2507 }
2508 self.depth += 1;
2509 ruby_prism::visit_call_node(self, node);
2510 self.depth -= 1;
2511 }
2512
2513 fn visit_local_variable_or_write_node(
2514 &mut self,
2515 node: &ruby_prism::LocalVariableOrWriteNode<'pr>,
2516 ) {
2517 let location = node.location();
2518 let value = node.value();
2519 self.op_assign(
2520 "or",
2521 location.start_offset(),
2522 location.end_offset(),
2523 Some(node.name().as_slice()),
2524 &value,
2525 );
2526 self.depth += 1;
2527 ruby_prism::visit_local_variable_or_write_node(self, node);
2528 self.depth -= 1;
2529 }
2530
2531 fn visit_local_variable_and_write_node(
2532 &mut self,
2533 node: &ruby_prism::LocalVariableAndWriteNode<'pr>,
2534 ) {
2535 let location = node.location();
2536 let value = node.value();
2537 self.op_assign(
2538 "and",
2539 location.start_offset(),
2540 location.end_offset(),
2541 Some(node.name().as_slice()),
2542 &value,
2543 );
2544 self.depth += 1;
2545 ruby_prism::visit_local_variable_and_write_node(self, node);
2546 self.depth -= 1;
2547 }
2548
2549 fn visit_instance_variable_or_write_node(
2550 &mut self,
2551 node: &ruby_prism::InstanceVariableOrWriteNode<'pr>,
2552 ) {
2553 let location = node.location();
2554 let value = node.value();
2555 self.op_assign(
2556 "or",
2557 location.start_offset(),
2558 location.end_offset(),
2559 Some(node.name().as_slice()),
2560 &value,
2561 );
2562 self.depth += 1;
2563 ruby_prism::visit_instance_variable_or_write_node(self, node);
2564 self.depth -= 1;
2565 }
2566
2567 fn visit_instance_variable_and_write_node(
2568 &mut self,
2569 node: &ruby_prism::InstanceVariableAndWriteNode<'pr>,
2570 ) {
2571 let location = node.location();
2572 let value = node.value();
2573 self.op_assign(
2574 "and",
2575 location.start_offset(),
2576 location.end_offset(),
2577 Some(node.name().as_slice()),
2578 &value,
2579 );
2580 self.depth += 1;
2581 ruby_prism::visit_instance_variable_and_write_node(self, node);
2582 self.depth -= 1;
2583 }
2584
2585 fn visit_global_variable_or_write_node(
2586 &mut self,
2587 node: &ruby_prism::GlobalVariableOrWriteNode<'pr>,
2588 ) {
2589 let location = node.location();
2590 let value = node.value();
2591 self.op_assign(
2592 "or",
2593 location.start_offset(),
2594 location.end_offset(),
2595 Some(node.name().as_slice()),
2596 &value,
2597 );
2598 self.depth += 1;
2599 ruby_prism::visit_global_variable_or_write_node(self, node);
2600 self.depth -= 1;
2601 }
2602
2603 fn visit_class_variable_or_write_node(
2604 &mut self,
2605 node: &ruby_prism::ClassVariableOrWriteNode<'pr>,
2606 ) {
2607 let location = node.location();
2608 let value = node.value();
2609 self.op_assign(
2610 "or",
2611 location.start_offset(),
2612 location.end_offset(),
2613 Some(node.name().as_slice()),
2614 &value,
2615 );
2616 self.depth += 1;
2617 ruby_prism::visit_class_variable_or_write_node(self, node);
2618 self.depth -= 1;
2619 }
2620
2621 fn visit_call_or_write_node(&mut self, node: &ruby_prism::CallOrWriteNode<'pr>) {
2622 let location = node.location();
2623 let value = node.value();
2624 self.op_assign(
2625 "or",
2626 location.start_offset(),
2627 location.end_offset(),
2628 None,
2629 &value,
2630 );
2631 self.depth += 1;
2632 ruby_prism::visit_call_or_write_node(self, node);
2633 self.depth -= 1;
2634 }
2635
2636 fn visit_call_and_write_node(&mut self, node: &ruby_prism::CallAndWriteNode<'pr>) {
2637 let location = node.location();
2638 let value = node.value();
2639 self.op_assign(
2640 "and",
2641 location.start_offset(),
2642 location.end_offset(),
2643 None,
2644 &value,
2645 );
2646 self.depth += 1;
2647 ruby_prism::visit_call_and_write_node(self, node);
2648 self.depth -= 1;
2649 }
2650
2651 fn visit_index_or_write_node(&mut self, node: &ruby_prism::IndexOrWriteNode<'pr>) {
2652 let location = node.location();
2653 let value = node.value();
2654 self.op_assign(
2655 "or",
2656 location.start_offset(),
2657 location.end_offset(),
2658 None,
2659 &value,
2660 );
2661 self.depth += 1;
2662 ruby_prism::visit_index_or_write_node(self, node);
2663 self.depth -= 1;
2664 }
2665
2666 fn visit_index_and_write_node(&mut self, node: &ruby_prism::IndexAndWriteNode<'pr>) {
2667 let location = node.location();
2668 let value = node.value();
2669 self.op_assign(
2670 "and",
2671 location.start_offset(),
2672 location.end_offset(),
2673 None,
2674 &value,
2675 );
2676 self.depth += 1;
2677 ruby_prism::visit_index_and_write_node(self, node);
2678 self.depth -= 1;
2679 }
2680
2681 fn visit_constant_or_write_node(&mut self, node: &ruby_prism::ConstantOrWriteNode<'pr>) {
2682 let location = node.location();
2683 let value = node.value();
2684 self.op_assign(
2685 "or",
2686 location.start_offset(),
2687 location.end_offset(),
2688 None,
2689 &value,
2690 );
2691 self.depth += 1;
2692 ruby_prism::visit_constant_or_write_node(self, node);
2693 self.depth -= 1;
2694 }
2695
2696 fn visit_def_node(&mut self, node: &DefNode<'pr>) {
2697 self.def_node(node);
2698 self.depth += 1;
2699 ruby_prism::visit_def_node(self, node);
2700 self.depth -= 1;
2701 }
2702
2703 fn visit_begin_node(&mut self, node: &BeginNode<'pr>) {
2704 if node.begin_keyword_loc().is_some() {
2708 self.begin_node(node, None);
2709 }
2710 self.depth += 1;
2711 ruby_prism::visit_begin_node(self, node);
2712 self.depth -= 1;
2713 }
2714
2715 fn visit_block_node(&mut self, node: &ruby_prism::BlockNode<'pr>) {
2716 if let Some(body) = node.body()
2717 && let Some(begin) = body.as_begin_node()
2718 {
2719 self.begin_node(&begin, Some(node.closing_loc().start_offset()));
2720 }
2721 self.depth += 1;
2722 ruby_prism::visit_block_node(self, node);
2723 self.depth -= 1;
2724 }
2725
2726 fn visit_lambda_node(&mut self, node: &ruby_prism::LambdaNode<'pr>) {
2727 if let Some(body) = node.body()
2728 && let Some(begin) = body.as_begin_node()
2729 {
2730 self.begin_node(&begin, Some(node.closing_loc().start_offset()));
2731 }
2732 self.depth += 1;
2733 ruby_prism::visit_lambda_node(self, node);
2734 self.depth -= 1;
2735 }
2736
2737 fn visit_rescue_modifier_node(&mut self, node: &RescueModifierNode<'pr>) {
2738 self.rescue_modifier(node);
2739 self.depth += 1;
2740 ruby_prism::visit_rescue_modifier_node(self, node);
2741 self.depth -= 1;
2742 }
2743}
2744
2745pub fn build_ruby_obligations(
2748 file: &str,
2749 source: &[u8],
2750 next_probe: &mut u64,
2751) -> Result<RubyFileObligations, RubyInstrumenterError> {
2752 let result = ruby_prism::parse(source);
2753 let errors = result
2754 .errors()
2755 .map(|error| error.message().to_owned())
2756 .collect::<Vec<_>>();
2757 if !errors.is_empty() {
2758 return Err(RubyInstrumenterError::Parse(errors.join("; ")));
2759 }
2760 let mut collector = Collector::new(file, source, next_probe);
2761 collector.visit(&result.node());
2762 if let Some(error) = collector.error.take() {
2763 return Err(error);
2764 }
2765 Ok(collector.finish())
2766}
2767
2768pub fn apply_edits(source: &[u8], edits: &[Edit]) -> Vec<u8> {
2770 let mut output =
2771 Vec::with_capacity(source.len() + edits.iter().map(|e| e.text.len()).sum::<usize>());
2772 let mut cursor = 0;
2773 for edit in edits {
2774 output.extend_from_slice(&source[cursor..edit.offset]);
2775 output.extend_from_slice(edit.text.as_bytes());
2776 cursor = edit.offset;
2777 }
2778 output.extend_from_slice(&source[cursor..]);
2779 output
2780}
2781
2782#[cfg(test)]
2783mod tests {
2784 use super::*;
2785
2786 const SOURCE: &str = r#"class Shapes
2787 def classify(a, b, c)
2788 if a && (b || c)
2789 :yes
2790 elsif a
2791 :half
2792 else
2793 :no
2794 end
2795 end
2796
2797 def loops(items, flag)
2798 total = 0
2799 items.each { |i| total += i if i > 2 && flag }
2800 while total > 100
2801 total -= 50
2802 end
2803 for x in items do total += x end
2804 total
2805 end
2806
2807 def logical(a, b)
2808 x = a || b
2809 @cache ||= {}
2810 @cache[a] ||= b
2811 y = a ? 1 : 2; z = a&.size
2812 [x, y, z]
2813 end
2814
2815 def guarded(s)
2816 Integer(s)
2817 rescue ArgumentError
2818 -1
2819 ensure
2820 @done = true
2821 end
2822
2823 def cases(v)
2824 case v
2825 when 0 then :zero
2826 else :other
2827 end
2828 v.to_s rescue "bad"
2829 end
2830end
2831"#;
2832
2833 #[test]
2834 fn discovers_obligations_with_stable_ids_and_newline_free_edits() {
2835 let mut probe = 0;
2836 let first = build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
2837 let mut probe = 0;
2838 let second =
2839 build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
2840 assert_eq!(first, second);
2841 let manifest = &first.manifest;
2842 let functions = manifest
2843 .points
2844 .iter()
2845 .filter(|point| point.kind == PointKind::Function)
2846 .map(|point| point.label.clone().unwrap())
2847 .collect::<Vec<_>>();
2848 assert_eq!(
2849 functions,
2850 ["classify", "loops", "logical", "guarded", "cases"]
2851 );
2852 let compound = manifest
2853 .decisions
2854 .iter()
2855 .find(|decision| decision.source == "a && (b || c)")
2856 .unwrap();
2857 assert_eq!(compound.conditions, ["a", "b", "c"]);
2858 assert_eq!(compound.kind, "if");
2859 assert!(
2860 manifest
2861 .decisions
2862 .iter()
2863 .any(|d| d.kind == "elsif" && d.conditions == ["a"])
2864 );
2865 assert!(manifest.decisions.iter().any(|d| d.kind == "ternary"));
2866 assert!(manifest.decisions.iter().any(|d| d.kind == "while"));
2867 for kind in [
2868 "for",
2869 "logical-or",
2870 "or-assign",
2871 "safe-navigation",
2872 "begin",
2873 "rescue-0",
2874 "case-when-0",
2875 "case-else",
2876 "rescue-modifier",
2877 ] {
2878 assert!(
2879 manifest.branches.iter().any(|branch| branch.kind == kind),
2880 "missing branch kind {kind}"
2881 );
2882 }
2883 for edit in &first.plan.edits {
2884 assert!(!edit.text.contains('\n'));
2885 }
2886 assert!(
2887 first
2888 .plan
2889 .edits
2890 .windows(2)
2891 .all(|pair| pair[0].offset <= pair[1].offset)
2892 );
2893 assert!(manifest.unmeasured.is_empty());
2896 assert!(manifest.limitations.is_empty());
2897 }
2898
2899 #[test]
2900 fn transformed_source_keeps_line_count_and_carries_probes() {
2901 let mut probe = 0;
2902 let obligations =
2903 build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
2904 let transformed =
2905 String::from_utf8(apply_edits(SOURCE.as_bytes(), &obligations.plan.edits)).unwrap();
2906 assert_eq!(transformed.lines().count(), SOURCE.lines().count());
2907 assert!(
2908 transformed.contains("if $__supercov.d(0, ($__supercov.c(0, 0, (a)) && ($__supercov.c(0, 1, (b)) || $__supercov.c(0, 2, (c)))))"),
2909 "{transformed}"
2910 );
2911 assert!(transformed.contains("while $__supercov.w("));
2912 assert!(transformed.contains("for x in $__supercov.f("));
2913 assert!(transformed.contains("do $__supercov.fb("));
2914 assert!(transformed.contains("x = $__supercov.l("));
2915 assert!(transformed.contains("($__supercov.pre("));
2916 assert!(transformed.contains("||= ($__supercov.es("));
2917 assert!(transformed.contains("rescue Exception => __supercov_e; $__supercov.p("));
2918 assert!(transformed.contains("$__supercov.h("));
2919 assert!(transformed.contains("$__supercov.ok("));
2920 assert!(transformed.contains("rescue $__supercov.hm("));
2921 assert!(transformed.contains("; $__supercov.s("));
2923 assert!(
2925 obligations
2926 .plan
2927 .branches
2928 .iter()
2929 .any(|branch| !branch.hits.is_empty() && branch.key.group == "if")
2930 );
2931 }
2932
2933 #[test]
2934 fn jumps_and_endless_bodies_take_wrapped_probes_and_literal_predicates_fold() {
2935 let source = "def inc(x) = x + 1\n\
2936 [1].each { |v| y = Integer(v) rescue next }\n\
2937 if false\n dead\nelse\n live\nend\n";
2938 let mut probe = 0;
2939 let obligations =
2940 build_ruby_obligations("lib/x.rb", source.as_bytes(), &mut probe).unwrap();
2941 let transformed =
2942 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
2943 assert!(
2944 transformed.contains("def inc(x) = ($__supercov.s("),
2945 "{transformed}"
2946 );
2947 assert!(
2948 transformed.contains("rescue ($__supercov.hm0("),
2949 "{transformed}"
2950 );
2951 assert!(
2953 obligations
2954 .plan
2955 .branches
2956 .iter()
2957 .all(|branch| branch.key.group != "if")
2958 );
2959 assert!(
2960 obligations
2961 .manifest
2962 .points
2963 .iter()
2964 .all(|point| point.source != "dead")
2965 );
2966 assert!(
2967 obligations
2968 .manifest
2969 .points
2970 .iter()
2971 .any(|point| point.source == "live")
2972 );
2973 }
2974
2975 #[test]
2976 fn void_valued_last_statements_are_probed_arm_by_arm() {
2977 let source =
2980 "def m(c)\n if c\n return 1\n else\n return 2\n end\nrescue\n nil\nend\n";
2981 let mut probe = 0;
2982 let obligations =
2983 build_ruby_obligations("lib/v.rb", source.as_bytes(), &mut probe).unwrap();
2984 let transformed =
2985 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
2986 assert_eq!(
2987 transformed.matches("$__supercov.ok(").count(),
2988 2,
2989 "{transformed}"
2990 );
2991 assert!(
2992 transformed.contains("return $__supercov.ok("),
2993 "{transformed}"
2994 );
2995 assert!(
2996 !obligations
2997 .manifest
2998 .unmeasured
2999 .iter()
3000 .any(|id| id.ends_with(":success")),
3001 "{:?}",
3002 obligations.manifest.unmeasured
3003 );
3004 assert!(!obligations.plan.probe_obligations.is_empty());
3005 }
3006
3007 #[test]
3008 fn expressions_that_can_return_are_never_wrapped() {
3009 let source = "def m(a, d)\n begin\n if a\n return d\n end\n ensure\n unlock\n end\n d\nend\n";
3012 let mut probe = 0;
3013 let obligations =
3014 build_ruby_obligations("lib/e.rb", source.as_bytes(), &mut probe).unwrap();
3015 let transformed =
3016 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3017 assert!(!transformed.contains("$__supercov.ok"), "{transformed}");
3018 assert!(
3019 obligations
3020 .manifest
3021 .unmeasured
3022 .iter()
3023 .any(|id| id.ends_with(":success")),
3024 "the begin's completion is declared instead"
3025 );
3026 let both = "def m(a, d)\n begin\n if a\n return d\n else\n d + 1\n end\n ensure\n unlock\n end\nend\n";
3028 let mut probe = 0;
3029 let obligations = build_ruby_obligations("lib/f.rb", both.as_bytes(), &mut probe).unwrap();
3030 let transformed =
3031 String::from_utf8(apply_edits(both.as_bytes(), &obligations.plan.edits)).unwrap();
3032 assert!(
3033 transformed.contains("return $__supercov.ok("),
3034 "{transformed}"
3035 );
3036 assert!(
3037 !obligations
3038 .manifest
3039 .unmeasured
3040 .iter()
3041 .any(|id| id.ends_with(":success")),
3042 "{:?}",
3043 obligations.manifest.unmeasured
3044 );
3045 }
3046
3047 #[test]
3048 fn stdlib_keys_shift_with_insertions_on_their_line() {
3049 let source = "def f(a, b)\n x = 1 if a && b\nend\n";
3050 let mut probe = 0;
3051 let obligations = build_ruby_obligations("m.rb", source.as_bytes(), &mut probe).unwrap();
3052 let then_key = obligations
3053 .plan
3054 .branches
3055 .iter()
3056 .find(|branch| branch.key.branch == "then")
3057 .unwrap();
3058 assert_eq!(then_key.key.span.start, [2, 2]);
3060 assert_eq!(then_key.key.span.end, [2, 7]);
3063 let else_key = obligations
3064 .plan
3065 .branches
3066 .iter()
3067 .find(|branch| branch.key.branch == "else")
3068 .unwrap();
3069 let inserted: usize = obligations
3072 .plan
3073 .edits
3074 .iter()
3075 .map(|edit| edit.text.len())
3076 .sum();
3077 assert_eq!(else_key.key.span.end, [2, 17 + inserted]);
3078 assert!(
3079 then_key.hits.len() == 1,
3080 "modifier body statement proven by the then key"
3081 );
3082 }
3083
3084 #[test]
3085 fn rejects_invalid_ruby() {
3086 let mut probe = 0;
3087 assert!(matches!(
3088 build_ruby_obligations("m.rb", b"def x(\n", &mut probe),
3089 Err(RubyInstrumenterError::Parse(_))
3090 ));
3091 }
3092}