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