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_decision<'n>(&mut self, operand: impl Fn() -> Node<'n>) {
2178 if !self.is_compound(operand()) {
2179 return;
2180 }
2181 let (root, _) = self.strip(operand());
2182 if self.tree_logicals.contains(&root.location().start_offset()) {
2183 return;
2184 }
2185 self.depth += 1;
2186 self.probe_decision(operand(), "value", None, "d");
2187 self.depth -= 1;
2188 }
2189
2190 fn value_logical(&mut self, op: &str, left: &Node<'_>, node_start: usize, node_end: usize) {
2191 let Some(id) = self.branch(
2192 node_start,
2193 node_end,
2194 &format!("logical-{op}"),
2195 [
2196 ("short-circuit", "short-circuited"),
2197 ("evaluated", "right operand evaluated"),
2198 ],
2199 ) else {
2200 return;
2201 };
2202 let key = self.probe_key(ProbeTarget::Logical {
2203 op: op.into(),
2204 short_circuit: format!("{id}:short-circuit"),
2205 evaluated: format!("{id}:evaluated"),
2206 });
2207 let left = left.location();
2208 self.depth += 1;
2209 self.wrap(
2210 left.start_offset(),
2211 left.end_offset(),
2212 format!("{RUBY_PROBE_RECEIVER}.l({key}, ("),
2213 );
2214 self.depth -= 1;
2215 }
2216
2217 fn op_assign(
2221 &mut self,
2222 op: &str,
2223 node_start: usize,
2224 node_end: usize,
2225 name: Option<&[u8]>,
2226 value: &Node<'_>,
2227 ) {
2228 let Some(id) = self.branch(
2229 node_start,
2230 node_end,
2231 &format!("{op}-assign"),
2232 [
2233 ("short-circuit", "assignment skipped"),
2234 ("evaluated", "value evaluated and assigned"),
2235 ],
2236 ) else {
2237 return;
2238 };
2239 match name {
2240 Some(name) => {
2241 let key = self.probe_key(ProbeTarget::Logical {
2242 op: op.into(),
2243 short_circuit: format!("{id}:short-circuit"),
2244 evaluated: format!("{id}:evaluated"),
2245 });
2246 let name = String::from_utf8_lossy(name);
2247 self.depth += 1;
2248 self.edit(
2249 node_start,
2250 EditRank::Opener,
2251 format!("({RUBY_PROBE_RECEIVER}.l({key}, {name}); "),
2252 node_end,
2253 );
2254 self.edit(node_end, EditRank::Closer, ")".into(), node_start);
2255 self.depth -= 1;
2256 }
2257 None => {
2258 let key = self.probe_key(ProbeTarget::Arrival {
2262 short_circuit: format!("{id}:short-circuit"),
2263 evaluated: format!("{id}:evaluated"),
2264 });
2265 let value_location = value.location();
2266 self.depth += 1;
2267 self.edit(
2268 node_start,
2269 EditRank::Opener,
2270 format!("({RUBY_PROBE_RECEIVER}.pre({key}); "),
2271 node_end,
2272 );
2273 self.edit(node_end, EditRank::Closer, ")".into(), node_start);
2274 self.depth += 1;
2275 self.edit(
2276 value_location.start_offset(),
2277 EditRank::Opener,
2278 format!("({RUBY_PROBE_RECEIVER}.es({key}); "),
2279 value_location.end_offset(),
2280 );
2281 self.edit(
2282 value_location.end_offset(),
2283 EditRank::Closer,
2284 ")".into(),
2285 value_location.start_offset(),
2286 );
2287 self.depth -= 2;
2288 }
2289 }
2290 }
2291
2292 fn begin_node(&mut self, node: &BeginNode<'_>, close: Option<usize>) {
2299 let has_rescue = node.rescue_clause().is_some();
2300 let has_ensure = node.ensure_clause().is_some();
2301 if !has_rescue && !has_ensure {
2302 return;
2303 }
2304 let location = node.location();
2305 let (start, end) = (location.start_offset(), location.end_offset());
2306 let Some(id) = self.branch(
2307 start,
2308 end,
2309 "begin",
2310 [
2311 ("success", "body completed"),
2312 ("raised", "exception raised"),
2313 ],
2314 ) else {
2315 return;
2316 };
2317 let mut handlers = Vec::new();
2318 let mut handler_edits = Vec::new();
2319 let mut rescue = node.rescue_clause();
2320 let mut index = 0;
2321 while let Some(clause) = rescue {
2322 let clause_location = clause.location();
2323 let Some(handler_id) = self.branch(
2324 clause_location.start_offset(),
2325 clause_location.end_offset(),
2326 &format!("rescue-{index}"),
2327 [("missed", "not selected"), ("selected", "selected")],
2328 ) else {
2329 return;
2330 };
2331 handlers.push(HandlerTarget {
2332 missed: format!("{handler_id}:missed"),
2333 selected: format!("{handler_id}:selected"),
2334 id: handler_id,
2335 });
2336 handler_edits.push(self.handler_probe_position(&clause));
2337 rescue = clause.subsequent();
2338 index += 1;
2339 }
2340 let key = self.probe_key(ProbeTarget::Try {
2341 success: format!("{id}:success"),
2342 raised: format!("{id}:raised"),
2343 id: id.clone(),
2344 handlers,
2345 });
2346 for (index, (offset, leading, scope_end)) in handler_edits.into_iter().enumerate() {
2347 let text = if leading {
2348 format!("; {RUBY_PROBE_RECEIVER}.h({key}, {index})")
2349 } else {
2350 format!("{RUBY_PROBE_RECEIVER}.h({key}, {index}); ")
2351 };
2352 self.edit(offset, EditRank::StatementProbe, text, scope_end);
2353 }
2354 let clause_offset = node
2356 .else_clause()
2357 .map(|clause| clause.else_keyword_loc().start_offset())
2358 .or_else(|| {
2359 node.ensure_clause()
2360 .map(|clause| clause.ensure_keyword_loc().start_offset())
2361 })
2362 .or_else(|| node.end_keyword_loc().map(|loc| loc.start_offset()))
2363 .or(close);
2364 match clause_offset {
2365 Some(offset) => self.edit(
2366 offset,
2367 EditRank::Clause,
2368 format!(
2369 "rescue Exception => __supercov_e; {RUBY_PROBE_RECEIVER}.p({key}); raise; "
2370 ),
2371 offset,
2372 ),
2373 None => {
2374 self.manifest.unmeasured.push(format!("{id}:raised"));
2375 let (line, _) = self.line_column(start);
2376 self.begin_unmeasured.push((id.clone(), line));
2377 }
2378 }
2379 if let Some(else_clause) = node.else_clause() {
2381 match else_clause
2382 .statements()
2383 .and_then(|statements| statements.body().iter().next())
2384 {
2385 Some(first) => self.edit(
2386 first.location().start_offset(),
2387 EditRank::StatementProbe,
2388 format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2389 first.location().end_offset(),
2390 ),
2391 None => self.edit(
2392 else_clause.else_keyword_loc().end_offset(),
2393 EditRank::StatementProbe,
2394 format!(" {RUBY_PROBE_RECEIVER}.ok0({key});"),
2395 else_clause.else_keyword_loc().end_offset(),
2396 ),
2397 }
2398 return;
2399 }
2400 let last = node
2401 .statements()
2402 .and_then(|statements| statements.body().iter().last());
2403 match last {
2404 Some(last) => {
2405 if !self.completion_probe(last, key) {
2406 self.manifest.unmeasured.push(format!("{id}:success"));
2407 let (line, _) = self.line_column(start);
2408 self.begin_unmeasured.push((id, line));
2409 }
2410 }
2411 None => {
2412 self.manifest.unmeasured.push(format!("{id}:success"));
2413 let (line, _) = self.line_column(start);
2414 self.begin_unmeasured.push((id, line));
2415 }
2416 }
2417 }
2418
2419 fn handler_probe_position(&self, clause: &RescueNode<'_>) -> (usize, bool, usize) {
2422 if let Some(first) = clause
2423 .statements()
2424 .and_then(|statements| statements.body().iter().next())
2425 {
2426 return (
2427 first.location().start_offset(),
2428 false,
2429 first.location().end_offset(),
2430 );
2431 }
2432 let offset = if let Some(then_keyword) = clause.then_keyword_loc() {
2433 then_keyword.end_offset()
2434 } else if let Some(reference) = clause.reference() {
2435 reference.location().end_offset()
2436 } else if let Some(last) = clause.exceptions().iter().last() {
2437 last.location().end_offset()
2438 } else {
2439 clause.keyword_loc().end_offset()
2440 };
2441 (offset, true, offset)
2442 }
2443
2444 fn jump_exposed(&self, node: &Node<'_>) -> bool {
2455 if Self::is_jump(node) {
2456 return true;
2457 }
2458 if let Some(if_node) = node.as_if_node() {
2459 let else_exposed = match if_node.subsequent() {
2460 Some(subsequent) => match subsequent.as_else_node() {
2461 Some(else_node) => self.arm_jump_exposed(else_node.statements()),
2462 None => self.jump_exposed(&subsequent),
2463 },
2464 None => false,
2465 };
2466 return else_exposed || self.arm_jump_exposed(if_node.statements());
2467 }
2468 if let Some(unless_node) = node.as_unless_node() {
2469 let else_exposed = match unless_node.else_clause() {
2470 Some(else_node) => self.arm_jump_exposed(else_node.statements()),
2471 None => false,
2472 };
2473 return else_exposed || self.arm_jump_exposed(unless_node.statements());
2474 }
2475 if let Some(begin) = node.as_begin_node() {
2476 if self.arm_jump_exposed(begin.statements())
2477 || begin
2478 .else_clause()
2479 .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()))
2480 {
2481 return true;
2482 }
2483 let mut rescue = begin.rescue_clause();
2484 while let Some(clause) = rescue {
2485 if self.arm_jump_exposed(clause.statements()) {
2486 return true;
2487 }
2488 rescue = clause.subsequent();
2489 }
2490 return false;
2491 }
2492 if let Some(case_node) = node.as_case_node() {
2493 return case_node
2494 .conditions()
2495 .iter()
2496 .any(|condition| match condition.as_when_node() {
2497 Some(when_node) => self.arm_jump_exposed(when_node.statements()),
2498 None => false,
2499 })
2500 || case_node
2501 .else_clause()
2502 .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()));
2503 }
2504 if let Some(case_node) = node.as_case_match_node() {
2505 return case_node
2506 .conditions()
2507 .iter()
2508 .any(|condition| match condition.as_in_node() {
2509 Some(in_node) => self.arm_jump_exposed(in_node.statements()),
2510 None => false,
2511 })
2512 || case_node
2513 .else_clause()
2514 .is_some_and(|else_node| self.arm_jump_exposed(else_node.statements()));
2515 }
2516 if let Some(parentheses) = node.as_parentheses_node() {
2517 return match parentheses.body() {
2518 Some(body) => match body.as_statements_node() {
2519 Some(statements) => self.arm_jump_exposed(Some(statements)),
2520 None => self.jump_exposed(&body),
2521 },
2522 None => false,
2523 };
2524 }
2525 false
2526 }
2527
2528 fn arm_jump_exposed(&self, statements: Option<StatementsNode<'_>>) -> bool {
2529 match statements.and_then(|statements| statements.body().iter().last()) {
2530 Some(last) => self.jump_exposed(&last),
2531 None => false,
2532 }
2533 }
2534
2535 fn probe_targets<'n>(&self, node: Node<'n>) -> Option<Vec<Node<'n>>> {
2541 if Self::is_jump(&node) {
2542 return Some(vec![node]);
2543 }
2544 if node.as_multi_write_node().is_some()
2545 || node.as_alias_method_node().is_some()
2546 || node.as_alias_global_variable_node().is_some()
2547 || node.as_undef_node().is_some()
2548 {
2549 return None;
2550 }
2551 if !self.jump_exposed(&node) {
2552 return Some(vec![node]);
2553 }
2554 if let Some(if_node) = node.as_if_node() {
2555 let mut targets = self.arm_targets(if_node.statements())?;
2556 match if_node.subsequent() {
2557 Some(subsequent) => match subsequent.as_else_node() {
2558 Some(else_node) => targets.extend(self.arm_targets(else_node.statements())?),
2559 None => targets.extend(self.probe_targets(subsequent)?),
2560 },
2561 None => return None,
2562 }
2563 return Some(targets);
2564 }
2565 if let Some(unless_node) = node.as_unless_node() {
2566 let mut targets = self.arm_targets(unless_node.statements())?;
2567 let else_node = unless_node.else_clause()?;
2568 targets.extend(self.arm_targets(else_node.statements())?);
2569 return Some(targets);
2570 }
2571 if let Some(case_node) = node.as_case_node() {
2572 let mut targets = Vec::new();
2573 for condition in case_node.conditions().iter() {
2574 let when_node = condition.as_when_node()?;
2575 targets.extend(self.arm_targets(when_node.statements())?);
2576 }
2577 targets.extend(self.arm_targets(case_node.else_clause()?.statements())?);
2578 return Some(targets);
2579 }
2580 if let Some(case_node) = node.as_case_match_node() {
2581 let mut targets = Vec::new();
2582 for condition in case_node.conditions().iter() {
2583 let in_node = condition.as_in_node()?;
2584 targets.extend(self.arm_targets(in_node.statements())?);
2585 }
2586 targets.extend(self.arm_targets(case_node.else_clause()?.statements())?);
2587 return Some(targets);
2588 }
2589 if let Some(begin) = node.as_begin_node() {
2590 let mut targets = match begin.else_clause() {
2591 Some(else_node) => self.arm_targets(else_node.statements())?,
2592 None => self.arm_targets(begin.statements())?,
2593 };
2594 let mut rescue = begin.rescue_clause();
2595 while let Some(clause) = rescue {
2596 targets.extend(self.arm_targets(clause.statements())?);
2597 rescue = clause.subsequent();
2598 }
2599 return Some(targets);
2600 }
2601 if let Some(parentheses) = node.as_parentheses_node() {
2602 let body = parentheses.body()?;
2603 return match body.as_statements_node() {
2604 Some(statements) => self.arm_targets(Some(statements)),
2605 None => self.probe_targets(body),
2606 };
2607 }
2608 None
2609 }
2610
2611 fn arm_targets<'n>(&self, statements: Option<StatementsNode<'n>>) -> Option<Vec<Node<'n>>> {
2612 let last = statements.and_then(|statements| statements.body().iter().last())?;
2613 self.probe_targets(last)
2614 }
2615
2616 fn completion_probe(&mut self, last: Node<'_>, key: u64) -> bool {
2621 let Some(targets) = self.probe_targets(last) else {
2622 return false;
2623 };
2624 for target in &targets {
2625 self.wrap_completion(target, key);
2626 }
2627 true
2628 }
2629
2630 fn wrap_completion(&mut self, last: &Node<'_>, key: u64) {
2632 let location = last.location();
2633 let (start, end) = (location.start_offset(), location.end_offset());
2634 let arguments = if let Some(node) = last.as_return_node() {
2635 Some((node.keyword_loc(), node.arguments()))
2636 } else if let Some(node) = last.as_break_node() {
2637 Some((node.keyword_loc(), node.arguments()))
2638 } else {
2639 last.as_next_node()
2640 .map(|node| (node.keyword_loc(), node.arguments()))
2641 };
2642 if let Some((keyword, arguments)) = arguments {
2643 match arguments {
2644 Some(arguments) => {
2645 let arguments_location = arguments.location();
2646 let multiple = arguments.arguments().iter().count() > 1
2647 || arguments
2648 .arguments()
2649 .iter()
2650 .any(|argument| argument.as_splat_node().is_some());
2651 let (open, close) = if multiple {
2652 (format!("{RUBY_PROBE_RECEIVER}.ok({key}, ["), "])")
2654 } else {
2655 (format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("), "))")
2656 };
2657 self.depth += 1;
2658 self.edit(
2659 arguments_location.start_offset(),
2660 EditRank::Opener,
2661 open,
2662 arguments_location.end_offset(),
2663 );
2664 self.edit(
2665 arguments_location.end_offset(),
2666 EditRank::Closer,
2667 close.into(),
2668 arguments_location.start_offset(),
2669 );
2670 self.depth -= 1;
2671 }
2672 None => self.edit(
2673 keyword.start_offset(),
2674 EditRank::StatementProbe,
2675 format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2676 end,
2677 ),
2678 }
2679 return;
2680 }
2681 if last.as_redo_node().is_some() || last.as_retry_node().is_some() {
2682 self.edit(
2683 start,
2684 EditRank::StatementProbe,
2685 format!("{RUBY_PROBE_RECEIVER}.ok0({key}); "),
2686 end,
2687 );
2688 return;
2689 }
2690 self.depth += 1;
2691 self.wrap(start, end, format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("));
2692 self.depth -= 1;
2693 }
2694
2695 fn rescue_modifier(&mut self, node: &RescueModifierNode<'_>) {
2696 let location = node.location();
2697 let (start, end) = (location.start_offset(), location.end_offset());
2698 let Some(id) = self.branch(
2699 start,
2700 end,
2701 "rescue-modifier",
2702 [
2703 ("success", "expression completed"),
2704 ("raised", "fallback used"),
2705 ],
2706 ) else {
2707 return;
2708 };
2709 let key = self.probe_key(ProbeTarget::Try {
2710 success: format!("{id}:success"),
2711 raised: format!("{id}:raised"),
2712 id,
2713 handlers: Vec::new(),
2714 });
2715 let expression = node.expression();
2716 let fallback_node = node.rescue_expression();
2717 let fallback = fallback_node.location();
2718 self.depth += 1;
2719 if Self::is_jump(&expression) {
2720 self.completion_probe(expression, key);
2723 } else {
2724 let expression = expression.location();
2725 self.wrap(
2726 expression.start_offset(),
2727 expression.end_offset(),
2728 format!("{RUBY_PROBE_RECEIVER}.ok({key}, ("),
2729 );
2730 }
2731 if Self::is_jump(&fallback_node) {
2732 self.edit(
2734 fallback.start_offset(),
2735 EditRank::Opener,
2736 format!("({RUBY_PROBE_RECEIVER}.hm0({key}); "),
2737 fallback.end_offset(),
2738 );
2739 self.edit(
2740 fallback.end_offset(),
2741 EditRank::Closer,
2742 ")".into(),
2743 fallback.start_offset(),
2744 );
2745 } else {
2746 self.wrap(
2747 fallback.start_offset(),
2748 fallback.end_offset(),
2749 format!("{RUBY_PROBE_RECEIVER}.hm({key}, ("),
2750 );
2751 }
2752 self.depth -= 1;
2753 }
2754
2755 fn is_jump(node: &Node<'_>) -> bool {
2757 node.as_return_node().is_some()
2758 || node.as_break_node().is_some()
2759 || node.as_next_node().is_some()
2760 || node.as_redo_node().is_some()
2761 || node.as_retry_node().is_some()
2762 }
2763
2764 fn def_node(&mut self, node: &DefNode<'_>) {
2765 let location = node.location();
2766 let (start, end) = (location.start_offset(), location.end_offset());
2767 let name = String::from_utf8_lossy(node.name().as_slice()).into_owned();
2768 let id = stable_id(self.file, "function", start, end, &name);
2769 if !self.point_ids.insert(id.clone()) {
2770 return;
2771 }
2772 self.push_point(&id, start, end, PointKind::Function, Some(name));
2773 let span = self.location_span(&location);
2774 self.methods.push(MethodKeyPlan {
2775 span,
2776 unshifted: span,
2777 id: id.clone(),
2778 });
2779 let body_statements = node.body().and_then(|body| {
2782 if let Some(statements) = body.as_statements_node() {
2783 Some(statements)
2784 } else {
2785 body.as_begin_node().and_then(|begin| begin.statements())
2786 }
2787 });
2788 let signature_end = node
2789 .rparen_loc()
2790 .map(|rparen| rparen.end_offset())
2791 .or_else(|| {
2792 node.parameters()
2793 .map(|parameters| parameters.location().end_offset())
2794 })
2795 .unwrap_or_else(|| node.name_loc().end_offset());
2796 self.body_proves(&body_statements, vec![id], signature_end, "; ", "");
2797 if let Some(body) = node.body()
2798 && let Some(begin) = body.as_begin_node()
2799 {
2800 self.begin_node(&begin, node.end_keyword_loc().map(|loc| loc.start_offset()));
2801 }
2802 if node.equal_loc().is_some()
2803 && let Some(body) = node.body()
2804 && let Some(statements) = body.as_statements_node()
2805 {
2806 for statement in statements.body().iter() {
2807 self.endless_bodies
2808 .insert(statement.location().start_offset());
2809 }
2810 }
2811 }
2812
2813 fn shifted(&self, span: PlanSpan, kind: KeyKind, edits: &[PendingEdit]) -> PlanSpan {
2829 let start_offset = self.line_starts[span.start[0] - 1] + span.start[1];
2830 let end_offset = self.line_starts[span.end[0] - 1] + span.end[1];
2831 let mut start_shift = 0;
2832 let mut end_shift = 0;
2833 for edit in edits {
2834 let (line, _) = self.line_column(edit.offset);
2835 let moves_start = edit.offset < start_offset
2836 || (edit.offset == start_offset
2837 && match (edit.rank, kind) {
2838 (_, KeyKind::Point) => true,
2839 (EditRank::Closer, _) => false,
2840 (EditRank::Opener, KeyKind::List) => end_offset < edit.scope,
2841 (EditRank::Opener, KeyKind::Node) => end_offset <= edit.scope,
2842 (_, KeyKind::List) => end_offset < edit.scope,
2843 (_, KeyKind::Node) => true,
2844 });
2845 let moves_end = edit.offset < end_offset
2846 || (edit.offset == end_offset
2847 && match (edit.rank, kind) {
2848 (_, KeyKind::Point) => true,
2849 (EditRank::Closer, KeyKind::List) => edit.scope >= start_offset,
2850 (EditRank::Closer, KeyKind::Node) => edit.scope > start_offset,
2851 _ => false,
2852 });
2853 if line == span.start[0] && moves_start {
2854 start_shift += edit.text.len();
2855 }
2856 if line == span.end[0] && moves_end {
2857 end_shift += edit.text.len();
2858 }
2859 }
2860 PlanSpan {
2861 start: [span.start[0], span.start[1] + start_shift],
2862 end: [span.end[0], span.end[1] + end_shift],
2863 }
2864 }
2865
2866 fn drop_ractor_insertions(
2870 &mut self,
2871 pending: Vec<PendingEdit>,
2872 blocks: &[(usize, usize)],
2873 ) -> Vec<PendingEdit> {
2874 let inside = |offset: usize| {
2875 blocks
2876 .iter()
2877 .any(|(start, end)| offset >= *start && offset < *end)
2878 };
2879 let (dropped, kept): (Vec<_>, Vec<_>) =
2880 pending.into_iter().partition(|edit| inside(edit.offset));
2881 let mut keys = BTreeSet::new();
2882 for edit in &dropped {
2883 keys.extend(probe_keys_in(&edit.text));
2884 }
2885 let mut targets = BTreeMap::new();
2886 for key in &keys {
2887 if let Some(target) = self.probes.remove(key) {
2888 targets.insert(*key, target);
2889 }
2890 }
2891 let mut unmeasured = probe_obligations_of(&targets);
2892 for target in targets.values() {
2893 for named in target_ids(target) {
2894 if let Some(implied) = self.implied.get(&named) {
2895 unmeasured.extend(implied.hits.iter().map(|id| obligation_of(id)));
2896 unmeasured.extend(implied.decisions.iter().map(|d| d.id.clone()));
2897 }
2898 }
2899 }
2900 unmeasured.sort();
2901 unmeasured.dedup();
2902 self.manifest.unmeasured.extend(unmeasured);
2903 for (start, end) in blocks {
2904 let (line, _) = self.line_column(*start);
2905 let source = self
2906 .text(*start, *end)
2907 .lines()
2908 .next()
2909 .unwrap_or_default()
2910 .to_owned();
2911 self.manifest.limitations.push(limitation(
2912 RACTOR_BLOCK_LIMITATION,
2913 self.file,
2914 line,
2915 &source,
2916 "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",
2917 ));
2918 }
2919 kept
2920 }
2921
2922 fn finish(mut self) -> RubyFileObligations {
2923 let mut pending = std::mem::take(&mut self.edits);
2924 pending.sort_by(|left, right| {
2925 left.offset
2926 .cmp(&right.offset)
2927 .then(left.rank.cmp(&right.rank))
2928 .then(left.order.cmp(&right.order))
2929 .then(left.sequence.cmp(&right.sequence))
2930 });
2931 let ractor_blocks = std::mem::take(&mut self.ractor_blocks);
2932 if !ractor_blocks.is_empty() {
2933 pending = self.drop_ractor_insertions(pending, &ractor_blocks);
2934 }
2935 let branches = std::mem::take(&mut self.branches)
2936 .into_iter()
2937 .map(|mut branch| {
2938 branch.key.span = self.shifted(branch.key.span, branch.key.kind, &pending);
2939 branch
2940 })
2941 .collect::<Vec<_>>();
2942 let cases: Vec<CasePlan> = std::mem::take(&mut self.cases)
2943 .into_iter()
2944 .map(|mut case| {
2945 for clause in &mut case.clauses {
2946 clause.key.span = self.shifted(clause.key.span, clause.key.kind, &pending);
2947 }
2948 if let Some(no_match) = &mut case.no_match {
2949 no_match.key.span =
2950 self.shifted(no_match.key.span, no_match.key.kind, &pending);
2951 }
2952 case
2953 })
2954 .collect();
2955 let methods: Vec<MethodKeyPlan> = std::mem::take(&mut self.methods)
2956 .into_iter()
2957 .map(|mut method| {
2958 method.span = self.shifted(method.span, KeyKind::Node, &pending);
2959 method
2960 })
2961 .collect();
2962 let edits = pending
2963 .into_iter()
2964 .map(|edit| Edit {
2965 offset: edit.offset,
2966 text: edit.text,
2967 rank: match edit.rank {
2968 EditRank::Clause => "clause",
2969 EditRank::StatementProbe => "statement",
2970 EditRank::Opener => "opener",
2971 EditRank::Closer => "closer",
2972 }
2973 .into(),
2974 scope: edit.scope,
2975 })
2976 .collect::<Vec<_>>();
2977 if let Some((id, line)) = self.begin_unmeasured.first() {
2978 let source = self
2979 .manifest
2980 .branches
2981 .iter()
2982 .find(|branch| &branch.id == id)
2983 .map(|branch| branch.source.lines().next().unwrap_or_default().to_owned())
2984 .unwrap_or_default();
2985 self.manifest.limitations.push(limitation(
2986 BEGIN_BODY_LIMITATION,
2987 self.file,
2988 *line,
2989 &source,
2990 "a begin body that is empty, or ends in a statement with no expression form, cannot have its completion observed",
2991 ));
2992 }
2993 self.manifest.unmeasured.sort();
2994 self.manifest.unmeasured.dedup();
2995 RubyFileObligations {
2996 manifest: self.manifest,
2997 plan: RubyFilePlan {
2998 refine_blocks: std::mem::take(&mut self.refine_blocks),
2999 ractor_blocks: ractor_blocks
3000 .iter()
3001 .map(|(start, end)| [*start, *end])
3002 .collect(),
3003 probe_obligations: {
3006 let keyed = stdlib_provable(&branches, &cases, &methods);
3007 probe_obligations_of(&self.probes)
3008 .into_iter()
3009 .filter(|id| !keyed.contains(id))
3010 .collect()
3011 },
3012 edits,
3013 lines: self.lines,
3014 statement_offsets: self.statement_offsets,
3015 branches,
3016 methods,
3017 cases,
3018 implied: self.implied,
3019 },
3020 probes: self.probes,
3021 }
3022 }
3023}
3024
3025fn limitation(id: &str, file: &str, line: usize, source: &str, reason: &str) -> serde_json::Value {
3026 json!({
3027 "id": id,
3028 "kind": "semantic-safety",
3029 "file": file,
3030 "line": line,
3031 "column": 0,
3032 "source": source,
3033 "reason": reason
3034 })
3035}
3036
3037impl<'pr> Visit<'pr> for Collector<'_> {
3038 fn visit_statements_node(&mut self, node: &StatementsNode<'pr>) {
3039 if !self
3040 .expression_lists
3041 .contains(&node.location().start_offset())
3042 {
3043 self.statements(node);
3044 }
3045 ruby_prism::visit_statements_node(self, node);
3046 }
3047
3048 fn visit_parentheses_node(&mut self, node: &ruby_prism::ParenthesesNode<'pr>) {
3049 if let Some(body) = node.body() {
3050 self.expression_lists.insert(body.location().start_offset());
3051 }
3052 ruby_prism::visit_parentheses_node(self, node);
3053 }
3054
3055 fn visit_embedded_statements_node(&mut self, node: &ruby_prism::EmbeddedStatementsNode<'pr>) {
3056 if let Some(statements) = node.statements() {
3057 self.expression_lists
3058 .insert(statements.location().start_offset());
3059 }
3060 ruby_prism::visit_embedded_statements_node(self, node);
3061 }
3062
3063 fn visit_if_node(&mut self, node: &IfNode<'pr>) {
3064 let offset = node.location().start_offset();
3065 if self.guard_nodes.contains(&offset) {
3066 self.depth += 1;
3070 self.visit(&node.predicate());
3071 self.depth -= 1;
3072 return;
3073 }
3074 if self.elsif_nodes.contains(&offset) {
3075 self.depth += 1;
3076 ruby_prism::visit_if_node(self, node);
3077 self.depth -= 1;
3078 return;
3079 }
3080 if let Some(truthy) = self.literal_truth(node.predicate()) {
3081 self.depth += 1;
3086 self.visit(&node.predicate());
3087 if node.if_keyword_loc().is_none() {
3091 if let Some(statements) = node.statements() {
3092 self.expression_lists
3093 .insert(statements.location().start_offset());
3094 }
3095 if let Some(subsequent) = node.subsequent()
3096 && let Some(else_node) = subsequent.as_else_node()
3097 && let Some(statements) = else_node.statements()
3098 {
3099 self.expression_lists
3100 .insert(statements.location().start_offset());
3101 }
3102 }
3103 if truthy {
3104 if let Some(statements) = node.statements() {
3105 self.visit_statements_node(&statements);
3106 }
3107 } else if let Some(subsequent) = node.subsequent() {
3108 match subsequent.as_if_node() {
3109 Some(elsif) => self.visit_if_node(&elsif),
3110 None => {
3111 if let Some(else_node) = subsequent.as_else_node()
3112 && let Some(statements) = else_node.statements()
3113 {
3114 self.visit_statements_node(&statements);
3115 }
3116 }
3117 }
3118 }
3119 self.depth -= 1;
3120 return;
3121 }
3122 let kind = if node.if_keyword_loc().is_none() {
3125 "ternary"
3126 } else {
3127 "if"
3128 };
3129 self.if_node(node, kind);
3130 let mut subsequent = node.subsequent();
3131 while let Some(next) = subsequent {
3132 match next.as_if_node() {
3133 Some(elsif) => {
3134 self.elsif_nodes.insert(elsif.location().start_offset());
3135 self.if_node(&elsif, "elsif");
3136 subsequent = elsif.subsequent();
3137 }
3138 None => break,
3139 }
3140 }
3141 self.depth += 1;
3142 ruby_prism::visit_if_node(self, node);
3146 self.depth -= 1;
3147 }
3148
3149 fn visit_unless_node(&mut self, node: &ruby_prism::UnlessNode<'pr>) {
3150 if self.guard_nodes.contains(&node.location().start_offset()) {
3151 self.depth += 1;
3152 self.visit(&node.predicate());
3153 self.depth -= 1;
3154 return;
3155 }
3156 if let Some(truthy) = self.literal_truth(node.predicate()) {
3157 self.depth += 1;
3158 self.visit(&node.predicate());
3159 if truthy {
3160 if let Some(else_node) = node.else_clause()
3161 && let Some(statements) = else_node.statements()
3162 {
3163 self.visit_statements_node(&statements);
3164 }
3165 } else if let Some(statements) = node.statements() {
3166 self.visit_statements_node(&statements);
3167 }
3168 self.depth -= 1;
3169 return;
3170 }
3171 self.unless_node(node);
3172 self.depth += 1;
3173 ruby_prism::visit_unless_node(self, node);
3174 self.depth -= 1;
3175 }
3176
3177 fn visit_while_node(&mut self, node: &WhileNode<'pr>) {
3178 self.loop_node(
3179 &node.location(),
3180 node.predicate(),
3181 node.statements(),
3182 node.is_begin_modifier(),
3183 false,
3184 );
3185 self.depth += 1;
3186 ruby_prism::visit_while_node(self, node);
3187 self.depth -= 1;
3188 }
3189
3190 fn visit_until_node(&mut self, node: &UntilNode<'pr>) {
3191 self.loop_node(
3192 &node.location(),
3193 node.predicate(),
3194 node.statements(),
3195 node.is_begin_modifier(),
3196 true,
3197 );
3198 self.depth += 1;
3199 ruby_prism::visit_until_node(self, node);
3200 self.depth -= 1;
3201 }
3202
3203 fn visit_for_node(&mut self, node: &ForNode<'pr>) {
3204 self.for_node(node);
3205 self.depth += 1;
3206 ruby_prism::visit_for_node(self, node);
3207 self.depth -= 1;
3208 }
3209
3210 fn visit_case_node(&mut self, node: &CaseNode<'pr>) {
3211 self.case_node(node);
3212 self.depth += 1;
3213 ruby_prism::visit_case_node(self, node);
3214 self.depth -= 1;
3215 }
3216
3217 fn visit_case_match_node(&mut self, node: &CaseMatchNode<'pr>) {
3218 self.case_match_node(node);
3219 self.depth += 1;
3220 ruby_prism::visit_case_match_node(self, node);
3221 self.depth -= 1;
3222 }
3223
3224 fn visit_and_node(&mut self, node: &AndNode<'pr>) {
3225 let location = node.location();
3226 if !self.tree_logicals.contains(&location.start_offset()) {
3227 let left = node.left();
3228 self.value_logical("and", &left, location.start_offset(), location.end_offset());
3229 self.value_decision(|| node.left());
3230 }
3231 self.depth += 1;
3232 ruby_prism::visit_and_node(self, node);
3233 self.depth -= 1;
3234 }
3235
3236 fn visit_or_node(&mut self, node: &OrNode<'pr>) {
3237 let location = node.location();
3238 if !self.tree_logicals.contains(&location.start_offset()) {
3239 let left = node.left();
3240 self.value_logical("or", &left, location.start_offset(), location.end_offset());
3241 self.value_decision(|| node.left());
3242 }
3243 self.depth += 1;
3244 ruby_prism::visit_or_node(self, node);
3245 self.depth -= 1;
3246 }
3247
3248 fn visit_call_node(&mut self, node: &CallNode<'pr>) {
3249 if node.name().as_slice() == b"!" && node.arguments().is_none() && node.block().is_none() {
3250 self.value_decision(|| node.as_node());
3251 }
3252 if node.name().as_slice() == b"new"
3253 && node.receiver().is_some_and(|receiver| {
3254 receiver
3255 .as_constant_read_node()
3256 .is_some_and(|constant| constant.name().as_slice() == b"Ractor")
3257 })
3258 && let Some(block) = node.block()
3259 && block.as_block_node().is_some()
3260 {
3261 let location = block.location();
3262 self.ractor_blocks
3263 .push((location.start_offset(), location.end_offset()));
3264 }
3265 if node.name().as_slice() == b"refine"
3266 && node.receiver().is_none()
3267 && let Some(block) = node.block()
3268 && block.as_block_node().is_some()
3269 {
3270 let location = block.location();
3271 self.refine_blocks
3272 .push([location.start_offset(), location.end_offset()]);
3273 }
3274 if node.is_safe_navigation() {
3275 self.safe_navigation(node);
3276 }
3277 if let Some(block) = node.block()
3278 && let Some(block) = block.as_block_node()
3279 && let Some(receiver) = node.receiver()
3280 && !node.is_safe_navigation()
3281 && ITERATORS.contains(&node.name().as_slice())
3282 {
3283 self.iterator_loop(node, &receiver, &block);
3284 }
3285 self.depth += 1;
3286 ruby_prism::visit_call_node(self, node);
3287 self.depth -= 1;
3288 }
3289
3290 fn visit_local_variable_or_write_node(
3291 &mut self,
3292 node: &ruby_prism::LocalVariableOrWriteNode<'pr>,
3293 ) {
3294 let location = node.location();
3295 let value = node.value();
3296 self.op_assign(
3297 "or",
3298 location.start_offset(),
3299 location.end_offset(),
3300 Some(node.name().as_slice()),
3301 &value,
3302 );
3303 self.depth += 1;
3304 ruby_prism::visit_local_variable_or_write_node(self, node);
3305 self.depth -= 1;
3306 }
3307
3308 fn visit_local_variable_and_write_node(
3309 &mut self,
3310 node: &ruby_prism::LocalVariableAndWriteNode<'pr>,
3311 ) {
3312 let location = node.location();
3313 let value = node.value();
3314 self.op_assign(
3315 "and",
3316 location.start_offset(),
3317 location.end_offset(),
3318 Some(node.name().as_slice()),
3319 &value,
3320 );
3321 self.depth += 1;
3322 ruby_prism::visit_local_variable_and_write_node(self, node);
3323 self.depth -= 1;
3324 }
3325
3326 fn visit_instance_variable_or_write_node(
3327 &mut self,
3328 node: &ruby_prism::InstanceVariableOrWriteNode<'pr>,
3329 ) {
3330 let location = node.location();
3331 let value = node.value();
3332 self.op_assign(
3333 "or",
3334 location.start_offset(),
3335 location.end_offset(),
3336 Some(node.name().as_slice()),
3337 &value,
3338 );
3339 self.depth += 1;
3340 ruby_prism::visit_instance_variable_or_write_node(self, node);
3341 self.depth -= 1;
3342 }
3343
3344 fn visit_instance_variable_and_write_node(
3345 &mut self,
3346 node: &ruby_prism::InstanceVariableAndWriteNode<'pr>,
3347 ) {
3348 let location = node.location();
3349 let value = node.value();
3350 self.op_assign(
3351 "and",
3352 location.start_offset(),
3353 location.end_offset(),
3354 Some(node.name().as_slice()),
3355 &value,
3356 );
3357 self.depth += 1;
3358 ruby_prism::visit_instance_variable_and_write_node(self, node);
3359 self.depth -= 1;
3360 }
3361
3362 fn visit_global_variable_or_write_node(
3363 &mut self,
3364 node: &ruby_prism::GlobalVariableOrWriteNode<'pr>,
3365 ) {
3366 let location = node.location();
3367 let value = node.value();
3368 self.op_assign(
3369 "or",
3370 location.start_offset(),
3371 location.end_offset(),
3372 Some(node.name().as_slice()),
3373 &value,
3374 );
3375 self.depth += 1;
3376 ruby_prism::visit_global_variable_or_write_node(self, node);
3377 self.depth -= 1;
3378 }
3379
3380 fn visit_class_variable_or_write_node(
3381 &mut self,
3382 node: &ruby_prism::ClassVariableOrWriteNode<'pr>,
3383 ) {
3384 let location = node.location();
3385 let value = node.value();
3386 self.op_assign(
3387 "or",
3388 location.start_offset(),
3389 location.end_offset(),
3390 None,
3395 &value,
3396 );
3397 self.depth += 1;
3398 ruby_prism::visit_class_variable_or_write_node(self, node);
3399 self.depth -= 1;
3400 }
3401
3402 fn visit_call_or_write_node(&mut self, node: &ruby_prism::CallOrWriteNode<'pr>) {
3403 let location = node.location();
3404 let value = node.value();
3405 self.op_assign(
3406 "or",
3407 location.start_offset(),
3408 location.end_offset(),
3409 None,
3410 &value,
3411 );
3412 self.depth += 1;
3413 ruby_prism::visit_call_or_write_node(self, node);
3414 self.depth -= 1;
3415 }
3416
3417 fn visit_call_and_write_node(&mut self, node: &ruby_prism::CallAndWriteNode<'pr>) {
3418 let location = node.location();
3419 let value = node.value();
3420 self.op_assign(
3421 "and",
3422 location.start_offset(),
3423 location.end_offset(),
3424 None,
3425 &value,
3426 );
3427 self.depth += 1;
3428 ruby_prism::visit_call_and_write_node(self, node);
3429 self.depth -= 1;
3430 }
3431
3432 fn visit_index_or_write_node(&mut self, node: &ruby_prism::IndexOrWriteNode<'pr>) {
3433 let location = node.location();
3434 let value = node.value();
3435 self.op_assign(
3436 "or",
3437 location.start_offset(),
3438 location.end_offset(),
3439 None,
3440 &value,
3441 );
3442 self.depth += 1;
3443 ruby_prism::visit_index_or_write_node(self, node);
3444 self.depth -= 1;
3445 }
3446
3447 fn visit_index_and_write_node(&mut self, node: &ruby_prism::IndexAndWriteNode<'pr>) {
3448 let location = node.location();
3449 let value = node.value();
3450 self.op_assign(
3451 "and",
3452 location.start_offset(),
3453 location.end_offset(),
3454 None,
3455 &value,
3456 );
3457 self.depth += 1;
3458 ruby_prism::visit_index_and_write_node(self, node);
3459 self.depth -= 1;
3460 }
3461
3462 fn visit_constant_or_write_node(&mut self, node: &ruby_prism::ConstantOrWriteNode<'pr>) {
3463 let location = node.location();
3464 let value = node.value();
3465 self.op_assign(
3466 "or",
3467 location.start_offset(),
3468 location.end_offset(),
3469 None,
3470 &value,
3471 );
3472 self.depth += 1;
3473 ruby_prism::visit_constant_or_write_node(self, node);
3474 self.depth -= 1;
3475 }
3476
3477 fn visit_def_node(&mut self, node: &DefNode<'pr>) {
3478 self.def_node(node);
3479 self.depth += 1;
3480 ruby_prism::visit_def_node(self, node);
3481 self.depth -= 1;
3482 }
3483
3484 fn visit_begin_node(&mut self, node: &BeginNode<'pr>) {
3485 if node.begin_keyword_loc().is_some() {
3489 self.begin_node(node, None);
3490 }
3491 self.depth += 1;
3492 ruby_prism::visit_begin_node(self, node);
3493 self.depth -= 1;
3494 }
3495
3496 fn visit_block_node(&mut self, node: &ruby_prism::BlockNode<'pr>) {
3497 if let Some(body) = node.body()
3498 && let Some(begin) = body.as_begin_node()
3499 {
3500 self.begin_node(&begin, Some(node.closing_loc().start_offset()));
3501 }
3502 self.depth += 1;
3503 ruby_prism::visit_block_node(self, node);
3504 self.depth -= 1;
3505 }
3506
3507 fn visit_lambda_node(&mut self, node: &ruby_prism::LambdaNode<'pr>) {
3508 if let Some(body) = node.body()
3509 && let Some(begin) = body.as_begin_node()
3510 {
3511 self.begin_node(&begin, Some(node.closing_loc().start_offset()));
3512 }
3513 self.depth += 1;
3514 ruby_prism::visit_lambda_node(self, node);
3515 self.depth -= 1;
3516 }
3517
3518 fn visit_rescue_modifier_node(&mut self, node: &RescueModifierNode<'pr>) {
3519 self.rescue_modifier(node);
3520 self.depth += 1;
3521 ruby_prism::visit_rescue_modifier_node(self, node);
3522 self.depth -= 1;
3523 }
3524}
3525
3526pub fn build_ruby_obligations(
3529 file: &str,
3530 source: &[u8],
3531 next_probe: &mut u64,
3532) -> Result<RubyFileObligations, RubyInstrumenterError> {
3533 let result = ruby_prism::parse(source);
3534 let errors = result
3535 .errors()
3536 .map(|error| error.message().to_owned())
3537 .collect::<Vec<_>>();
3538 if !errors.is_empty() {
3539 return Err(RubyInstrumenterError::Parse(errors.join("; ")));
3540 }
3541 let mut collector = Collector::new(file, source, next_probe);
3542 collector.visit(&result.node());
3543 if let Some(error) = collector.error.take() {
3544 return Err(error);
3545 }
3546 Ok(collector.finish())
3547}
3548
3549pub fn apply_edits(source: &[u8], edits: &[Edit]) -> Vec<u8> {
3551 let mut output =
3552 Vec::with_capacity(source.len() + edits.iter().map(|e| e.text.len()).sum::<usize>());
3553 let mut cursor = 0;
3554 for edit in edits {
3555 output.extend_from_slice(&source[cursor..edit.offset]);
3556 output.extend_from_slice(edit.text.as_bytes());
3557 cursor = edit.offset;
3558 }
3559 output.extend_from_slice(&source[cursor..]);
3560 output
3561}
3562
3563#[cfg(test)]
3564mod tests {
3565 use super::*;
3566
3567 #[test]
3568 fn a_ternary_on_a_literal_still_parses_once_probed() {
3569 for condition in ["true", "1", "1.0", "'s'", ":s", "false", "nil", "(true)"] {
3573 let source = format!("def go\n x = {condition} ? :yes : :no\n x\nend\n");
3574 let mut next = 0;
3575 let obligations =
3576 build_ruby_obligations("lib/l.rb", source.as_bytes(), &mut next).unwrap();
3577 let probed = apply_edits(source.as_bytes(), &obligations.plan.edits);
3578 let parsed = ruby_prism::parse(&probed);
3579 let errors = parsed
3580 .errors()
3581 .map(|error| error.message().to_owned())
3582 .collect::<Vec<_>>();
3583 assert!(
3584 errors.is_empty(),
3585 "{condition}: {errors:?}\n{}",
3586 String::from_utf8_lossy(&probed)
3587 );
3588 }
3589 }
3590
3591 const SOURCE: &str = r#"class Shapes
3592 def classify(a, b, c)
3593 if a && (b || c)
3594 :yes
3595 elsif a
3596 :half
3597 else
3598 :no
3599 end
3600 end
3601
3602 def loops(items, flag)
3603 total = 0
3604 items.each { |i| total += i if i > 2 && flag }
3605 while total > 100
3606 total -= 50
3607 end
3608 for x in items do total += x end
3609 total
3610 end
3611
3612 def logical(a, b)
3613 x = a || b
3614 @cache ||= {}
3615 @cache[a] ||= b
3616 y = a ? 1 : 2; z = a&.size
3617 [x, y, z]
3618 end
3619
3620 def guarded(s)
3621 Integer(s)
3622 rescue ArgumentError
3623 -1
3624 ensure
3625 @done = true
3626 end
3627
3628 def cases(v)
3629 case v
3630 when 0 then :zero
3631 else :other
3632 end
3633 v.to_s rescue "bad"
3634 end
3635end
3636"#;
3637
3638 #[test]
3639 fn discovers_obligations_with_stable_ids_and_newline_free_edits() {
3640 let mut probe = 0;
3641 let first = build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
3642 let mut probe = 0;
3643 let second =
3644 build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
3645 assert_eq!(first, second);
3646 let manifest = &first.manifest;
3647 let functions = manifest
3648 .points
3649 .iter()
3650 .filter(|point| point.kind == PointKind::Function)
3651 .map(|point| point.label.clone().unwrap())
3652 .collect::<Vec<_>>();
3653 assert_eq!(
3654 functions,
3655 ["classify", "loops", "logical", "guarded", "cases"]
3656 );
3657 let compound = manifest
3658 .decisions
3659 .iter()
3660 .find(|decision| decision.source == "a && (b || c)")
3661 .unwrap();
3662 assert_eq!(compound.conditions, ["a", "b", "c"]);
3663 assert_eq!(compound.kind, "if");
3664 assert!(
3665 manifest
3666 .decisions
3667 .iter()
3668 .any(|d| d.kind == "elsif" && d.conditions == ["a"])
3669 );
3670 assert!(manifest.decisions.iter().any(|d| d.kind == "ternary"));
3671 assert!(manifest.decisions.iter().any(|d| d.kind == "while"));
3672 for kind in [
3673 "for",
3674 "logical-or",
3675 "or-assign",
3676 "safe-navigation",
3677 "begin",
3678 "rescue-0",
3679 "case-when-0",
3680 "case-else",
3681 "rescue-modifier",
3682 ] {
3683 assert!(
3684 manifest.branches.iter().any(|branch| branch.kind == kind),
3685 "missing branch kind {kind}"
3686 );
3687 }
3688 for edit in &first.plan.edits {
3689 assert!(!edit.text.contains('\n'));
3690 }
3691 assert!(
3692 first
3693 .plan
3694 .edits
3695 .windows(2)
3696 .all(|pair| pair[0].offset <= pair[1].offset)
3697 );
3698 assert!(manifest.unmeasured.is_empty());
3701 assert!(manifest.limitations.is_empty());
3702 }
3703
3704 #[test]
3705 fn transformed_source_keeps_line_count_and_carries_probes() {
3706 let mut probe = 0;
3707 let obligations =
3708 build_ruby_obligations("lib/shapes.rb", SOURCE.as_bytes(), &mut probe).unwrap();
3709 let transformed =
3710 String::from_utf8(apply_edits(SOURCE.as_bytes(), &obligations.plan.edits)).unwrap();
3711 assert_eq!(transformed.lines().count(), SOURCE.lines().count());
3712 assert!(
3713 transformed.contains("if $__supercov.d(0, ($__supercov.c(0, 0, (a)) && ($__supercov.c(0, 1, (b)) || $__supercov.c(0, 2, (c)))))"),
3714 "{transformed}"
3715 );
3716 assert!(transformed.contains("while $__supercov.w("));
3717 assert!(transformed.contains("for x in $__supercov.f("));
3718 assert!(transformed.contains("do $__supercov.fb("));
3719 assert!(transformed.contains("x = $__supercov.l("));
3720 assert!(transformed.contains("($__supercov.pre("));
3721 assert!(transformed.contains("||= ($__supercov.es("));
3722 assert!(transformed.contains("rescue Exception => __supercov_e; $__supercov.p("));
3723 assert!(transformed.contains("$__supercov.h("));
3724 assert!(transformed.contains("$__supercov.ok("));
3725 assert!(transformed.contains("rescue $__supercov.hm("));
3726 assert!(transformed.contains("; $__supercov.s("));
3728 assert!(
3730 obligations
3731 .plan
3732 .branches
3733 .iter()
3734 .any(|branch| !branch.hits.is_empty() && branch.key.group == "if")
3735 );
3736 }
3737
3738 #[test]
3739 fn jumps_and_endless_bodies_take_wrapped_probes_and_literal_predicates_fold() {
3740 let source = "def inc(x) = x + 1\n\
3741 [1].each { |v| y = Integer(v) rescue next }\n\
3742 if false\n dead\nelse\n live\nend\n";
3743 let mut probe = 0;
3744 let obligations =
3745 build_ruby_obligations("lib/x.rb", source.as_bytes(), &mut probe).unwrap();
3746 let transformed =
3747 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3748 assert!(
3749 transformed.contains("def inc(x) = ($__supercov.s("),
3750 "{transformed}"
3751 );
3752 assert!(
3753 transformed.contains("rescue ($__supercov.hm0("),
3754 "{transformed}"
3755 );
3756 assert!(
3758 obligations
3759 .plan
3760 .branches
3761 .iter()
3762 .all(|branch| branch.key.group != "if")
3763 );
3764 assert!(
3765 obligations
3766 .manifest
3767 .points
3768 .iter()
3769 .all(|point| point.source != "dead")
3770 );
3771 assert!(
3772 obligations
3773 .manifest
3774 .points
3775 .iter()
3776 .any(|point| point.source == "live")
3777 );
3778 }
3779
3780 #[test]
3781 fn void_valued_last_statements_are_probed_arm_by_arm() {
3782 let source =
3785 "def m(c)\n if c\n return 1\n else\n return 2\n end\nrescue\n nil\nend\n";
3786 let mut probe = 0;
3787 let obligations =
3788 build_ruby_obligations("lib/v.rb", source.as_bytes(), &mut probe).unwrap();
3789 let transformed =
3790 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3791 assert_eq!(
3792 transformed.matches("$__supercov.ok(").count(),
3793 2,
3794 "{transformed}"
3795 );
3796 assert!(
3797 transformed.contains("return $__supercov.ok("),
3798 "{transformed}"
3799 );
3800 assert!(
3801 !obligations
3802 .manifest
3803 .unmeasured
3804 .iter()
3805 .any(|id| id.ends_with(":success")),
3806 "{:?}",
3807 obligations.manifest.unmeasured
3808 );
3809 assert!(!obligations.plan.probe_obligations.is_empty());
3810 }
3811
3812 #[test]
3813 fn expressions_that_can_return_are_never_wrapped() {
3814 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";
3817 let mut probe = 0;
3818 let obligations =
3819 build_ruby_obligations("lib/e.rb", source.as_bytes(), &mut probe).unwrap();
3820 let transformed =
3821 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
3822 assert!(!transformed.contains("$__supercov.ok"), "{transformed}");
3823 assert!(
3824 obligations
3825 .manifest
3826 .unmeasured
3827 .iter()
3828 .any(|id| id.ends_with(":success")),
3829 "the begin's completion is declared instead"
3830 );
3831 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";
3833 let mut probe = 0;
3834 let obligations = build_ruby_obligations("lib/f.rb", both.as_bytes(), &mut probe).unwrap();
3835 let transformed =
3836 String::from_utf8(apply_edits(both.as_bytes(), &obligations.plan.edits)).unwrap();
3837 assert!(
3838 transformed.contains("return $__supercov.ok("),
3839 "{transformed}"
3840 );
3841 assert!(
3842 !obligations
3843 .manifest
3844 .unmeasured
3845 .iter()
3846 .any(|id| id.ends_with(":success")),
3847 "{:?}",
3848 obligations.manifest.unmeasured
3849 );
3850 }
3851
3852 #[test]
3853 fn stdlib_keys_shift_with_insertions_on_their_line() {
3854 let source = "def f(a, b)\n x = 1 if a && b\nend\n";
3855 let mut probe = 0;
3856 let obligations = build_ruby_obligations("m.rb", source.as_bytes(), &mut probe).unwrap();
3857 let then_key = obligations
3858 .plan
3859 .branches
3860 .iter()
3861 .find(|branch| branch.key.branch == "then")
3862 .unwrap();
3863 assert_eq!(then_key.key.span.start, [2, 2]);
3865 assert_eq!(then_key.key.span.end, [2, 7]);
3868 let else_key = obligations
3869 .plan
3870 .branches
3871 .iter()
3872 .find(|branch| branch.key.branch == "else")
3873 .unwrap();
3874 let inserted: usize = obligations
3877 .plan
3878 .edits
3879 .iter()
3880 .map(|edit| edit.text.len())
3881 .sum();
3882 assert_eq!(else_key.key.span.end, [2, 17 + inserted]);
3883 assert!(
3884 then_key.hits.len() == 1,
3885 "modifier body statement proven by the then key"
3886 );
3887 }
3888
3889 #[test]
3890 fn line_events_prove_bodies_and_probes_prove_the_rest() {
3891 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";
3897 let mut probe = 0;
3898 let obligations =
3899 build_ruby_obligations("lib/m.rb", source.as_bytes(), &mut probe).unwrap();
3900 let plan = &obligations.plan;
3901 let manifest = &obligations.manifest;
3902 let function = |name: &str| {
3903 manifest
3904 .points
3905 .iter()
3906 .find(|point| {
3907 point.kind == PointKind::Function && point.label.as_deref() == Some(name)
3908 })
3909 .unwrap()
3910 .id
3911 .clone()
3912 };
3913 let statement_on = |line: usize| plan.lines.get(&line).cloned().unwrap();
3914 let transformed = String::from_utf8(apply_edits(source.as_bytes(), &plan.edits)).unwrap();
3915 assert_eq!(transformed.lines().count(), source.lines().count());
3916
3917 assert_eq!(
3920 plan.implied[&statement_on(2)].hits,
3921 [function("both")],
3922 "the `if` statement opens the method"
3923 );
3924 let three = &plan.implied[&statement_on(3)];
3925 assert!(three.hits.is_empty());
3926 assert_eq!(three.decisions.len(), 1);
3927 assert!(three.decisions[0].value);
3928 let five = &plan.implied[&statement_on(5)];
3929 assert!(!five.decisions[0].value);
3930 assert!(!transformed.contains(".d(") || transformed.matches(".d(").count() == 1);
3931
3932 let guard_decision = manifest
3935 .decisions
3936 .iter()
3937 .find(|d| d.kind == "if" && d.conditions == ["a"] && d.line == 10)
3938 .unwrap();
3939 let outcome_true = format!("{}:outcome:true", guard_decision.id);
3940 assert!(
3941 plan.implied.contains_key(&outcome_true),
3942 "modifier body implied by its outcome"
3943 );
3944 assert!(transformed.contains("return 0 if $__supercov.d("));
3945 assert!(
3946 !transformed.contains(".c("),
3947 "a lone condition needs no wrapper"
3948 );
3949 assert!(transformed.contains("$__supercov.n("));
3951 assert!(transformed.contains("else $__supercov.hs("));
3954 assert!(transformed.contains("when 1 then $__supercov.s("));
3955 assert!(transformed.contains("def short = ($__supercov.s("));
3957 assert!(transformed.contains("def empty; $__supercov.hs("));
3958 let empty_probe = plan
3959 .edits
3960 .iter()
3961 .find(|edit| edit.text.contains(".hs(") && edit.text.starts_with("; "))
3962 .unwrap();
3963 assert!(empty_probe.text.contains("hs"));
3964 for id in [
3968 guard_decision.id.clone(),
3969 function("empty"),
3970 function("short"),
3971 ] {
3972 assert!(
3973 !plan.probe_obligations.contains(&id),
3974 "{id} is key-provable on 3.3"
3975 );
3976 }
3977 let safe = manifest
3978 .branches
3979 .iter()
3980 .find(|b| b.kind == "safe-navigation")
3981 .unwrap();
3982 assert!(!plan.probe_obligations.contains(&safe.id));
3983 }
3984
3985 #[test]
3986 fn ractor_blocks_get_no_probes_and_declare_what_only_probes_could_prove() {
3987 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";
3991 let mut probe = 0;
3992 let obligations =
3993 build_ruby_obligations("lib/r.rb", source.as_bytes(), &mut probe).unwrap();
3994 let plan = &obligations.plan;
3995 let manifest = &obligations.manifest;
3996 let transformed = String::from_utf8(apply_edits(source.as_bytes(), &plan.edits)).unwrap();
3997 let block_line = transformed.lines().nth(1).unwrap();
3998 assert!(
3999 !block_line.contains("$__supercov"),
4000 "no probe inside the Ractor block: {block_line}"
4001 );
4002 assert!(
4003 transformed
4004 .lines()
4005 .nth(5)
4006 .unwrap()
4007 .contains("$__supercov.d("),
4008 "the ternary outside is probed"
4009 );
4010 let inside = manifest.decisions.iter().find(|d| d.line == 2).unwrap();
4011 assert!(
4012 manifest.unmeasured.contains(&inside.id),
4013 "the block's ternary leaves the denominator"
4014 );
4015 let outside = manifest.decisions.iter().find(|d| d.line == 6).unwrap();
4016 assert!(!manifest.unmeasured.contains(&outside.id));
4017 assert_eq!(plan.ractor_blocks.len(), 1);
4018 assert!(
4019 manifest
4020 .limitations
4021 .iter()
4022 .any(|l| l["id"] == RACTOR_BLOCK_LIMITATION && l["line"] == 2)
4023 );
4024 assert!(!plan.probe_obligations.contains(&inside.id));
4026 }
4027
4028 #[test]
4029 fn an_empty_guarded_in_body_takes_its_probe_after_the_guard_wrapper() {
4030 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";
4034 let mut probe = 0;
4035 let obligations =
4036 build_ruby_obligations("lib/g.rb", source.as_bytes(), &mut probe).unwrap();
4037 let transformed =
4038 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
4039 let guard_line = transformed.lines().nth(3).unwrap();
4040 assert!(
4041 guard_line.contains(")) then $__supercov.hs("),
4042 "probe after the wrapper: {guard_line}"
4043 );
4044 assert!(
4045 !guard_line.contains("then $__supercov.hs(")
4046 || !guard_line.contains("hs(")
4047 || guard_line.matches(')').count() == guard_line.matches('(').count(),
4048 "balanced: {guard_line}"
4049 );
4050 }
4051
4052 #[test]
4053 fn a_class_variable_or_assignment_is_never_read_before_it_exists() {
4054 let source = "class C\n def self.ext\n @@ext ||= {}\n end\nend\n";
4055 let mut probe = 0;
4056 let obligations =
4057 build_ruby_obligations("lib/c.rb", source.as_bytes(), &mut probe).unwrap();
4058 let transformed =
4059 String::from_utf8(apply_edits(source.as_bytes(), &obligations.plan.edits)).unwrap();
4060 let line = transformed.lines().nth(2).unwrap();
4061 assert!(
4062 !line.contains(".l("),
4063 "no read of @@ext before the assignment: {line}"
4064 );
4065 assert!(
4066 line.contains(".pre(") && line.contains(".es("),
4067 "arrival form: {line}"
4068 );
4069 }
4070
4071 #[test]
4072 fn a_predicate_ruby_folds_leaves_no_obligations_in_the_dead_arm() {
4073 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";
4077 let mut probe = 0;
4078 let obligations =
4079 build_ruby_obligations("lib/f.rb", source.as_bytes(), &mut probe).unwrap();
4080 let manifest = &obligations.manifest;
4081 assert!(
4082 !manifest.decisions.iter().any(|d| d.kind == "ternary"),
4083 "the dead arm's ternary is not an obligation"
4084 );
4085 assert!(
4086 !manifest.decisions.iter().any(|d| d.kind == "if"),
4087 "a folded predicate is no decision"
4088 );
4089 assert!(
4090 !manifest.points.iter().any(|p| p.line == 8),
4091 "the dead else arm holds no statement"
4092 );
4093 assert!(
4094 manifest.points.iter().any(|p| p.line == 6),
4095 "the live arm does"
4096 );
4097 }
4098
4099 #[test]
4100 fn rejects_invalid_ruby() {
4101 let mut probe = 0;
4102 assert!(matches!(
4103 build_ruby_obligations("m.rb", b"def x(\n", &mut probe),
4104 Err(RubyInstrumenterError::Parse(_))
4105 ));
4106 }
4107}