1use crate::types::{FileId, Range};
4use serde::{Deserialize, Serialize};
5use smallvec::SmallVec;
6use std::sync::Arc;
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub enum SourceInfo {
26 Original {
30 file_id: FileId,
31 start_offset: usize,
32 end_offset: usize,
33 },
34 Substring {
39 parent: Arc<SourceInfo>,
40 start_offset: usize,
41 end_offset: usize,
42 },
43 Concat { pieces: Vec<SourcePiece> },
48 Generated {
57 by: By,
58 #[serde(default, skip_serializing_if = "SmallVec::is_empty")]
59 from: SmallVec<[Anchor; 2]>,
60 },
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
73pub struct By {
74 pub kind: String,
78
79 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
83 pub data: serde_json::Value,
84}
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub enum AnchorRole {
93 Invocation,
99
100 ValueSource,
105
106 Other(String),
118}
119
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
123pub struct Anchor {
124 pub role: AnchorRole,
125 pub source_info: Arc<SourceInfo>,
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130pub struct SourcePiece {
131 pub source_info: SourceInfo,
133 pub offset_in_concat: usize,
135 pub length: usize,
137}
138
139impl Default for SourceInfo {
140 fn default() -> Self {
141 SourceInfo::Original {
142 file_id: FileId(0),
143 start_offset: 0,
144 end_offset: 0,
145 }
146 }
147}
148
149impl SourceInfo {
150 #[deprecated(
159 since = "0.1.0",
160 note = "Use SourceInfo::for_test() in tests, or the appropriate Generated{by: <kind>} in production. See provenance-contract.md."
161 )]
162 #[doc(hidden)]
163 #[allow(clippy::should_implement_trait)]
168 pub fn default() -> Self {
169 <Self as Default>::default()
170 }
171
172 pub fn original(file_id: FileId, start_offset: usize, end_offset: usize) -> Self {
174 SourceInfo::Original {
175 file_id,
176 start_offset,
177 end_offset,
178 }
179 }
180
181 pub fn from_range(file_id: FileId, range: Range) -> Self {
186 SourceInfo::Original {
187 file_id,
188 start_offset: range.start.offset,
189 end_offset: range.end.offset,
190 }
191 }
192
193 pub fn substring(parent: SourceInfo, start: usize, end: usize) -> Self {
195 SourceInfo::Substring {
196 parent: Arc::new(parent),
197 start_offset: start,
198 end_offset: end,
199 }
200 }
201
202 pub fn concat(pieces: Vec<(SourceInfo, usize)>) -> Self {
204 let source_pieces: Vec<SourcePiece> = pieces
205 .into_iter()
206 .map(|(source_info, length)| SourcePiece {
207 source_info,
208 offset_in_concat: 0, length,
210 })
211 .collect();
212
213 let mut cumulative_offset = 0;
215 let pieces_with_offsets: Vec<SourcePiece> = source_pieces
216 .into_iter()
217 .map(|mut piece| {
218 piece.offset_in_concat = cumulative_offset;
219 cumulative_offset += piece.length;
220 piece
221 })
222 .collect();
223
224 SourceInfo::Concat {
225 pieces: pieces_with_offsets,
226 }
227 }
228
229 pub fn generated(by: By) -> Self {
235 SourceInfo::Generated {
236 by,
237 from: SmallVec::new(),
238 }
239 }
240
241 pub fn for_test() -> Self {
247 SourceInfo::Generated {
248 by: By::test_scaffold(),
249 from: SmallVec::new(),
250 }
251 }
252
253 pub fn invocation_anchor(&self) -> Option<&Arc<SourceInfo>> {
259 match self {
260 SourceInfo::Generated { from, .. } => from
261 .iter()
262 .find(|a| matches!(a.role, AnchorRole::Invocation))
263 .map(|a| &a.source_info),
264 _ => None,
265 }
266 }
267
268 pub fn value_source_anchor(&self) -> Option<&Arc<SourceInfo>> {
274 match self {
275 SourceInfo::Generated { from, .. } => from
276 .iter()
277 .find(|a| matches!(a.role, AnchorRole::ValueSource))
278 .map(|a| &a.source_info),
279 _ => None,
280 }
281 }
282
283 pub fn anchors_with_role<'a>(
289 &'a self,
290 role: &'a AnchorRole,
291 ) -> Box<dyn Iterator<Item = &'a Arc<SourceInfo>> + 'a> {
292 match self {
293 SourceInfo::Generated { from, .. } => Box::new(
294 from.iter()
295 .filter(move |a| &a.role == role)
296 .map(|a| &a.source_info),
297 ),
298 _ => Box::new(std::iter::empty()),
299 }
300 }
301
302 pub fn append_anchor(&mut self, role: AnchorRole, source_info: Arc<SourceInfo>) {
310 match self {
311 SourceInfo::Generated { from, .. } => {
312 from.push(Anchor { role, source_info });
313 }
314 _ => panic!("append_anchor called on non-Generated SourceInfo"),
315 }
316 }
317
318 pub fn combine(&self, other: &SourceInfo) -> Self {
323 let self_length = self.length();
324 let other_length = other.length();
325
326 SourceInfo::concat(vec![
327 (self.clone(), self_length),
328 (other.clone(), other_length),
329 ])
330 }
331
332 pub fn length(&self) -> usize {
334 match self {
335 SourceInfo::Original {
336 start_offset,
337 end_offset,
338 ..
339 } => end_offset - start_offset,
340 SourceInfo::Substring {
341 start_offset,
342 end_offset,
343 ..
344 } => end_offset - start_offset,
345 SourceInfo::Concat { pieces } => pieces.iter().map(|p| p.length).sum(),
346 SourceInfo::Generated { .. } => 0,
347 }
348 }
349
350 pub fn start_offset(&self) -> usize {
356 match self {
357 SourceInfo::Original { start_offset, .. } => *start_offset,
358 SourceInfo::Substring { start_offset, .. } => *start_offset,
359 SourceInfo::Concat { .. } => 0,
360 SourceInfo::Generated { .. } => 0,
361 }
362 }
363
364 pub fn end_offset(&self) -> usize {
370 match self {
371 SourceInfo::Original { end_offset, .. } => *end_offset,
372 SourceInfo::Substring { end_offset, .. } => *end_offset,
373 SourceInfo::Concat { .. } => self.length(),
374 SourceInfo::Generated { .. } => 0,
375 }
376 }
377
378 pub fn resolve_byte_range(&self) -> Option<(usize, usize, usize)> {
389 match self {
390 SourceInfo::Original {
391 file_id,
392 start_offset,
393 end_offset,
394 } => Some((file_id.0, *start_offset, *end_offset)),
395 SourceInfo::Substring {
396 parent,
397 start_offset,
398 end_offset,
399 } => {
400 let (fid, parent_start, _) = parent.resolve_byte_range()?;
401 Some((fid, parent_start + start_offset, parent_start + end_offset))
402 }
403 SourceInfo::Concat { .. } => None,
404 SourceInfo::Generated { .. } => self
405 .invocation_anchor()
406 .and_then(|si| si.resolve_byte_range()),
407 }
408 }
409
410 pub fn preimage_in(&self, target: FileId) -> Option<std::ops::Range<usize>> {
457 match self {
458 SourceInfo::Original {
459 file_id,
460 start_offset,
461 end_offset,
462 } if *file_id == target => Some(*start_offset..*end_offset),
463 SourceInfo::Original { .. } => None,
464 SourceInfo::Substring { parent, .. }
465 if matches!(**parent, SourceInfo::Concat { .. }) =>
466 {
467 None
475 }
476 SourceInfo::Substring {
477 parent,
478 start_offset,
479 end_offset,
480 } => {
481 let parent_range = parent.preimage_in(target)?;
482 Some(parent_range.start + start_offset..parent_range.start + end_offset)
483 }
484 SourceInfo::Concat { pieces } => {
485 let ranges: Vec<std::ops::Range<usize>> = pieces
486 .iter()
487 .map(|p| p.source_info.preimage_in(target))
488 .collect::<Option<Vec<_>>>()?;
489 if ranges.is_empty() {
490 return None;
491 }
492 if ranges.windows(2).all(|w| w[0].end == w[1].start) {
493 let first = ranges.first().unwrap().start;
494 let last = ranges.last().unwrap().end;
495 Some(first..last)
496 } else {
497 None
498 }
499 }
500 SourceInfo::Generated { .. } => self
501 .invocation_anchor()
502 .and_then(|si| si.preimage_in(target)),
503 }
504 }
505
506 pub fn remap_file_ids<F>(&mut self, map: &F)
514 where
515 F: Fn(FileId) -> FileId,
516 {
517 match self {
518 SourceInfo::Original { file_id, .. } => {
519 *file_id = map(*file_id);
520 }
521 SourceInfo::Substring { parent, .. } => {
522 let parent = Arc::make_mut(parent);
524 parent.remap_file_ids(map);
525 }
526 SourceInfo::Concat { pieces } => {
527 for piece in pieces {
528 piece.source_info.remap_file_ids(map);
529 }
530 }
531 SourceInfo::Generated { from, .. } => {
532 for anchor in from {
533 let inner = Arc::make_mut(&mut anchor.source_info);
535 inner.remap_file_ids(map);
536 }
537 }
538 }
539 }
540
541 pub fn root_file_id(&self) -> Option<FileId> {
550 match self {
551 SourceInfo::Original { file_id, .. } => Some(*file_id),
552 SourceInfo::Substring { parent, .. } => parent.root_file_id(),
553 SourceInfo::Concat { pieces } => {
554 pieces.iter().find_map(|p| p.source_info.root_file_id())
555 }
556 SourceInfo::Generated { .. } => {
557 self.invocation_anchor().and_then(|si| si.root_file_id())
558 }
559 }
560 }
561
562 pub fn collect_file_ids(&self, out: &mut std::collections::HashSet<FileId>) {
568 match self {
569 SourceInfo::Original { file_id, .. } => {
570 out.insert(*file_id);
571 }
572 SourceInfo::Substring { parent, .. } => parent.collect_file_ids(out),
573 SourceInfo::Concat { pieces } => {
574 for piece in pieces {
575 piece.source_info.collect_file_ids(out);
576 }
577 }
578 SourceInfo::Generated { from, .. } => {
579 for anchor in from {
580 anchor.source_info.collect_file_ids(out);
581 }
582 }
583 }
584 }
585}
586
587impl By {
588 pub fn filter(filter_path: impl Into<String>, line: usize) -> Self {
598 Self {
599 kind: "filter".to_string(),
600 data: serde_json::json!({
601 "filter_path": filter_path.into(),
602 "line": line,
603 }),
604 }
605 }
606
607 pub fn sectionize() -> Self {
610 Self {
611 kind: "sectionize".to_string(),
612 data: serde_json::Value::Null,
613 }
614 }
615
616 pub fn user_edit() -> Self {
619 Self {
620 kind: "user-edit".to_string(),
621 data: serde_json::Value::Null,
622 }
623 }
624
625 pub fn shortcode(name: impl Into<String>) -> Self {
634 Self {
635 kind: "shortcode".to_string(),
636 data: serde_json::json!({ "name": name.into() }),
637 }
638 }
639
640 pub fn include() -> Self {
645 Self {
646 kind: "include".to_string(),
647 data: serde_json::Value::Null,
648 }
649 }
650
651 pub fn title_block() -> Self {
653 Self {
654 kind: "title-block".to_string(),
655 data: serde_json::Value::Null,
656 }
657 }
658
659 pub fn footnotes() -> Self {
661 Self {
662 kind: "footnotes".to_string(),
663 data: serde_json::Value::Null,
664 }
665 }
666
667 pub fn revealjs() -> Self {
673 Self {
674 kind: "revealjs".to_string(),
675 data: serde_json::Value::Null,
676 }
677 }
678
679 pub fn appendix() -> Self {
681 Self {
682 kind: "appendix".to_string(),
683 data: serde_json::Value::Null,
684 }
685 }
686
687 pub fn tree_sitter_postprocess() -> Self {
690 Self {
691 kind: "tree-sitter-postprocess".to_string(),
692 data: serde_json::Value::Null,
693 }
694 }
695
696 pub fn unknown() -> Self {
706 Self {
707 kind: "unknown".to_string(),
708 data: serde_json::Value::Null,
709 }
710 }
711
712 pub fn test_scaffold() -> Self {
717 Self {
718 kind: "test-scaffold".to_string(),
719 data: serde_json::Value::Null,
720 }
721 }
722
723 pub fn citeproc() -> Self {
732 Self {
733 kind: "citeproc".to_string(),
734 data: serde_json::Value::Null,
735 }
736 }
737
738 pub fn jupyter_output() -> Self {
746 Self {
747 kind: "jupyter-output".to_string(),
748 data: serde_json::Value::Null,
749 }
750 }
751
752 pub fn callout() -> Self {
763 Self {
764 kind: "callout".to_string(),
765 data: serde_json::Value::Null,
766 }
767 }
768
769 pub fn config_default() -> Self {
773 Self {
774 kind: "config-default".to_string(),
775 data: serde_json::Value::Null,
776 }
777 }
778
779 pub fn programmatic_config() -> Self {
784 Self {
785 kind: "programmatic-config".to_string(),
786 data: serde_json::Value::Null,
787 }
788 }
789
790 pub fn is_programmatic_sentinel(&self) -> bool {
795 matches!(
796 self.kind.as_str(),
797 "config-default" | "programmatic-config" | "unknown"
798 )
799 }
800
801 pub fn raw(kind: impl Into<String>, data: serde_json::Value) -> Self {
809 Self {
810 kind: kind.into(),
811 data,
812 }
813 }
814
815 pub fn is_atomic_kind(&self) -> bool {
827 matches!(
828 self.kind.as_str(),
829 "filter"
830 | "shortcode"
831 | "title-block"
832 | "tree-sitter-postprocess"
833 | "citeproc"
834 | "jupyter-output"
835 )
836 }
837
838 pub fn is_kind(&self, kind: &str) -> bool {
840 self.kind == kind
841 }
842
843 pub fn as_filter(&self) -> Option<(&str, usize)> {
849 if self.kind != "filter" {
850 return None;
851 }
852 let path = self.data.get("filter_path")?.as_str()?;
853 let line = self.data.get("line")?.as_u64()? as usize;
854 Some((path, line))
855 }
856}
857
858impl Anchor {
859 pub fn invocation(source_info: Arc<SourceInfo>) -> Self {
861 Self {
862 role: AnchorRole::Invocation,
863 source_info,
864 }
865 }
866
867 pub fn value_source(source_info: Arc<SourceInfo>) -> Self {
869 Self {
870 role: AnchorRole::ValueSource,
871 source_info,
872 }
873 }
874}
875
876#[cfg(test)]
877mod tests {
878 use super::*;
879 use crate::types::{FileId, Location, Range};
880
881 #[test]
882 fn test_original_source_info() {
883 let file_id = FileId(0);
884 let range = Range {
885 start: Location {
886 offset: 0,
887 row: 0,
888 column: 0,
889 },
890 end: Location {
891 offset: 10,
892 row: 0,
893 column: 10,
894 },
895 };
896
897 let info = SourceInfo::from_range(file_id, range.clone());
898
899 assert_eq!(info.start_offset(), 0);
900 assert_eq!(info.end_offset(), 10);
901 assert_eq!(info.length(), 10);
902 match info {
903 SourceInfo::Original {
904 file_id: mapped_id, ..
905 } => {
906 assert_eq!(mapped_id, file_id);
907 }
908 _ => panic!("Expected Original mapping"),
909 }
910 }
911
912 #[test]
913 fn test_remap_file_ids_original() {
914 let mut info = SourceInfo::original(FileId(0), 0, 10);
915 info.remap_file_ids(&|id| FileId(id.0 + 1));
916 match info {
917 SourceInfo::Original { file_id, .. } => assert_eq!(file_id, FileId(1)),
918 _ => panic!("Expected Original"),
919 }
920 }
921
922 #[test]
923 fn test_remap_file_ids_substring() {
924 let parent = SourceInfo::original(FileId(0), 0, 100);
925 let mut info = SourceInfo::substring(parent, 5, 20);
926 info.remap_file_ids(&|id| FileId(id.0 + 7));
927 match info {
928 SourceInfo::Substring { parent, .. } => match &*parent {
929 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(7)),
930 _ => panic!("Expected Original parent"),
931 },
932 _ => panic!("Expected Substring"),
933 }
934 }
935
936 #[test]
937 fn test_remap_file_ids_concat() {
938 let a = SourceInfo::original(FileId(0), 0, 5);
939 let b = SourceInfo::original(FileId(3), 5, 10);
940 let mut info = SourceInfo::concat(vec![(a, 5), (b, 5)]);
941 info.remap_file_ids(&|id| FileId(id.0 + 10));
942 match info {
943 SourceInfo::Concat { pieces } => {
944 match &pieces[0].source_info {
945 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(10)),
946 _ => panic!("Expected Original"),
947 }
948 match &pieces[1].source_info {
949 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(13)),
950 _ => panic!("Expected Original"),
951 }
952 }
953 _ => panic!("Expected Concat"),
954 }
955 }
956
957 #[test]
958 fn test_remap_file_ids_generated_empty_from_is_noop() {
959 let mut info = SourceInfo::generated(By::filter("foo.lua", 42));
960 info.remap_file_ids(&|_| FileId(99));
961 match info {
962 SourceInfo::Generated { by, from } => {
963 assert!(from.is_empty());
964 let (path, line) = by.as_filter().unwrap();
965 assert_eq!(path, "foo.lua");
966 assert_eq!(line, 42);
967 }
968 _ => panic!("Expected Generated"),
969 }
970 }
971
972 #[test]
977 fn test_by_filter_builder() {
978 let by = By::filter("a.lua", 7);
979 assert_eq!(by.kind, "filter");
980 assert_eq!(by.as_filter(), Some(("a.lua", 7)));
981 }
982
983 #[test]
984 fn test_by_sectionize_builder() {
985 let by = By::sectionize();
986 assert_eq!(by.kind, "sectionize");
987 assert!(by.data.is_null());
988 }
989
990 #[test]
991 fn test_by_user_edit_builder() {
992 assert_eq!(By::user_edit().kind, "user-edit");
993 }
994
995 #[test]
996 fn test_by_shortcode_builder_records_name() {
997 let by = By::shortcode("meta");
998 assert_eq!(by.kind, "shortcode");
999 assert_eq!(by.data.get("name").and_then(|v| v.as_str()), Some("meta"));
1000 }
1001
1002 #[test]
1003 fn test_by_include_title_footnotes_appendix_tree_sitter_builders() {
1004 assert_eq!(By::include().kind, "include");
1005 assert_eq!(By::title_block().kind, "title-block");
1006 assert_eq!(By::footnotes().kind, "footnotes");
1007 assert_eq!(By::appendix().kind, "appendix");
1008 assert_eq!(
1009 By::tree_sitter_postprocess().kind,
1010 "tree-sitter-postprocess"
1011 );
1012 }
1013
1014 #[test]
1015 fn test_by_raw_builder_accepts_any_kind() {
1016 let by = By::raw("ext/my-plugin/foo", serde_json::json!({"k": 1}));
1017 assert_eq!(by.kind, "ext/my-plugin/foo");
1018 assert_eq!(by.data.get("k").and_then(|v| v.as_u64()), Some(1));
1019 }
1020
1021 #[test]
1022 fn test_by_is_atomic_kind() {
1023 assert!(By::filter("x.lua", 1).is_atomic_kind());
1024 assert!(By::shortcode("meta").is_atomic_kind());
1025 assert!(By::title_block().is_atomic_kind());
1026 assert!(By::tree_sitter_postprocess().is_atomic_kind());
1027 assert!(By::citeproc().is_atomic_kind());
1028 assert!(By::jupyter_output().is_atomic_kind());
1029
1030 assert!(!By::callout().is_atomic_kind());
1031
1032 assert!(!By::sectionize().is_atomic_kind());
1033 assert!(!By::user_edit().is_atomic_kind());
1034 assert!(!By::include().is_atomic_kind());
1035 assert!(!By::footnotes().is_atomic_kind());
1036 assert!(!By::appendix().is_atomic_kind());
1037 assert!(!By::unknown().is_atomic_kind());
1038 assert!(!By::test_scaffold().is_atomic_kind());
1039 assert!(!By::config_default().is_atomic_kind());
1040 assert!(!By::programmatic_config().is_atomic_kind());
1041 assert!(!By::raw("ext/anywhere/foo", serde_json::Value::Null).is_atomic_kind());
1042 }
1043
1044 #[test]
1045 fn test_by_unknown_constructor() {
1046 let by = By::unknown();
1047 assert_eq!(by.kind, "unknown");
1048 assert!(by.data.is_null());
1049 assert!(!by.is_atomic_kind());
1053 }
1054
1055 #[test]
1056 fn test_by_test_scaffold_constructor() {
1057 let by = By::test_scaffold();
1058 assert_eq!(by.kind, "test-scaffold");
1059 assert!(by.data.is_null());
1060 assert!(!by.is_atomic_kind());
1061 assert!(!by.is_programmatic_sentinel());
1063 }
1064
1065 #[test]
1066 fn test_by_config_default_constructor() {
1067 let by = By::config_default();
1068 assert_eq!(by.kind, "config-default");
1069 assert!(by.data.is_null());
1070 assert!(!by.is_atomic_kind());
1071 }
1072
1073 #[test]
1074 fn test_by_programmatic_config_constructor() {
1075 let by = By::programmatic_config();
1076 assert_eq!(by.kind, "programmatic-config");
1077 assert!(by.data.is_null());
1078 assert!(!by.is_atomic_kind());
1079 }
1080
1081 #[test]
1082 fn test_by_citeproc_constructor() {
1083 let by = By::citeproc();
1084 assert_eq!(by.kind, "citeproc");
1085 assert!(by.data.is_null());
1086 assert!(by.is_atomic_kind());
1088 assert!(!by.is_programmatic_sentinel());
1090 }
1091
1092 #[test]
1093 fn test_by_jupyter_output_constructor() {
1094 let by = By::jupyter_output();
1095 assert_eq!(by.kind, "jupyter-output");
1096 assert!(by.data.is_null());
1097 assert!(by.is_atomic_kind());
1099 assert!(!by.is_programmatic_sentinel());
1100 }
1101
1102 #[test]
1103 fn test_by_callout_constructor() {
1104 let by = By::callout();
1105 assert_eq!(by.kind, "callout");
1106 assert!(by.data.is_null());
1107 assert!(!by.is_atomic_kind());
1109 assert!(!by.is_programmatic_sentinel());
1110 }
1111
1112 #[test]
1113 fn test_by_is_programmatic_sentinel() {
1114 assert!(By::config_default().is_programmatic_sentinel());
1115 assert!(By::programmatic_config().is_programmatic_sentinel());
1116 assert!(By::unknown().is_programmatic_sentinel());
1117
1118 assert!(!By::user_edit().is_programmatic_sentinel());
1119 assert!(!By::filter("x.lua", 1).is_programmatic_sentinel());
1120 assert!(!By::shortcode("meta").is_programmatic_sentinel());
1121 assert!(!By::test_scaffold().is_programmatic_sentinel());
1122 assert!(!By::sectionize().is_programmatic_sentinel());
1123 }
1124
1125 #[test]
1126 fn test_source_info_for_test() {
1127 let si = SourceInfo::for_test();
1128 match si {
1129 SourceInfo::Generated { by, from } => {
1130 assert_eq!(by.kind, "test-scaffold");
1131 assert!(from.is_empty());
1132 }
1133 _ => panic!("for_test() must return Generated"),
1134 }
1135 }
1136
1137 #[test]
1138 fn test_by_is_kind() {
1139 let by = By::shortcode("meta");
1140 assert!(by.is_kind("shortcode"));
1141 assert!(!by.is_kind("filter"));
1142 }
1143
1144 #[test]
1145 fn test_by_as_filter_rejects_non_filter() {
1146 assert!(By::sectionize().as_filter().is_none());
1147 let by = By {
1149 kind: "filter".to_string(),
1150 data: serde_json::json!({ "filter_path": "x.lua" }),
1151 };
1152 assert!(by.as_filter().is_none());
1153 }
1154
1155 #[test]
1156 fn test_anchor_invocation_value_source_constructors() {
1157 let original = Arc::new(SourceInfo::original(FileId(1), 0, 5));
1158 let inv = Anchor::invocation(Arc::clone(&original));
1159 let vs = Anchor::value_source(Arc::clone(&original));
1160 assert!(matches!(inv.role, AnchorRole::Invocation));
1161 assert!(matches!(vs.role, AnchorRole::ValueSource));
1162 }
1163
1164 #[test]
1165 fn test_by_json_round_trip() {
1166 let by = By::shortcode("meta");
1167 let json = serde_json::to_string(&by).unwrap();
1168 let back: By = serde_json::from_str(&json).unwrap();
1169 assert_eq!(by, back);
1170 }
1171
1172 #[test]
1173 fn test_anchor_json_round_trip() {
1174 let anchor = Anchor::invocation(Arc::new(SourceInfo::original(FileId(2), 10, 20)));
1175 let json = serde_json::to_string(&anchor).unwrap();
1176 let back: Anchor = serde_json::from_str(&json).unwrap();
1177 assert_eq!(anchor, back);
1178 }
1179
1180 #[test]
1181 fn test_generated_json_round_trip_empty_from() {
1182 let info = SourceInfo::generated(By::sectionize());
1183 let json = serde_json::to_string(&info).unwrap();
1184 let back: SourceInfo = serde_json::from_str(&json).unwrap();
1185 assert_eq!(info, back);
1186 }
1187
1188 #[test]
1189 fn test_generated_json_round_trip_with_invocation_anchor() {
1190 let mut info = SourceInfo::generated(By::shortcode("meta"));
1191 info.append_anchor(
1192 AnchorRole::Invocation,
1193 Arc::new(SourceInfo::original(FileId(5), 100, 110)),
1194 );
1195 let json = serde_json::to_string(&info).unwrap();
1196 let back: SourceInfo = serde_json::from_str(&json).unwrap();
1197 assert_eq!(info, back);
1198 }
1199
1200 #[test]
1201 fn test_generated_json_round_trip_multi_anchor() {
1202 let mut info = SourceInfo::generated(By::shortcode("meta"));
1203 info.append_anchor(
1204 AnchorRole::Invocation,
1205 Arc::new(SourceInfo::original(FileId(5), 100, 110)),
1206 );
1207 info.append_anchor(
1208 AnchorRole::ValueSource,
1209 Arc::new(SourceInfo::original(FileId(7), 200, 220)),
1210 );
1211 let json = serde_json::to_string(&info).unwrap();
1212 let back: SourceInfo = serde_json::from_str(&json).unwrap();
1213 assert_eq!(info, back);
1214 }
1215
1216 #[test]
1217 fn test_generated_length_start_end_are_zero() {
1218 let info = SourceInfo::generated(By::sectionize());
1219 assert_eq!(info.length(), 0);
1220 assert_eq!(info.start_offset(), 0);
1221 assert_eq!(info.end_offset(), 0);
1222 }
1223
1224 #[test]
1225 fn test_generated_resolve_byte_range_recurses_through_substring() {
1226 let parent = SourceInfo::original(FileId(42), 100, 200);
1227 let sub = SourceInfo::substring(parent, 10, 20);
1228 let mut info = SourceInfo::generated(By::shortcode("meta"));
1229 info.append_anchor(AnchorRole::Invocation, Arc::new(sub));
1230 assert_eq!(info.resolve_byte_range(), Some((42, 110, 120)));
1231 }
1232
1233 #[test]
1234 fn test_generated_resolve_byte_range_empty_returns_none() {
1235 let info = SourceInfo::generated(By::sectionize());
1236 assert!(info.resolve_byte_range().is_none());
1237 }
1238
1239 #[test]
1240 fn test_generated_resolve_byte_range_value_source_only_returns_none() {
1241 let mut info = SourceInfo::generated(By::shortcode("meta"));
1242 info.append_anchor(
1243 AnchorRole::ValueSource,
1244 Arc::new(SourceInfo::original(FileId(5), 100, 110)),
1245 );
1246 assert!(info.resolve_byte_range().is_none());
1247 }
1248
1249 #[test]
1250 fn test_generated_remap_file_ids_walks_anchors() {
1251 let mut info = SourceInfo::generated(By::shortcode("meta"));
1252 info.append_anchor(
1253 AnchorRole::Invocation,
1254 Arc::new(SourceInfo::original(FileId(0), 0, 5)),
1255 );
1256 info.append_anchor(
1257 AnchorRole::ValueSource,
1258 Arc::new(SourceInfo::original(FileId(3), 10, 20)),
1259 );
1260 info.remap_file_ids(&|id| FileId(id.0 + 10));
1261 match &info {
1262 SourceInfo::Generated { from, .. } => {
1263 assert_eq!(from.len(), 2);
1264 match from[0].source_info.as_ref() {
1265 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(10)),
1266 _ => panic!("Expected Original anchor 0"),
1267 }
1268 match from[1].source_info.as_ref() {
1269 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(13)),
1270 _ => panic!("Expected Original anchor 1"),
1271 }
1272 }
1273 _ => panic!("Expected Generated"),
1274 }
1275 }
1276
1277 #[test]
1278 fn test_root_file_id_per_variant() {
1279 let original = SourceInfo::original(FileId(7), 0, 5);
1281 assert_eq!(original.root_file_id(), Some(FileId(7)));
1282
1283 let sub = SourceInfo::substring(original.clone(), 0, 5);
1285 assert_eq!(sub.root_file_id(), Some(FileId(7)));
1286
1287 let empty_gen = SourceInfo::generated(By::sectionize());
1289 let real = SourceInfo::original(FileId(42), 0, 5);
1290 let concat = SourceInfo::concat(vec![(empty_gen, 0), (real, 5)]);
1291 assert_eq!(concat.root_file_id(), Some(FileId(42)));
1292
1293 let mut g = SourceInfo::generated(By::shortcode("meta"));
1295 g.append_anchor(
1296 AnchorRole::Invocation,
1297 Arc::new(SourceInfo::original(FileId(9), 0, 1)),
1298 );
1299 assert_eq!(g.root_file_id(), Some(FileId(9)));
1300
1301 let mut g2 = SourceInfo::generated(By::shortcode("meta"));
1303 g2.append_anchor(
1304 AnchorRole::ValueSource,
1305 Arc::new(SourceInfo::original(FileId(9), 0, 1)),
1306 );
1307 assert_eq!(g2.root_file_id(), None);
1308
1309 let g3 = SourceInfo::generated(By::sectionize());
1311 assert_eq!(g3.root_file_id(), None);
1312 }
1313
1314 #[test]
1315 fn test_collect_file_ids_walks_every_anchor_role() {
1316 let mut info = SourceInfo::generated(By::shortcode("meta"));
1317 info.append_anchor(
1318 AnchorRole::Invocation,
1319 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1320 );
1321 info.append_anchor(
1322 AnchorRole::ValueSource,
1323 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1324 );
1325 info.append_anchor(
1326 AnchorRole::Other("dispatch".to_string()),
1327 Arc::new(SourceInfo::original(FileId(3), 0, 1)),
1328 );
1329 let mut out = std::collections::HashSet::new();
1330 info.collect_file_ids(&mut out);
1331 assert!(out.contains(&FileId(1)));
1332 assert!(out.contains(&FileId(2)));
1333 assert!(out.contains(&FileId(3)));
1334 assert_eq!(out.len(), 3);
1335 }
1336
1337 #[test]
1338 fn test_collect_file_ids_walks_concat_and_substring() {
1339 let inner = SourceInfo::original(FileId(5), 0, 100);
1340 let sub = SourceInfo::substring(inner, 10, 20);
1341 let other = SourceInfo::original(FileId(11), 0, 5);
1342 let concat = SourceInfo::concat(vec![(sub, 10), (other, 5)]);
1343 let mut out = std::collections::HashSet::new();
1344 concat.collect_file_ids(&mut out);
1345 assert!(out.contains(&FileId(5)));
1346 assert!(out.contains(&FileId(11)));
1347 assert_eq!(out.len(), 2);
1348 }
1349
1350 #[test]
1351 fn test_invocation_anchor_accessor() {
1352 let mut info = SourceInfo::generated(By::shortcode("meta"));
1353 assert!(info.invocation_anchor().is_none());
1354 info.append_anchor(
1355 AnchorRole::ValueSource,
1356 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1357 );
1358 assert!(info.invocation_anchor().is_none());
1359 info.append_anchor(
1360 AnchorRole::Invocation,
1361 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1362 );
1363 assert!(info.invocation_anchor().is_some());
1364 assert!(
1366 SourceInfo::original(FileId(0), 0, 0)
1367 .invocation_anchor()
1368 .is_none()
1369 );
1370 }
1371
1372 #[test]
1373 fn test_value_source_anchor_accessor() {
1374 let mut info = SourceInfo::generated(By::shortcode("meta"));
1375 assert!(info.value_source_anchor().is_none());
1376 info.append_anchor(
1377 AnchorRole::Invocation,
1378 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1379 );
1380 assert!(info.value_source_anchor().is_none());
1381 info.append_anchor(
1382 AnchorRole::ValueSource,
1383 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1384 );
1385 assert!(info.value_source_anchor().is_some());
1386 }
1387
1388 #[test]
1389 fn test_anchors_with_role() {
1390 let mut info = SourceInfo::generated(By::shortcode("meta"));
1391 info.append_anchor(
1392 AnchorRole::Invocation,
1393 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1394 );
1395 info.append_anchor(
1396 AnchorRole::ValueSource,
1397 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1398 );
1399 info.append_anchor(
1400 AnchorRole::Other("ext/foo".to_string()),
1401 Arc::new(SourceInfo::original(FileId(3), 0, 1)),
1402 );
1403 assert_eq!(info.anchors_with_role(&AnchorRole::Invocation).count(), 1);
1404 assert_eq!(info.anchors_with_role(&AnchorRole::ValueSource).count(), 1);
1405 assert_eq!(
1406 info.anchors_with_role(&AnchorRole::Other("ext/foo".to_string()))
1407 .count(),
1408 1
1409 );
1410 assert_eq!(
1411 info.anchors_with_role(&AnchorRole::Other("missing".to_string()))
1412 .count(),
1413 0
1414 );
1415 }
1416
1417 #[test]
1418 fn test_append_anchor_preserves_order() {
1419 let mut info = SourceInfo::generated(By::shortcode("meta"));
1420 info.append_anchor(
1421 AnchorRole::Invocation,
1422 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1423 );
1424 info.append_anchor(
1425 AnchorRole::ValueSource,
1426 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1427 );
1428 match info {
1429 SourceInfo::Generated { from, .. } => {
1430 assert_eq!(from.len(), 2);
1431 assert!(matches!(from[0].role, AnchorRole::Invocation));
1432 assert!(matches!(from[1].role, AnchorRole::ValueSource));
1433 }
1434 _ => panic!("Expected Generated"),
1435 }
1436 }
1437
1438 #[test]
1439 fn test_combine_with_generated_is_zero_length_piece() {
1440 let original = SourceInfo::original(FileId(0), 10, 20);
1441 let generated = SourceInfo::generated(By::sectionize());
1442 let combined = original.combine(&generated);
1443 match &combined {
1444 SourceInfo::Concat { pieces } => {
1445 assert_eq!(pieces.len(), 2);
1446 assert_eq!(pieces[1].length, 0);
1447 }
1448 _ => panic!("Expected Concat"),
1449 }
1450 assert_eq!(combined.length(), 10);
1452 }
1453
1454 #[test]
1455 fn test_source_info_serialization() {
1456 let file_id = FileId(0);
1457 let range = Range {
1458 start: Location {
1459 offset: 0,
1460 row: 0,
1461 column: 0,
1462 },
1463 end: Location {
1464 offset: 10,
1465 row: 0,
1466 column: 10,
1467 },
1468 };
1469
1470 let info = SourceInfo::from_range(file_id, range);
1471 let json = serde_json::to_string(&info).unwrap();
1472 let deserialized: SourceInfo = serde_json::from_str(&json).unwrap();
1473
1474 assert_eq!(info, deserialized);
1475 }
1476
1477 #[test]
1478 fn test_substring_source_info() {
1479 let file_id = FileId(0);
1480 let parent_range = Range {
1481 start: Location {
1482 offset: 0,
1483 row: 0,
1484 column: 0,
1485 },
1486 end: Location {
1487 offset: 100,
1488 row: 0,
1489 column: 100,
1490 },
1491 };
1492 let parent = SourceInfo::from_range(file_id, parent_range);
1493
1494 let substring = SourceInfo::substring(parent, 10, 20);
1495
1496 assert_eq!(substring.start_offset(), 10);
1497 assert_eq!(substring.end_offset(), 20);
1498 assert_eq!(substring.length(), 10);
1499
1500 match substring {
1501 SourceInfo::Substring {
1502 start_offset,
1503 end_offset,
1504 ..
1505 } => {
1506 assert_eq!(start_offset, 10);
1507 assert_eq!(end_offset, 20);
1508 }
1509 _ => panic!("Expected Substring mapping"),
1510 }
1511 }
1512
1513 #[test]
1514 fn test_concat_source_info() {
1515 let file_id1 = FileId(0);
1516 let file_id2 = FileId(1);
1517
1518 let info1 = SourceInfo::from_range(
1519 file_id1,
1520 Range {
1521 start: Location {
1522 offset: 0,
1523 row: 0,
1524 column: 0,
1525 },
1526 end: Location {
1527 offset: 10,
1528 row: 0,
1529 column: 10,
1530 },
1531 },
1532 );
1533
1534 let info2 = SourceInfo::from_range(
1535 file_id2,
1536 Range {
1537 start: Location {
1538 offset: 0,
1539 row: 0,
1540 column: 0,
1541 },
1542 end: Location {
1543 offset: 15,
1544 row: 0,
1545 column: 15,
1546 },
1547 },
1548 );
1549
1550 let concat = SourceInfo::concat(vec![(info1, 10), (info2, 15)]);
1551
1552 assert_eq!(concat.start_offset(), 0);
1553 assert_eq!(concat.end_offset(), 25); assert_eq!(concat.length(), 25);
1555
1556 match concat {
1557 SourceInfo::Concat { pieces } => {
1558 assert_eq!(pieces.len(), 2);
1559 assert_eq!(pieces[0].offset_in_concat, 0);
1560 assert_eq!(pieces[0].length, 10);
1561 assert_eq!(pieces[1].offset_in_concat, 10);
1562 assert_eq!(pieces[1].length, 15);
1563 }
1564 _ => panic!("Expected Concat mapping"),
1565 }
1566 }
1567
1568 #[test]
1569 fn test_combine_two_sources() {
1570 let file_id = FileId(0);
1571
1572 let info1 = SourceInfo::from_range(
1574 file_id,
1575 Range {
1576 start: Location {
1577 offset: 0,
1578 row: 0,
1579 column: 0,
1580 },
1581 end: Location {
1582 offset: 10,
1583 row: 0,
1584 column: 10,
1585 },
1586 },
1587 );
1588
1589 let info2 = SourceInfo::from_range(
1590 file_id,
1591 Range {
1592 start: Location {
1593 offset: 15,
1594 row: 0,
1595 column: 15,
1596 },
1597 end: Location {
1598 offset: 25,
1599 row: 0,
1600 column: 25,
1601 },
1602 },
1603 );
1604
1605 let combined = info1.combine(&info2);
1607
1608 assert_eq!(combined.start_offset(), 0);
1610 assert_eq!(combined.end_offset(), 20);
1611 assert_eq!(combined.length(), 20);
1612
1613 match combined {
1614 SourceInfo::Concat { pieces } => {
1615 assert_eq!(pieces.len(), 2);
1616 assert_eq!(pieces[0].length, 10);
1617 assert_eq!(pieces[0].offset_in_concat, 0);
1618 assert_eq!(pieces[1].length, 10);
1619 assert_eq!(pieces[1].offset_in_concat, 10);
1620 }
1621 _ => panic!("Expected Concat mapping"),
1622 }
1623 }
1624
1625 #[test]
1626 fn test_combine_preserves_source_tracking() {
1627 let file_id1 = FileId(5);
1629 let file_id2 = FileId(10);
1630
1631 let info1 = SourceInfo::from_range(
1632 file_id1,
1633 Range {
1634 start: Location {
1635 offset: 100,
1636 row: 5,
1637 column: 0,
1638 },
1639 end: Location {
1640 offset: 105,
1641 row: 5,
1642 column: 5,
1643 },
1644 },
1645 );
1646
1647 let info2 = SourceInfo::from_range(
1648 file_id2,
1649 Range {
1650 start: Location {
1651 offset: 200,
1652 row: 10,
1653 column: 0,
1654 },
1655 end: Location {
1656 offset: 207,
1657 row: 10,
1658 column: 7,
1659 },
1660 },
1661 );
1662
1663 let combined = info1.combine(&info2);
1664
1665 match combined {
1667 SourceInfo::Concat { pieces } => {
1668 assert_eq!(pieces.len(), 2);
1669
1670 match &pieces[0].source_info {
1672 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, file_id1),
1673 _ => panic!("Expected Original mapping for first piece"),
1674 }
1675
1676 match &pieces[1].source_info {
1678 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, file_id2),
1679 _ => panic!("Expected Original mapping for second piece"),
1680 }
1681 }
1682 _ => panic!("Expected Concat mapping"),
1683 }
1684 }
1685
1686 #[test]
1688 fn test_json_serialization_original() {
1689 let file_id = FileId(0);
1690 let range = Range {
1691 start: Location {
1692 offset: 10,
1693 row: 1,
1694 column: 5,
1695 },
1696 end: Location {
1697 offset: 50,
1698 row: 3,
1699 column: 10,
1700 },
1701 };
1702
1703 let info = SourceInfo::from_range(file_id, range);
1704 let json = serde_json::to_value(&info).unwrap();
1705
1706 assert_eq!(json["Original"]["file_id"], 0);
1708 assert_eq!(json["Original"]["start_offset"], 10);
1709 assert_eq!(json["Original"]["end_offset"], 50);
1710
1711 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1713 assert_eq!(info, deserialized);
1714 }
1715
1716 #[test]
1718 fn test_json_serialization_substring() {
1719 let file_id = FileId(0);
1720 let parent_range = Range {
1721 start: Location {
1722 offset: 0,
1723 row: 0,
1724 column: 0,
1725 },
1726 end: Location {
1727 offset: 100,
1728 row: 5,
1729 column: 20,
1730 },
1731 };
1732 let parent = SourceInfo::from_range(file_id, parent_range);
1733
1734 let substring = SourceInfo::substring(parent, 10, 30);
1735 let json = serde_json::to_value(&substring).unwrap();
1736
1737 assert_eq!(json["Substring"]["start_offset"], 10);
1739 assert_eq!(json["Substring"]["end_offset"], 30);
1740
1741 assert!(json["Substring"]["parent"].is_object());
1743 assert_eq!(json["Substring"]["parent"]["Original"]["file_id"], 0);
1744
1745 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1747 assert_eq!(substring, deserialized);
1748 }
1749
1750 #[test]
1752 fn test_json_serialization_nested_substring() {
1753 let file_id = FileId(0);
1754
1755 let file_range = Range {
1757 start: Location {
1758 offset: 0,
1759 row: 0,
1760 column: 0,
1761 },
1762 end: Location {
1763 offset: 200,
1764 row: 10,
1765 column: 0,
1766 },
1767 };
1768 let file_info = SourceInfo::from_range(file_id, file_range);
1769
1770 let yaml_info = SourceInfo::substring(file_info, 4, 150);
1772
1773 let value_info = SourceInfo::substring(yaml_info, 20, 35);
1775
1776 let json = serde_json::to_value(&value_info).unwrap();
1777
1778 assert_eq!(json["Substring"]["start_offset"], 20);
1780 assert_eq!(json["Substring"]["end_offset"], 35);
1781 assert_eq!(json["Substring"]["parent"]["Substring"]["start_offset"], 4);
1782 assert_eq!(
1783 json["Substring"]["parent"]["Substring"]["parent"]["Original"]["file_id"],
1784 0
1785 );
1786
1787 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1789 assert_eq!(value_info, deserialized);
1790 }
1791
1792 #[test]
1794 fn test_json_serialization_concat() {
1795 let file_id1 = FileId(0);
1796 let file_id2 = FileId(1);
1797
1798 let info1 = SourceInfo::from_range(
1799 file_id1,
1800 Range {
1801 start: Location {
1802 offset: 0,
1803 row: 0,
1804 column: 0,
1805 },
1806 end: Location {
1807 offset: 10,
1808 row: 0,
1809 column: 10,
1810 },
1811 },
1812 );
1813
1814 let info2 = SourceInfo::from_range(
1815 file_id2,
1816 Range {
1817 start: Location {
1818 offset: 20,
1819 row: 2,
1820 column: 0,
1821 },
1822 end: Location {
1823 offset: 30,
1824 row: 2,
1825 column: 10,
1826 },
1827 },
1828 );
1829
1830 let combined = info1.combine(&info2);
1831 let json = serde_json::to_value(&combined).unwrap();
1832
1833 assert!(json["Concat"]["pieces"].is_array());
1835 let pieces = json["Concat"]["pieces"].as_array().unwrap();
1836 assert_eq!(pieces.len(), 2);
1837
1838 assert_eq!(pieces[0]["offset_in_concat"], 0);
1840 assert_eq!(pieces[0]["length"], 10);
1841 assert_eq!(pieces[0]["source_info"]["Original"]["file_id"], 0);
1842
1843 assert_eq!(pieces[1]["offset_in_concat"], 10);
1845 assert_eq!(pieces[1]["length"], 10);
1846 assert_eq!(pieces[1]["source_info"]["Original"]["file_id"], 1);
1847
1848 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1850 assert_eq!(combined, deserialized);
1851 }
1852
1853 #[test]
1855 fn test_json_serialization_complex_nested() {
1856 let file_id = FileId(0);
1857
1858 let qmd_file = SourceInfo::from_range(
1860 file_id,
1861 Range {
1862 start: Location {
1863 offset: 0,
1864 row: 0,
1865 column: 0,
1866 },
1867 end: Location {
1868 offset: 500,
1869 row: 20,
1870 column: 0,
1871 },
1872 },
1873 );
1874
1875 let yaml_frontmatter = SourceInfo::substring(qmd_file.clone(), 4, 200);
1877
1878 let yaml_key = SourceInfo::substring(yaml_frontmatter.clone(), 10, 20);
1880
1881 let yaml_value = SourceInfo::substring(yaml_frontmatter, 25, 50);
1883
1884 let combined = yaml_key.combine(&yaml_value);
1886
1887 let json = serde_json::to_value(&combined).unwrap();
1888
1889 assert!(json.is_object());
1891 assert!(json["Concat"].is_object());
1892
1893 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1895 assert_eq!(combined, deserialized);
1896 }
1897
1898 #[test]
1903 fn test_preimage_in_original_same_file() {
1904 let info = SourceInfo::original(FileId(0), 10, 25);
1905 assert_eq!(info.preimage_in(FileId(0)), Some(10..25));
1906 }
1907
1908 #[test]
1909 fn test_preimage_in_original_different_file_returns_none() {
1910 let info = SourceInfo::original(FileId(0), 10, 25);
1911 assert_eq!(info.preimage_in(FileId(1)), None);
1912 }
1913
1914 #[test]
1915 fn test_preimage_in_substring_composes_offsets() {
1916 let parent = SourceInfo::original(FileId(0), 100, 200);
1920 let info = SourceInfo::substring(parent, 5, 15);
1921 assert_eq!(info.preimage_in(FileId(0)), Some(105..115));
1922 }
1923
1924 #[test]
1925 fn test_preimage_in_substring_different_file_returns_none() {
1926 let parent = SourceInfo::original(FileId(0), 100, 200);
1927 let info = SourceInfo::substring(parent, 5, 15);
1928 assert_eq!(info.preimage_in(FileId(7)), None);
1929 }
1930
1931 #[test]
1932 fn test_preimage_in_substring_chain() {
1933 let root = SourceInfo::original(FileId(0), 1000, 2000);
1936 let mid = SourceInfo::substring(root, 100, 500);
1937 let leaf = SourceInfo::substring(mid, 10, 50);
1938 assert_eq!(leaf.preimage_in(FileId(0)), Some(1110..1150));
1939 }
1940
1941 #[test]
1942 fn test_preimage_in_concat_contiguous() {
1943 let a = SourceInfo::original(FileId(0), 10, 15);
1945 let b = SourceInfo::original(FileId(0), 15, 25);
1946 let info = SourceInfo::concat(vec![(a, 5), (b, 10)]);
1947 assert_eq!(info.preimage_in(FileId(0)), Some(10..25));
1948 }
1949
1950 #[test]
1951 fn test_preimage_in_concat_gappy_returns_none() {
1952 let a = SourceInfo::original(FileId(0), 10, 15);
1954 let b = SourceInfo::original(FileId(0), 20, 25);
1955 let info = SourceInfo::concat(vec![(a, 5), (b, 5)]);
1956 assert_eq!(info.preimage_in(FileId(0)), None);
1957 }
1958
1959 #[test]
1960 fn test_preimage_in_concat_overlapping_returns_none() {
1961 let a = SourceInfo::original(FileId(0), 10, 20);
1963 let b = SourceInfo::original(FileId(0), 15, 25);
1964 let info = SourceInfo::concat(vec![(a, 10), (b, 10)]);
1965 assert_eq!(info.preimage_in(FileId(0)), None);
1966 }
1967
1968 #[test]
1969 fn test_preimage_in_concat_mixed_files_returns_none() {
1970 let a = SourceInfo::original(FileId(0), 10, 15);
1973 let b = SourceInfo::original(FileId(1), 15, 25);
1974 let info = SourceInfo::concat(vec![(a, 5), (b, 10)]);
1975 assert_eq!(info.preimage_in(FileId(0)), None);
1976 }
1977
1978 #[test]
1986 fn test_preimage_in_substring_over_concat_parent_returns_none() {
1987 let it = SourceInfo::original(FileId(0), 1, 3);
1992 let escaped_quote = SourceInfo::original(FileId(0), 3, 5);
1993 let s = SourceInfo::original(FileId(0), 5, 6);
1994 let concat = SourceInfo::concat(vec![(it, 2), (escaped_quote, 1), (s, 1)]);
1995
1996 let sub = SourceInfo::substring(concat, 0, 4);
2000 assert_eq!(sub.preimage_in(FileId(0)), None);
2001 }
2002
2003 #[test]
2004 fn test_preimage_in_bare_concat_over_gap_free_pieces_is_gating() {
2005 let it = SourceInfo::original(FileId(0), 1, 3);
2009 let escaped_quote = SourceInfo::original(FileId(0), 3, 5);
2010 let s = SourceInfo::original(FileId(0), 5, 6);
2011 let concat = SourceInfo::concat(vec![(it, 2), (escaped_quote, 1), (s, 1)]);
2012
2013 assert_eq!(concat.preimage_in(FileId(0)), Some(1..6));
2014 }
2015
2016 #[test]
2017 fn test_preimage_in_cell_options_multi_option_shape_is_gating() {
2018 let opt_a = SourceInfo::original(FileId(0), 3, 4);
2024 let opt_b = SourceInfo::original(FileId(0), 10, 11);
2025 let concat = SourceInfo::concat(vec![(opt_a, 1), (opt_b, 1)]);
2026
2027 assert_eq!(concat.preimage_in(FileId(0)), None);
2028 let sub = SourceInfo::substring(concat, 0, 2);
2029 assert_eq!(sub.preimage_in(FileId(0)), None);
2030 }
2031
2032 #[test]
2033 fn test_preimage_in_cell_options_single_option_shape_through_substring_returns_none() {
2034 let opt = SourceInfo::original(FileId(0), 5, 8);
2042 let concat = SourceInfo::concat(vec![(opt, 3)]);
2043
2044 assert_eq!(concat.preimage_in(FileId(0)), Some(5..8));
2045
2046 let sub = SourceInfo::substring(concat, 0, 3);
2047 assert_eq!(sub.preimage_in(FileId(0)), None);
2048 }
2049
2050 #[test]
2051 fn test_preimage_in_concat_contiguous_hull_with_zero_content_piece() {
2052 let a = SourceInfo::original(FileId(0), 4, 7);
2059 let zero_content = SourceInfo::original(FileId(0), 7, 11);
2060 let b = SourceInfo::original(FileId(0), 11, 14);
2061 let concat = SourceInfo::concat(vec![(a, 3), (zero_content, 0), (b, 3)]);
2062 assert_eq!(concat.preimage_in(FileId(0)), Some(4..14));
2063
2064 let a2 = SourceInfo::original(FileId(0), 4, 7);
2067 let b2 = SourceInfo::original(FileId(0), 11, 14);
2068 let concat_missing_piece = SourceInfo::concat(vec![(a2, 3), (b2, 3)]);
2069 assert_eq!(concat_missing_piece.preimage_in(FileId(0)), None);
2070 }
2071
2072 #[test]
2073 fn test_preimage_in_generated_no_anchors_returns_none() {
2074 let info = SourceInfo::generated(By::sectionize());
2077 assert_eq!(info.preimage_in(FileId(0)), None);
2078 }
2079
2080 #[test]
2081 fn test_preimage_in_generated_with_invocation_in_target() {
2082 let token = SourceInfo::original(FileId(0), 50, 70);
2085 let mut info = SourceInfo::generated(By::shortcode("meta"));
2086 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
2087 assert_eq!(info.preimage_in(FileId(0)), Some(50..70));
2088 }
2089
2090 #[test]
2091 fn test_preimage_in_generated_with_invocation_outside_target() {
2092 let token = SourceInfo::original(FileId(0), 50, 70);
2094 let mut info = SourceInfo::generated(By::shortcode("meta"));
2095 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
2096 assert_eq!(info.preimage_in(FileId(1)), None);
2097 }
2098
2099 #[test]
2100 fn test_preimage_in_generated_walks_through_substring_in_invocation() {
2101 let root = SourceInfo::original(FileId(0), 100, 200);
2104 let token = SourceInfo::substring(root, 10, 30);
2105 let mut info = SourceInfo::generated(By::shortcode("meta"));
2106 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
2107 assert_eq!(info.preimage_in(FileId(0)), Some(110..130));
2108 }
2109
2110 #[test]
2115 fn test_preimage_in_generated_value_source_only_returns_none() {
2116 let meta_si = SourceInfo::original(FileId(0), 10, 25);
2120 let mut info = SourceInfo::generated(By::appendix());
2121 info.append_anchor(AnchorRole::ValueSource, Arc::new(meta_si));
2122 assert_eq!(info.preimage_in(FileId(0)), None);
2123 }
2124
2125 #[test]
2126 fn test_preimage_in_generated_other_only_returns_none() {
2127 let lua_si = SourceInfo::original(FileId(0), 10, 25);
2129 let mut info = SourceInfo::generated(By::filter("upper.lua", 14));
2130 info.append_anchor(
2131 AnchorRole::Other("ext/my-ext/dispatch".to_string()),
2132 Arc::new(lua_si),
2133 );
2134 assert_eq!(info.preimage_in(FileId(0)), None);
2135 }
2136
2137 #[test]
2138 fn test_preimage_in_generated_invocation_plus_value_source_walks_invocation_only() {
2139 let token = SourceInfo::original(FileId(0), 50, 70);
2145 let value = SourceInfo::original(FileId(1), 200, 215);
2146 let mut info = SourceInfo::generated(By::shortcode("meta"));
2147 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
2148 info.append_anchor(AnchorRole::ValueSource, Arc::new(value));
2149
2150 assert_eq!(info.preimage_in(FileId(0)), Some(50..70));
2151 assert_eq!(info.preimage_in(FileId(1)), None);
2152 }
2153}