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>> {
441 match self {
442 SourceInfo::Original {
443 file_id,
444 start_offset,
445 end_offset,
446 } if *file_id == target => Some(*start_offset..*end_offset),
447 SourceInfo::Original { .. } => None,
448 SourceInfo::Substring {
449 parent,
450 start_offset,
451 end_offset,
452 } => {
453 let parent_range = parent.preimage_in(target)?;
454 Some(parent_range.start + start_offset..parent_range.start + end_offset)
455 }
456 SourceInfo::Concat { pieces } => {
457 let ranges: Vec<std::ops::Range<usize>> = pieces
458 .iter()
459 .map(|p| p.source_info.preimage_in(target))
460 .collect::<Option<Vec<_>>>()?;
461 if ranges.is_empty() {
462 return None;
463 }
464 if ranges.windows(2).all(|w| w[0].end == w[1].start) {
465 let first = ranges.first().unwrap().start;
466 let last = ranges.last().unwrap().end;
467 Some(first..last)
468 } else {
469 None
470 }
471 }
472 SourceInfo::Generated { .. } => self
473 .invocation_anchor()
474 .and_then(|si| si.preimage_in(target)),
475 }
476 }
477
478 pub fn remap_file_ids<F>(&mut self, map: &F)
486 where
487 F: Fn(FileId) -> FileId,
488 {
489 match self {
490 SourceInfo::Original { file_id, .. } => {
491 *file_id = map(*file_id);
492 }
493 SourceInfo::Substring { parent, .. } => {
494 let parent = Arc::make_mut(parent);
496 parent.remap_file_ids(map);
497 }
498 SourceInfo::Concat { pieces } => {
499 for piece in pieces {
500 piece.source_info.remap_file_ids(map);
501 }
502 }
503 SourceInfo::Generated { from, .. } => {
504 for anchor in from {
505 let inner = Arc::make_mut(&mut anchor.source_info);
507 inner.remap_file_ids(map);
508 }
509 }
510 }
511 }
512
513 pub fn root_file_id(&self) -> Option<FileId> {
522 match self {
523 SourceInfo::Original { file_id, .. } => Some(*file_id),
524 SourceInfo::Substring { parent, .. } => parent.root_file_id(),
525 SourceInfo::Concat { pieces } => {
526 pieces.iter().find_map(|p| p.source_info.root_file_id())
527 }
528 SourceInfo::Generated { .. } => {
529 self.invocation_anchor().and_then(|si| si.root_file_id())
530 }
531 }
532 }
533
534 pub fn collect_file_ids(&self, out: &mut std::collections::HashSet<FileId>) {
540 match self {
541 SourceInfo::Original { file_id, .. } => {
542 out.insert(*file_id);
543 }
544 SourceInfo::Substring { parent, .. } => parent.collect_file_ids(out),
545 SourceInfo::Concat { pieces } => {
546 for piece in pieces {
547 piece.source_info.collect_file_ids(out);
548 }
549 }
550 SourceInfo::Generated { from, .. } => {
551 for anchor in from {
552 anchor.source_info.collect_file_ids(out);
553 }
554 }
555 }
556 }
557}
558
559impl By {
560 pub fn filter(filter_path: impl Into<String>, line: usize) -> Self {
570 Self {
571 kind: "filter".to_string(),
572 data: serde_json::json!({
573 "filter_path": filter_path.into(),
574 "line": line,
575 }),
576 }
577 }
578
579 pub fn sectionize() -> Self {
582 Self {
583 kind: "sectionize".to_string(),
584 data: serde_json::Value::Null,
585 }
586 }
587
588 pub fn user_edit() -> Self {
591 Self {
592 kind: "user-edit".to_string(),
593 data: serde_json::Value::Null,
594 }
595 }
596
597 pub fn shortcode(name: impl Into<String>) -> Self {
606 Self {
607 kind: "shortcode".to_string(),
608 data: serde_json::json!({ "name": name.into() }),
609 }
610 }
611
612 pub fn include() -> Self {
617 Self {
618 kind: "include".to_string(),
619 data: serde_json::Value::Null,
620 }
621 }
622
623 pub fn title_block() -> Self {
625 Self {
626 kind: "title-block".to_string(),
627 data: serde_json::Value::Null,
628 }
629 }
630
631 pub fn footnotes() -> Self {
633 Self {
634 kind: "footnotes".to_string(),
635 data: serde_json::Value::Null,
636 }
637 }
638
639 pub fn revealjs() -> Self {
645 Self {
646 kind: "revealjs".to_string(),
647 data: serde_json::Value::Null,
648 }
649 }
650
651 pub fn appendix() -> Self {
653 Self {
654 kind: "appendix".to_string(),
655 data: serde_json::Value::Null,
656 }
657 }
658
659 pub fn tree_sitter_postprocess() -> Self {
662 Self {
663 kind: "tree-sitter-postprocess".to_string(),
664 data: serde_json::Value::Null,
665 }
666 }
667
668 pub fn unknown() -> Self {
678 Self {
679 kind: "unknown".to_string(),
680 data: serde_json::Value::Null,
681 }
682 }
683
684 pub fn test_scaffold() -> Self {
689 Self {
690 kind: "test-scaffold".to_string(),
691 data: serde_json::Value::Null,
692 }
693 }
694
695 pub fn citeproc() -> Self {
704 Self {
705 kind: "citeproc".to_string(),
706 data: serde_json::Value::Null,
707 }
708 }
709
710 pub fn jupyter_output() -> Self {
718 Self {
719 kind: "jupyter-output".to_string(),
720 data: serde_json::Value::Null,
721 }
722 }
723
724 pub fn callout() -> Self {
735 Self {
736 kind: "callout".to_string(),
737 data: serde_json::Value::Null,
738 }
739 }
740
741 pub fn config_default() -> Self {
745 Self {
746 kind: "config-default".to_string(),
747 data: serde_json::Value::Null,
748 }
749 }
750
751 pub fn programmatic_config() -> Self {
756 Self {
757 kind: "programmatic-config".to_string(),
758 data: serde_json::Value::Null,
759 }
760 }
761
762 pub fn is_programmatic_sentinel(&self) -> bool {
767 matches!(
768 self.kind.as_str(),
769 "config-default" | "programmatic-config" | "unknown"
770 )
771 }
772
773 pub fn raw(kind: impl Into<String>, data: serde_json::Value) -> Self {
781 Self {
782 kind: kind.into(),
783 data,
784 }
785 }
786
787 pub fn is_atomic_kind(&self) -> bool {
799 matches!(
800 self.kind.as_str(),
801 "filter"
802 | "shortcode"
803 | "title-block"
804 | "tree-sitter-postprocess"
805 | "citeproc"
806 | "jupyter-output"
807 )
808 }
809
810 pub fn is_kind(&self, kind: &str) -> bool {
812 self.kind == kind
813 }
814
815 pub fn as_filter(&self) -> Option<(&str, usize)> {
821 if self.kind != "filter" {
822 return None;
823 }
824 let path = self.data.get("filter_path")?.as_str()?;
825 let line = self.data.get("line")?.as_u64()? as usize;
826 Some((path, line))
827 }
828}
829
830impl Anchor {
831 pub fn invocation(source_info: Arc<SourceInfo>) -> Self {
833 Self {
834 role: AnchorRole::Invocation,
835 source_info,
836 }
837 }
838
839 pub fn value_source(source_info: Arc<SourceInfo>) -> Self {
841 Self {
842 role: AnchorRole::ValueSource,
843 source_info,
844 }
845 }
846}
847
848#[cfg(test)]
849mod tests {
850 use super::*;
851 use crate::types::{FileId, Location, Range};
852
853 #[test]
854 fn test_original_source_info() {
855 let file_id = FileId(0);
856 let range = Range {
857 start: Location {
858 offset: 0,
859 row: 0,
860 column: 0,
861 },
862 end: Location {
863 offset: 10,
864 row: 0,
865 column: 10,
866 },
867 };
868
869 let info = SourceInfo::from_range(file_id, range.clone());
870
871 assert_eq!(info.start_offset(), 0);
872 assert_eq!(info.end_offset(), 10);
873 assert_eq!(info.length(), 10);
874 match info {
875 SourceInfo::Original {
876 file_id: mapped_id, ..
877 } => {
878 assert_eq!(mapped_id, file_id);
879 }
880 _ => panic!("Expected Original mapping"),
881 }
882 }
883
884 #[test]
885 fn test_remap_file_ids_original() {
886 let mut info = SourceInfo::original(FileId(0), 0, 10);
887 info.remap_file_ids(&|id| FileId(id.0 + 1));
888 match info {
889 SourceInfo::Original { file_id, .. } => assert_eq!(file_id, FileId(1)),
890 _ => panic!("Expected Original"),
891 }
892 }
893
894 #[test]
895 fn test_remap_file_ids_substring() {
896 let parent = SourceInfo::original(FileId(0), 0, 100);
897 let mut info = SourceInfo::substring(parent, 5, 20);
898 info.remap_file_ids(&|id| FileId(id.0 + 7));
899 match info {
900 SourceInfo::Substring { parent, .. } => match &*parent {
901 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(7)),
902 _ => panic!("Expected Original parent"),
903 },
904 _ => panic!("Expected Substring"),
905 }
906 }
907
908 #[test]
909 fn test_remap_file_ids_concat() {
910 let a = SourceInfo::original(FileId(0), 0, 5);
911 let b = SourceInfo::original(FileId(3), 5, 10);
912 let mut info = SourceInfo::concat(vec![(a, 5), (b, 5)]);
913 info.remap_file_ids(&|id| FileId(id.0 + 10));
914 match info {
915 SourceInfo::Concat { pieces } => {
916 match &pieces[0].source_info {
917 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(10)),
918 _ => panic!("Expected Original"),
919 }
920 match &pieces[1].source_info {
921 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(13)),
922 _ => panic!("Expected Original"),
923 }
924 }
925 _ => panic!("Expected Concat"),
926 }
927 }
928
929 #[test]
930 fn test_remap_file_ids_generated_empty_from_is_noop() {
931 let mut info = SourceInfo::generated(By::filter("foo.lua", 42));
932 info.remap_file_ids(&|_| FileId(99));
933 match info {
934 SourceInfo::Generated { by, from } => {
935 assert!(from.is_empty());
936 let (path, line) = by.as_filter().unwrap();
937 assert_eq!(path, "foo.lua");
938 assert_eq!(line, 42);
939 }
940 _ => panic!("Expected Generated"),
941 }
942 }
943
944 #[test]
949 fn test_by_filter_builder() {
950 let by = By::filter("a.lua", 7);
951 assert_eq!(by.kind, "filter");
952 assert_eq!(by.as_filter(), Some(("a.lua", 7)));
953 }
954
955 #[test]
956 fn test_by_sectionize_builder() {
957 let by = By::sectionize();
958 assert_eq!(by.kind, "sectionize");
959 assert!(by.data.is_null());
960 }
961
962 #[test]
963 fn test_by_user_edit_builder() {
964 assert_eq!(By::user_edit().kind, "user-edit");
965 }
966
967 #[test]
968 fn test_by_shortcode_builder_records_name() {
969 let by = By::shortcode("meta");
970 assert_eq!(by.kind, "shortcode");
971 assert_eq!(by.data.get("name").and_then(|v| v.as_str()), Some("meta"));
972 }
973
974 #[test]
975 fn test_by_include_title_footnotes_appendix_tree_sitter_builders() {
976 assert_eq!(By::include().kind, "include");
977 assert_eq!(By::title_block().kind, "title-block");
978 assert_eq!(By::footnotes().kind, "footnotes");
979 assert_eq!(By::appendix().kind, "appendix");
980 assert_eq!(
981 By::tree_sitter_postprocess().kind,
982 "tree-sitter-postprocess"
983 );
984 }
985
986 #[test]
987 fn test_by_raw_builder_accepts_any_kind() {
988 let by = By::raw("ext/my-plugin/foo", serde_json::json!({"k": 1}));
989 assert_eq!(by.kind, "ext/my-plugin/foo");
990 assert_eq!(by.data.get("k").and_then(|v| v.as_u64()), Some(1));
991 }
992
993 #[test]
994 fn test_by_is_atomic_kind() {
995 assert!(By::filter("x.lua", 1).is_atomic_kind());
996 assert!(By::shortcode("meta").is_atomic_kind());
997 assert!(By::title_block().is_atomic_kind());
998 assert!(By::tree_sitter_postprocess().is_atomic_kind());
999 assert!(By::citeproc().is_atomic_kind());
1000 assert!(By::jupyter_output().is_atomic_kind());
1001
1002 assert!(!By::callout().is_atomic_kind());
1003
1004 assert!(!By::sectionize().is_atomic_kind());
1005 assert!(!By::user_edit().is_atomic_kind());
1006 assert!(!By::include().is_atomic_kind());
1007 assert!(!By::footnotes().is_atomic_kind());
1008 assert!(!By::appendix().is_atomic_kind());
1009 assert!(!By::unknown().is_atomic_kind());
1010 assert!(!By::test_scaffold().is_atomic_kind());
1011 assert!(!By::config_default().is_atomic_kind());
1012 assert!(!By::programmatic_config().is_atomic_kind());
1013 assert!(!By::raw("ext/anywhere/foo", serde_json::Value::Null).is_atomic_kind());
1014 }
1015
1016 #[test]
1017 fn test_by_unknown_constructor() {
1018 let by = By::unknown();
1019 assert_eq!(by.kind, "unknown");
1020 assert!(by.data.is_null());
1021 assert!(!by.is_atomic_kind());
1025 }
1026
1027 #[test]
1028 fn test_by_test_scaffold_constructor() {
1029 let by = By::test_scaffold();
1030 assert_eq!(by.kind, "test-scaffold");
1031 assert!(by.data.is_null());
1032 assert!(!by.is_atomic_kind());
1033 assert!(!by.is_programmatic_sentinel());
1035 }
1036
1037 #[test]
1038 fn test_by_config_default_constructor() {
1039 let by = By::config_default();
1040 assert_eq!(by.kind, "config-default");
1041 assert!(by.data.is_null());
1042 assert!(!by.is_atomic_kind());
1043 }
1044
1045 #[test]
1046 fn test_by_programmatic_config_constructor() {
1047 let by = By::programmatic_config();
1048 assert_eq!(by.kind, "programmatic-config");
1049 assert!(by.data.is_null());
1050 assert!(!by.is_atomic_kind());
1051 }
1052
1053 #[test]
1054 fn test_by_citeproc_constructor() {
1055 let by = By::citeproc();
1056 assert_eq!(by.kind, "citeproc");
1057 assert!(by.data.is_null());
1058 assert!(by.is_atomic_kind());
1060 assert!(!by.is_programmatic_sentinel());
1062 }
1063
1064 #[test]
1065 fn test_by_jupyter_output_constructor() {
1066 let by = By::jupyter_output();
1067 assert_eq!(by.kind, "jupyter-output");
1068 assert!(by.data.is_null());
1069 assert!(by.is_atomic_kind());
1071 assert!(!by.is_programmatic_sentinel());
1072 }
1073
1074 #[test]
1075 fn test_by_callout_constructor() {
1076 let by = By::callout();
1077 assert_eq!(by.kind, "callout");
1078 assert!(by.data.is_null());
1079 assert!(!by.is_atomic_kind());
1081 assert!(!by.is_programmatic_sentinel());
1082 }
1083
1084 #[test]
1085 fn test_by_is_programmatic_sentinel() {
1086 assert!(By::config_default().is_programmatic_sentinel());
1087 assert!(By::programmatic_config().is_programmatic_sentinel());
1088 assert!(By::unknown().is_programmatic_sentinel());
1089
1090 assert!(!By::user_edit().is_programmatic_sentinel());
1091 assert!(!By::filter("x.lua", 1).is_programmatic_sentinel());
1092 assert!(!By::shortcode("meta").is_programmatic_sentinel());
1093 assert!(!By::test_scaffold().is_programmatic_sentinel());
1094 assert!(!By::sectionize().is_programmatic_sentinel());
1095 }
1096
1097 #[test]
1098 fn test_source_info_for_test() {
1099 let si = SourceInfo::for_test();
1100 match si {
1101 SourceInfo::Generated { by, from } => {
1102 assert_eq!(by.kind, "test-scaffold");
1103 assert!(from.is_empty());
1104 }
1105 _ => panic!("for_test() must return Generated"),
1106 }
1107 }
1108
1109 #[test]
1110 fn test_by_is_kind() {
1111 let by = By::shortcode("meta");
1112 assert!(by.is_kind("shortcode"));
1113 assert!(!by.is_kind("filter"));
1114 }
1115
1116 #[test]
1117 fn test_by_as_filter_rejects_non_filter() {
1118 assert!(By::sectionize().as_filter().is_none());
1119 let by = By {
1121 kind: "filter".to_string(),
1122 data: serde_json::json!({ "filter_path": "x.lua" }),
1123 };
1124 assert!(by.as_filter().is_none());
1125 }
1126
1127 #[test]
1128 fn test_anchor_invocation_value_source_constructors() {
1129 let original = Arc::new(SourceInfo::original(FileId(1), 0, 5));
1130 let inv = Anchor::invocation(Arc::clone(&original));
1131 let vs = Anchor::value_source(Arc::clone(&original));
1132 assert!(matches!(inv.role, AnchorRole::Invocation));
1133 assert!(matches!(vs.role, AnchorRole::ValueSource));
1134 }
1135
1136 #[test]
1137 fn test_by_json_round_trip() {
1138 let by = By::shortcode("meta");
1139 let json = serde_json::to_string(&by).unwrap();
1140 let back: By = serde_json::from_str(&json).unwrap();
1141 assert_eq!(by, back);
1142 }
1143
1144 #[test]
1145 fn test_anchor_json_round_trip() {
1146 let anchor = Anchor::invocation(Arc::new(SourceInfo::original(FileId(2), 10, 20)));
1147 let json = serde_json::to_string(&anchor).unwrap();
1148 let back: Anchor = serde_json::from_str(&json).unwrap();
1149 assert_eq!(anchor, back);
1150 }
1151
1152 #[test]
1153 fn test_generated_json_round_trip_empty_from() {
1154 let info = SourceInfo::generated(By::sectionize());
1155 let json = serde_json::to_string(&info).unwrap();
1156 let back: SourceInfo = serde_json::from_str(&json).unwrap();
1157 assert_eq!(info, back);
1158 }
1159
1160 #[test]
1161 fn test_generated_json_round_trip_with_invocation_anchor() {
1162 let mut info = SourceInfo::generated(By::shortcode("meta"));
1163 info.append_anchor(
1164 AnchorRole::Invocation,
1165 Arc::new(SourceInfo::original(FileId(5), 100, 110)),
1166 );
1167 let json = serde_json::to_string(&info).unwrap();
1168 let back: SourceInfo = serde_json::from_str(&json).unwrap();
1169 assert_eq!(info, back);
1170 }
1171
1172 #[test]
1173 fn test_generated_json_round_trip_multi_anchor() {
1174 let mut info = SourceInfo::generated(By::shortcode("meta"));
1175 info.append_anchor(
1176 AnchorRole::Invocation,
1177 Arc::new(SourceInfo::original(FileId(5), 100, 110)),
1178 );
1179 info.append_anchor(
1180 AnchorRole::ValueSource,
1181 Arc::new(SourceInfo::original(FileId(7), 200, 220)),
1182 );
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_length_start_end_are_zero() {
1190 let info = SourceInfo::generated(By::sectionize());
1191 assert_eq!(info.length(), 0);
1192 assert_eq!(info.start_offset(), 0);
1193 assert_eq!(info.end_offset(), 0);
1194 }
1195
1196 #[test]
1197 fn test_generated_resolve_byte_range_recurses_through_substring() {
1198 let parent = SourceInfo::original(FileId(42), 100, 200);
1199 let sub = SourceInfo::substring(parent, 10, 20);
1200 let mut info = SourceInfo::generated(By::shortcode("meta"));
1201 info.append_anchor(AnchorRole::Invocation, Arc::new(sub));
1202 assert_eq!(info.resolve_byte_range(), Some((42, 110, 120)));
1203 }
1204
1205 #[test]
1206 fn test_generated_resolve_byte_range_empty_returns_none() {
1207 let info = SourceInfo::generated(By::sectionize());
1208 assert!(info.resolve_byte_range().is_none());
1209 }
1210
1211 #[test]
1212 fn test_generated_resolve_byte_range_value_source_only_returns_none() {
1213 let mut info = SourceInfo::generated(By::shortcode("meta"));
1214 info.append_anchor(
1215 AnchorRole::ValueSource,
1216 Arc::new(SourceInfo::original(FileId(5), 100, 110)),
1217 );
1218 assert!(info.resolve_byte_range().is_none());
1219 }
1220
1221 #[test]
1222 fn test_generated_remap_file_ids_walks_anchors() {
1223 let mut info = SourceInfo::generated(By::shortcode("meta"));
1224 info.append_anchor(
1225 AnchorRole::Invocation,
1226 Arc::new(SourceInfo::original(FileId(0), 0, 5)),
1227 );
1228 info.append_anchor(
1229 AnchorRole::ValueSource,
1230 Arc::new(SourceInfo::original(FileId(3), 10, 20)),
1231 );
1232 info.remap_file_ids(&|id| FileId(id.0 + 10));
1233 match &info {
1234 SourceInfo::Generated { from, .. } => {
1235 assert_eq!(from.len(), 2);
1236 match from[0].source_info.as_ref() {
1237 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(10)),
1238 _ => panic!("Expected Original anchor 0"),
1239 }
1240 match from[1].source_info.as_ref() {
1241 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(13)),
1242 _ => panic!("Expected Original anchor 1"),
1243 }
1244 }
1245 _ => panic!("Expected Generated"),
1246 }
1247 }
1248
1249 #[test]
1250 fn test_root_file_id_per_variant() {
1251 let original = SourceInfo::original(FileId(7), 0, 5);
1253 assert_eq!(original.root_file_id(), Some(FileId(7)));
1254
1255 let sub = SourceInfo::substring(original.clone(), 0, 5);
1257 assert_eq!(sub.root_file_id(), Some(FileId(7)));
1258
1259 let empty_gen = SourceInfo::generated(By::sectionize());
1261 let real = SourceInfo::original(FileId(42), 0, 5);
1262 let concat = SourceInfo::concat(vec![(empty_gen, 0), (real, 5)]);
1263 assert_eq!(concat.root_file_id(), Some(FileId(42)));
1264
1265 let mut g = SourceInfo::generated(By::shortcode("meta"));
1267 g.append_anchor(
1268 AnchorRole::Invocation,
1269 Arc::new(SourceInfo::original(FileId(9), 0, 1)),
1270 );
1271 assert_eq!(g.root_file_id(), Some(FileId(9)));
1272
1273 let mut g2 = SourceInfo::generated(By::shortcode("meta"));
1275 g2.append_anchor(
1276 AnchorRole::ValueSource,
1277 Arc::new(SourceInfo::original(FileId(9), 0, 1)),
1278 );
1279 assert_eq!(g2.root_file_id(), None);
1280
1281 let g3 = SourceInfo::generated(By::sectionize());
1283 assert_eq!(g3.root_file_id(), None);
1284 }
1285
1286 #[test]
1287 fn test_collect_file_ids_walks_every_anchor_role() {
1288 let mut info = SourceInfo::generated(By::shortcode("meta"));
1289 info.append_anchor(
1290 AnchorRole::Invocation,
1291 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1292 );
1293 info.append_anchor(
1294 AnchorRole::ValueSource,
1295 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1296 );
1297 info.append_anchor(
1298 AnchorRole::Other("dispatch".to_string()),
1299 Arc::new(SourceInfo::original(FileId(3), 0, 1)),
1300 );
1301 let mut out = std::collections::HashSet::new();
1302 info.collect_file_ids(&mut out);
1303 assert!(out.contains(&FileId(1)));
1304 assert!(out.contains(&FileId(2)));
1305 assert!(out.contains(&FileId(3)));
1306 assert_eq!(out.len(), 3);
1307 }
1308
1309 #[test]
1310 fn test_collect_file_ids_walks_concat_and_substring() {
1311 let inner = SourceInfo::original(FileId(5), 0, 100);
1312 let sub = SourceInfo::substring(inner, 10, 20);
1313 let other = SourceInfo::original(FileId(11), 0, 5);
1314 let concat = SourceInfo::concat(vec![(sub, 10), (other, 5)]);
1315 let mut out = std::collections::HashSet::new();
1316 concat.collect_file_ids(&mut out);
1317 assert!(out.contains(&FileId(5)));
1318 assert!(out.contains(&FileId(11)));
1319 assert_eq!(out.len(), 2);
1320 }
1321
1322 #[test]
1323 fn test_invocation_anchor_accessor() {
1324 let mut info = SourceInfo::generated(By::shortcode("meta"));
1325 assert!(info.invocation_anchor().is_none());
1326 info.append_anchor(
1327 AnchorRole::ValueSource,
1328 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1329 );
1330 assert!(info.invocation_anchor().is_none());
1331 info.append_anchor(
1332 AnchorRole::Invocation,
1333 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1334 );
1335 assert!(info.invocation_anchor().is_some());
1336 assert!(
1338 SourceInfo::original(FileId(0), 0, 0)
1339 .invocation_anchor()
1340 .is_none()
1341 );
1342 }
1343
1344 #[test]
1345 fn test_value_source_anchor_accessor() {
1346 let mut info = SourceInfo::generated(By::shortcode("meta"));
1347 assert!(info.value_source_anchor().is_none());
1348 info.append_anchor(
1349 AnchorRole::Invocation,
1350 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1351 );
1352 assert!(info.value_source_anchor().is_none());
1353 info.append_anchor(
1354 AnchorRole::ValueSource,
1355 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1356 );
1357 assert!(info.value_source_anchor().is_some());
1358 }
1359
1360 #[test]
1361 fn test_anchors_with_role() {
1362 let mut info = SourceInfo::generated(By::shortcode("meta"));
1363 info.append_anchor(
1364 AnchorRole::Invocation,
1365 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1366 );
1367 info.append_anchor(
1368 AnchorRole::ValueSource,
1369 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1370 );
1371 info.append_anchor(
1372 AnchorRole::Other("ext/foo".to_string()),
1373 Arc::new(SourceInfo::original(FileId(3), 0, 1)),
1374 );
1375 assert_eq!(info.anchors_with_role(&AnchorRole::Invocation).count(), 1);
1376 assert_eq!(info.anchors_with_role(&AnchorRole::ValueSource).count(), 1);
1377 assert_eq!(
1378 info.anchors_with_role(&AnchorRole::Other("ext/foo".to_string()))
1379 .count(),
1380 1
1381 );
1382 assert_eq!(
1383 info.anchors_with_role(&AnchorRole::Other("missing".to_string()))
1384 .count(),
1385 0
1386 );
1387 }
1388
1389 #[test]
1390 fn test_append_anchor_preserves_order() {
1391 let mut info = SourceInfo::generated(By::shortcode("meta"));
1392 info.append_anchor(
1393 AnchorRole::Invocation,
1394 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1395 );
1396 info.append_anchor(
1397 AnchorRole::ValueSource,
1398 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1399 );
1400 match info {
1401 SourceInfo::Generated { from, .. } => {
1402 assert_eq!(from.len(), 2);
1403 assert!(matches!(from[0].role, AnchorRole::Invocation));
1404 assert!(matches!(from[1].role, AnchorRole::ValueSource));
1405 }
1406 _ => panic!("Expected Generated"),
1407 }
1408 }
1409
1410 #[test]
1411 fn test_combine_with_generated_is_zero_length_piece() {
1412 let original = SourceInfo::original(FileId(0), 10, 20);
1413 let generated = SourceInfo::generated(By::sectionize());
1414 let combined = original.combine(&generated);
1415 match &combined {
1416 SourceInfo::Concat { pieces } => {
1417 assert_eq!(pieces.len(), 2);
1418 assert_eq!(pieces[1].length, 0);
1419 }
1420 _ => panic!("Expected Concat"),
1421 }
1422 assert_eq!(combined.length(), 10);
1424 }
1425
1426 #[test]
1427 fn test_source_info_serialization() {
1428 let file_id = FileId(0);
1429 let range = Range {
1430 start: Location {
1431 offset: 0,
1432 row: 0,
1433 column: 0,
1434 },
1435 end: Location {
1436 offset: 10,
1437 row: 0,
1438 column: 10,
1439 },
1440 };
1441
1442 let info = SourceInfo::from_range(file_id, range);
1443 let json = serde_json::to_string(&info).unwrap();
1444 let deserialized: SourceInfo = serde_json::from_str(&json).unwrap();
1445
1446 assert_eq!(info, deserialized);
1447 }
1448
1449 #[test]
1450 fn test_substring_source_info() {
1451 let file_id = FileId(0);
1452 let parent_range = Range {
1453 start: Location {
1454 offset: 0,
1455 row: 0,
1456 column: 0,
1457 },
1458 end: Location {
1459 offset: 100,
1460 row: 0,
1461 column: 100,
1462 },
1463 };
1464 let parent = SourceInfo::from_range(file_id, parent_range);
1465
1466 let substring = SourceInfo::substring(parent, 10, 20);
1467
1468 assert_eq!(substring.start_offset(), 10);
1469 assert_eq!(substring.end_offset(), 20);
1470 assert_eq!(substring.length(), 10);
1471
1472 match substring {
1473 SourceInfo::Substring {
1474 start_offset,
1475 end_offset,
1476 ..
1477 } => {
1478 assert_eq!(start_offset, 10);
1479 assert_eq!(end_offset, 20);
1480 }
1481 _ => panic!("Expected Substring mapping"),
1482 }
1483 }
1484
1485 #[test]
1486 fn test_concat_source_info() {
1487 let file_id1 = FileId(0);
1488 let file_id2 = FileId(1);
1489
1490 let info1 = SourceInfo::from_range(
1491 file_id1,
1492 Range {
1493 start: Location {
1494 offset: 0,
1495 row: 0,
1496 column: 0,
1497 },
1498 end: Location {
1499 offset: 10,
1500 row: 0,
1501 column: 10,
1502 },
1503 },
1504 );
1505
1506 let info2 = SourceInfo::from_range(
1507 file_id2,
1508 Range {
1509 start: Location {
1510 offset: 0,
1511 row: 0,
1512 column: 0,
1513 },
1514 end: Location {
1515 offset: 15,
1516 row: 0,
1517 column: 15,
1518 },
1519 },
1520 );
1521
1522 let concat = SourceInfo::concat(vec![(info1, 10), (info2, 15)]);
1523
1524 assert_eq!(concat.start_offset(), 0);
1525 assert_eq!(concat.end_offset(), 25); assert_eq!(concat.length(), 25);
1527
1528 match concat {
1529 SourceInfo::Concat { pieces } => {
1530 assert_eq!(pieces.len(), 2);
1531 assert_eq!(pieces[0].offset_in_concat, 0);
1532 assert_eq!(pieces[0].length, 10);
1533 assert_eq!(pieces[1].offset_in_concat, 10);
1534 assert_eq!(pieces[1].length, 15);
1535 }
1536 _ => panic!("Expected Concat mapping"),
1537 }
1538 }
1539
1540 #[test]
1541 fn test_combine_two_sources() {
1542 let file_id = FileId(0);
1543
1544 let info1 = SourceInfo::from_range(
1546 file_id,
1547 Range {
1548 start: Location {
1549 offset: 0,
1550 row: 0,
1551 column: 0,
1552 },
1553 end: Location {
1554 offset: 10,
1555 row: 0,
1556 column: 10,
1557 },
1558 },
1559 );
1560
1561 let info2 = SourceInfo::from_range(
1562 file_id,
1563 Range {
1564 start: Location {
1565 offset: 15,
1566 row: 0,
1567 column: 15,
1568 },
1569 end: Location {
1570 offset: 25,
1571 row: 0,
1572 column: 25,
1573 },
1574 },
1575 );
1576
1577 let combined = info1.combine(&info2);
1579
1580 assert_eq!(combined.start_offset(), 0);
1582 assert_eq!(combined.end_offset(), 20);
1583 assert_eq!(combined.length(), 20);
1584
1585 match combined {
1586 SourceInfo::Concat { pieces } => {
1587 assert_eq!(pieces.len(), 2);
1588 assert_eq!(pieces[0].length, 10);
1589 assert_eq!(pieces[0].offset_in_concat, 0);
1590 assert_eq!(pieces[1].length, 10);
1591 assert_eq!(pieces[1].offset_in_concat, 10);
1592 }
1593 _ => panic!("Expected Concat mapping"),
1594 }
1595 }
1596
1597 #[test]
1598 fn test_combine_preserves_source_tracking() {
1599 let file_id1 = FileId(5);
1601 let file_id2 = FileId(10);
1602
1603 let info1 = SourceInfo::from_range(
1604 file_id1,
1605 Range {
1606 start: Location {
1607 offset: 100,
1608 row: 5,
1609 column: 0,
1610 },
1611 end: Location {
1612 offset: 105,
1613 row: 5,
1614 column: 5,
1615 },
1616 },
1617 );
1618
1619 let info2 = SourceInfo::from_range(
1620 file_id2,
1621 Range {
1622 start: Location {
1623 offset: 200,
1624 row: 10,
1625 column: 0,
1626 },
1627 end: Location {
1628 offset: 207,
1629 row: 10,
1630 column: 7,
1631 },
1632 },
1633 );
1634
1635 let combined = info1.combine(&info2);
1636
1637 match combined {
1639 SourceInfo::Concat { pieces } => {
1640 assert_eq!(pieces.len(), 2);
1641
1642 match &pieces[0].source_info {
1644 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, file_id1),
1645 _ => panic!("Expected Original mapping for first piece"),
1646 }
1647
1648 match &pieces[1].source_info {
1650 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, file_id2),
1651 _ => panic!("Expected Original mapping for second piece"),
1652 }
1653 }
1654 _ => panic!("Expected Concat mapping"),
1655 }
1656 }
1657
1658 #[test]
1660 fn test_json_serialization_original() {
1661 let file_id = FileId(0);
1662 let range = Range {
1663 start: Location {
1664 offset: 10,
1665 row: 1,
1666 column: 5,
1667 },
1668 end: Location {
1669 offset: 50,
1670 row: 3,
1671 column: 10,
1672 },
1673 };
1674
1675 let info = SourceInfo::from_range(file_id, range);
1676 let json = serde_json::to_value(&info).unwrap();
1677
1678 assert_eq!(json["Original"]["file_id"], 0);
1680 assert_eq!(json["Original"]["start_offset"], 10);
1681 assert_eq!(json["Original"]["end_offset"], 50);
1682
1683 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1685 assert_eq!(info, deserialized);
1686 }
1687
1688 #[test]
1690 fn test_json_serialization_substring() {
1691 let file_id = FileId(0);
1692 let parent_range = Range {
1693 start: Location {
1694 offset: 0,
1695 row: 0,
1696 column: 0,
1697 },
1698 end: Location {
1699 offset: 100,
1700 row: 5,
1701 column: 20,
1702 },
1703 };
1704 let parent = SourceInfo::from_range(file_id, parent_range);
1705
1706 let substring = SourceInfo::substring(parent, 10, 30);
1707 let json = serde_json::to_value(&substring).unwrap();
1708
1709 assert_eq!(json["Substring"]["start_offset"], 10);
1711 assert_eq!(json["Substring"]["end_offset"], 30);
1712
1713 assert!(json["Substring"]["parent"].is_object());
1715 assert_eq!(json["Substring"]["parent"]["Original"]["file_id"], 0);
1716
1717 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1719 assert_eq!(substring, deserialized);
1720 }
1721
1722 #[test]
1724 fn test_json_serialization_nested_substring() {
1725 let file_id = FileId(0);
1726
1727 let file_range = Range {
1729 start: Location {
1730 offset: 0,
1731 row: 0,
1732 column: 0,
1733 },
1734 end: Location {
1735 offset: 200,
1736 row: 10,
1737 column: 0,
1738 },
1739 };
1740 let file_info = SourceInfo::from_range(file_id, file_range);
1741
1742 let yaml_info = SourceInfo::substring(file_info, 4, 150);
1744
1745 let value_info = SourceInfo::substring(yaml_info, 20, 35);
1747
1748 let json = serde_json::to_value(&value_info).unwrap();
1749
1750 assert_eq!(json["Substring"]["start_offset"], 20);
1752 assert_eq!(json["Substring"]["end_offset"], 35);
1753 assert_eq!(json["Substring"]["parent"]["Substring"]["start_offset"], 4);
1754 assert_eq!(
1755 json["Substring"]["parent"]["Substring"]["parent"]["Original"]["file_id"],
1756 0
1757 );
1758
1759 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1761 assert_eq!(value_info, deserialized);
1762 }
1763
1764 #[test]
1766 fn test_json_serialization_concat() {
1767 let file_id1 = FileId(0);
1768 let file_id2 = FileId(1);
1769
1770 let info1 = SourceInfo::from_range(
1771 file_id1,
1772 Range {
1773 start: Location {
1774 offset: 0,
1775 row: 0,
1776 column: 0,
1777 },
1778 end: Location {
1779 offset: 10,
1780 row: 0,
1781 column: 10,
1782 },
1783 },
1784 );
1785
1786 let info2 = SourceInfo::from_range(
1787 file_id2,
1788 Range {
1789 start: Location {
1790 offset: 20,
1791 row: 2,
1792 column: 0,
1793 },
1794 end: Location {
1795 offset: 30,
1796 row: 2,
1797 column: 10,
1798 },
1799 },
1800 );
1801
1802 let combined = info1.combine(&info2);
1803 let json = serde_json::to_value(&combined).unwrap();
1804
1805 assert!(json["Concat"]["pieces"].is_array());
1807 let pieces = json["Concat"]["pieces"].as_array().unwrap();
1808 assert_eq!(pieces.len(), 2);
1809
1810 assert_eq!(pieces[0]["offset_in_concat"], 0);
1812 assert_eq!(pieces[0]["length"], 10);
1813 assert_eq!(pieces[0]["source_info"]["Original"]["file_id"], 0);
1814
1815 assert_eq!(pieces[1]["offset_in_concat"], 10);
1817 assert_eq!(pieces[1]["length"], 10);
1818 assert_eq!(pieces[1]["source_info"]["Original"]["file_id"], 1);
1819
1820 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1822 assert_eq!(combined, deserialized);
1823 }
1824
1825 #[test]
1827 fn test_json_serialization_complex_nested() {
1828 let file_id = FileId(0);
1829
1830 let qmd_file = SourceInfo::from_range(
1832 file_id,
1833 Range {
1834 start: Location {
1835 offset: 0,
1836 row: 0,
1837 column: 0,
1838 },
1839 end: Location {
1840 offset: 500,
1841 row: 20,
1842 column: 0,
1843 },
1844 },
1845 );
1846
1847 let yaml_frontmatter = SourceInfo::substring(qmd_file.clone(), 4, 200);
1849
1850 let yaml_key = SourceInfo::substring(yaml_frontmatter.clone(), 10, 20);
1852
1853 let yaml_value = SourceInfo::substring(yaml_frontmatter, 25, 50);
1855
1856 let combined = yaml_key.combine(&yaml_value);
1858
1859 let json = serde_json::to_value(&combined).unwrap();
1860
1861 assert!(json.is_object());
1863 assert!(json["Concat"].is_object());
1864
1865 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1867 assert_eq!(combined, deserialized);
1868 }
1869
1870 #[test]
1875 fn test_preimage_in_original_same_file() {
1876 let info = SourceInfo::original(FileId(0), 10, 25);
1877 assert_eq!(info.preimage_in(FileId(0)), Some(10..25));
1878 }
1879
1880 #[test]
1881 fn test_preimage_in_original_different_file_returns_none() {
1882 let info = SourceInfo::original(FileId(0), 10, 25);
1883 assert_eq!(info.preimage_in(FileId(1)), None);
1884 }
1885
1886 #[test]
1887 fn test_preimage_in_substring_composes_offsets() {
1888 let parent = SourceInfo::original(FileId(0), 100, 200);
1892 let info = SourceInfo::substring(parent, 5, 15);
1893 assert_eq!(info.preimage_in(FileId(0)), Some(105..115));
1894 }
1895
1896 #[test]
1897 fn test_preimage_in_substring_different_file_returns_none() {
1898 let parent = SourceInfo::original(FileId(0), 100, 200);
1899 let info = SourceInfo::substring(parent, 5, 15);
1900 assert_eq!(info.preimage_in(FileId(7)), None);
1901 }
1902
1903 #[test]
1904 fn test_preimage_in_substring_chain() {
1905 let root = SourceInfo::original(FileId(0), 1000, 2000);
1908 let mid = SourceInfo::substring(root, 100, 500);
1909 let leaf = SourceInfo::substring(mid, 10, 50);
1910 assert_eq!(leaf.preimage_in(FileId(0)), Some(1110..1150));
1911 }
1912
1913 #[test]
1914 fn test_preimage_in_concat_contiguous() {
1915 let a = SourceInfo::original(FileId(0), 10, 15);
1917 let b = SourceInfo::original(FileId(0), 15, 25);
1918 let info = SourceInfo::concat(vec![(a, 5), (b, 10)]);
1919 assert_eq!(info.preimage_in(FileId(0)), Some(10..25));
1920 }
1921
1922 #[test]
1923 fn test_preimage_in_concat_gappy_returns_none() {
1924 let a = SourceInfo::original(FileId(0), 10, 15);
1926 let b = SourceInfo::original(FileId(0), 20, 25);
1927 let info = SourceInfo::concat(vec![(a, 5), (b, 5)]);
1928 assert_eq!(info.preimage_in(FileId(0)), None);
1929 }
1930
1931 #[test]
1932 fn test_preimage_in_concat_overlapping_returns_none() {
1933 let a = SourceInfo::original(FileId(0), 10, 20);
1935 let b = SourceInfo::original(FileId(0), 15, 25);
1936 let info = SourceInfo::concat(vec![(a, 10), (b, 10)]);
1937 assert_eq!(info.preimage_in(FileId(0)), None);
1938 }
1939
1940 #[test]
1941 fn test_preimage_in_concat_mixed_files_returns_none() {
1942 let a = SourceInfo::original(FileId(0), 10, 15);
1945 let b = SourceInfo::original(FileId(1), 15, 25);
1946 let info = SourceInfo::concat(vec![(a, 5), (b, 10)]);
1947 assert_eq!(info.preimage_in(FileId(0)), None);
1948 }
1949
1950 #[test]
1951 fn test_preimage_in_generated_no_anchors_returns_none() {
1952 let info = SourceInfo::generated(By::sectionize());
1955 assert_eq!(info.preimage_in(FileId(0)), None);
1956 }
1957
1958 #[test]
1959 fn test_preimage_in_generated_with_invocation_in_target() {
1960 let token = SourceInfo::original(FileId(0), 50, 70);
1963 let mut info = SourceInfo::generated(By::shortcode("meta"));
1964 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
1965 assert_eq!(info.preimage_in(FileId(0)), Some(50..70));
1966 }
1967
1968 #[test]
1969 fn test_preimage_in_generated_with_invocation_outside_target() {
1970 let token = SourceInfo::original(FileId(0), 50, 70);
1972 let mut info = SourceInfo::generated(By::shortcode("meta"));
1973 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
1974 assert_eq!(info.preimage_in(FileId(1)), None);
1975 }
1976
1977 #[test]
1978 fn test_preimage_in_generated_walks_through_substring_in_invocation() {
1979 let root = SourceInfo::original(FileId(0), 100, 200);
1982 let token = SourceInfo::substring(root, 10, 30);
1983 let mut info = SourceInfo::generated(By::shortcode("meta"));
1984 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
1985 assert_eq!(info.preimage_in(FileId(0)), Some(110..130));
1986 }
1987
1988 #[test]
1993 fn test_preimage_in_generated_value_source_only_returns_none() {
1994 let meta_si = SourceInfo::original(FileId(0), 10, 25);
1998 let mut info = SourceInfo::generated(By::appendix());
1999 info.append_anchor(AnchorRole::ValueSource, Arc::new(meta_si));
2000 assert_eq!(info.preimage_in(FileId(0)), None);
2001 }
2002
2003 #[test]
2004 fn test_preimage_in_generated_other_only_returns_none() {
2005 let lua_si = SourceInfo::original(FileId(0), 10, 25);
2007 let mut info = SourceInfo::generated(By::filter("upper.lua", 14));
2008 info.append_anchor(
2009 AnchorRole::Other("ext/my-ext/dispatch".to_string()),
2010 Arc::new(lua_si),
2011 );
2012 assert_eq!(info.preimage_in(FileId(0)), None);
2013 }
2014
2015 #[test]
2016 fn test_preimage_in_generated_invocation_plus_value_source_walks_invocation_only() {
2017 let token = SourceInfo::original(FileId(0), 50, 70);
2023 let value = SourceInfo::original(FileId(1), 200, 215);
2024 let mut info = SourceInfo::generated(By::shortcode("meta"));
2025 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
2026 info.append_anchor(AnchorRole::ValueSource, Arc::new(value));
2027
2028 assert_eq!(info.preimage_in(FileId(0)), Some(50..70));
2029 assert_eq!(info.preimage_in(FileId(1)), None);
2030 }
2031}