1use std::collections::{BTreeMap, BTreeSet};
31
32use ruby_prism::{
33 AndNode, BeginNode, CallNode, CaseMatchNode, CaseNode, DefNode, ForNode, IfNode, Location,
34 Node, OrNode, RescueModifierNode, RescueNode, StatementsNode, UnlessNode, UntilNode, Visit,
35 WhileNode,
36};
37use serde::{Deserialize, Serialize};
38use serde_json::json;
39use sha2::{Digest, Sha256};
40
41use crate::{
42 coverage_analysis::PointKind,
43 coverage_report::{
44 BranchAlternativeMeta, BranchMeta, CoverageManifest, DecisionMeta, PointMeta,
45 },
46};
47
48pub const RUBY_PROBE_PLAN_VERSION: u32 = 1;
49pub const RUBY_PROBE_RECEIVER: &str = "$__supercov";
52
53const ITERATORS: &[&[u8]] = &[
57 b"each",
58 b"each_with_index",
59 b"each_with_object",
60 b"each_pair",
61 b"each_key",
62 b"each_value",
63 b"each_char",
64 b"each_byte",
65 b"each_line",
66 b"each_slice",
67 b"each_cons",
68 b"each_entry",
69 b"each_index",
70 b"reverse_each",
71 b"map",
72 b"collect",
73 b"flat_map",
74 b"collect_concat",
75 b"filter_map",
76 b"select",
77 b"filter",
78 b"reject",
79 b"find",
80 b"detect",
81 b"find_index",
82 b"find_all",
83 b"all?",
84 b"any?",
85 b"none?",
86 b"one?",
87 b"count",
88 b"sum",
89 b"min_by",
90 b"max_by",
91 b"sort_by",
92 b"group_by",
93 b"partition",
94 b"inject",
95 b"reduce",
96 b"take_while",
97 b"drop_while",
98 b"times",
99 b"upto",
100 b"downto",
101 b"step",
102];
103pub const BEGIN_BODY_LIMITATION: &str = "ruby-begin-completion-unmeasured";
104pub const RACTOR_BLOCK_LIMITATION: &str = "ruby-ractor-block-unprobed";
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum RubyInstrumenterError {
112 Parse(String),
113 InvalidRange,
114}
115
116impl std::fmt::Display for RubyInstrumenterError {
117 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 match self {
119 Self::Parse(error) => write!(formatter, "Ruby parse failed: {error}"),
120 Self::InvalidRange => write!(formatter, "Ruby parser returned an invalid range"),
121 }
122 }
123}
124
125impl std::error::Error for RubyInstrumenterError {}
126
127#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
131#[serde(rename_all = "camelCase")]
132pub enum KeyKind {
133 List,
137 Node,
139 Point,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
145#[serde(from = "[[usize; 2]; 2]", into = "[[usize; 2]; 2]")]
146pub struct PlanSpan {
147 pub start: [usize; 2],
148 pub end: [usize; 2],
149}
150
151impl From<[[usize; 2]; 2]> for PlanSpan {
152 fn from(value: [[usize; 2]; 2]) -> Self {
153 Self {
154 start: value[0],
155 end: value[1],
156 }
157 }
158}
159
160impl From<PlanSpan> for [[usize; 2]; 2] {
161 fn from(value: PlanSpan) -> Self {
162 [value.start, value.end]
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase", deny_unknown_fields)]
171pub struct Edit {
172 pub offset: usize,
173 pub text: String,
174 pub rank: String,
178 pub scope: usize,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase", deny_unknown_fields)]
189pub struct StdlibKey {
190 pub group: String,
191 pub branch: String,
192 pub kind: KeyKind,
195 pub span: PlanSpan,
197 pub unshifted: PlanSpan,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase", deny_unknown_fields)]
204pub struct StdlibDecision {
205 pub id: String,
206 pub value: bool,
207 pub outcome: String,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase", deny_unknown_fields)]
212pub struct BranchKeyPlan {
213 pub key: StdlibKey,
214 pub hits: Vec<String>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub decision: Option<StdlibDecision>,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase", deny_unknown_fields)]
224pub struct MethodKeyPlan {
225 pub span: PlanSpan,
226 pub unshifted: PlanSpan,
227 pub id: String,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
235#[serde(rename_all = "camelCase", deny_unknown_fields)]
236pub struct ImpliedPlan {
237 #[serde(default, skip_serializing_if = "Vec::is_empty")]
238 pub hits: Vec<String>,
239 #[serde(default, skip_serializing_if = "Vec::is_empty")]
240 pub decisions: Vec<StdlibDecision>,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase", deny_unknown_fields)]
245pub struct CaseClausePlan {
246 pub key: StdlibKey,
247 pub missed: String,
248 pub selected: String,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase", deny_unknown_fields)]
253pub struct CaseNoMatchPlan {
254 pub key: StdlibKey,
255 pub matched: String,
256 pub unmatched: String,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase", deny_unknown_fields)]
264pub struct CasePlan {
265 pub clauses: Vec<CaseClausePlan>,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub no_match: Option<CaseNoMatchPlan>,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(untagged)]
273pub enum ConditionTree {
274 Leaf(usize),
275 Node {
276 op: String,
277 items: Vec<ConditionTree>,
278 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
279 negate: bool,
280 },
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase", deny_unknown_fields)]
285pub struct DerivedLogical {
286 pub previous_leaves: Vec<usize>,
287 pub operand_leaves: Vec<usize>,
288 pub short_circuit: String,
289 pub evaluated: String,
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(rename_all = "camelCase", deny_unknown_fields)]
294pub struct LoopTarget {
295 pub id: String,
296 pub zero: String,
297 pub entered: String,
298 pub until: bool,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303#[serde(rename_all = "camelCase", deny_unknown_fields)]
304pub struct HandlerTarget {
305 pub id: String,
306 pub missed: String,
307 pub selected: String,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313#[serde(tag = "kind", rename_all = "camelCase", deny_unknown_fields)]
314pub enum ProbeTarget {
315 #[serde(rename_all = "camelCase")]
317 Statement { id: String },
318 #[serde(rename_all = "camelCase")]
320 Decision {
321 id: String,
322 width: usize,
323 not: Vec<bool>,
324 tree: ConditionTree,
325 outcome_true: String,
326 outcome_false: String,
327 logical: Vec<DerivedLogical>,
328 #[serde(rename = "loop", skip_serializing_if = "Option::is_none")]
329 loop_: Option<LoopTarget>,
330 },
331 #[serde(rename_all = "camelCase")]
333 For {
334 id: String,
335 zero: String,
336 entered: String,
337 },
338 #[serde(rename_all = "camelCase")]
340 Logical {
341 op: String,
342 short_circuit: String,
343 evaluated: String,
344 },
345 #[serde(rename_all = "camelCase")]
349 Arrival {
350 short_circuit: String,
351 evaluated: String,
352 },
353 #[serde(rename_all = "camelCase")]
356 SafeNavigation { nil: String, called: String },
357 #[serde(rename_all = "camelCase")]
360 Hits { ids: Vec<String> },
361 #[serde(rename_all = "camelCase")]
364 Try {
365 id: String,
366 success: String,
367 raised: String,
368 handlers: Vec<HandlerTarget>,
369 },
370}
371
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
373#[serde(rename_all = "camelCase", deny_unknown_fields)]
374pub struct RubyFilePlan {
375 pub edits: Vec<Edit>,
376 pub lines: BTreeMap<usize, String>,
378 pub statement_offsets: BTreeMap<String, [usize; 2]>,
382 pub branches: Vec<BranchKeyPlan>,
383 pub methods: Vec<MethodKeyPlan>,
384 pub cases: Vec<CasePlan>,
385 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
387 pub implied: BTreeMap<String, ImpliedPlan>,
388 #[serde(default)]
391 pub probe_obligations: Vec<String>,
392 #[serde(default, skip_serializing_if = "Vec::is_empty")]
395 pub ractor_blocks: Vec<[usize; 2]>,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
399#[serde(rename_all = "camelCase", deny_unknown_fields)]
400pub struct RubyProbePlan {
401 pub version: u32,
402 pub root: String,
403 pub receiver: String,
404 pub files: BTreeMap<String, RubyFilePlan>,
405 pub probes: BTreeMap<u64, ProbeTarget>,
406 #[serde(default)]
409 pub probe_obligations: Vec<String>,
410}
411
412impl RubyProbePlan {
413 pub fn probe_obligations(&self) -> Vec<String> {
418 let mut ids = self
419 .files
420 .values()
421 .flat_map(|file| file.probe_obligations.iter().cloned())
422 .collect::<Vec<_>>();
423 ids.sort();
424 ids.dedup();
425 ids
426 }
427}
428
429fn probe_obligations_of(probes: &BTreeMap<u64, ProbeTarget>) -> Vec<String> {
432 {
433 let mut ids = BTreeSet::new();
434 for target in probes.values() {
435 match target {
436 ProbeTarget::Statement { id } => {
437 ids.insert(id.clone());
438 }
439 ProbeTarget::Decision {
440 id,
441 outcome_true,
442 logical,
443 loop_,
444 ..
445 } => {
446 ids.insert(id.clone());
447 ids.insert(branch_of(outcome_true));
448 for derived in logical {
449 ids.insert(branch_of(&derived.short_circuit));
450 }
451 if let Some(loop_) = loop_ {
452 ids.insert(loop_.id.clone());
453 }
454 }
455 ProbeTarget::For { id, .. } => {
456 ids.insert(id.clone());
457 }
458 ProbeTarget::Logical { short_circuit, .. } => {
459 ids.insert(branch_of(short_circuit));
460 }
461 ProbeTarget::Arrival { short_circuit, .. } => {
462 ids.insert(branch_of(short_circuit));
463 }
464 ProbeTarget::SafeNavigation { nil, .. } => {
465 ids.insert(branch_of(nil));
466 }
467 ProbeTarget::Hits { ids: proven } => {
468 for id in proven {
469 ids.insert(obligation_of(id));
470 }
471 }
472 ProbeTarget::Try { id, handlers, .. } => {
473 ids.insert(id.clone());
474 for handler in handlers {
475 ids.insert(handler.id.clone());
476 }
477 }
478 }
479 }
480 ids.into_iter().collect()
481 }
482}
483
484fn probe_keys_in(text: &str) -> Vec<u64> {
486 let mut keys = Vec::new();
487 let mut rest = text;
488 while let Some(position) = rest.find(RUBY_PROBE_RECEIVER) {
489 rest = &rest[position + RUBY_PROBE_RECEIVER.len()..];
490 let Some(open) = rest.find('(') else { break };
491 let digits: String = rest[open + 1..]
492 .chars()
493 .take_while(char::is_ascii_digit)
494 .collect();
495 if let Ok(key) = digits.parse() {
496 keys.push(key);
497 }
498 rest = &rest[open + 1..];
499 }
500 keys
501}
502
503fn target_ids(target: &ProbeTarget) -> Vec<String> {
506 match target {
507 ProbeTarget::Statement { id } => vec![id.clone()],
508 ProbeTarget::Decision {
509 id,
510 outcome_true,
511 outcome_false,
512 logical,
513 loop_,
514 ..
515 } => {
516 let mut ids = vec![id.clone(), outcome_true.clone(), outcome_false.clone()];
517 for derived in logical {
518 ids.push(derived.short_circuit.clone());
519 ids.push(derived.evaluated.clone());
520 }
521 if let Some(loop_) = loop_ {
522 ids.push(loop_.zero.clone());
523 ids.push(loop_.entered.clone());
524 }
525 ids
526 }
527 ProbeTarget::For { id, zero, entered } => vec![id.clone(), zero.clone(), entered.clone()],
528 ProbeTarget::Logical {
529 short_circuit,
530 evaluated,
531 ..
532 }
533 | ProbeTarget::Arrival {
534 short_circuit,
535 evaluated,
536 } => vec![short_circuit.clone(), evaluated.clone()],
537 ProbeTarget::Try {
538 id,
539 success,
540 raised,
541 handlers,
542 } => {
543 let mut ids = vec![id.clone(), success.clone(), raised.clone()];
544 for handler in handlers {
545 ids.push(handler.id.clone());
546 ids.push(handler.missed.clone());
547 ids.push(handler.selected.clone());
548 }
549 ids
550 }
551 ProbeTarget::SafeNavigation { nil, called } => vec![nil.clone(), called.clone()],
552 ProbeTarget::Hits { ids } => ids.clone(),
553 }
554}
555
556fn branch_of(alternative: &str) -> String {
559 alternative
560 .rsplit_once(':')
561 .map(|(branch, _)| branch.to_owned())
562 .unwrap_or_else(|| alternative.to_owned())
563}
564
565fn obligation_of(id: &str) -> String {
570 if id.matches(':').count() >= 3 {
571 branch_of(id)
572 } else {
573 id.to_owned()
574 }
575}
576
577fn stdlib_provable(
580 branches: &[BranchKeyPlan],
581 cases: &[CasePlan],
582 methods: &[MethodKeyPlan],
583) -> BTreeSet<String> {
584 let mut ids = BTreeSet::new();
585 for branch in branches {
586 for hit in &branch.hits {
587 ids.insert(obligation_of(hit));
588 }
589 if let Some(decision) = &branch.decision {
590 ids.insert(decision.id.clone());
591 ids.insert(obligation_of(&decision.outcome));
592 }
593 }
594 for case in cases {
595 for clause in &case.clauses {
596 ids.insert(obligation_of(&clause.missed));
597 ids.insert(obligation_of(&clause.selected));
598 }
599 if let Some(no_match) = &case.no_match {
600 ids.insert(obligation_of(&no_match.matched));
601 ids.insert(obligation_of(&no_match.unmatched));
602 }
603 }
604 for method in methods {
605 ids.insert(method.id.clone());
606 }
607 ids
608}
609
610#[derive(Debug, Clone, PartialEq)]
611pub struct RubyFileObligations {
612 pub manifest: CoverageManifest,
613 pub plan: RubyFilePlan,
614 pub probes: BTreeMap<u64, ProbeTarget>,
615}
616
617fn stable_id(file: &str, kind: &str, start: usize, end: usize, suffix: &str) -> String {
618 let mut hash = Sha256::new();
619 for value in [file, kind, &start.to_string(), &end.to_string(), suffix] {
620 hash.update(value.as_bytes());
621 hash.update([0]);
622 }
623 let digest = hash.finalize();
624 let mut encoded = String::with_capacity(24);
625 for byte in &digest[..12] {
626 use std::fmt::Write as _;
627 write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail");
628 }
629 format!("rb:{kind}:{encoded}")
630}
631
632#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
633enum EditRank {
634 Clause,
635 StatementProbe,
636 Opener,
637 Closer,
638}
639
640#[derive(Debug, Clone)]
641struct PendingEdit {
642 offset: usize,
643 rank: EditRank,
644 order: i64,
646 sequence: usize,
647 text: String,
648 scope: usize,
649}
650
651#[derive(Debug, Clone)]
654enum KeyProof {
655 Probe,
657 Implied(String),
661}
662
663#[derive(Debug, Clone)]
664struct KeyStatement {
665 index: usize,
666 proof: KeyProof,
667}
668
669#[derive(Debug, Default)]
673struct DecisionProof {
674 true_statements: Vec<String>,
675 false_statements: Vec<String>,
676}
677
678struct Collector<'a> {
679 file: &'a str,
680 source: &'a [u8],
681 line_starts: Vec<usize>,
682 manifest: CoverageManifest,
683 lines: BTreeMap<usize, String>,
684 statement_offsets: BTreeMap<String, [usize; 2]>,
685 branches: Vec<BranchKeyPlan>,
686 methods: Vec<MethodKeyPlan>,
687 cases: Vec<CasePlan>,
688 implied: BTreeMap<String, ImpliedPlan>,
689 probes: BTreeMap<u64, ProbeTarget>,
690 edits: Vec<PendingEdit>,
691 next_probe: &'a mut u64,
692 point_ids: BTreeSet<String>,
693 decision_ids: BTreeSet<String>,
694 branch_ids: BTreeSet<String>,
695 claimed_lines: BTreeSet<usize>,
696 key_statements: BTreeMap<usize, KeyStatement>,
699 endless_bodies: std::collections::BTreeSet<usize>,
703 tree_logicals: BTreeSet<usize>,
705 expression_lists: BTreeSet<usize>,
708 guard_nodes: BTreeSet<usize>,
710 elsif_nodes: BTreeSet<usize>,
712 depth: i64,
713 begin_unmeasured: Vec<(String, usize)>,
714 ractor_blocks: Vec<(usize, usize)>,
716 error: Option<RubyInstrumenterError>,
717}
718
719impl<'a> Collector<'a> {
720 fn new(file: &'a str, source: &'a [u8], next_probe: &'a mut u64) -> Self {
721 let mut line_starts = vec![0];
722 line_starts.extend(
723 source
724 .iter()
725 .enumerate()
726 .filter_map(|(index, byte)| (*byte == b'\n').then_some(index + 1)),
727 );
728 Self {
729 file,
730 source,
731 line_starts,
732 manifest: CoverageManifest {
733 unmeasured: Vec::new(),
734 decisions: Vec::new(),
735 points: Vec::new(),
736 branches: Vec::new(),
737 limitations: Vec::new(),
738 scope: None,
739 },
740 lines: BTreeMap::new(),
741 statement_offsets: BTreeMap::new(),
742 branches: Vec::new(),
743 methods: Vec::new(),
744 cases: Vec::new(),
745 implied: BTreeMap::new(),
746 probes: BTreeMap::new(),
747 edits: Vec::new(),
748 next_probe,
749 point_ids: BTreeSet::new(),
750 decision_ids: BTreeSet::new(),
751 branch_ids: BTreeSet::new(),
752 claimed_lines: BTreeSet::new(),
753 key_statements: BTreeMap::new(),
754 endless_bodies: std::collections::BTreeSet::new(),
755 tree_logicals: BTreeSet::new(),
756 expression_lists: BTreeSet::new(),
757 guard_nodes: BTreeSet::new(),
758 elsif_nodes: BTreeSet::new(),
759 depth: 0,
760 begin_unmeasured: Vec::new(),
761 ractor_blocks: Vec::new(),
762 error: None,
763 }
764 }
765
766 fn line_column(&self, offset: usize) -> (usize, usize) {
769 let line_index = self.line_starts.partition_point(|start| *start <= offset) - 1;
770 (line_index + 1, offset - self.line_starts[line_index])
771 }
772
773 fn span(&self, start: usize, end: usize) -> PlanSpan {
774 let (start_line, start_column) = self.line_column(start);
775 let (end_line, end_column) = self.line_column(end);
776 PlanSpan {
777 start: [start_line, start_column],
778 end: [end_line, end_column],
779 }
780 }
781
782 fn point_span(&self, offset: usize) -> PlanSpan {
783 let (line, column) = self.line_column(offset);
784 PlanSpan {
785 start: [line, column],
786 end: [line, column],
787 }
788 }
789
790 fn location_span(&self, location: &Location<'_>) -> PlanSpan {
791 self.span(location.start_offset(), location.end_offset())
792 }
793
794 fn node_span(&self, node: &Node<'_>) -> PlanSpan {
795 self.location_span(&node.location())
796 }
797
798 fn text(&self, start: usize, end: usize) -> String {
799 String::from_utf8_lossy(&self.source[start.min(end)..end.min(self.source.len())])
800 .trim()
801 .to_owned()
802 }
803
804 fn statements_span(&self, statements: &Option<StatementsNode<'_>>) -> Option<PlanSpan> {
805 statements
806 .as_ref()
807 .map(|statements| self.location_span(&statements.location()))
808 }
809
810 fn edit(&mut self, offset: usize, rank: EditRank, text: String, scope: usize) {
813 debug_assert!(!text.contains('\n'));
814 let order = match rank {
815 EditRank::Opener => self.depth,
816 EditRank::Closer => -self.depth,
817 _ => 0,
818 };
819 let sequence = self.edits.len();
820 self.edits.push(PendingEdit {
821 offset,
822 rank,
823 order,
824 sequence,
825 text,
826 scope,
827 });
828 }
829
830 fn probe_key(&mut self, target: ProbeTarget) -> u64 {
831 let key = *self.next_probe;
832 *self.next_probe += 1;
833 self.probes.insert(key, target);
834 key
835 }
836
837 fn wrap(&mut self, start: usize, end: usize, opener: String) {
838 self.edit(start, EditRank::Opener, opener, end);
839 self.edit(end, EditRank::Closer, "))".into(), start);
840 }
841
842 fn push_point(
845 &mut self,
846 id: &str,
847 start: usize,
848 end: usize,
849 kind: PointKind,
850 label: Option<String>,
851 ) {
852 let (line, column) = self.line_column(start);
853 self.manifest.points.push(PointMeta {
854 id: id.into(),
855 kind,
856 file: self.file.into(),
857 line,
858 column,
859 source: self.text(start, end),
860 label,
861 });
862 }
863
864 fn branch<const N: usize>(
865 &mut self,
866 start: usize,
867 end: usize,
868 kind: &str,
869 alternatives: [(&str, &str); N],
870 ) -> Option<String> {
871 let id = stable_id(self.file, "branch", start, end, kind);
872 let source = self.text(start, end);
873 self.branch_with_id(id, start, end, kind, source, alternatives)
874 }
875
876 fn branch_with_id<const N: usize>(
877 &mut self,
878 id: String,
879 start: usize,
880 _end: usize,
881 kind: &str,
882 source: String,
883 alternatives: [(&str, &str); N],
884 ) -> Option<String> {
885 if !self.branch_ids.insert(id.clone()) {
886 return None;
887 }
888 let (line, column) = self.line_column(start);
889 self.manifest.branches.push(BranchMeta {
890 id: id.clone(),
891 kind: kind.into(),
892 file: self.file.into(),
893 line,
894 column,
895 source,
896 alternatives: alternatives
897 .into_iter()
898 .map(|(suffix, label)| BranchAlternativeMeta {
899 id: format!("{id}:{suffix}"),
900 label: label.into(),
901 })
902 .collect(),
903 });
904 Some(id)
905 }
906
907 fn stdlib(
908 &mut self,
909 group: &str,
910 branch: &str,
911 span: PlanSpan,
912 kind: KeyKind,
913 hits: Vec<String>,
914 ) -> usize {
915 self.branches.push(BranchKeyPlan {
916 key: StdlibKey {
917 group: group.into(),
918 branch: branch.into(),
919 kind,
920 span,
921 unshifted: span,
922 },
923 hits,
924 decision: None,
925 });
926 self.branches.len() - 1
927 }
928
929 fn statements(&mut self, statements: &StatementsNode<'_>) {
932 for statement in statements.body().iter() {
933 self.statement(&statement);
934 }
935 }
936
937 fn statement(&mut self, node: &Node<'_>) {
938 let location = node.location();
939 let (start, end) = (location.start_offset(), location.end_offset());
940 let id = stable_id(self.file, "statement", start, end, "");
941 if !self.point_ids.insert(id.clone()) {
942 return;
943 }
944 self.push_point(&id, start, end, PointKind::Statement, None);
945 let (line, _) = self.line_column(start);
946 if let Some(key) = self.key_statements.get(&start).cloned() {
947 self.branches[key.index].hits.push(id.clone());
950 if let KeyProof::Implied(by) = key.proof {
951 self.claimed_lines.insert(line);
955 self.implied.entry(by).or_default().hits.push(id);
956 return;
957 }
958 }
959 if self.needs_probe(node) {
960 self.claimed_lines.insert(line);
963 let key = self.probe_key(ProbeTarget::Statement { id });
964 self.statement_probe(start, end, key);
965 } else if self.claimed_lines.insert(line) {
966 self.lines.insert(line, id.clone());
970 self.statement_offsets.insert(id, [start, end]);
971 } else {
972 let key = self.probe_key(ProbeTarget::Statement { id });
973 self.statement_probe(start, end, key);
974 }
975 }
976
977 fn statement_probe(&mut self, start: usize, end: usize, key: u64) {
980 if self.endless_bodies.contains(&start) {
981 self.depth += 1;
982 self.edit(
983 start,
984 EditRank::Opener,
985 format!("({RUBY_PROBE_RECEIVER}.s({key}); "),
986 end,
987 );
988 self.edit(end, EditRank::Closer, ")".into(), start);
989 self.depth -= 1;
990 } else {
991 self.edit(
992 start,
993 EditRank::StatementProbe,
994 format!("{RUBY_PROBE_RECEIVER}.s({key}); "),
995 end,
996 );
997 }
998 }
999
1000 fn needs_probe(&self, node: &Node<'_>) -> bool {
1001 let value = if let Some(write) = node.as_local_variable_write_node() {
1002 Some(write.value())
1003 } else if let Some(write) = node.as_instance_variable_write_node() {
1004 Some(write.value())
1005 } else if let Some(write) = node.as_class_variable_write_node() {
1006 Some(write.value())
1007 } else if let Some(write) = node.as_global_variable_write_node() {
1008 Some(write.value())
1009 } else if let Some(write) = node.as_constant_write_node() {
1010 Some(write.value())
1011 } else {
1012 node.as_multi_write_node().map(|write| write.value())
1013 };
1014 value.is_some_and(|value| {
1017 value
1018 .as_begin_node()
1019 .is_some_and(|begin| begin.begin_keyword_loc().is_some())
1020 || value.as_parentheses_node().is_some()
1021 })
1022 }
1023
1024 fn key_body(&mut self, statements: Option<StatementsNode<'_>>, index: usize, proof: KeyProof) {
1026 if let Some(first) = statements.and_then(|statements| statements.body().iter().next()) {
1027 self.key_statements.insert(
1028 first.location().start_offset(),
1029 KeyStatement { index, proof },
1030 );
1031 }
1032 }
1033
1034 fn claim_line_at(&mut self, offset: usize) {
1038 let (line, _) = self.line_column(offset);
1039 self.claimed_lines.insert(line);
1040 }
1041
1042 fn first_statement_id(&self, statements: &Option<StatementsNode<'_>>) -> Option<String> {
1046 let first = statements.as_ref()?.body().iter().next()?;
1047 let location = first.location();
1048 Some(stable_id(
1049 self.file,
1050 "statement",
1051 location.start_offset(),
1052 location.end_offset(),
1053 "",
1054 ))
1055 }
1056
1057 fn body_proves(
1061 &mut self,
1062 statements: &Option<StatementsNode<'_>>,
1063 ids: Vec<String>,
1064 offset: usize,
1065 before: &str,
1066 after: &str,
1067 ) {
1068 match self.first_statement_id(statements) {
1069 Some(first) => self.implied.entry(first).or_default().hits.extend(ids),
1070 None => {
1071 let key = self.probe_key(ProbeTarget::Hits { ids });
1072 self.edit(
1076 offset,
1077 EditRank::Closer,
1078 format!("{before}{RUBY_PROBE_RECEIVER}.hs({key}){after}"),
1079 offset,
1080 );
1081 }
1082 }
1083 }
1084
1085 fn strip<'n>(&self, node: Node<'n>) -> (Node<'n>, usize) {
1089 let mut current = node;
1090 let mut not = 0;
1091 loop {
1092 if let Some(parens) = current.as_parentheses_node() {
1093 if !parens.is_multiple_statements()
1094 && let Some(body) = parens.body()
1095 && let Some(statements) = body.as_statements_node()
1096 && statements.body().iter().count() == 1
1097 && let Some(inner) = statements.body().iter().next()
1098 {
1099 current = inner;
1100 continue;
1101 }
1102 break;
1103 }
1104 if let Some(call) = current.as_call_node()
1105 && call.name().as_slice() == b"!"
1106 && call.arguments().is_none()
1107 && call.block().is_none()
1108 && let Some(receiver) = call.receiver()
1109 {
1110 not += 1;
1111 current = receiver;
1112 continue;
1113 }
1114 break;
1115 }
1116 (current, not)
1117 }
1118
1119 fn tree(
1120 &mut self,
1121 node: Node<'_>,
1122 leaves: &mut Vec<(usize, usize, usize)>,
1123 logicals: &mut Vec<(String, Vec<Vec<usize>>)>,
1124 ) -> ConditionTree {
1125 let (operand, not) = self.strip(node);
1126 let logical: Option<(&str, Node<'_>, Node<'_>, usize)> =
1127 if let Some(and) = operand.as_and_node() {
1128 Some((
1129 "and",
1130 and.left(),
1131 and.right(),
1132 operand.location().start_offset(),
1133 ))
1134 } else if let Some(or) = operand.as_or_node() {
1135 Some((
1136 "or",
1137 or.left(),
1138 or.right(),
1139 operand.location().start_offset(),
1140 ))
1141 } else {
1142 None
1143 };
1144 if let Some((op, left, right, offset)) = logical {
1145 self.tree_logicals.insert(offset);
1146 let first = leaves.len();
1147 let left_tree = self.tree(left, leaves, logicals);
1148 let middle = leaves.len();
1149 let right_tree = self.tree(right, leaves, logicals);
1150 logicals.push((
1151 op.into(),
1152 vec![(first..middle).collect(), (middle..leaves.len()).collect()],
1153 ));
1154 return ConditionTree::Node {
1155 op: op.into(),
1156 items: vec![left_tree, right_tree],
1157 negate: not % 2 == 1,
1158 };
1159 }
1160 let location = operand.location();
1161 leaves.push((location.start_offset(), location.end_offset(), not));
1162 ConditionTree::Leaf(leaves.len() - 1)
1163 }
1164
1165 fn probe_decision(
1169 &mut self,
1170 predicate: Node<'_>,
1171 kind: &str,
1172 loop_: Option<LoopTarget>,
1173 wrapper: &str,
1174 ) -> Option<u64> {
1175 let location = predicate.location();
1176 let (start, end) = (location.start_offset(), location.end_offset());
1177 let id = stable_id(self.file, "decision", start, end, kind);
1178 if !self.decision_ids.insert(id.clone()) {
1179 return None;
1180 }
1181 let mut leaves = Vec::new();
1182 let mut logicals = Vec::new();
1183 let tree = self.tree(predicate, &mut leaves, &mut logicals);
1184 let conditions = leaves
1185 .iter()
1186 .map(|(leaf_start, leaf_end, not)| {
1187 let text = self.text(*leaf_start, *leaf_end);
1188 if not % 2 == 1 {
1189 format!("!{text}")
1190 } else {
1191 text
1192 }
1193 })
1194 .collect::<Vec<_>>();
1195 let (line, column) = self.line_column(start);
1196 let source = self.text(start, end);
1197 self.manifest.decisions.push(DecisionMeta {
1198 id: id.clone(),
1199 file: self.file.into(),
1200 line,
1201 column,
1202 source: source.clone(),
1203 conditions,
1204 kind: kind.into(),
1205 });
1206 let outcome_id = format!("{id}:outcome");
1207 self.branch_with_id(
1208 outcome_id.clone(),
1209 start,
1210 end,
1211 kind,
1212 source,
1213 [("true", "true"), ("false", "false")],
1214 );
1215 let mut derived = Vec::new();
1216 for (op, groups) in logicals {
1217 let right_leaf = groups[1].first().copied().unwrap_or(0);
1219 let (right_start, right_end, _) = leaves[right_leaf];
1220 if let Some(branch_id) = self.branch(
1221 right_start,
1222 right_end,
1223 &format!("logical-{op}"),
1224 [
1225 ("short-circuit", "short-circuited"),
1226 ("evaluated", "right operand evaluated"),
1227 ],
1228 ) {
1229 derived.push(DerivedLogical {
1230 previous_leaves: groups[0].clone(),
1231 operand_leaves: groups[1].clone(),
1232 short_circuit: format!("{branch_id}:short-circuit"),
1233 evaluated: format!("{branch_id}:evaluated"),
1234 });
1235 }
1236 }
1237 let key = self.probe_key(ProbeTarget::Decision {
1238 id,
1239 width: leaves.len(),
1240 not: leaves.iter().map(|(_, _, not)| not % 2 == 1).collect(),
1241 tree,
1242 outcome_true: format!("{outcome_id}:true"),
1243 outcome_false: format!("{outcome_id}:false"),
1244 logical: derived,
1245 loop_,
1246 });
1247 self.depth += 1;
1248 self.wrap(
1249 start,
1250 end,
1251 format!("{RUBY_PROBE_RECEIVER}.{wrapper}({key}, ("),
1252 );
1253 self.depth += 1;
1254 if leaves.len() > 1 {
1257 for (index, (leaf_start, leaf_end, _)) in leaves.iter().enumerate() {
1258 self.wrap(
1259 *leaf_start,
1260 *leaf_end,
1261 format!("{RUBY_PROBE_RECEIVER}.c({key}, {index}, ("),
1262 );
1263 }
1264 }
1265 self.depth -= 2;
1266 Some(key)
1267 }
1268
1269 fn stdlib_decision(
1272 &mut self,
1273 predicate: Node<'_>,
1274 kind: &str,
1275 then_index: usize,
1276 else_index: usize,
1277 then_is_true: bool,
1278 proof: DecisionProof,
1279 ) {
1280 let location = predicate.location();
1281 let (start, end) = (location.start_offset(), location.end_offset());
1282 let id = stable_id(self.file, "decision", start, end, kind);
1283 if !self.decision_ids.insert(id.clone()) {
1284 return;
1285 }
1286 let (operand, not) = self.strip(predicate);
1287 let operand_location = operand.location();
1288 let mut condition = self.text(
1289 operand_location.start_offset(),
1290 operand_location.end_offset(),
1291 );
1292 if not % 2 == 1 {
1293 condition = format!("!{condition}");
1294 }
1295 let (line, column) = self.line_column(start);
1296 let source = self.text(start, end);
1297 self.manifest.decisions.push(DecisionMeta {
1298 id: id.clone(),
1299 file: self.file.into(),
1300 line,
1301 column,
1302 source: source.clone(),
1303 conditions: vec![condition],
1304 kind: kind.into(),
1305 });
1306 let outcome_id = format!("{id}:outcome");
1307 self.branch_with_id(
1308 outcome_id.clone(),
1309 start,
1310 end,
1311 kind,
1312 source,
1313 [("true", "true"), ("false", "false")],
1314 );
1315 let (true_index, false_index) = if then_is_true {
1316 (then_index, else_index)
1317 } else {
1318 (else_index, then_index)
1319 };
1320 self.branches[true_index].decision = Some(StdlibDecision {
1321 id: id.clone(),
1322 value: true,
1323 outcome: format!("{outcome_id}:true"),
1324 });
1325 self.branches[false_index].decision = Some(StdlibDecision {
1326 id: id.clone(),
1327 value: false,
1328 outcome: format!("{outcome_id}:false"),
1329 });
1330 if !proof.true_statements.is_empty() && !proof.false_statements.is_empty() {
1335 for (statements, value) in [
1336 (&proof.true_statements, true),
1337 (&proof.false_statements, false),
1338 ] {
1339 for statement in statements {
1340 self.implied
1341 .entry(statement.clone())
1342 .or_default()
1343 .decisions
1344 .push(StdlibDecision {
1345 id: id.clone(),
1346 value,
1347 outcome: format!("{outcome_id}:{value}"),
1348 });
1349 }
1350 }
1351 } else {
1352 let key = self.probe_key(ProbeTarget::Decision {
1353 id,
1354 width: 1,
1355 not: vec![not % 2 == 1],
1356 tree: ConditionTree::Leaf(0),
1357 outcome_true: format!("{outcome_id}:true"),
1358 outcome_false: format!("{outcome_id}:false"),
1359 logical: Vec::new(),
1360 loop_: None,
1361 });
1362 self.depth += 1;
1363 self.wrap(start, end, format!("{RUBY_PROBE_RECEIVER}.d({key}, ("));
1364 self.depth -= 1;
1365 }
1366 }
1367
1368 fn decision_outcomes(&self, predicate: &Node<'_>, kind: &str) -> (String, String) {
1371 let location = predicate.location();
1372 let id = stable_id(
1373 self.file,
1374 "decision",
1375 location.start_offset(),
1376 location.end_offset(),
1377 kind,
1378 );
1379 (format!("{id}:outcome:true"), format!("{id}:outcome:false"))
1380 }
1381
1382 fn chain_false_statements(&self, subsequent: Option<Node<'_>>) -> Option<Vec<String>> {
1386 let mut ids = Vec::new();
1387 let mut current = subsequent;
1388 loop {
1389 let node = current?;
1390 if let Some(elsif) = node.as_if_node() {
1391 ids.push(self.first_statement_id(&elsif.statements())?);
1392 current = elsif.subsequent();
1393 } else if let Some(else_node) = node.as_else_node() {
1394 ids.push(self.first_statement_id(&else_node.statements())?);
1395 return Some(ids);
1396 } else {
1397 return None;
1398 }
1399 }
1400 }
1401
1402 fn literal_truth(&self, predicate: Node<'_>) -> Option<bool> {
1407 let mut node = predicate;
1408 loop {
1409 let inner = node.as_parentheses_node().and_then(|parens| {
1410 let body = parens.body()?;
1411 let statements = body.as_statements_node()?;
1412 let mut iter = statements.body().iter();
1413 let only = iter.next()?;
1414 iter.next().is_none().then_some(only)
1415 });
1416 match inner {
1417 Some(inner) => node = inner,
1418 None => break,
1419 }
1420 }
1421 if node.as_true_node().is_some()
1422 || node.as_integer_node().is_some()
1423 || node.as_float_node().is_some()
1424 || node.as_rational_node().is_some()
1425 || node.as_imaginary_node().is_some()
1426 || node.as_string_node().is_some()
1427 || node.as_symbol_node().is_some()
1428 {
1429 Some(true)
1430 } else if node.as_false_node().is_some() || node.as_nil_node().is_some() {
1431 Some(false)
1432 } else if let Some(and) = node.as_and_node() {
1433 match (
1436 self.literal_truth(and.left()),
1437 self.literal_truth(and.right()),
1438 ) {
1439 (Some(false), _) | (_, Some(false)) => Some(false),
1440 (Some(true), Some(true)) => Some(true),
1441 _ => None,
1442 }
1443 } else if let Some(or) = node.as_or_node() {
1444 match (
1445 self.literal_truth(or.left()),
1446 self.literal_truth(or.right()),
1447 ) {
1448 (Some(true), _) | (_, Some(true)) => Some(true),
1449 (Some(false), Some(false)) => Some(false),
1450 _ => None,
1451 }
1452 } else {
1453 None
1454 }
1455 }
1456
1457 fn is_compound(&self, predicate: Node<'_>) -> bool {
1458 let (operand, _) = self.strip(predicate);
1459 operand.as_and_node().is_some() || operand.as_or_node().is_some()
1460 }
1461
1462 fn predicate_decision<'n>(
1465 &mut self,
1466 predicate: impl Fn() -> Node<'n>,
1467 kind: &str,
1468 then_index: usize,
1469 else_index: usize,
1470 then_is_true: bool,
1471 proof: DecisionProof,
1472 ) {
1473 if self.is_compound(predicate()) {
1474 self.probe_decision(predicate(), kind, None, "d");
1475 } else {
1476 self.stdlib_decision(
1477 predicate(),
1478 kind,
1479 then_index,
1480 else_index,
1481 then_is_true,
1482 proof,
1483 );
1484 }
1485 }
1486
1487 fn if_node(&mut self, node: &IfNode<'_>, kind: &str) {
1490 let location = node.location();
1491 let node_span = self.location_span(&location);
1492 let then_statements = self.statements_span(&node.statements());
1493 let (then_span, then_kind) = match then_statements {
1495 Some(span) => (span, KeyKind::List),
1496 None => (
1497 self.point_span(node.predicate().location().end_offset()),
1498 KeyKind::Point,
1499 ),
1500 };
1501 let (else_span, else_kind) = match node.subsequent() {
1502 Some(subsequent) => match subsequent.as_else_node() {
1503 Some(else_node) => match self.statements_span(&else_node.statements()) {
1504 Some(span) => (span, KeyKind::List),
1505 None => (self.node_span(&subsequent), KeyKind::Node),
1506 },
1507 None => (self.node_span(&subsequent), KeyKind::Node),
1508 },
1509 None => (node_span, KeyKind::Node),
1510 };
1511 let then_index = self.stdlib("if", "then", then_span, then_kind, Vec::new());
1512 let else_index = self.stdlib("if", "else", else_span, else_kind, Vec::new());
1513 if kind == "ternary" {
1514 if let Some(statements) = node.statements() {
1516 self.expression_lists
1517 .insert(statements.location().start_offset());
1518 }
1519 if let Some(subsequent) = node.subsequent()
1520 && let Some(else_node) = subsequent.as_else_node()
1521 && let Some(statements) = else_node.statements()
1522 {
1523 self.expression_lists
1524 .insert(statements.location().start_offset());
1525 }
1526 } else {
1527 let modifier = node.statements().is_some_and(|statements| {
1530 statements.location().start_offset() < node.predicate().location().start_offset()
1531 });
1532 let then_proof = if modifier {
1533 KeyProof::Implied(self.decision_outcomes(&node.predicate(), kind).0)
1534 } else {
1535 KeyProof::Probe
1536 };
1537 self.claim_line_at(node.predicate().location().end_offset());
1538 self.key_body(node.statements(), then_index, then_proof);
1539 if let Some(subsequent) = node.subsequent()
1540 && let Some(else_node) = subsequent.as_else_node()
1541 {
1542 self.claim_line_at(else_node.else_keyword_loc().end_offset());
1543 self.key_body(else_node.statements(), else_index, KeyProof::Probe);
1544 }
1545 }
1546 let proof = if kind == "ternary" {
1547 DecisionProof::default()
1549 } else {
1550 DecisionProof {
1551 true_statements: self
1552 .first_statement_id(&node.statements())
1553 .into_iter()
1554 .collect(),
1555 false_statements: self
1556 .chain_false_statements(node.subsequent())
1557 .unwrap_or_default(),
1558 }
1559 };
1560 self.predicate_decision(
1561 || node.predicate(),
1562 kind,
1563 then_index,
1564 else_index,
1565 true,
1566 proof,
1567 );
1568 }
1569
1570 fn unless_node(&mut self, node: &UnlessNode<'_>) {
1571 let node_span = self.location_span(&node.location());
1572 let then_statements = self.statements_span(&node.statements());
1573 let (then_span, then_kind) = match then_statements {
1574 Some(span) => (span, KeyKind::List),
1575 None => (
1576 self.point_span(node.predicate().location().end_offset()),
1577 KeyKind::Point,
1578 ),
1579 };
1580 let (else_span, else_kind) = match node.else_clause() {
1581 Some(else_node) => match self.statements_span(&else_node.statements()) {
1582 Some(span) => (span, KeyKind::List),
1583 None => (self.location_span(&else_node.location()), KeyKind::Node),
1584 },
1585 None => (node_span, KeyKind::Node),
1586 };
1587 let then_index = self.stdlib("unless", "then", then_span, then_kind, Vec::new());
1588 let else_index = self.stdlib("unless", "else", else_span, else_kind, Vec::new());
1589 let modifier = node.statements().is_some_and(|statements| {
1590 statements.location().start_offset() < node.predicate().location().start_offset()
1591 });
1592 let then_proof = if modifier {
1593 KeyProof::Implied(self.decision_outcomes(&node.predicate(), "unless").1)
1594 } else {
1595 KeyProof::Probe
1596 };
1597 self.claim_line_at(node.predicate().location().end_offset());
1598 self.key_body(node.statements(), then_index, then_proof);
1599 if let Some(else_node) = node.else_clause() {
1600 self.claim_line_at(else_node.else_keyword_loc().end_offset());
1601 self.key_body(else_node.statements(), else_index, KeyProof::Probe);
1602 }
1603 let proof = DecisionProof {
1605 true_statements: node
1606 .else_clause()
1607 .and_then(|else_node| self.first_statement_id(&else_node.statements()))
1608 .into_iter()
1609 .collect(),
1610 false_statements: self
1611 .first_statement_id(&node.statements())
1612 .into_iter()
1613 .collect(),
1614 };
1615 self.predicate_decision(
1616 || node.predicate(),
1617 "unless",
1618 then_index,
1619 else_index,
1620 false,
1621 proof,
1622 );
1623 }
1624
1625 fn loop_node(
1626 &mut self,
1627 location: &Location<'_>,
1628 predicate: Node<'_>,
1629 statements: Option<StatementsNode<'_>>,
1630 begin_modifier: bool,
1631 until: bool,
1632 ) {
1633 let kind = if until { "until" } else { "while" };
1634 let (start, end) = (location.start_offset(), location.end_offset());
1635 let loop_target = if begin_modifier {
1636 None
1638 } else {
1639 self.branch(
1640 start,
1641 end,
1642 kind,
1643 [("zero", "zero iterations"), ("entered", "entered")],
1644 )
1645 .map(|id| LoopTarget {
1646 zero: format!("{id}:zero"),
1647 entered: format!("{id}:entered"),
1648 id,
1649 until,
1650 })
1651 };
1652 if let Some(body_span) = self.statements_span(&statements) {
1656 let index = self.stdlib(kind, "body", body_span, KeyKind::List, Vec::new());
1657 let modifier = !begin_modifier
1658 && statements.as_ref().is_some_and(|statements| {
1659 statements.location().start_offset() < predicate.location().start_offset()
1660 });
1661 let proof = match (&loop_target, modifier) {
1662 (Some(target), true) => KeyProof::Implied(target.entered.clone()),
1663 _ => KeyProof::Probe,
1664 };
1665 self.key_body(statements, index, proof);
1666 }
1667 self.probe_decision(predicate, kind, loop_target, "w");
1668 }
1669
1670 fn for_node(&mut self, node: &ForNode<'_>) {
1671 let location = node.location();
1672 let Some(id) = self.branch(
1673 location.start_offset(),
1674 location.end_offset(),
1675 "for",
1676 [("zero", "zero iterations"), ("entered", "entered")],
1677 ) else {
1678 return;
1679 };
1680 let key = self.probe_key(ProbeTarget::For {
1681 zero: format!("{id}:zero"),
1682 entered: format!("{id}:entered"),
1683 id: id.clone(),
1684 });
1685 let collection = node.collection().location();
1686 self.depth += 1;
1687 self.wrap(
1688 collection.start_offset(),
1689 collection.end_offset(),
1690 format!("{RUBY_PROBE_RECEIVER}.f({key}, ("),
1691 );
1692 self.depth -= 1;
1693 match node
1694 .statements()
1695 .and_then(|statements| statements.body().iter().next())
1696 {
1697 Some(first) => self.edit(
1698 first.location().start_offset(),
1699 EditRank::StatementProbe,
1700 format!("{RUBY_PROBE_RECEIVER}.fb({key}); "),
1701 first.location().end_offset(),
1702 ),
1703 None => {
1704 self.manifest.unmeasured.push(format!("{id}:entered"));
1705 self.manifest.unmeasured.push(format!("{id}:zero"));
1706 }
1707 }
1708 }
1709
1710 fn iterator_loop(
1715 &mut self,
1716 node: &CallNode<'_>,
1717 receiver: &Node<'_>,
1718 block: &ruby_prism::BlockNode<'_>,
1719 ) {
1720 let location = node.location();
1721 let name = String::from_utf8_lossy(node.name().as_slice()).into_owned();
1722 let Some(id) = self.branch(
1723 location.start_offset(),
1724 location.end_offset(),
1725 &format!("iterator-{}", name.trim_end_matches(['?', '!'])),
1726 [("zero", "zero iterations"), ("entered", "entered")],
1727 ) else {
1728 return;
1729 };
1730 let first = block.body().and_then(|body| {
1731 if let Some(statements) = body.as_statements_node() {
1732 statements.body().iter().next()
1733 } else if let Some(begin) = body.as_begin_node() {
1734 begin
1735 .statements()
1736 .and_then(|statements| statements.body().iter().next())
1737 } else {
1738 None
1739 }
1740 });
1741 let Some(first) = first else {
1742 self.manifest.unmeasured.push(format!("{id}:entered"));
1745 self.manifest.unmeasured.push(format!("{id}:zero"));
1746 return;
1747 };
1748 let key = self.probe_key(ProbeTarget::For {
1749 zero: format!("{id}:zero"),
1750 entered: format!("{id}:entered"),
1751 id,
1752 });
1753 let receiver_location = receiver.location();
1754 self.depth += 1;
1755 self.wrap(
1756 receiver_location.start_offset(),
1757 receiver_location.end_offset(),
1758 format!("{RUBY_PROBE_RECEIVER}.f({key}, ("),
1759 );
1760 self.depth -= 1;
1761 self.edit(
1762 first.location().start_offset(),
1763 EditRank::StatementProbe,
1764 format!("{RUBY_PROBE_RECEIVER}.fb({key}); "),
1765 first.location().end_offset(),
1766 );
1767 }
1768
1769 fn case_node(&mut self, node: &CaseNode<'_>) {
1770 let node_span = self.location_span(&node.location());
1771 let (start, end) = (node.location().start_offset(), node.location().end_offset());
1772 let no_match_id = node
1775 .else_clause()
1776 .is_none()
1777 .then(|| stable_id(self.file, "branch", start, end, "case-no-match"));
1778 let else_id = node.else_clause().map(|else_node| {
1780 stable_id(
1781 self.file,
1782 "branch",
1783 else_node.location().start_offset(),
1784 else_node.location().end_offset(),
1785 "case-else",
1786 )
1787 });
1788 let mut clauses = Vec::new();
1789 let mut clause_ids = Vec::new();
1790 for (index, condition) in node.conditions().iter().enumerate() {
1791 let Some(when) = condition.as_when_node() else {
1792 continue;
1793 };
1794 let Some(id) = self.branch(
1795 when.location().start_offset(),
1796 when.location().end_offset(),
1797 &format!("case-when-{index}"),
1798 [("missed", "not selected"), ("selected", "selected")],
1799 ) else {
1800 return;
1801 };
1802 let statements = self.statements_span(&when.statements());
1803 let span = statements.unwrap_or_else(|| self.location_span(&when.location()));
1804 let key_index = self.stdlib(
1805 "case",
1806 "when",
1807 span,
1808 if statements.is_some() {
1809 KeyKind::List
1810 } else {
1811 KeyKind::Node
1812 },
1813 vec![format!("{id}:selected")],
1814 );
1815 self.key_body(when.statements(), key_index, KeyProof::Probe);
1816 let proven =
1817 self.clause_proof(&id, &clause_ids, no_match_id.as_deref(), else_id.as_deref());
1818 let (offset, before) = match when.then_keyword_loc() {
1821 Some(then) => (then.end_offset(), " "),
1822 None => (
1823 when.conditions()
1824 .iter()
1825 .last()
1826 .map(|condition| condition.location().end_offset())
1827 .unwrap_or_else(|| when.location().end_offset()),
1828 " then ",
1829 ),
1830 };
1831 self.claim_line_at(offset);
1832 self.body_proves(&when.statements(), proven, offset, before, "");
1833 clauses.push(CaseClausePlan {
1834 key: self.branches[key_index].key.clone(),
1835 missed: format!("{id}:missed"),
1836 selected: format!("{id}:selected"),
1837 });
1838 clause_ids.push(id);
1839 }
1840 let no_match = match node.else_clause() {
1841 Some(else_node) => {
1842 let Some(id) = self.branch(
1843 else_node.location().start_offset(),
1844 else_node.location().end_offset(),
1845 "case-else",
1846 [("missed", "not selected"), ("selected", "selected")],
1847 ) else {
1848 return;
1849 };
1850 let statements = self.statements_span(&else_node.statements());
1851 let span = statements.unwrap_or_else(|| self.location_span(&else_node.location()));
1852 let key_index = self.stdlib(
1853 "case",
1854 "else",
1855 span,
1856 if statements.is_some() {
1857 KeyKind::List
1858 } else {
1859 KeyKind::Node
1860 },
1861 vec![format!("{id}:selected")],
1862 );
1863 self.key_body(else_node.statements(), key_index, KeyProof::Probe);
1864 let proven = self.clause_proof(&id, &clause_ids, None, None);
1865 self.claim_line_at(else_node.else_keyword_loc().end_offset());
1866 self.body_proves(
1867 &else_node.statements(),
1868 proven,
1869 else_node.else_keyword_loc().end_offset(),
1870 " ",
1871 "",
1872 );
1873 clauses.push(CaseClausePlan {
1874 key: self.branches[key_index].key.clone(),
1875 missed: format!("{id}:missed"),
1876 selected: format!("{id}:selected"),
1877 });
1878 None
1879 }
1880 None => self
1881 .branch(
1882 start,
1883 end,
1884 "case-no-match",
1885 [
1886 ("matched", "some clause matched"),
1887 ("unmatched", "no clause matched"),
1888 ],
1889 )
1890 .map(|id| {
1891 let key_index = self.stdlib(
1892 "case",
1893 "else",
1894 node_span,
1895 KeyKind::Node,
1896 vec![format!("{id}:unmatched")],
1897 );
1898 self.no_match_probe(&id, &clause_ids, node.end_keyword_loc().start_offset());
1902 CaseNoMatchPlan {
1903 key: self.branches[key_index].key.clone(),
1904 matched: format!("{id}:matched"),
1905 unmatched: format!("{id}:unmatched"),
1906 }
1907 }),
1908 };
1909 self.cases.push(CasePlan { clauses, no_match });
1910 }
1911
1912 fn clause_proof(
1916 &self,
1917 id: &str,
1918 earlier: &[String],
1919 no_match_id: Option<&str>,
1920 else_id: Option<&str>,
1921 ) -> Vec<String> {
1922 let mut ids = vec![format!("{id}:selected")];
1923 ids.extend(earlier.iter().map(|clause| format!("{clause}:missed")));
1924 if let Some(no_match) = no_match_id {
1925 ids.push(format!("{no_match}:matched"));
1926 }
1927 if let Some(else_id) = else_id {
1928 ids.push(format!("{else_id}:missed"));
1929 }
1930 ids
1931 }
1932
1933 fn no_match_probe(&mut self, id: &str, clauses: &[String], end_keyword: usize) {
1934 let mut ids = vec![format!("{id}:unmatched")];
1935 ids.extend(clauses.iter().map(|clause| format!("{clause}:missed")));
1936 let key = self.probe_key(ProbeTarget::Hits { ids });
1937 self.edit(
1938 end_keyword,
1939 EditRank::StatementProbe,
1940 format!("else {RUBY_PROBE_RECEIVER}.hs({key}); "),
1941 end_keyword,
1942 );
1943 }
1944
1945 fn case_match_node(&mut self, node: &CaseMatchNode<'_>) {
1946 let node_span = self.location_span(&node.location());
1947 let (start, end) = (node.location().start_offset(), node.location().end_offset());
1948 let no_match_id = node
1949 .else_clause()
1950 .is_none()
1951 .then(|| stable_id(self.file, "branch", start, end, "case-no-match"));
1952 let else_id = node.else_clause().map(|else_node| {
1954 stable_id(
1955 self.file,
1956 "branch",
1957 else_node.location().start_offset(),
1958 else_node.location().end_offset(),
1959 "case-else",
1960 )
1961 });
1962 let mut clauses = Vec::new();
1963 let mut clause_ids = Vec::new();
1964 for (index, condition) in node.conditions().iter().enumerate() {
1965 let Some(in_node) = condition.as_in_node() else {
1966 continue;
1967 };
1968 let Some(id) = self.branch(
1969 in_node.location().start_offset(),
1970 in_node.location().end_offset(),
1971 &format!("case-in-{index}"),
1972 [("missed", "not selected"), ("selected", "selected")],
1973 ) else {
1974 return;
1975 };
1976 let statements = self.statements_span(&in_node.statements());
1977 let span = statements.unwrap_or_else(|| self.location_span(&in_node.location()));
1978 let key_index = self.stdlib(
1979 "case",
1980 "in",
1981 span,
1982 if statements.is_some() {
1983 KeyKind::List
1984 } else {
1985 KeyKind::Node
1986 },
1987 vec![format!("{id}:selected")],
1988 );
1989 self.key_body(in_node.statements(), key_index, KeyProof::Probe);
1990 let proven =
1991 self.clause_proof(&id, &clause_ids, no_match_id.as_deref(), else_id.as_deref());
1992 let (offset, before) = match in_node.then_loc() {
1993 Some(then) => (then.end_offset(), " "),
1994 None => (in_node.pattern().location().end_offset(), " then "),
1995 };
1996 self.claim_line_at(offset);
1997 self.body_proves(&in_node.statements(), proven, offset, before, "");
1998 clauses.push(CaseClausePlan {
1999 key: self.branches[key_index].key.clone(),
2000 missed: format!("{id}:missed"),
2001 selected: format!("{id}:selected"),
2002 });
2003 clause_ids.push(id);
2004 let pattern = in_node.pattern();
2007 if let Some(guard) = pattern.as_if_node() {
2008 self.guard_nodes.insert(pattern.location().start_offset());
2009 self.probe_decision(guard.predicate(), "in-guard", None, "d");
2010 } else if let Some(guard) = pattern.as_unless_node() {
2011 self.guard_nodes.insert(pattern.location().start_offset());
2012 self.probe_decision(guard.predicate(), "in-guard-unless", None, "d");
2013 }
2014 }
2015 let no_match = match node.else_clause() {
2016 Some(else_node) => {
2017 let Some(id) = self.branch(
2018 else_node.location().start_offset(),
2019 else_node.location().end_offset(),
2020 "case-else",
2021 [("missed", "not selected"), ("selected", "selected")],
2022 ) else {
2023 return;
2024 };
2025 let statements = self.statements_span(&else_node.statements());
2026 let span = statements.unwrap_or_else(|| self.location_span(&else_node.location()));
2027 let key_index = self.stdlib(
2028 "case",
2029 "else",
2030 span,
2031 if statements.is_some() {
2032 KeyKind::List
2033 } else {
2034 KeyKind::Node
2035 },
2036 vec![format!("{id}:selected")],
2037 );
2038 self.key_body(else_node.statements(), key_index, KeyProof::Probe);
2039 let proven = self.clause_proof(&id, &clause_ids, None, None);
2040 self.claim_line_at(else_node.else_keyword_loc().end_offset());
2041 self.body_proves(
2042 &else_node.statements(),
2043 proven,
2044 else_node.else_keyword_loc().end_offset(),
2045 " ",
2046 "",
2047 );
2048 clauses.push(CaseClausePlan {
2049 key: self.branches[key_index].key.clone(),
2050 missed: format!("{id}:missed"),
2051 selected: format!("{id}:selected"),
2052 });
2053 None
2054 }
2055 None => self
2056 .branch(
2057 start,
2058 end,
2059 "case-no-match",
2060 [
2061 ("matched", "some pattern matched"),
2062 ("unmatched", "no pattern matched"),
2063 ],
2064 )
2065 .map(|id| {
2066 let key_index = self.stdlib(
2067 "case",
2068 "else",
2069 node_span,
2070 KeyKind::Node,
2071 vec![format!("{id}:unmatched")],
2072 );
2073 self.no_match_probe(&id, &clause_ids, node.end_keyword_loc().start_offset());
2074 CaseNoMatchPlan {
2075 key: self.branches[key_index].key.clone(),
2076 matched: format!("{id}:matched"),
2077 unmatched: format!("{id}:unmatched"),
2078 }
2079 }),
2080 };
2081 self.cases.push(CasePlan { clauses, no_match });
2082 }
2083
2084 fn safe_navigation(&mut self, node: &CallNode<'_>) {
2085 let location = node.location();
2086 let Some(id) = self.branch(
2087 location.start_offset(),
2088 location.end_offset(),
2089 "safe-navigation",
2090 [("nil", "receiver nil"), ("called", "method called")],
2091 ) else {
2092 return;
2093 };
2094 let end = match node.arguments() {
2098 Some(arguments) => node
2099 .closing_loc()
2100 .map(|closing| closing.end_offset())
2101 .unwrap_or_else(|| arguments.location().end_offset()),
2102 None => node
2103 .message_loc()
2104 .map(|message| message.end_offset())
2105 .unwrap_or_else(|| location.end_offset()),
2106 };
2107 let (start_line, start_column) = self.line_column(location.start_offset());
2108 let (end_line, end_column) = self.line_column(end);
2109 let span = PlanSpan {
2110 start: [start_line, start_column],
2111 end: [end_line, end_column],
2112 };
2113 self.stdlib(
2114 "&.",
2115 "then",
2116 span,
2117 KeyKind::Node,
2118 vec![format!("{id}:called")],
2119 );
2120 self.stdlib("&.", "else", span, KeyKind::Node, vec![format!("{id}:nil")]);
2121 if let Some(receiver) = node.receiver() {
2123 let key = self.probe_key(ProbeTarget::SafeNavigation {
2124 nil: format!("{id}:nil"),
2125 called: format!("{id}:called"),
2126 });
2127 let receiver = receiver.location();
2128 self.depth += 1;
2129 self.wrap(
2130 receiver.start_offset(),
2131 receiver.end_offset(),
2132 format!("{RUBY_PROBE_RECEIVER}.n({key}, ("),
2133 );
2134 self.depth -= 1;
2135 }
2136 }
2137
2138 fn value_logical(&mut self, op: &str, left: &Node<'_>, node_start: usize, node_end: usize) {
2139 let Some(id) = self.branch(
2140 node_start,
2141 node_end,
2142 &format!("logical-{op}"),
2143 [
2144 ("short-circuit", "short-circuited"),
2145 ("evaluated", "right operand evaluated"),
2146 ],
2147 ) else {
2148 return;
2149 };
2150 let key = self.probe_key(ProbeTarget::Logical {
2151 op: op.into(),
2152 short_circuit: format!("{id}:short-circuit"),
2153 evaluated: format!("{id}:evaluated"),
2154 });
2155 let left = left.location();
2156 self.depth += 1;
2157 self.wrap(
2158 left.start_offset(),
2159 left.end_offset(),
2160 format!("{RUBY_PROBE_RECEIVER}.l({key}, ("),
2161 );
2162 self.depth -= 1;
2163 }
2164
2165 fn op_assign(
2169 &mut self,
2170 op: &str,
2171 node_start: usize,
2172 node_end: usize,
2173 name: Option<&[u8]>,
2174 value: &Node<'_>,
2175 ) {
2176 let Some(id) = self.branch(
2177 node_start,
2178 node_end,
2179 &format!("{op}-assign"),
2180 [
2181 ("short-circuit", "assignment skipped"),
2182 ("evaluated", "value evaluated and assigned"),
2183 ],
2184 ) else {
2185 return;
2186 };
2187 match name {
2188 Some(name) => {
2189 let key = self.probe_key(ProbeTarget::Logical {
2190 op: op.into(),
2191 short_circuit: format!("{id}:short-circuit"),
2192 evaluated: format!("{id}:evaluated"),
2193 });
2194 let name = String::from_utf8_lossy(name);
2195 self.depth += 1;
2196 self.edit(
2197 node_start,
2198 EditRank::Opener,
2199 format!("({RUBY_PROBE_RECEIVER}.l({key}, {name}); "),
2200 node_end,
2201 );
2202 self.edit(node_end, EditRank::Closer, ")".into(), node_start);
2203 self.depth -= 1;
2204 }
2205 None => {
2206 let key = self.probe_key(ProbeTarget::Arrival {
2210 short_circuit: format!("{id}:short-circuit"),
2211 evaluated: format!("{id}:evaluated"),
2212 });
2213 let value_location = value.location();
2214 self.depth += 1;
2215 self.edit(
2216 node_start,
2217 EditRank::Opener,
2218 format!("({RUBY_PROBE_RECEIVER}.pre({key}); "),
2219 node_end,
2220 );
2221 self.edit(node_end, EditRank::Closer, ")".into(), node_start);
2222 self.depth += 1;
2223 self.edit(
2224 value_location.start_offset(),
2225 EditRank::Opener,
2226 format!("({RUBY_PROBE_RECEIVER}.es({key}); "),
2227 value_location.end_offset(),
2228 );
2229 self.edit(
2230 value_location.end_offset(),
2231 EditRank::Closer,
2232 ")".into(),
2233 value_location.start_offset(),
2234 );
2235 self.depth -= 2;
2236 }
2237 }
2238 }
2239
2240 fn begin_node(&mut self, node: &BeginNode<'_>, close: Option<usize>) {
2247 let has_rescue = node.rescue_clause().is_some();
2248 let has_ensure = node.ensure_clause().is_some();
2249 if !has_rescue && !has_ensure {
2250 return;
2251 }
2252 let location = node.location();
2253 let (start, end) = (location.start_offset(), location.end_offset());
2254 let Some(id) = self.branch(
2255 start,
2256 end,
2257 "begin",
2258 [
2259 ("success", "body completed"),
2260 ("raised", "exception raised"),
2261 ],
2262 ) else {
2263 return;
2264 };
2265 let mut handlers = Vec::new();
2266 let mut handler_edits = Vec::new();
2267 let mut rescue = node.rescue_clause();
2268 let mut index = 0;
2269 while let Some(clause) = rescue {
2270 let clause_location = clause.location();
2271 let Some(handler_id) = self.branch(
2272 clause_location.start_offset(),
2273 clause_location.end_offset(),
2274 &format!("rescue-{index}"),
2275 [("missed", "not selected"), ("selected", "selected")],
2276 ) else {
2277 return;
2278 };
2279 handlers.push(HandlerTarget {
2280 missed: format!("{handler_id}:missed"),
2281 selected: format!("{handler_id}:selected"),
2282 id: handler_id,
2283 });
2284 handler_edits.push(self.handler_probe_position(&clause));
2285 rescue = clause.subsequent();
2286 index += 1;
2287 }
2288 let key = self.probe_key(ProbeTarget::Try {
2289 success: format!("{id}:success"),
2290 raised: format!("{id}:raised"),
2291 id: id.clone(),
2292 handlers,
2293 });
2294 for (index, (offset, leading, scope_end)) in handler_edits.into_iter().enumerate() {
2295 let text = if leading {
2296 format!("; {RUBY_PROBE_RECEIVER}.h({key}, {index})")
2297 } else {
2298 format!("{RUBY_PROBE_RECEIVER}.h({key}, {index}); ")
2299 };
2300 self.edit(offset, EditRank::StatementProbe, text, scope_end);
2301 }
2302 let clause_offset = node
2304 .else_clause()
2305 .map(|clause| clause.else_keyword_loc().start_offset())
2306 .or_else(|| {
2307 node.ensure_clause()
2308 .map(|clause| clause.ensure_keyword_loc().start_offset())
2309 })
2310 .or_else(|| node.end_keyword_loc().map(|loc| loc.start_offset()))
2311 .or(close);
2312 match clause_offset {
2313 Some(offset) => self.edit(
2314 offset,
2315 EditRank::Clause,
2316 format!(
2317 "rescue Exception => __supercov_e; {RUBY_PROBE_RECEIVER}.p({key}); raise; "
2318 ),
2319 offset,
2320 ),
2321 None => {
2322 self.manifest.unmeasured.push(format!("{id}:raised"));
2323 let (line, _) = self.line_column(start);
2324 self.begin_unmeasured.push((id.clone(), line));
2325 }
2326 }
2327 if let Some(else_clause) = node.else_clause() {
2329 match else_clause
2330 .statements()
2331 .and_then(|statements| statements.body().iter().next())
2332 {
2333 Some(first) => self.edit(
2334 first.location().start_offset(),
2335 EditRank::StatementProbe,
2336 format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2337 first.location().end_offset(),
2338 ),
2339 None => self.edit(
2340 else_clause.else_keyword_loc().end_offset(),
2341 EditRank::StatementProbe,
2342 format!(" {RUBY_PROBE_RECEIVER}.ok0({key});"),
2343 else_clause.else_keyword_loc().end_offset(),
2344 ),
2345 }
2346 return;
2347 }
2348 let last = node
2349 .statements()
2350 .and_then(|statements| statements.body().iter().last());
2351 match last {
2352 Some(last) => {
2353 if !self.completion_probe(last, key) {
2354 self.manifest.unmeasured.push(format!("{id}:success"));
2355 let (line, _) = self.line_column(start);
2356 self.begin_unmeasured.push((id, line));
2357 }
2358 }
2359 None => {
2360 self.manifest.unmeasured.push(format!("{id}:success"));
2361 let (line, _) = self.line_column(start);
2362 self.begin_unmeasured.push((id, line));
2363 }
2364 }
2365 }
2366
2367 fn handler_probe_position(&self, clause: &RescueNode<'_>) -> (usize, bool, usize) {
2370 if let Some(first) = clause
2371 .statements()
2372 .and_then(|statements| statements.body().iter().next())
2373 {
2374 return (
2375 first.location().start_offset(),
2376 false,
2377 first.location().end_offset(),
2378 );
2379 }
2380 let offset = if let Some(then_keyword) = clause.then_keyword_loc() {
2381 then_keyword.end_offset()
2382 } else if let Some(reference) = clause.reference() {
2383 reference.location().end_offset()
2384 } else if let Some(last) = clause.exceptions().iter().last() {
2385 last.location().end_offset()
2386 } else {
2387 clause.keyword_loc().end_offset()
2388 };
2389 (offset, true, offset)
2390 }
2391
2392 fn jump_exposed(&self, node: &Node<'_>) -> bool {
2403 if Self::is_jump(node) {
2404 return true;
2405 }
2406 if let Some(if_node) = node.as_if_node() {
2407 let else_exposed = match if_node.subsequent() {
2408 Some(subsequent) => match subsequent.as_else_node() {
2409 Some(else_node) => self.arm_jump_exposed(else_node.statements()),
2410 None => self.jump_exposed(&subsequent),
2411 },
2412 None => false,
2413 };
2414 return else_exposed || self.arm_jump_exposed(if_node.statements());
2415 }
2416 if let Some(unless_node) = node.as_unless_node() {
2417 let else_exposed = match unless_node.else_clause() {
2418 Some(else_node) => self.arm_jump_exposed(else_node.statements()),
2419 None => false,
2420 };
2421 return else_exposed || self.arm_jump_exposed(unless_node.statements());
2422 }
2423 if let Some(begin) = node.as_begin_node() {
2424 if self.arm_jump_exposed(begin.statements())
2425 || begin
2426 .else_clause()
2427 .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()))
2428 {
2429 return true;
2430 }
2431 let mut rescue = begin.rescue_clause();
2432 while let Some(clause) = rescue {
2433 if self.arm_jump_exposed(clause.statements()) {
2434 return true;
2435 }
2436 rescue = clause.subsequent();
2437 }
2438 return false;
2439 }
2440 if let Some(case_node) = node.as_case_node() {
2441 return case_node
2442 .conditions()
2443 .iter()
2444 .any(|condition| match condition.as_when_node() {
2445 Some(when_node) => self.arm_jump_exposed(when_node.statements()),
2446 None => false,
2447 })
2448 || case_node
2449 .else_clause()
2450 .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()));
2451 }
2452 if let Some(case_node) = node.as_case_match_node() {
2453 return case_node
2454 .conditions()
2455 .iter()
2456 .any(|condition| match condition.as_in_node() {
2457 Some(in_node) => self.arm_jump_exposed(in_node.statements()),
2458 None => false,
2459 })
2460 || case_node
2461 .else_clause()
2462 .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()));
2463 }
2464 if let Some(parentheses) = node.as_parentheses_node() {
2465 return match parentheses.body() {
2466 Some(body) => match body.as_statements_node() {
2467 Some(statements) => self.arm_jump_exposed(Some(statements)),
2468 None => self.jump_exposed(&body),
2469 },
2470 None => false,
2471 };
2472 }
2473 false
2474 }
2475
2476 fn arm_jump_exposed(&self, statements: Option<StatementsNode<'_>>) -> bool {
2477 match statements.and_then(|statements| statements.body().iter().last()) {
2478 Some(last) => self.jump_exposed(&last),
2479 None => false,
2480 }
2481 }
2482
2483 fn probe_targets<'n>(&self, node: Node<'n>) -> Option<Vec<Node<'n>>> {
2489 if Self::is_jump(&node) {
2490 return Some(vec![node]);
2491 }
2492 if node.as_multi_write_node().is_some()
2493 || node.as_alias_method_node().is_some()
2494 || node.as_alias_global_variable_node().is_some()
2495 || node.as_undef_node().is_some()
2496 {
2497 return None;
2498 }
2499 if !self.jump_exposed(&node) {
2500 return Some(vec![node]);
2501 }
2502 if let Some(if_node) = node.as_if_node() {
2503 let mut targets = self.arm_targets(if_node.statements())?;
2504 match if_node.subsequent() {
2505 Some(subsequent) => match subsequent.as_else_node() {
2506 Some(else_node) => targets.extend(self.arm_targets(else_node.statements())?),
2507 None => targets.extend(self.probe_targets(subsequent)?),
2508 },
2509 None => return None,
2510 }
2511 return Some(targets);
2512 }
2513 if let Some(unless_node) = node.as_unless_node() {
2514 let mut targets = self.arm_targets(unless_node.statements())?;
2515 let else_node = unless_node.else_clause()?;
2516 targets.extend(self.arm_targets(else_node.statements())?);
2517 return Some(targets);
2518 }
2519 if let Some(case_node) = node.as_case_node() {
2520 let mut targets = Vec::new();
2521 for condition in case_node.conditions().iter() {
2522 let when_node = condition.as_when_node()?;
2523 targets.extend(self.arm_targets(when_node.statements())?);
2524 }
2525 targets.extend(self.arm_targets(case_node.else_clause()?.statements())?);
2526 return Some(targets);
2527 }
2528 if let Some(case_node) = node.as_case_match_node() {
2529 let mut targets = Vec::new();
2530 for condition in case_node.conditions().iter() {
2531 let in_node = condition.as_in_node()?;
2532 targets.extend(self.arm_targets(in_node.statements())?);
2533 }
2534 targets.extend(self.arm_targets(case_node.else_clause()?.statements())?);
2535 return Some(targets);
2536 }
2537 if let Some(begin) = node.as_begin_node() {
2538 let mut targets = match begin.else_clause() {
2539 Some(else_node) => self.arm_targets(else_node.statements())?,
2540 None => self.arm_targets(begin.statements())?,
2541 };
2542 let mut rescue = begin.rescue_clause();
2543 while let Some(clause) = rescue {
2544 targets.extend(self.arm_targets(clause.statements())?);
2545 rescue = clause.subsequent();
2546 }
2547 return Some(targets);
2548 }
2549 if let Some(parentheses) = node.as_parentheses_node() {
2550 let body = parentheses.body()?;
2551 return match body.as_statements_node() {
2552 Some(statements) => self.arm_targets(Some(statements)),
2553 None => self.probe_targets(body),
2554 };
2555 }
2556 None
2557 }
2558
2559 fn arm_targets<'n>(&self, statements: Option<StatementsNode<'n>>) -> Option<Vec<Node<'n>>> {
2560 let last = statements.and_then(|statements| statements.body().iter().last())?;
2561 self.probe_targets(last)
2562 }
2563
2564 fn completion_probe(&mut self, last: Node<'_>, key: u64) -> bool {
2569 let Some(targets) = self.probe_targets(last) else {
2570 return false;
2571 };
2572 for target in &targets {
2573 self.wrap_completion(target, key);
2574 }
2575 true
2576 }
2577
2578 fn wrap_completion(&mut self, last: &Node<'_>, key: u64) {
2580 let location = last.location();
2581 let (start, end) = (location.start_offset(), location.end_offset());
2582 let arguments = if let Some(node) = last.as_return_node() {
2583 Some((node.keyword_loc(), node.arguments()))
2584 } else if let Some(node) = last.as_break_node() {
2585 Some((node.keyword_loc(), node.arguments()))
2586 } else {
2587 last.as_next_node()
2588 .map(|node| (node.keyword_loc(), node.arguments()))
2589 };
2590 if let Some((keyword, arguments)) = arguments {
2591 match arguments {
2592 Some(arguments) => {
2593 let arguments_location = arguments.location();
2594 let multiple = arguments.arguments().iter().count() > 1
2595 || arguments
2596 .arguments()
2597 .iter()
2598 .any(|argument| argument.as_splat_node().is_some());
2599 let (open, close) = if multiple {
2600 (format!("{RUBY_PROBE_RECEIVER}.ok({key}, ["), "])")
2602 } else {
2603 (format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("), "))")
2604 };
2605 self.depth += 1;
2606 self.edit(
2607 arguments_location.start_offset(),
2608 EditRank::Opener,
2609 open,
2610 arguments_location.end_offset(),
2611 );
2612 self.edit(
2613 arguments_location.end_offset(),
2614 EditRank::Closer,
2615 close.into(),
2616 arguments_location.start_offset(),
2617 );
2618 self.depth -= 1;
2619 }
2620 None => self.edit(
2621 keyword.start_offset(),
2622 EditRank::StatementProbe,
2623 format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2624 end,
2625 ),
2626 }
2627 return;
2628 }
2629 if last.as_redo_node().is_some() || last.as_retry_node().is_some() {
2630 self.edit(
2631 start,
2632 EditRank::StatementProbe,
2633 format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2634 end,
2635 );
2636 return;
2637 }
2638 self.depth += 1;
2639 self.wrap(start, end, format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("));
2640 self.depth -= 1;
2641 }
2642
2643 fn rescue_modifier(&mut self, node: &RescueModifierNode<'_>) {
2644 let location = node.location();
2645 let (start, end) = (location.start_offset(), location.end_offset());
2646 let Some(id) = self.branch(
2647 start,
2648 end,
2649 "rescue-modifier",
2650 [
2651 ("success", "expression completed"),
2652 ("raised", "fallback used"),
2653 ],
2654 ) else {
2655 return;
2656 };
2657 let key = self.probe_key(ProbeTarget::Try {
2658 success: format!("{id}:success"),
2659 raised: format!("{id}:raised"),
2660 id,
2661 handlers: Vec::new(),
2662 });
2663 let expression = node.expression();
2664 let fallback_node = node.rescue_expression();
2665 let fallback = fallback_node.location();
2666 self.depth += 1;
2667 if Self::is_jump(&expression) {
2668 self.completion_probe(expression, key);
2671 } else {
2672 let expression = expression.location();
2673 self.wrap(
2674 expression.start_offset(),
2675 expression.end_offset(),
2676 format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("),
2677 );
2678 }
2679 if Self::is_jump(&fallback_node) {
2680 self.edit(
2682 fallback.start_offset(),
2683 EditRank::Opener,
2684 format!("({RUBY_PROBE_RECEIVER}.hm0({key}); "),
2685 fallback.end_offset(),
2686 );
2687 self.edit(
2688 fallback.end_offset(),
2689 EditRank::Closer,
2690 ")".into(),
2691 fallback.start_offset(),
2692 );
2693 } else {
2694 self.wrap(
2695 fallback.start_offset(),
2696 fallback.end_offset(),
2697 format!("{RUBY_PROBE_RECEIVER}.hm({key}, ("),
2698 );
2699 }
2700 self.depth -= 1;
2701 }
2702
2703 fn is_jump(node: &Node<'_>) -> bool {
2705 node.as_return_node().is_some()
2706 || node.as_break_node().is_some()
2707 || node.as_next_node().is_some()
2708 || node.as_redo_node().is_some()
2709 || node.as_retry_node().is_some()
2710 }
2711
2712 fn def_node(&mut self, node: &DefNode<'_>) {
2713 let location = node.location();
2714 let (start, end) = (location.start_offset(), location.end_offset());
2715 let name = String::from_utf8_lossy(node.name().as_slice()).into_owned();
2716 let id = stable_id(self.file, "function", start, end, &name);
2717 if !self.point_ids.insert(id.clone()) {
2718 return;
2719 }
2720 self.push_point(&id, start, end, PointKind::Function, Some(name));
2721 let span = self.location_span(&location);
2722 self.methods.push(MethodKeyPlan {
2723 span,
2724 unshifted: span,
2725 id: id.clone(),
2726 });
2727 let body_statements = node.body().and_then(|body| {
2730 if let Some(statements) = body.as_statements_node() {
2731 Some(statements)
2732 } else {
2733 body.as_begin_node().and_then(|begin| begin.statements())
2734 }
2735 });
2736 let signature_end = node
2737 .rparen_loc()
2738 .map(|rparen| rparen.end_offset())
2739 .or_else(|| {
2740 node.parameters()
2741 .map(|parameters| parameters.location().end_offset())
2742 })
2743 .unwrap_or_else(|| node.name_loc().end_offset());
2744 self.body_proves(&body_statements, vec![id], signature_end, "; ", "");
2745 if let Some(body) = node.body()
2746 && let Some(begin) = body.as_begin_node()
2747 {
2748 self.begin_node(&begin, node.end_keyword_loc().map(|loc| loc.start_offset()));
2749 }
2750 if node.equal_loc().is_some()
2751 && let Some(body) = node.body()
2752 && let Some(statements) = body.as_statements_node()
2753 {
2754 for statement in statements.body().iter() {
2755 self.endless_bodies
2756 .insert(statement.location().start_offset());
2757 }
2758 }
2759 }
2760
2761 fn shifted(&self, span: PlanSpan, kind: KeyKind, edits: &[PendingEdit]) -> PlanSpan {
2777 let start_offset = self.line_starts[span.start[0] - 1] + span.start[1];
2778 let end_offset = self.line_starts[span.end[0] - 1] + span.end[1];
2779 let mut start_shift = 0;
2780 let mut end_shift = 0;
2781 for edit in edits {
2782 let (line, _) = self.line_column(edit.offset);
2783 let moves_start = edit.offset < start_offset
2784 || (edit.offset == start_offset
2785 && match (edit.rank, kind) {
2786 (_, KeyKind::Point) => true,
2787 (EditRank::Closer, _) => false,
2788 (EditRank::Opener, KeyKind::List) => end_offset < edit.scope,
2789 (EditRank::Opener, KeyKind::Node) => end_offset <= edit.scope,
2790 (_, KeyKind::List) => end_offset < edit.scope,
2791 (_, KeyKind::Node) => true,
2792 });
2793 let moves_end = edit.offset < end_offset
2794 || (edit.offset == end_offset
2795 && match (edit.rank, kind) {
2796 (_, KeyKind::Point) => true,
2797 (EditRank::Closer, KeyKind::List) => edit.scope >= start_offset,
2798 (EditRank::Closer, KeyKind::Node) => edit.scope > start_offset,
2799 _ => false,
2800 });
2801 if line == span.start[0] && moves_start {
2802 start_shift += edit.text.len();
2803 }
2804 if line == span.end[0] && moves_end {
2805 end_shift += edit.text.len();
2806 }
2807 }
2808 PlanSpan {
2809 start: [span.start[0], span.start[1] + start_shift],
2810 end: [span.end[0], span.end[1] + end_shift],
2811 }
2812 }
2813
2814 fn drop_ractor_insertions(
2818 &mut self,
2819 pending: Vec<PendingEdit>,
2820 blocks: &[(usize, usize)],
2821 ) -> Vec<PendingEdit> {
2822 let inside = |offset: usize| {
2823 blocks
2824 .iter()
2825 .any(|(start, end)| offset >= *start && offset < *end)
2826 };
2827 let (dropped, kept): (Vec<_>, Vec<_>) =
2828 pending.into_iter().partition(|edit| inside(edit.offset));
2829 let mut keys = BTreeSet::new();
2830 for edit in &dropped {
2831 keys.extend(probe_keys_in(&edit.text));
2832 }
2833 let mut targets = BTreeMap::new();
2834 for key in &keys {
2835 if let Some(target) = self.probes.remove(key) {
2836 targets.insert(*key, target);
2837 }
2838 }
2839 let mut unmeasured = probe_obligations_of(&targets);
2840 for target in targets.values() {
2841 for named in target_ids(target) {
2842 if let Some(implied) = self.implied.get(&named) {
2843 unmeasured.extend(implied.hits.iter().map(|id| obligation_of(id)));
2844 unmeasured.extend(implied.decisions.iter().map(|d| d.id.clone()));
2845 }
2846 }
2847 }
2848 unmeasured.sort();
2849 unmeasured.dedup();
2850 self.manifest.unmeasured.extend(unmeasured);
2851 for (start, end) in blocks {
2852 let (line, _) = self.line_column(*start);
2853 let source = self
2854 .text(*start, *end)
2855 .lines()
2856 .next()
2857 .unwrap_or_default()
2858 .to_owned();
2859 self.manifest.limitations.push(limitation(
2860 RACTOR_BLOCK_LIMITATION,
2861 self.file,
2862 line,
2863 &source,
2864 "a Ractor block cannot call Supercov's probes (a non-main Ractor cannot read the probe receiver), so what only a probe could prove inside it is unmeasured; its lines are still counted",
2865 ));
2866 }
2867 kept
2868 }
2869
2870 fn finish(mut self) -> RubyFileObligations {
2871 let mut pending = std::mem::take(&mut self.edits);
2872 pending.sort_by(|left, right| {
2873 left.offset
2874 .cmp(&right.offset)
2875 .then(left.rank.cmp(&right.rank))
2876 .then(left.order.cmp(&right.order))
2877 .then(left.sequence.cmp(&right.sequence))
2878 });
2879 let ractor_blocks = std::mem::take(&mut self.ractor_blocks);
2880 if !ractor_blocks.is_empty() {
2881 pending = self.drop_ractor_insertions(pending, &ractor_blocks);
2882 }
2883 let branches = std::mem::take(&mut self.branches)
2884 .into_iter()
2885 .map(|mut branch| {
2886 branch.key.span = self.shifted(branch.key.span, branch.key.kind, &pending);
2887 branch
2888 })
2889 .collect::<Vec<_>>();
2890 let cases: Vec<CasePlan> = std::mem::take(&mut self.cases)
2891 .into_iter()
2892 .map(|mut case| {
2893 for clause in &mut case.clauses {
2894 clause.key.span = self.shifted(clause.key.span, clause.key.kind, &pending);
2895 }
2896 if let Some(no_match) = &mut case.no_match {
2897 no_match.key.span =
2898 self.shifted(no_match.key.span, no_match.key.kind, &pending);
2899 }
2900 case
2901 })
2902 .collect();
2903 let methods: Vec<MethodKeyPlan> = std::mem::take(&mut self.methods)
2904 .into_iter()
2905 .map(|mut method| {
2906 method.span = self.shifted(method.span, KeyKind::Node, &pending);
2907 method
2908 })
2909 .collect();
2910 let edits = pending
2911 .into_iter()
2912 .map(|edit| Edit {
2913 offset: edit.offset,
2914 text: edit.text,
2915 rank: match edit.rank {
2916 EditRank::Clause => "clause",
2917 EditRank::StatementProbe => "statement",
2918 EditRank::Opener => "opener",
2919 EditRank::Closer => "closer",
2920 }
2921 .into(),
2922 scope: edit.scope,
2923 })
2924 .collect::<Vec<_>>();
2925 if let Some((id, line)) = self.begin_unmeasured.first() {
2926 let source = self
2927 .manifest
2928 .branches
2929 .iter()
2930 .find(|branch| &branch.id == id)
2931 .map(|branch| branch.source.lines().next().unwrap_or_default().to_owned())
2932 .unwrap_or_default();
2933 self.manifest.limitations.push(limitation(
2934 BEGIN_BODY_LIMITATION,
2935 self.file,
2936 *line,
2937 &source,
2938 "a begin body that is empty, or ends in a statement with no expression form, cannot have its completion observed",
2939 ));
2940 }
2941 self.manifest.unmeasured.sort();
2942 self.manifest.unmeasured.dedup();
2943 RubyFileObligations {
2944 manifest: self.manifest,
2945 plan: RubyFilePlan {
2946 ractor_blocks: ractor_blocks
2947 .iter()
2948 .map(|(start, end)| [*start, *end])
2949 .collect(),
2950 probe_obligations: {
2953 let keyed = stdlib_provable(&branches, &cases, &methods);
2954 probe_obligations_of(&self.probes)
2955 .into_iter()
2956 .filter(|id| !keyed.contains(id))
2957 .collect()
2958 },
2959 edits,
2960 lines: self.lines,
2961 statement_offsets: self.statement_offsets,
2962 branches,
2963 methods,
2964 cases,
2965 implied: self.implied,
2966 },
2967 probes: self.probes,
2968 }
2969 }
2970}
2971
2972fn limitation(id: &str, file: &str, line: usize, source: &str, reason: &str) -> serde_json::Value {
2973 json!({
2974 "id": id,
2975 "kind": "semantic-safety",
2976 "file": file,
2977 "line": line,
2978 "column": 0,
2979 "source": source,
2980 "reason": reason
2981 })
2982}
2983
2984impl<'pr> Visit<'pr> for Collector<'_> {
2985 fn visit_statements_node(&mut self, node: &StatementsNode<'pr>) {
2986 if !self
2987 .expression_lists
2988 .contains(&node.location().start_offset())
2989 {
2990 self.statements(node);
2991 }
2992 ruby_prism::visit_statements_node(self, node);
2993 }
2994
2995 fn visit_parentheses_node(&mut self, node: &ruby_prism::ParenthesesNode<'pr>) {
2996 if let Some(body) = node.body() {
2997 self.expression_lists.insert(body.location().start_offset());
2998 }
2999 ruby_prism::visit_parentheses_node(self, node);
3000 }
3001
3002 fn visit_embedded_statements_node(&mut self, node: &ruby_prism::EmbeddedStatementsNode<'pr>) {
3003 if let Some(statements) = node.statements() {
3004 self.expression_lists
3005 .insert(statements.location().start_offset());
3006 }
3007 ruby_prism::visit_embedded_statements_node(self, node);
3008 }
3009
3010 fn visit_if_node(&mut self, node: &IfNode<'pr>) {
3011 let offset = node.location().start_offset();
3012 if self.guard_nodes.contains(&offset) {
3013 self.depth += 1;
3017 self.visit(&node.predicate());
3018 self.depth -= 1;
3019 return;
3020 }
3021 if self.elsif_nodes.contains(&offset) {
3022 self.depth += 1;
3023 ruby_prism::visit_if_node(self, node);
3024 self.depth -= 1;
3025 return;
3026 }
3027 if let Some(truthy) = self.literal_truth(node.predicate()) {
3028 self.depth += 1;
3033 self.visit(&node.predicate());
3034 if truthy {
3035 if let Some(statements) = node.statements() {
3036 self.visit_statements_node(&statements);
3037 }
3038 } else if let Some(subsequent) = node.subsequent() {
3039 match subsequent.as_if_node() {
3040 Some(elsif) => self.visit_if_node(&elsif),
3041 None => {
3042 if let Some(else_node) = subsequent.as_else_node()
3043 && let Some(statements) = else_node.statements()
3044 {
3045 self.visit_statements_node(&statements);
3046 }
3047 }
3048 }
3049 }
3050 self.depth -= 1;
3051 return;
3052 }
3053 let kind = if node.if_keyword_loc().is_none() {
3056 "ternary"
3057 } else {
3058 "if"
3059 };
3060 self.if_node(node, kind);
3061 let mut subsequent = node.subsequent();
3062 while let Some(next) = subsequent {
3063 match next.as_if_node() {
3064 Some(elsif) => {
3065 self.elsif_nodes.insert(elsif.location().start_offset());
3066 self.if_node(&elsif, "elsif");
3067 subsequent = elsif.subsequent();
3068 }
3069 None => break,
3070 }
3071 }
3072 self.depth += 1;
3073 ruby_prism::visit_if_node(self, node);
3077 self.depth -= 1;
3078 }
3079
3080 fn visit_unless_node(&mut self, node: &ruby_prism::UnlessNode<'pr>) {
3081 if self.guard_nodes.contains(&node.location().start_offset()) {
3082 self.depth += 1;
3083 self.visit(&node.predicate());
3084 self.depth -= 1;
3085 return;
3086 }
3087 if let Some(truthy) = self.literal_truth(node.predicate()) {
3088 self.depth += 1;
3089 self.visit(&node.predicate());
3090 if truthy {
3091 if let Some(else_node) = node.else_clause()
3092 && let Some(statements) = else_node.statements()
3093 {
3094 self.visit_statements_node(&statements);
3095 }
3096 } else if let Some(statements) = node.statements() {
3097 self.visit_statements_node(&statements);
3098 }
3099 self.depth -= 1;
3100 return;
3101 }
3102 self.unless_node(node);
3103 self.depth += 1;
3104 ruby_prism::visit_unless_node(self, node);
3105 self.depth -= 1;
3106 }
3107
3108 fn visit_while_node(&mut self, node: &WhileNode<'pr>) {
3109 self.loop_node(
3110 &node.location(),
3111 node.predicate(),
3112 node.statements(),
3113 node.is_begin_modifier(),
3114 false,
3115 );
3116 self.depth += 1;
3117 ruby_prism::visit_while_node(self, node);
3118 self.depth -= 1;
3119 }
3120
3121 fn visit_until_node(&mut self, node: &UntilNode<'pr>) {
3122 self.loop_node(
3123 &node.location(),
3124 node.predicate(),
3125 node.statements(),
3126 node.is_begin_modifier(),
3127 true,
3128 );
3129 self.depth += 1;
3130 ruby_prism::visit_until_node(self, node);
3131 self.depth -= 1;
3132 }
3133
3134 fn visit_for_node(&mut self, node: &ForNode<'pr>) {
3135 self.for_node(node);
3136 self.depth += 1;
3137 ruby_prism::visit_for_node(self, node);
3138 self.depth -= 1;
3139 }
3140
3141 fn visit_case_node(&mut self, node: &CaseNode<'pr>) {
3142 self.case_node(node);
3143 self.depth += 1;
3144 ruby_prism::visit_case_node(self, node);
3145 self.depth -= 1;
3146 }
3147
3148 fn visit_case_match_node(&mut self, node: &CaseMatchNode<'pr>) {
3149 self.case_match_node(node);
3150 self.depth += 1;
3151 ruby_prism::visit_case_match_node(self, node);
3152 self.depth -= 1;
3153 }
3154
3155 fn visit_and_node(&mut self, node: &AndNode<'pr>) {
3156 let location = node.location();
3157 if !self.tree_logicals.contains(&location.start_offset()) {
3158 let left = node.left();
3159 self.value_logical("and", &left, location.start_offset(), location.end_offset());
3160 }
3161 self.depth += 1;
3162 ruby_prism::visit_and_node(self, node);
3163 self.depth -= 1;
3164 }
3165
3166 fn visit_or_node(&mut self, node: &OrNode<'pr>) {
3167 let location = node.location();
3168 if !self.tree_logicals.contains(&location.start_offset()) {
3169 let left = node.left();
3170 self.value_logical("or", &left, location.start_offset(), location.end_offset());
3171 }
3172 self.depth += 1;
3173 ruby_prism::visit_or_node(self, node);
3174 self.depth -= 1;
3175 }
3176
3177 fn visit_call_node(&mut self, node: &CallNode<'pr>) {
3178 if node.name().as_slice() == b"new"
3179 && node.receiver().is_some_and(|receiver| {
3180 receiver
3181 .as_constant_read_node()
3182 .is_some_and(|constant| constant.name().as_slice() == b"Ractor")
3183 })
3184 && let Some(block) = node.block()
3185 && block.as_block_node().is_some()
3186 {
3187 let location = block.location();
3188 self.ractor_blocks
3189 .push((location.start_offset(), location.end_offset()));
3190 }
3191 if node.is_safe_navigation() {
3192 self.safe_navigation(node);
3193 }
3194 if let Some(block) = node.block()
3195 && let Some(block) = block.as_block_node()
3196 && let Some(receiver) = node.receiver()
3197 && !node.is_safe_navigation()
3198 && ITERATORS.contains(&node.name().as_slice())
3199 {
3200 self.iterator_loop(node, &receiver, &block);
3201 }
3202 self.depth += 1;
3203 ruby_prism::visit_call_node(self, node);
3204 self.depth -= 1;
3205 }
3206
3207 fn visit_local_variable_or_write_node(
3208 &mut self,
3209 node: &ruby_prism::LocalVariableOrWriteNode<'pr>,
3210 ) {
3211 let location = node.location();
3212 let value = node.value();
3213 self.op_assign(
3214 "or",
3215 location.start_offset(),
3216 location.end_offset(),
3217 Some(node.name().as_slice()),
3218 &value,
3219 );
3220 self.depth += 1;
3221 ruby_prism::visit_local_variable_or_write_node(self, node);
3222 self.depth -= 1;
3223 }
3224
3225 fn visit_local_variable_and_write_node(
3226 &mut self,
3227 node: &ruby_prism::LocalVariableAndWriteNode<'pr>,
3228 ) {
3229 let location = node.location();
3230 let value = node.value();
3231 self.op_assign(
3232 "and",
3233 location.start_offset(),
3234 location.end_offset(),
3235 Some(node.name().as_slice()),
3236 &value,
3237 );
3238 self.depth += 1;
3239 ruby_prism::visit_local_variable_and_write_node(self, node);
3240 self.depth -= 1;
3241 }
3242
3243 fn visit_instance_variable_or_write_node(
3244 &mut self,
3245 node: &ruby_prism::InstanceVariableOrWriteNode<'pr>,
3246 ) {
3247 let location = node.location();
3248 let value = node.value();
3249 self.op_assign(
3250 "or",
3251 location.start_offset(),
3252 location.end_offset(),
3253 Some(node.name().as_slice()),
3254 &value,
3255 );
3256 self.depth += 1;
3257 ruby_prism::visit_instance_variable_or_write_node(self, node);
3258 self.depth -= 1;
3259 }
3260
3261 fn visit_instance_variable_and_write_node(
3262 &mut self,
3263 node: &ruby_prism::InstanceVariableAndWriteNode<'pr>,
3264 ) {
3265 let location = node.location();
3266 let value = node.value();
3267 self.op_assign(
3268 "and",
3269 location.start_offset(),
3270 location.end_offset(),
3271 Some(node.name().as_slice()),
3272 &value,
3273 );
3274 self.depth += 1;
3275 ruby_prism::visit_instance_variable_and_write_node(self, node);
3276 self.depth -= 1;
3277 }
3278
3279 fn visit_global_variable_or_write_node(
3280 &mut self,
3281 node: &ruby_prism::GlobalVariableOrWriteNode<'pr>,
3282 ) {
3283 let location = node.location();
3284 let value = node.value();
3285 self.op_assign(
3286 "or",
3287 location.start_offset(),
3288 location.end_offset(),
3289 Some(node.name().as_slice()),
3290 &value,
3291 );
3292 self.depth += 1;
3293 ruby_prism::visit_global_variable_or_write_node(self, node);
3294 self.depth -= 1;
3295 }
3296
3297 fn visit_class_variable_or_write_node(
3298 &mut self,
3299 node: &ruby_prism::ClassVariableOrWriteNode<'pr>,
3300 ) {
3301 let location = node.location();
3302 let value = node.value();
3303 self.op_assign(
3304 "or",
3305 location.start_offset(),
3306 location.end_offset(),
3307 None,
3312 &value,
3313 );
3314 self.depth += 1;
3315 ruby_prism::visit_class_variable_or_write_node(self, node);
3316 self.depth -= 1;
3317 }
3318
3319 fn visit_call_or_write_node(&mut self, node: &ruby_prism::CallOrWriteNode<'pr>) {
3320 let location = node.location();
3321 let value = node.value();
3322 self.op_assign(
3323 "or",
3324 location.start_offset(),
3325 location.end_offset(),
3326 None,
3327 &value,
3328 );
3329 self.depth += 1;
3330 ruby_prism::visit_call_or_write_node(self, node);
3331 self.depth -= 1;
3332 }
3333
3334 fn visit_call_and_write_node(&mut self, node: &ruby_prism::CallAndWriteNode<'pr>) {
3335 let location = node.location();
3336 let value = node.value();
3337 self.op_assign(
3338 "and",
3339 location.start_offset(),
3340 location.end_offset(),
3341 None,
3342 &value,
3343 );
3344 self.depth += 1;
3345 ruby_prism::visit_call_and_write_node(self, node);
3346 self.depth -= 1;
3347 }
3348
3349 fn visit_index_or_write_node(&mut self, node: &ruby_prism::IndexOrWriteNode<'pr>) {
3350 let location = node.location();
3351 let value = node.value();
3352 self.op_assign(
3353 "or",
3354 location.start_offset(),
3355 location.end_offset(),
3356 None,
3357 &value,
3358 );
3359 self.depth += 1;
3360 ruby_prism::visit_index_or_write_node(self, node);
3361 self.depth -= 1;
3362 }
3363
3364 fn visit_index_and_write_node(&mut self, node: &ruby_prism::IndexAndWriteNode<'pr>) {
3365 let location = node.location();
3366 let value = node.value();
3367 self.op_assign(
3368 "and",
3369 location.start_offset(),
3370 location.end_offset(),
3371 None,
3372 &value,
3373 );
3374 self.depth += 1;
3375 ruby_prism::visit_index_and_write_node(self, node);
3376 self.depth -= 1;
3377 }
3378
3379 fn visit_constant_or_write_node(&mut self, node: &ruby_prism::ConstantOrWriteNode<'pr>) {
3380 let location = node.location();
3381 let value = node.value();
3382 self.op_assign(
3383 "or",
3384 location.start_offset(),
3385 location.end_offset(),
3386 None,
3387 &value,
3388 );
3389 self.depth += 1;
3390 ruby_prism::visit_constant_or_write_node(self, node);
3391 self.depth -= 1;
3392 }
3393
3394 fn visit_def_node(&mut self, node: &DefNode<'pr>) {
3395 self.def_node(node);
3396 self.depth += 1;
3397 ruby_prism::visit_def_node(self, node);
3398 self.depth -= 1;
3399 }
3400
3401 fn visit_begin_node(&mut self, node: &BeginNode<'pr>) {
3402 if node.begin_keyword_loc().is_some() {
3406 self.begin_node(node, None);
3407 }
3408 self.depth += 1;
3409 ruby_prism::visit_begin_node(self, node);
3410 self.depth -= 1;
3411 }
3412
3413 fn visit_block_node(&mut self, node: &ruby_prism::BlockNode<'pr>) {
3414 if let Some(body) = node.body()
3415 && let Some(begin) = body.as_begin_node()
3416 {
3417 self.begin_node(&begin, Some(node.closing_loc().start_offset()));
3418 }
3419 self.depth += 1;
3420 ruby_prism::visit_block_node(self, node);
3421 self.depth -= 1;
3422 }
3423
3424 fn visit_lambda_node(&mut self, node: &ruby_prism::LambdaNode<'pr>) {
3425 if let Some(body) = node.body()
3426 && let Some(begin) = body.as_begin_node()
3427 {
3428 self.begin_node(&begin, Some(node.closing_loc().start_offset()));
3429 }
3430 self.depth += 1;
3431 ruby_prism::visit_lambda_node(self, node);
3432 self.depth -= 1;
3433 }
3434
3435 fn visit_rescue_modifier_node(&mut self, node: &RescueModifierNode<'pr>) {
3436 self.rescue_modifier(node);
3437 self.depth += 1;
3438 ruby_prism::visit_rescue_modifier_node(self, node);
3439 self.depth -= 1;
3440 }
3441}
3442
3443pub fn build_ruby_obligations(
3446 file: &str,
3447 source: &[u8],
3448 next_probe: &mut u64,
3449) -> Result<RubyFileObligations, RubyInstrumenterError> {
3450 let result = ruby_prism::parse(source);
3451 let errors = result
3452 .errors()
3453 .map(|error| error.message().to_owned())
3454 .collect::<Vec<_>>();
3455 if !errors.is_empty() {
3456 return Err(RubyInstrumenterError::Parse(errors.join("; ")));
3457 }
3458 let mut collector = Collector::new(file, source, next_probe);
3459 collector.visit(&result.node());
3460 if let Some(error) = collector.error.take() {
3461 return Err(error);
3462 }
3463 Ok(collector.finish())
3464}
3465
3466pub fn apply_edits(source: &[u8], edits: &[Edit]) -> Vec<u8> {
3468 let mut output =
3469 Vec::with_capacity(source.len() + edits.iter().map(|e| e.text.len()).sum::<usize>());
3470 let mut cursor = 0;
3471 for edit in edits {
3472 output.extend_from_slice(&source[cursor..edit.offset]);
3473 output.extend_from_slice(edit.text.as_bytes());
3474 cursor = edit.offset;
3475 }
3476 output.extend_from_slice(&source[cursor..]);
3477 output
3478}
3479
3480#[cfg(test)]
3481mod tests {
3482 use super::*;
3483
3484 const SOURCE: &str = r#"class Shapes
3485 def classify(a, b, c)
3486 if a && (b || c)
3487 :yes
3488 elsif a
3489 :half
3490 else
3491 :no
3492 end
3493 end
3494
3495 def loops(items, flag)
3496 total = 0
3497 items.each { |i| total += i if i > 2 && flag }
3498 while total > 100
3499 total -= 50
3500 end
3501 for x in items do total += x end
3502 total
3503 end
3504
3505 def logical(a, b)
3506 x = a || b
3507 @cache ||= {}
3508 @cache[a] ||= b
3509 y = a ? 1 : 2; z = a&.size
3510 [x, y, z]
3511 end
3512
3513 def guarded(s)
3514 Integer(s)
3515 rescue ArgumentError
3516 -1
3517 ensure
3518 @done = true
3519 end
3520
3521 def cases(v)
3522 case v
3523 when 0 then :zero
3524 else :other
3525 end
3526 v.to_s rescue "bad"
3527 end
3528end
3529"#;
3530
3531 #[test]
3532 fn discovers_obligations_with_stable_ids_and_newline_free_edits() {
3533 let mut probe = 0;
3534 let first = build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
3535 let mut probe = 0;
3536 let second =
3537 build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
3538 assert_eq!(first, second);
3539 let manifest = &first.manifest;
3540 let functions = manifest
3541 .points
3542 .iter()
3543 .filter(|point| point.kind == PointKind::Function)
3544 .map(|point| point.label.clone().unwrap())
3545 .collect::<Vec<_>>();
3546 assert_eq!(
3547 functions,
3548 ["classify", "loops", "logical", "guarded", "cases"]
3549 );
3550 let compound = manifest
3551 .decisions
3552 .iter()
3553 .find(|decision| decision.source == "a && (b || c)")
3554 .unwrap();
3555 assert_eq!(compound.conditions, ["a", "b", "c"]);
3556 assert_eq!(compound.kind, "if");
3557 assert!(
3558 manifest
3559 .decisions
3560 .iter()
3561 .any(|d| d.kind == "elsif" && d.conditions == ["a"])
3562 );
3563 assert!(manifest.decisions.iter().any(|d| d.kind == "ternary"));
3564 assert!(manifest.decisions.iter().any(|d| d.kind == "while"));
3565 for kind in [
3566 "for",
3567 "logical-or",
3568 "or-assign",
3569 "safe-navigation",
3570 "begin",
3571 "rescue-0",
3572 "case-when-0",
3573 "case-else",
3574 "rescue-modifier",
3575 ] {
3576 assert!(
3577 manifest.branches.iter().any(|branch| branch.kind == kind),
3578 "missing branch kind {kind}"
3579 );
3580 }
3581 for edit in &first.plan.edits {
3582 assert!(!edit.text.contains('\n'));
3583 }
3584 assert!(
3585 first
3586 .plan
3587 .edits
3588 .windows(2)
3589 .all(|pair| pair[0].offset <= pair[1].offset)
3590 );
3591 assert!(manifest.unmeasured.is_empty());
3594 assert!(manifest.limitations.is_empty());
3595 }
3596
3597 #[test]
3598 fn transformed_source_keeps_line_count_and_carries_probes() {
3599 let mut probe = 0;
3600 let obligations =
3601 build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
3602 let transformed =
3603 String::from_utf8(apply_edits(SOURCE.as_bytes(), &obligations.plan.edits)).unwrap();
3604 assert_eq!(transformed.lines().count(), SOURCE.lines().count());
3605 assert!(
3606 transformed.contains("if $__supercov.d(0, ($__supercov.c(0, 0, (a)) && ($__supercov.c(0, 1, (b)) || $__supercov.c(0, 2, (c)))))"),
3607 "{transformed}"
3608 );
3609 assert!(transformed.contains("while $__supercov.w("));
3610 assert!(transformed.contains("for x in $__supercov.f("));
3611 assert!(transformed.contains("do $__supercov.fb("));
3612 assert!(transformed.contains("x = $__supercov.l("));
3613 assert!(transformed.contains("($__supercov.pre("));
3614 assert!(transformed.contains("||= ($__supercov.es("));
3615 assert!(transformed.contains("rescue Exception => __supercov_e; $__supercov.p("));
3616 assert!(transformed.contains("$__supercov.h("));
3617 assert!(transformed.contains("$__supercov.ok("));
3618 assert!(transformed.contains("rescue $__supercov.hm("));
3619 assert!(transformed.contains("; $__supercov.s("));
3621 assert!(
3623 obligations
3624 .plan
3625 .branches
3626 .iter()
3627 .any(|branch| !branch.hits.is_empty() && branch.key.group == "if")
3628 );
3629 }
3630
3631 #[test]
3632 fn jumps_and_endless_bodies_take_wrapped_probes_and_literal_predicates_fold() {
3633 let source = "def inc(x) = x + 1\n\
3634 [1].each { |v| y = Integer(v) rescue next }\n\
3635 if false\n dead\nelse\n live\nend\n";
3636 let mut probe = 0;
3637 let obligations =
3638 build_ruby_obligations("lib/x.rb", source.as_bytes(), &mut probe).unwrap();
3639 let transformed =
3640 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3641 assert!(
3642 transformed.contains("def inc(x) = ($__supercov.s("),
3643 "{transformed}"
3644 );
3645 assert!(
3646 transformed.contains("rescue ($__supercov.hm0("),
3647 "{transformed}"
3648 );
3649 assert!(
3651 obligations
3652 .plan
3653 .branches
3654 .iter()
3655 .all(|branch| branch.key.group != "if")
3656 );
3657 assert!(
3658 obligations
3659 .manifest
3660 .points
3661 .iter()
3662 .all(|point| point.source != "dead")
3663 );
3664 assert!(
3665 obligations
3666 .manifest
3667 .points
3668 .iter()
3669 .any(|point| point.source == "live")
3670 );
3671 }
3672
3673 #[test]
3674 fn void_valued_last_statements_are_probed_arm_by_arm() {
3675 let source =
3678 "def m(c)\n if c\n return 1\n else\n return 2\n end\nrescue\n nil\nend\n";
3679 let mut probe = 0;
3680 let obligations =
3681 build_ruby_obligations("lib/v.rb", source.as_bytes(), &mut probe).unwrap();
3682 let transformed =
3683 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3684 assert_eq!(
3685 transformed.matches("$__supercov.ok(").count(),
3686 2,
3687 "{transformed}"
3688 );
3689 assert!(
3690 transformed.contains("return $__supercov.ok("),
3691 "{transformed}"
3692 );
3693 assert!(
3694 !obligations
3695 .manifest
3696 .unmeasured
3697 .iter()
3698 .any(|id| id.ends_with(":success")),
3699 "{:?}",
3700 obligations.manifest.unmeasured
3701 );
3702 assert!(!obligations.plan.probe_obligations.is_empty());
3703 }
3704
3705 #[test]
3706 fn expressions_that_can_return_are_never_wrapped() {
3707 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";
3710 let mut probe = 0;
3711 let obligations =
3712 build_ruby_obligations("lib/e.rb", source.as_bytes(), &mut probe).unwrap();
3713 let transformed =
3714 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3715 assert!(!transformed.contains("$__supercov.ok"), "{transformed}");
3716 assert!(
3717 obligations
3718 .manifest
3719 .unmeasured
3720 .iter()
3721 .any(|id| id.ends_with(":success")),
3722 "the begin's completion is declared instead"
3723 );
3724 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";
3726 let mut probe = 0;
3727 let obligations = build_ruby_obligations("lib/f.rb", both.as_bytes(), &mut probe).unwrap();
3728 let transformed =
3729 String::from_utf8(apply_edits(both.as_bytes(), &obligations.plan.edits)).unwrap();
3730 assert!(
3731 transformed.contains("return $__supercov.ok("),
3732 "{transformed}"
3733 );
3734 assert!(
3735 !obligations
3736 .manifest
3737 .unmeasured
3738 .iter()
3739 .any(|id| id.ends_with(":success")),
3740 "{:?}",
3741 obligations.manifest.unmeasured
3742 );
3743 }
3744
3745 #[test]
3746 fn stdlib_keys_shift_with_insertions_on_their_line() {
3747 let source = "def f(a, b)\n x = 1 if a && b\nend\n";
3748 let mut probe = 0;
3749 let obligations = build_ruby_obligations("m.rb", source.as_bytes(), &mut probe).unwrap();
3750 let then_key = obligations
3751 .plan
3752 .branches
3753 .iter()
3754 .find(|branch| branch.key.branch == "then")
3755 .unwrap();
3756 assert_eq!(then_key.key.span.start, [2, 2]);
3758 assert_eq!(then_key.key.span.end, [2, 7]);
3761 let else_key = obligations
3762 .plan
3763 .branches
3764 .iter()
3765 .find(|branch| branch.key.branch == "else")
3766 .unwrap();
3767 let inserted: usize = obligations
3770 .plan
3771 .edits
3772 .iter()
3773 .map(|edit| edit.text.len())
3774 .sum();
3775 assert_eq!(else_key.key.span.end, [2, 17 + inserted]);
3776 assert!(
3777 then_key.hits.len() == 1,
3778 "modifier body statement proven by the then key"
3779 );
3780 }
3781
3782 #[test]
3783 fn line_events_prove_bodies_and_probes_prove_the_rest() {
3784 let source = "def both(a)\n if a\n 1\n else\n 2\n end\nend\n\ndef guard(a)\n return 0 if a\n a&.size\nend\n\ndef pick(v)\n case v\n when 1 then :one\n when 2\n :two\n end\nend\n\ndef short = 3\n\ndef empty; end\n";
3790 let mut probe = 0;
3791 let obligations =
3792 build_ruby_obligations("lib/m.rb", source.as_bytes(), &mut probe).unwrap();
3793 let plan = &obligations.plan;
3794 let manifest = &obligations.manifest;
3795 let function = |name: &str| {
3796 manifest
3797 .points
3798 .iter()
3799 .find(|point| {
3800 point.kind == PointKind::Function && point.label.as_deref() == Some(name)
3801 })
3802 .unwrap()
3803 .id
3804 .clone()
3805 };
3806 let statement_on = |line: usize| plan.lines.get(&line).cloned().unwrap();
3807 let transformed = String::from_utf8(apply_edits(source.as_bytes(), &plan.edits)).unwrap();
3808 assert_eq!(transformed.lines().count(), source.lines().count());
3809
3810 assert_eq!(
3813 plan.implied[&statement_on(2)].hits,
3814 [function("both")],
3815 "the `if` statement opens the method"
3816 );
3817 let three = &plan.implied[&statement_on(3)];
3818 assert!(three.hits.is_empty());
3819 assert_eq!(three.decisions.len(), 1);
3820 assert!(three.decisions[0].value);
3821 let five = &plan.implied[&statement_on(5)];
3822 assert!(!five.decisions[0].value);
3823 assert!(!transformed.contains(".d(") || transformed.matches(".d(").count() == 1);
3824
3825 let guard_decision = manifest
3828 .decisions
3829 .iter()
3830 .find(|d| d.kind == "if" && d.conditions == ["a"] && d.line == 10)
3831 .unwrap();
3832 let outcome_true = format!("{}:outcome:true", guard_decision.id);
3833 assert!(
3834 plan.implied.contains_key(&outcome_true),
3835 "modifier body implied by its outcome"
3836 );
3837 assert!(transformed.contains("return 0 if $__supercov.d("));
3838 assert!(
3839 !transformed.contains(".c("),
3840 "a lone condition needs no wrapper"
3841 );
3842 assert!(transformed.contains("$__supercov.n("));
3844 assert!(transformed.contains("else $__supercov.hs("));
3847 assert!(transformed.contains("when 1 then $__supercov.s("));
3848 assert!(transformed.contains("def short = ($__supercov.s("));
3850 assert!(transformed.contains("def empty; $__supercov.hs("));
3851 let empty_probe = plan
3852 .edits
3853 .iter()
3854 .find(|edit| edit.text.contains(".hs(") && edit.text.starts_with("; "))
3855 .unwrap();
3856 assert!(empty_probe.text.contains("hs"));
3857 for id in [
3861 guard_decision.id.clone(),
3862 function("empty"),
3863 function("short"),
3864 ] {
3865 assert!(
3866 !plan.probe_obligations.contains(&id),
3867 "{id} is key-provable on 3.3"
3868 );
3869 }
3870 let safe = manifest
3871 .branches
3872 .iter()
3873 .find(|b| b.kind == "safe-navigation")
3874 .unwrap();
3875 assert!(!plan.probe_obligations.contains(&safe.id));
3876 }
3877
3878 #[test]
3879 fn ractor_blocks_get_no_probes_and_declare_what_only_probes_could_prove() {
3880 let source = "def inside(a)\n Ractor.new(a) { |v| v ? 1 : 2 }.take\nend\n\ndef outside(a)\n a ? 1 : 2\nend\n";
3884 let mut probe = 0;
3885 let obligations =
3886 build_ruby_obligations("lib/r.rb", source.as_bytes(), &mut probe).unwrap();
3887 let plan = &obligations.plan;
3888 let manifest = &obligations.manifest;
3889 let transformed = String::from_utf8(apply_edits(source.as_bytes(), &plan.edits)).unwrap();
3890 let block_line = transformed.lines().nth(1).unwrap();
3891 assert!(
3892 !block_line.contains("$__supercov"),
3893 "no probe inside the Ractor block: {block_line}"
3894 );
3895 assert!(
3896 transformed
3897 .lines()
3898 .nth(5)
3899 .unwrap()
3900 .contains("$__supercov.d("),
3901 "the ternary outside is probed"
3902 );
3903 let inside = manifest.decisions.iter().find(|d| d.line == 2).unwrap();
3904 assert!(
3905 manifest.unmeasured.contains(&inside.id),
3906 "the block's ternary leaves the denominator"
3907 );
3908 let outside = manifest.decisions.iter().find(|d| d.line == 6).unwrap();
3909 assert!(!manifest.unmeasured.contains(&outside.id));
3910 assert_eq!(plan.ractor_blocks.len(), 1);
3911 assert!(
3912 manifest
3913 .limitations
3914 .iter()
3915 .any(|l| l["id"] == RACTOR_BLOCK_LIMITATION && l["line"] == 2)
3916 );
3917 assert!(!plan.probe_obligations.contains(&inside.id));
3919 }
3920
3921 #[test]
3922 fn an_empty_guarded_in_body_takes_its_probe_after_the_guard_wrapper() {
3923 let source = "def f(data)\n case data\n in String then data = 1\n in Array if data.all? { _1 > 0 }\n else\n raise TypeError\n end\nend\n";
3927 let mut probe = 0;
3928 let obligations =
3929 build_ruby_obligations("lib/g.rb", source.as_bytes(), &mut probe).unwrap();
3930 let transformed =
3931 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3932 let guard_line = transformed.lines().nth(3).unwrap();
3933 assert!(
3934 guard_line.contains(")) then $__supercov.hs("),
3935 "probe after the wrapper: {guard_line}"
3936 );
3937 assert!(
3938 !guard_line.contains("then $__supercov.hs(")
3939 || !guard_line.contains("hs(")
3940 || guard_line.matches(')').count() == guard_line.matches('(').count(),
3941 "balanced: {guard_line}"
3942 );
3943 }
3944
3945 #[test]
3946 fn a_class_variable_or_assignment_is_never_read_before_it_exists() {
3947 let source = "class C\n def self.ext\n @@ext ||= {}\n end\nend\n";
3948 let mut probe = 0;
3949 let obligations =
3950 build_ruby_obligations("lib/c.rb", source.as_bytes(), &mut probe).unwrap();
3951 let transformed =
3952 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3953 let line = transformed.lines().nth(2).unwrap();
3954 assert!(
3955 !line.contains(".l("),
3956 "no read of @@ext before the assignment: {line}"
3957 );
3958 assert!(
3959 line.contains(".pre(") && line.contains(".es("),
3960 "arrival form: {line}"
3961 );
3962 }
3963
3964 #[test]
3965 fn a_predicate_ruby_folds_leaves_no_obligations_in_the_dead_arm() {
3966 let source = "def f(x)\n if x and false\n x ? 1 : 2\n end\n if x or true\n 3\n else\n 4\n end\nend\n";
3970 let mut probe = 0;
3971 let obligations =
3972 build_ruby_obligations("lib/f.rb", source.as_bytes(), &mut probe).unwrap();
3973 let manifest = &obligations.manifest;
3974 assert!(
3975 !manifest.decisions.iter().any(|d| d.kind == "ternary"),
3976 "the dead arm's ternary is not an obligation"
3977 );
3978 assert!(
3979 !manifest.decisions.iter().any(|d| d.kind == "if"),
3980 "a folded predicate is no decision"
3981 );
3982 assert!(
3983 !manifest.points.iter().any(|p| p.line == 8),
3984 "the dead else arm holds no statement"
3985 );
3986 assert!(
3987 manifest.points.iter().any(|p| p.line == 6),
3988 "the live arm does"
3989 );
3990 }
3991
3992 #[test]
3993 fn rejects_invalid_ruby() {
3994 let mut probe = 0;
3995 assert!(matches!(
3996 build_ruby_obligations("m.rb", b"def x(\n", &mut probe),
3997 Err(RubyInstrumenterError::Parse(_))
3998 ));
3999 }
4000}