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(Box<Generated>),
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub struct Generated {
72 pub by: By,
73 #[serde(default, skip_serializing_if = "SmallVec::is_empty")]
74 pub from: SmallVec<[Anchor; 2]>,
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub struct By {
88 pub kind: String,
92
93 #[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
97 pub data: serde_json::Value,
98}
99
100#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub enum AnchorRole {
107 Invocation,
113
114 ValueSource,
119
120 Other(String),
132}
133
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137pub struct Anchor {
138 pub role: AnchorRole,
139 pub source_info: Arc<SourceInfo>,
140}
141
142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144pub struct SourcePiece {
145 pub source_info: SourceInfo,
147 pub offset_in_concat: usize,
149 pub length: usize,
151}
152
153impl Default for SourceInfo {
154 fn default() -> Self {
155 SourceInfo::Original {
156 file_id: FileId(0),
157 start_offset: 0,
158 end_offset: 0,
159 }
160 }
161}
162
163impl SourceInfo {
164 #[deprecated(
173 since = "0.1.0",
174 note = "Use SourceInfo::for_test() in tests, or the appropriate Generated{by: <kind>} in production. See provenance-contract.md."
175 )]
176 #[doc(hidden)]
177 #[allow(clippy::should_implement_trait)]
182 pub fn default() -> Self {
183 <Self as Default>::default()
184 }
185
186 pub fn original(file_id: FileId, start_offset: usize, end_offset: usize) -> Self {
188 SourceInfo::Original {
189 file_id,
190 start_offset,
191 end_offset,
192 }
193 }
194
195 pub fn from_range(file_id: FileId, range: Range) -> Self {
200 SourceInfo::Original {
201 file_id,
202 start_offset: range.start.offset,
203 end_offset: range.end.offset,
204 }
205 }
206
207 pub fn substring(parent: SourceInfo, start: usize, end: usize) -> Self {
209 SourceInfo::Substring {
210 parent: Arc::new(parent),
211 start_offset: start,
212 end_offset: end,
213 }
214 }
215
216 pub fn concat(pieces: Vec<(SourceInfo, usize)>) -> Self {
218 let source_pieces: Vec<SourcePiece> = pieces
219 .into_iter()
220 .map(|(source_info, length)| SourcePiece {
221 source_info,
222 offset_in_concat: 0, length,
224 })
225 .collect();
226
227 let mut cumulative_offset = 0;
229 let pieces_with_offsets: Vec<SourcePiece> = source_pieces
230 .into_iter()
231 .map(|mut piece| {
232 piece.offset_in_concat = cumulative_offset;
233 cumulative_offset += piece.length;
234 piece
235 })
236 .collect();
237
238 SourceInfo::Concat {
239 pieces: pieces_with_offsets,
240 }
241 }
242
243 pub fn generated(by: By) -> Self {
248 Self::generated_with(by, SmallVec::new())
249 }
250
251 pub fn generated_with(by: By, from: impl Into<SmallVec<[Anchor; 2]>>) -> Self {
256 SourceInfo::Generated(Box::new(Generated {
257 by,
258 from: from.into(),
259 }))
260 }
261
262 pub fn as_generated(&self) -> Option<&Generated> {
264 match self {
265 SourceInfo::Generated(g) => Some(g),
266 _ => None,
267 }
268 }
269
270 pub fn as_generated_mut(&mut self) -> Option<&mut Generated> {
273 match self {
274 SourceInfo::Generated(g) => Some(g),
275 _ => None,
276 }
277 }
278
279 pub fn for_test() -> Self {
285 Self::generated(By::test_scaffold())
286 }
287
288 pub fn invocation_anchor(&self) -> Option<&Arc<SourceInfo>> {
294 match self {
295 SourceInfo::Generated(g) => g
296 .from
297 .iter()
298 .find(|a| matches!(a.role, AnchorRole::Invocation))
299 .map(|a| &a.source_info),
300 _ => None,
301 }
302 }
303
304 pub fn value_source_anchor(&self) -> Option<&Arc<SourceInfo>> {
310 match self {
311 SourceInfo::Generated(g) => g
312 .from
313 .iter()
314 .find(|a| matches!(a.role, AnchorRole::ValueSource))
315 .map(|a| &a.source_info),
316 _ => None,
317 }
318 }
319
320 pub fn anchors_with_role<'a>(
326 &'a self,
327 role: &'a AnchorRole,
328 ) -> Box<dyn Iterator<Item = &'a Arc<SourceInfo>> + 'a> {
329 match self {
330 SourceInfo::Generated(g) => Box::new(
331 g.from
332 .iter()
333 .filter(move |a| &a.role == role)
334 .map(|a| &a.source_info),
335 ),
336 _ => Box::new(std::iter::empty()),
337 }
338 }
339
340 pub fn append_anchor(&mut self, role: AnchorRole, source_info: Arc<SourceInfo>) {
348 match self {
349 SourceInfo::Generated(g) => g.from.push(Anchor { role, source_info }),
350 _ => panic!("append_anchor called on non-Generated SourceInfo"),
351 }
352 }
353
354 pub fn combine(&self, other: &SourceInfo) -> Self {
359 let self_length = self.length();
360 let other_length = other.length();
361
362 SourceInfo::concat(vec![
363 (self.clone(), self_length),
364 (other.clone(), other_length),
365 ])
366 }
367
368 pub fn length(&self) -> usize {
370 match self {
371 SourceInfo::Original {
372 start_offset,
373 end_offset,
374 ..
375 } => end_offset - start_offset,
376 SourceInfo::Substring {
377 start_offset,
378 end_offset,
379 ..
380 } => end_offset - start_offset,
381 SourceInfo::Concat { pieces } => pieces.iter().map(|p| p.length).sum(),
382 SourceInfo::Generated(..) => 0,
383 }
384 }
385
386 pub fn start_offset(&self) -> usize {
392 match self {
393 SourceInfo::Original { start_offset, .. } => *start_offset,
394 SourceInfo::Substring { start_offset, .. } => *start_offset,
395 SourceInfo::Concat { .. } => 0,
396 SourceInfo::Generated(..) => 0,
397 }
398 }
399
400 pub fn end_offset(&self) -> usize {
406 match self {
407 SourceInfo::Original { end_offset, .. } => *end_offset,
408 SourceInfo::Substring { end_offset, .. } => *end_offset,
409 SourceInfo::Concat { .. } => self.length(),
410 SourceInfo::Generated(..) => 0,
411 }
412 }
413
414 pub fn resolve_byte_range(&self) -> Option<(usize, usize, usize)> {
425 match self {
426 SourceInfo::Original {
427 file_id,
428 start_offset,
429 end_offset,
430 } => Some((file_id.0, *start_offset, *end_offset)),
431 SourceInfo::Substring {
432 parent,
433 start_offset,
434 end_offset,
435 } => {
436 let (fid, parent_start, _) = parent.resolve_byte_range()?;
437 Some((fid, parent_start + start_offset, parent_start + end_offset))
438 }
439 SourceInfo::Concat { .. } => None,
440 SourceInfo::Generated(..) => self
441 .invocation_anchor()
442 .and_then(|si| si.resolve_byte_range()),
443 }
444 }
445
446 pub fn preimage_in(&self, target: FileId) -> Option<std::ops::Range<usize>> {
493 match self {
494 SourceInfo::Original {
495 file_id,
496 start_offset,
497 end_offset,
498 } if *file_id == target => Some(*start_offset..*end_offset),
499 SourceInfo::Original { .. } => None,
500 SourceInfo::Substring { parent, .. }
501 if matches!(**parent, SourceInfo::Concat { .. }) =>
502 {
503 None
511 }
512 SourceInfo::Substring {
513 parent,
514 start_offset,
515 end_offset,
516 } => {
517 let parent_range = parent.preimage_in(target)?;
518 Some(parent_range.start + start_offset..parent_range.start + end_offset)
519 }
520 SourceInfo::Concat { pieces } => {
521 let ranges: Vec<std::ops::Range<usize>> = pieces
522 .iter()
523 .map(|p| p.source_info.preimage_in(target))
524 .collect::<Option<Vec<_>>>()?;
525 if ranges.is_empty() {
526 return None;
527 }
528 if ranges.windows(2).all(|w| w[0].end == w[1].start) {
529 let first = ranges.first().unwrap().start;
530 let last = ranges.last().unwrap().end;
531 Some(first..last)
532 } else {
533 None
534 }
535 }
536 SourceInfo::Generated(..) => self
537 .invocation_anchor()
538 .and_then(|si| si.preimage_in(target)),
539 }
540 }
541
542 pub fn remap_file_ids<F>(&mut self, map: &F)
550 where
551 F: Fn(FileId) -> FileId,
552 {
553 match self {
554 SourceInfo::Original { file_id, .. } => {
555 *file_id = map(*file_id);
556 }
557 SourceInfo::Substring { parent, .. } => {
558 let parent = Arc::make_mut(parent);
560 parent.remap_file_ids(map);
561 }
562 SourceInfo::Concat { pieces } => {
563 for piece in pieces {
564 piece.source_info.remap_file_ids(map);
565 }
566 }
567 SourceInfo::Generated(g) => {
568 for anchor in g.from.iter_mut() {
569 let inner = Arc::make_mut(&mut anchor.source_info);
571 inner.remap_file_ids(map);
572 }
573 }
574 }
575 }
576
577 pub fn root_file_id(&self) -> Option<FileId> {
586 match self {
587 SourceInfo::Original { file_id, .. } => Some(*file_id),
588 SourceInfo::Substring { parent, .. } => parent.root_file_id(),
589 SourceInfo::Concat { pieces } => {
590 pieces.iter().find_map(|p| p.source_info.root_file_id())
591 }
592 SourceInfo::Generated(..) => self.invocation_anchor().and_then(|si| si.root_file_id()),
593 }
594 }
595
596 pub fn collect_file_ids(&self, out: &mut std::collections::HashSet<FileId>) {
602 match self {
603 SourceInfo::Original { file_id, .. } => {
604 out.insert(*file_id);
605 }
606 SourceInfo::Substring { parent, .. } => parent.collect_file_ids(out),
607 SourceInfo::Concat { pieces } => {
608 for piece in pieces {
609 piece.source_info.collect_file_ids(out);
610 }
611 }
612 SourceInfo::Generated(g) => {
613 for anchor in &g.from {
614 anchor.source_info.collect_file_ids(out);
615 }
616 }
617 }
618 }
619}
620
621impl By {
622 pub fn filter(filter_path: impl Into<String>, line: usize) -> Self {
632 Self {
633 kind: "filter".to_string(),
634 data: serde_json::json!({
635 "filter_path": filter_path.into(),
636 "line": line,
637 }),
638 }
639 }
640
641 pub fn sectionize() -> Self {
644 Self {
645 kind: "sectionize".to_string(),
646 data: serde_json::Value::Null,
647 }
648 }
649
650 pub fn user_edit() -> Self {
653 Self {
654 kind: "user-edit".to_string(),
655 data: serde_json::Value::Null,
656 }
657 }
658
659 pub fn shortcode(name: impl Into<String>) -> Self {
668 Self {
669 kind: "shortcode".to_string(),
670 data: serde_json::json!({ "name": name.into() }),
671 }
672 }
673
674 pub fn include() -> Self {
679 Self {
680 kind: "include".to_string(),
681 data: serde_json::Value::Null,
682 }
683 }
684
685 pub fn title_block() -> Self {
687 Self {
688 kind: "title-block".to_string(),
689 data: serde_json::Value::Null,
690 }
691 }
692
693 pub fn footnotes() -> Self {
695 Self {
696 kind: "footnotes".to_string(),
697 data: serde_json::Value::Null,
698 }
699 }
700
701 pub fn revealjs() -> Self {
707 Self {
708 kind: "revealjs".to_string(),
709 data: serde_json::Value::Null,
710 }
711 }
712
713 pub fn appendix() -> Self {
715 Self {
716 kind: "appendix".to_string(),
717 data: serde_json::Value::Null,
718 }
719 }
720
721 pub fn tree_sitter_postprocess() -> Self {
724 Self {
725 kind: "tree-sitter-postprocess".to_string(),
726 data: serde_json::Value::Null,
727 }
728 }
729
730 pub fn unknown() -> Self {
740 Self {
741 kind: "unknown".to_string(),
742 data: serde_json::Value::Null,
743 }
744 }
745
746 pub fn test_scaffold() -> Self {
751 Self {
752 kind: "test-scaffold".to_string(),
753 data: serde_json::Value::Null,
754 }
755 }
756
757 pub fn citeproc() -> Self {
766 Self {
767 kind: "citeproc".to_string(),
768 data: serde_json::Value::Null,
769 }
770 }
771
772 pub fn jupyter_output() -> Self {
780 Self {
781 kind: "jupyter-output".to_string(),
782 data: serde_json::Value::Null,
783 }
784 }
785
786 pub fn callout() -> Self {
797 Self {
798 kind: "callout".to_string(),
799 data: serde_json::Value::Null,
800 }
801 }
802
803 pub fn config_default() -> Self {
807 Self {
808 kind: "config-default".to_string(),
809 data: serde_json::Value::Null,
810 }
811 }
812
813 pub fn programmatic_config() -> Self {
818 Self {
819 kind: "programmatic-config".to_string(),
820 data: serde_json::Value::Null,
821 }
822 }
823
824 pub fn is_programmatic_sentinel(&self) -> bool {
829 matches!(
830 self.kind.as_str(),
831 "config-default" | "programmatic-config" | "unknown"
832 )
833 }
834
835 pub fn raw(kind: impl Into<String>, data: serde_json::Value) -> Self {
843 Self {
844 kind: kind.into(),
845 data,
846 }
847 }
848
849 pub fn is_atomic_kind(&self) -> bool {
861 matches!(
862 self.kind.as_str(),
863 "filter"
864 | "shortcode"
865 | "title-block"
866 | "tree-sitter-postprocess"
867 | "citeproc"
868 | "jupyter-output"
869 )
870 }
871
872 pub fn is_kind(&self, kind: &str) -> bool {
874 self.kind == kind
875 }
876
877 pub fn as_filter(&self) -> Option<(&str, usize)> {
883 if self.kind != "filter" {
884 return None;
885 }
886 let path = self.data.get("filter_path")?.as_str()?;
887 let line = self.data.get("line")?.as_u64()? as usize;
888 Some((path, line))
889 }
890}
891
892impl Anchor {
893 pub fn invocation(source_info: Arc<SourceInfo>) -> Self {
895 Self {
896 role: AnchorRole::Invocation,
897 source_info,
898 }
899 }
900
901 pub fn value_source(source_info: Arc<SourceInfo>) -> Self {
903 Self {
904 role: AnchorRole::ValueSource,
905 source_info,
906 }
907 }
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913 use crate::types::{FileId, Location, Range};
914
915 #[test]
916 fn test_original_source_info() {
917 let file_id = FileId(0);
918 let range = Range {
919 start: Location {
920 offset: 0,
921 row: 0,
922 column: 0,
923 },
924 end: Location {
925 offset: 10,
926 row: 0,
927 column: 10,
928 },
929 };
930
931 let info = SourceInfo::from_range(file_id, range.clone());
932
933 assert_eq!(info.start_offset(), 0);
934 assert_eq!(info.end_offset(), 10);
935 assert_eq!(info.length(), 10);
936 match info {
937 SourceInfo::Original {
938 file_id: mapped_id, ..
939 } => {
940 assert_eq!(mapped_id, file_id);
941 }
942 _ => panic!("Expected Original mapping"),
943 }
944 }
945
946 #[test]
947 fn test_remap_file_ids_original() {
948 let mut info = SourceInfo::original(FileId(0), 0, 10);
949 info.remap_file_ids(&|id| FileId(id.0 + 1));
950 match info {
951 SourceInfo::Original { file_id, .. } => assert_eq!(file_id, FileId(1)),
952 _ => panic!("Expected Original"),
953 }
954 }
955
956 #[test]
957 fn test_remap_file_ids_substring() {
958 let parent = SourceInfo::original(FileId(0), 0, 100);
959 let mut info = SourceInfo::substring(parent, 5, 20);
960 info.remap_file_ids(&|id| FileId(id.0 + 7));
961 match info {
962 SourceInfo::Substring { parent, .. } => match &*parent {
963 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(7)),
964 _ => panic!("Expected Original parent"),
965 },
966 _ => panic!("Expected Substring"),
967 }
968 }
969
970 #[test]
971 fn test_remap_file_ids_concat() {
972 let a = SourceInfo::original(FileId(0), 0, 5);
973 let b = SourceInfo::original(FileId(3), 5, 10);
974 let mut info = SourceInfo::concat(vec![(a, 5), (b, 5)]);
975 info.remap_file_ids(&|id| FileId(id.0 + 10));
976 match info {
977 SourceInfo::Concat { pieces } => {
978 match &pieces[0].source_info {
979 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(10)),
980 _ => panic!("Expected Original"),
981 }
982 match &pieces[1].source_info {
983 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(13)),
984 _ => panic!("Expected Original"),
985 }
986 }
987 _ => panic!("Expected Concat"),
988 }
989 }
990
991 #[test]
992 fn test_remap_file_ids_generated_empty_from_is_noop() {
993 let mut info = SourceInfo::generated(By::filter("foo.lua", 42));
994 info.remap_file_ids(&|_| FileId(99));
995 match info {
996 SourceInfo::Generated(g) => {
997 let Generated { by, from } = *g;
998 assert!(from.is_empty());
999 let (path, line) = by.as_filter().unwrap();
1000 assert_eq!(path, "foo.lua");
1001 assert_eq!(line, 42);
1002 }
1003 _ => panic!("Expected Generated"),
1004 }
1005 }
1006
1007 #[test]
1012 fn test_by_filter_builder() {
1013 let by = By::filter("a.lua", 7);
1014 assert_eq!(by.kind, "filter");
1015 assert_eq!(by.as_filter(), Some(("a.lua", 7)));
1016 }
1017
1018 #[test]
1019 fn test_by_sectionize_builder() {
1020 let by = By::sectionize();
1021 assert_eq!(by.kind, "sectionize");
1022 assert!(by.data.is_null());
1023 }
1024
1025 #[test]
1026 fn test_by_user_edit_builder() {
1027 assert_eq!(By::user_edit().kind, "user-edit");
1028 }
1029
1030 #[test]
1031 fn test_by_shortcode_builder_records_name() {
1032 let by = By::shortcode("meta");
1033 assert_eq!(by.kind, "shortcode");
1034 assert_eq!(by.data.get("name").and_then(|v| v.as_str()), Some("meta"));
1035 }
1036
1037 #[test]
1038 fn test_by_include_title_footnotes_appendix_tree_sitter_builders() {
1039 assert_eq!(By::include().kind, "include");
1040 assert_eq!(By::title_block().kind, "title-block");
1041 assert_eq!(By::footnotes().kind, "footnotes");
1042 assert_eq!(By::appendix().kind, "appendix");
1043 assert_eq!(
1044 By::tree_sitter_postprocess().kind,
1045 "tree-sitter-postprocess"
1046 );
1047 }
1048
1049 #[test]
1050 fn test_by_raw_builder_accepts_any_kind() {
1051 let by = By::raw("ext/my-plugin/foo", serde_json::json!({"k": 1}));
1052 assert_eq!(by.kind, "ext/my-plugin/foo");
1053 assert_eq!(by.data.get("k").and_then(|v| v.as_u64()), Some(1));
1054 }
1055
1056 #[test]
1057 fn test_by_is_atomic_kind() {
1058 assert!(By::filter("x.lua", 1).is_atomic_kind());
1059 assert!(By::shortcode("meta").is_atomic_kind());
1060 assert!(By::title_block().is_atomic_kind());
1061 assert!(By::tree_sitter_postprocess().is_atomic_kind());
1062 assert!(By::citeproc().is_atomic_kind());
1063 assert!(By::jupyter_output().is_atomic_kind());
1064
1065 assert!(!By::callout().is_atomic_kind());
1066
1067 assert!(!By::sectionize().is_atomic_kind());
1068 assert!(!By::user_edit().is_atomic_kind());
1069 assert!(!By::include().is_atomic_kind());
1070 assert!(!By::footnotes().is_atomic_kind());
1071 assert!(!By::appendix().is_atomic_kind());
1072 assert!(!By::unknown().is_atomic_kind());
1073 assert!(!By::test_scaffold().is_atomic_kind());
1074 assert!(!By::config_default().is_atomic_kind());
1075 assert!(!By::programmatic_config().is_atomic_kind());
1076 assert!(!By::raw("ext/anywhere/foo", serde_json::Value::Null).is_atomic_kind());
1077 }
1078
1079 #[test]
1080 fn test_by_unknown_constructor() {
1081 let by = By::unknown();
1082 assert_eq!(by.kind, "unknown");
1083 assert!(by.data.is_null());
1084 assert!(!by.is_atomic_kind());
1088 }
1089
1090 #[test]
1091 fn test_by_test_scaffold_constructor() {
1092 let by = By::test_scaffold();
1093 assert_eq!(by.kind, "test-scaffold");
1094 assert!(by.data.is_null());
1095 assert!(!by.is_atomic_kind());
1096 assert!(!by.is_programmatic_sentinel());
1098 }
1099
1100 #[test]
1101 fn test_by_config_default_constructor() {
1102 let by = By::config_default();
1103 assert_eq!(by.kind, "config-default");
1104 assert!(by.data.is_null());
1105 assert!(!by.is_atomic_kind());
1106 }
1107
1108 #[test]
1109 fn test_by_programmatic_config_constructor() {
1110 let by = By::programmatic_config();
1111 assert_eq!(by.kind, "programmatic-config");
1112 assert!(by.data.is_null());
1113 assert!(!by.is_atomic_kind());
1114 }
1115
1116 #[test]
1117 fn test_by_citeproc_constructor() {
1118 let by = By::citeproc();
1119 assert_eq!(by.kind, "citeproc");
1120 assert!(by.data.is_null());
1121 assert!(by.is_atomic_kind());
1123 assert!(!by.is_programmatic_sentinel());
1125 }
1126
1127 #[test]
1128 fn test_by_jupyter_output_constructor() {
1129 let by = By::jupyter_output();
1130 assert_eq!(by.kind, "jupyter-output");
1131 assert!(by.data.is_null());
1132 assert!(by.is_atomic_kind());
1134 assert!(!by.is_programmatic_sentinel());
1135 }
1136
1137 #[test]
1138 fn test_by_callout_constructor() {
1139 let by = By::callout();
1140 assert_eq!(by.kind, "callout");
1141 assert!(by.data.is_null());
1142 assert!(!by.is_atomic_kind());
1144 assert!(!by.is_programmatic_sentinel());
1145 }
1146
1147 #[test]
1148 fn test_by_is_programmatic_sentinel() {
1149 assert!(By::config_default().is_programmatic_sentinel());
1150 assert!(By::programmatic_config().is_programmatic_sentinel());
1151 assert!(By::unknown().is_programmatic_sentinel());
1152
1153 assert!(!By::user_edit().is_programmatic_sentinel());
1154 assert!(!By::filter("x.lua", 1).is_programmatic_sentinel());
1155 assert!(!By::shortcode("meta").is_programmatic_sentinel());
1156 assert!(!By::test_scaffold().is_programmatic_sentinel());
1157 assert!(!By::sectionize().is_programmatic_sentinel());
1158 }
1159
1160 #[test]
1161 fn test_source_info_for_test() {
1162 let si = SourceInfo::for_test();
1163 match si {
1164 SourceInfo::Generated(g) => {
1165 let Generated { by, from } = *g;
1166 assert_eq!(by.kind, "test-scaffold");
1167 assert!(from.is_empty());
1168 }
1169 _ => panic!("for_test() must return Generated"),
1170 }
1171 }
1172
1173 #[test]
1174 fn test_by_is_kind() {
1175 let by = By::shortcode("meta");
1176 assert!(by.is_kind("shortcode"));
1177 assert!(!by.is_kind("filter"));
1178 }
1179
1180 #[test]
1181 fn test_by_as_filter_rejects_non_filter() {
1182 assert!(By::sectionize().as_filter().is_none());
1183 let by = By {
1185 kind: "filter".to_string(),
1186 data: serde_json::json!({ "filter_path": "x.lua" }),
1187 };
1188 assert!(by.as_filter().is_none());
1189 }
1190
1191 #[test]
1192 fn test_anchor_invocation_value_source_constructors() {
1193 let original = Arc::new(SourceInfo::original(FileId(1), 0, 5));
1194 let inv = Anchor::invocation(Arc::clone(&original));
1195 let vs = Anchor::value_source(Arc::clone(&original));
1196 assert!(matches!(inv.role, AnchorRole::Invocation));
1197 assert!(matches!(vs.role, AnchorRole::ValueSource));
1198 }
1199
1200 #[test]
1201 fn test_by_json_round_trip() {
1202 let by = By::shortcode("meta");
1203 let json = serde_json::to_string(&by).unwrap();
1204 let back: By = serde_json::from_str(&json).unwrap();
1205 assert_eq!(by, back);
1206 }
1207
1208 #[test]
1209 fn test_anchor_json_round_trip() {
1210 let anchor = Anchor::invocation(Arc::new(SourceInfo::original(FileId(2), 10, 20)));
1211 let json = serde_json::to_string(&anchor).unwrap();
1212 let back: Anchor = serde_json::from_str(&json).unwrap();
1213 assert_eq!(anchor, back);
1214 }
1215
1216 #[test]
1217 fn test_generated_json_round_trip_empty_from() {
1218 let info = SourceInfo::generated(By::sectionize());
1219 let json = serde_json::to_string(&info).unwrap();
1220 let back: SourceInfo = serde_json::from_str(&json).unwrap();
1221 assert_eq!(info, back);
1222 }
1223
1224 #[test]
1225 fn test_generated_json_round_trip_with_invocation_anchor() {
1226 let mut info = SourceInfo::generated(By::shortcode("meta"));
1227 info.append_anchor(
1228 AnchorRole::Invocation,
1229 Arc::new(SourceInfo::original(FileId(5), 100, 110)),
1230 );
1231 let json = serde_json::to_string(&info).unwrap();
1232 let back: SourceInfo = serde_json::from_str(&json).unwrap();
1233 assert_eq!(info, back);
1234 }
1235
1236 #[test]
1237 fn test_generated_json_round_trip_multi_anchor() {
1238 let mut info = SourceInfo::generated(By::shortcode("meta"));
1239 info.append_anchor(
1240 AnchorRole::Invocation,
1241 Arc::new(SourceInfo::original(FileId(5), 100, 110)),
1242 );
1243 info.append_anchor(
1244 AnchorRole::ValueSource,
1245 Arc::new(SourceInfo::original(FileId(7), 200, 220)),
1246 );
1247 let json = serde_json::to_string(&info).unwrap();
1248 let back: SourceInfo = serde_json::from_str(&json).unwrap();
1249 assert_eq!(info, back);
1250 }
1251
1252 #[test]
1253 fn test_generated_length_start_end_are_zero() {
1254 let info = SourceInfo::generated(By::sectionize());
1255 assert_eq!(info.length(), 0);
1256 assert_eq!(info.start_offset(), 0);
1257 assert_eq!(info.end_offset(), 0);
1258 }
1259
1260 #[test]
1261 fn test_generated_resolve_byte_range_recurses_through_substring() {
1262 let parent = SourceInfo::original(FileId(42), 100, 200);
1263 let sub = SourceInfo::substring(parent, 10, 20);
1264 let mut info = SourceInfo::generated(By::shortcode("meta"));
1265 info.append_anchor(AnchorRole::Invocation, Arc::new(sub));
1266 assert_eq!(info.resolve_byte_range(), Some((42, 110, 120)));
1267 }
1268
1269 #[test]
1270 fn test_generated_resolve_byte_range_empty_returns_none() {
1271 let info = SourceInfo::generated(By::sectionize());
1272 assert!(info.resolve_byte_range().is_none());
1273 }
1274
1275 #[test]
1276 fn test_generated_resolve_byte_range_value_source_only_returns_none() {
1277 let mut info = SourceInfo::generated(By::shortcode("meta"));
1278 info.append_anchor(
1279 AnchorRole::ValueSource,
1280 Arc::new(SourceInfo::original(FileId(5), 100, 110)),
1281 );
1282 assert!(info.resolve_byte_range().is_none());
1283 }
1284
1285 #[test]
1286 fn test_generated_remap_file_ids_walks_anchors() {
1287 let mut info = SourceInfo::generated(By::shortcode("meta"));
1288 info.append_anchor(
1289 AnchorRole::Invocation,
1290 Arc::new(SourceInfo::original(FileId(0), 0, 5)),
1291 );
1292 info.append_anchor(
1293 AnchorRole::ValueSource,
1294 Arc::new(SourceInfo::original(FileId(3), 10, 20)),
1295 );
1296 info.remap_file_ids(&|id| FileId(id.0 + 10));
1297 match &info {
1298 SourceInfo::Generated(g) => {
1299 let from = &g.from;
1300 assert_eq!(from.len(), 2);
1301 match from[0].source_info.as_ref() {
1302 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(10)),
1303 _ => panic!("Expected Original anchor 0"),
1304 }
1305 match from[1].source_info.as_ref() {
1306 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, FileId(13)),
1307 _ => panic!("Expected Original anchor 1"),
1308 }
1309 }
1310 _ => panic!("Expected Generated"),
1311 }
1312 }
1313
1314 #[test]
1315 fn test_root_file_id_per_variant() {
1316 let original = SourceInfo::original(FileId(7), 0, 5);
1318 assert_eq!(original.root_file_id(), Some(FileId(7)));
1319
1320 let sub = SourceInfo::substring(original.clone(), 0, 5);
1322 assert_eq!(sub.root_file_id(), Some(FileId(7)));
1323
1324 let empty_gen = SourceInfo::generated(By::sectionize());
1326 let real = SourceInfo::original(FileId(42), 0, 5);
1327 let concat = SourceInfo::concat(vec![(empty_gen, 0), (real, 5)]);
1328 assert_eq!(concat.root_file_id(), Some(FileId(42)));
1329
1330 let mut g = SourceInfo::generated(By::shortcode("meta"));
1332 g.append_anchor(
1333 AnchorRole::Invocation,
1334 Arc::new(SourceInfo::original(FileId(9), 0, 1)),
1335 );
1336 assert_eq!(g.root_file_id(), Some(FileId(9)));
1337
1338 let mut g2 = SourceInfo::generated(By::shortcode("meta"));
1340 g2.append_anchor(
1341 AnchorRole::ValueSource,
1342 Arc::new(SourceInfo::original(FileId(9), 0, 1)),
1343 );
1344 assert_eq!(g2.root_file_id(), None);
1345
1346 let g3 = SourceInfo::generated(By::sectionize());
1348 assert_eq!(g3.root_file_id(), None);
1349 }
1350
1351 #[test]
1352 fn test_collect_file_ids_walks_every_anchor_role() {
1353 let mut info = SourceInfo::generated(By::shortcode("meta"));
1354 info.append_anchor(
1355 AnchorRole::Invocation,
1356 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1357 );
1358 info.append_anchor(
1359 AnchorRole::ValueSource,
1360 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1361 );
1362 info.append_anchor(
1363 AnchorRole::Other("dispatch".to_string()),
1364 Arc::new(SourceInfo::original(FileId(3), 0, 1)),
1365 );
1366 let mut out = std::collections::HashSet::new();
1367 info.collect_file_ids(&mut out);
1368 assert!(out.contains(&FileId(1)));
1369 assert!(out.contains(&FileId(2)));
1370 assert!(out.contains(&FileId(3)));
1371 assert_eq!(out.len(), 3);
1372 }
1373
1374 #[test]
1375 fn test_collect_file_ids_walks_concat_and_substring() {
1376 let inner = SourceInfo::original(FileId(5), 0, 100);
1377 let sub = SourceInfo::substring(inner, 10, 20);
1378 let other = SourceInfo::original(FileId(11), 0, 5);
1379 let concat = SourceInfo::concat(vec![(sub, 10), (other, 5)]);
1380 let mut out = std::collections::HashSet::new();
1381 concat.collect_file_ids(&mut out);
1382 assert!(out.contains(&FileId(5)));
1383 assert!(out.contains(&FileId(11)));
1384 assert_eq!(out.len(), 2);
1385 }
1386
1387 #[test]
1388 fn test_invocation_anchor_accessor() {
1389 let mut info = SourceInfo::generated(By::shortcode("meta"));
1390 assert!(info.invocation_anchor().is_none());
1391 info.append_anchor(
1392 AnchorRole::ValueSource,
1393 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1394 );
1395 assert!(info.invocation_anchor().is_none());
1396 info.append_anchor(
1397 AnchorRole::Invocation,
1398 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1399 );
1400 assert!(info.invocation_anchor().is_some());
1401 assert!(
1403 SourceInfo::original(FileId(0), 0, 0)
1404 .invocation_anchor()
1405 .is_none()
1406 );
1407 }
1408
1409 #[test]
1410 fn test_value_source_anchor_accessor() {
1411 let mut info = SourceInfo::generated(By::shortcode("meta"));
1412 assert!(info.value_source_anchor().is_none());
1413 info.append_anchor(
1414 AnchorRole::Invocation,
1415 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1416 );
1417 assert!(info.value_source_anchor().is_none());
1418 info.append_anchor(
1419 AnchorRole::ValueSource,
1420 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1421 );
1422 assert!(info.value_source_anchor().is_some());
1423 }
1424
1425 #[test]
1426 fn test_anchors_with_role() {
1427 let mut info = SourceInfo::generated(By::shortcode("meta"));
1428 info.append_anchor(
1429 AnchorRole::Invocation,
1430 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1431 );
1432 info.append_anchor(
1433 AnchorRole::ValueSource,
1434 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1435 );
1436 info.append_anchor(
1437 AnchorRole::Other("ext/foo".to_string()),
1438 Arc::new(SourceInfo::original(FileId(3), 0, 1)),
1439 );
1440 assert_eq!(info.anchors_with_role(&AnchorRole::Invocation).count(), 1);
1441 assert_eq!(info.anchors_with_role(&AnchorRole::ValueSource).count(), 1);
1442 assert_eq!(
1443 info.anchors_with_role(&AnchorRole::Other("ext/foo".to_string()))
1444 .count(),
1445 1
1446 );
1447 assert_eq!(
1448 info.anchors_with_role(&AnchorRole::Other("missing".to_string()))
1449 .count(),
1450 0
1451 );
1452 }
1453
1454 #[test]
1455 fn test_append_anchor_preserves_order() {
1456 let mut info = SourceInfo::generated(By::shortcode("meta"));
1457 info.append_anchor(
1458 AnchorRole::Invocation,
1459 Arc::new(SourceInfo::original(FileId(1), 0, 1)),
1460 );
1461 info.append_anchor(
1462 AnchorRole::ValueSource,
1463 Arc::new(SourceInfo::original(FileId(2), 0, 1)),
1464 );
1465 match info {
1466 SourceInfo::Generated(g) => {
1467 let from = &g.from;
1468 assert_eq!(from.len(), 2);
1469 assert!(matches!(from[0].role, AnchorRole::Invocation));
1470 assert!(matches!(from[1].role, AnchorRole::ValueSource));
1471 }
1472 _ => panic!("Expected Generated"),
1473 }
1474 }
1475
1476 #[test]
1477 fn test_combine_with_generated_is_zero_length_piece() {
1478 let original = SourceInfo::original(FileId(0), 10, 20);
1479 let generated = SourceInfo::generated(By::sectionize());
1480 let combined = original.combine(&generated);
1481 match &combined {
1482 SourceInfo::Concat { pieces } => {
1483 assert_eq!(pieces.len(), 2);
1484 assert_eq!(pieces[1].length, 0);
1485 }
1486 _ => panic!("Expected Concat"),
1487 }
1488 assert_eq!(combined.length(), 10);
1490 }
1491
1492 #[test]
1493 fn test_source_info_serialization() {
1494 let file_id = FileId(0);
1495 let range = Range {
1496 start: Location {
1497 offset: 0,
1498 row: 0,
1499 column: 0,
1500 },
1501 end: Location {
1502 offset: 10,
1503 row: 0,
1504 column: 10,
1505 },
1506 };
1507
1508 let info = SourceInfo::from_range(file_id, range);
1509 let json = serde_json::to_string(&info).unwrap();
1510 let deserialized: SourceInfo = serde_json::from_str(&json).unwrap();
1511
1512 assert_eq!(info, deserialized);
1513 }
1514
1515 #[test]
1516 fn test_substring_source_info() {
1517 let file_id = FileId(0);
1518 let parent_range = Range {
1519 start: Location {
1520 offset: 0,
1521 row: 0,
1522 column: 0,
1523 },
1524 end: Location {
1525 offset: 100,
1526 row: 0,
1527 column: 100,
1528 },
1529 };
1530 let parent = SourceInfo::from_range(file_id, parent_range);
1531
1532 let substring = SourceInfo::substring(parent, 10, 20);
1533
1534 assert_eq!(substring.start_offset(), 10);
1535 assert_eq!(substring.end_offset(), 20);
1536 assert_eq!(substring.length(), 10);
1537
1538 match substring {
1539 SourceInfo::Substring {
1540 start_offset,
1541 end_offset,
1542 ..
1543 } => {
1544 assert_eq!(start_offset, 10);
1545 assert_eq!(end_offset, 20);
1546 }
1547 _ => panic!("Expected Substring mapping"),
1548 }
1549 }
1550
1551 #[test]
1552 fn test_concat_source_info() {
1553 let file_id1 = FileId(0);
1554 let file_id2 = FileId(1);
1555
1556 let info1 = SourceInfo::from_range(
1557 file_id1,
1558 Range {
1559 start: Location {
1560 offset: 0,
1561 row: 0,
1562 column: 0,
1563 },
1564 end: Location {
1565 offset: 10,
1566 row: 0,
1567 column: 10,
1568 },
1569 },
1570 );
1571
1572 let info2 = SourceInfo::from_range(
1573 file_id2,
1574 Range {
1575 start: Location {
1576 offset: 0,
1577 row: 0,
1578 column: 0,
1579 },
1580 end: Location {
1581 offset: 15,
1582 row: 0,
1583 column: 15,
1584 },
1585 },
1586 );
1587
1588 let concat = SourceInfo::concat(vec![(info1, 10), (info2, 15)]);
1589
1590 assert_eq!(concat.start_offset(), 0);
1591 assert_eq!(concat.end_offset(), 25); assert_eq!(concat.length(), 25);
1593
1594 match concat {
1595 SourceInfo::Concat { pieces } => {
1596 assert_eq!(pieces.len(), 2);
1597 assert_eq!(pieces[0].offset_in_concat, 0);
1598 assert_eq!(pieces[0].length, 10);
1599 assert_eq!(pieces[1].offset_in_concat, 10);
1600 assert_eq!(pieces[1].length, 15);
1601 }
1602 _ => panic!("Expected Concat mapping"),
1603 }
1604 }
1605
1606 #[test]
1607 fn test_combine_two_sources() {
1608 let file_id = FileId(0);
1609
1610 let info1 = SourceInfo::from_range(
1612 file_id,
1613 Range {
1614 start: Location {
1615 offset: 0,
1616 row: 0,
1617 column: 0,
1618 },
1619 end: Location {
1620 offset: 10,
1621 row: 0,
1622 column: 10,
1623 },
1624 },
1625 );
1626
1627 let info2 = SourceInfo::from_range(
1628 file_id,
1629 Range {
1630 start: Location {
1631 offset: 15,
1632 row: 0,
1633 column: 15,
1634 },
1635 end: Location {
1636 offset: 25,
1637 row: 0,
1638 column: 25,
1639 },
1640 },
1641 );
1642
1643 let combined = info1.combine(&info2);
1645
1646 assert_eq!(combined.start_offset(), 0);
1648 assert_eq!(combined.end_offset(), 20);
1649 assert_eq!(combined.length(), 20);
1650
1651 match combined {
1652 SourceInfo::Concat { pieces } => {
1653 assert_eq!(pieces.len(), 2);
1654 assert_eq!(pieces[0].length, 10);
1655 assert_eq!(pieces[0].offset_in_concat, 0);
1656 assert_eq!(pieces[1].length, 10);
1657 assert_eq!(pieces[1].offset_in_concat, 10);
1658 }
1659 _ => panic!("Expected Concat mapping"),
1660 }
1661 }
1662
1663 #[test]
1664 fn test_combine_preserves_source_tracking() {
1665 let file_id1 = FileId(5);
1667 let file_id2 = FileId(10);
1668
1669 let info1 = SourceInfo::from_range(
1670 file_id1,
1671 Range {
1672 start: Location {
1673 offset: 100,
1674 row: 5,
1675 column: 0,
1676 },
1677 end: Location {
1678 offset: 105,
1679 row: 5,
1680 column: 5,
1681 },
1682 },
1683 );
1684
1685 let info2 = SourceInfo::from_range(
1686 file_id2,
1687 Range {
1688 start: Location {
1689 offset: 200,
1690 row: 10,
1691 column: 0,
1692 },
1693 end: Location {
1694 offset: 207,
1695 row: 10,
1696 column: 7,
1697 },
1698 },
1699 );
1700
1701 let combined = info1.combine(&info2);
1702
1703 match combined {
1705 SourceInfo::Concat { pieces } => {
1706 assert_eq!(pieces.len(), 2);
1707
1708 match &pieces[0].source_info {
1710 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, file_id1),
1711 _ => panic!("Expected Original mapping for first piece"),
1712 }
1713
1714 match &pieces[1].source_info {
1716 SourceInfo::Original { file_id, .. } => assert_eq!(*file_id, file_id2),
1717 _ => panic!("Expected Original mapping for second piece"),
1718 }
1719 }
1720 _ => panic!("Expected Concat mapping"),
1721 }
1722 }
1723
1724 #[test]
1726 fn test_json_serialization_original() {
1727 let file_id = FileId(0);
1728 let range = Range {
1729 start: Location {
1730 offset: 10,
1731 row: 1,
1732 column: 5,
1733 },
1734 end: Location {
1735 offset: 50,
1736 row: 3,
1737 column: 10,
1738 },
1739 };
1740
1741 let info = SourceInfo::from_range(file_id, range);
1742 let json = serde_json::to_value(&info).unwrap();
1743
1744 assert_eq!(json["Original"]["file_id"], 0);
1746 assert_eq!(json["Original"]["start_offset"], 10);
1747 assert_eq!(json["Original"]["end_offset"], 50);
1748
1749 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1751 assert_eq!(info, deserialized);
1752 }
1753
1754 #[test]
1756 fn test_json_serialization_substring() {
1757 let file_id = FileId(0);
1758 let parent_range = Range {
1759 start: Location {
1760 offset: 0,
1761 row: 0,
1762 column: 0,
1763 },
1764 end: Location {
1765 offset: 100,
1766 row: 5,
1767 column: 20,
1768 },
1769 };
1770 let parent = SourceInfo::from_range(file_id, parent_range);
1771
1772 let substring = SourceInfo::substring(parent, 10, 30);
1773 let json = serde_json::to_value(&substring).unwrap();
1774
1775 assert_eq!(json["Substring"]["start_offset"], 10);
1777 assert_eq!(json["Substring"]["end_offset"], 30);
1778
1779 assert!(json["Substring"]["parent"].is_object());
1781 assert_eq!(json["Substring"]["parent"]["Original"]["file_id"], 0);
1782
1783 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1785 assert_eq!(substring, deserialized);
1786 }
1787
1788 #[test]
1790 fn test_json_serialization_nested_substring() {
1791 let file_id = FileId(0);
1792
1793 let file_range = Range {
1795 start: Location {
1796 offset: 0,
1797 row: 0,
1798 column: 0,
1799 },
1800 end: Location {
1801 offset: 200,
1802 row: 10,
1803 column: 0,
1804 },
1805 };
1806 let file_info = SourceInfo::from_range(file_id, file_range);
1807
1808 let yaml_info = SourceInfo::substring(file_info, 4, 150);
1810
1811 let value_info = SourceInfo::substring(yaml_info, 20, 35);
1813
1814 let json = serde_json::to_value(&value_info).unwrap();
1815
1816 assert_eq!(json["Substring"]["start_offset"], 20);
1818 assert_eq!(json["Substring"]["end_offset"], 35);
1819 assert_eq!(json["Substring"]["parent"]["Substring"]["start_offset"], 4);
1820 assert_eq!(
1821 json["Substring"]["parent"]["Substring"]["parent"]["Original"]["file_id"],
1822 0
1823 );
1824
1825 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1827 assert_eq!(value_info, deserialized);
1828 }
1829
1830 #[test]
1832 fn test_json_serialization_concat() {
1833 let file_id1 = FileId(0);
1834 let file_id2 = FileId(1);
1835
1836 let info1 = SourceInfo::from_range(
1837 file_id1,
1838 Range {
1839 start: Location {
1840 offset: 0,
1841 row: 0,
1842 column: 0,
1843 },
1844 end: Location {
1845 offset: 10,
1846 row: 0,
1847 column: 10,
1848 },
1849 },
1850 );
1851
1852 let info2 = SourceInfo::from_range(
1853 file_id2,
1854 Range {
1855 start: Location {
1856 offset: 20,
1857 row: 2,
1858 column: 0,
1859 },
1860 end: Location {
1861 offset: 30,
1862 row: 2,
1863 column: 10,
1864 },
1865 },
1866 );
1867
1868 let combined = info1.combine(&info2);
1869 let json = serde_json::to_value(&combined).unwrap();
1870
1871 assert!(json["Concat"]["pieces"].is_array());
1873 let pieces = json["Concat"]["pieces"].as_array().unwrap();
1874 assert_eq!(pieces.len(), 2);
1875
1876 assert_eq!(pieces[0]["offset_in_concat"], 0);
1878 assert_eq!(pieces[0]["length"], 10);
1879 assert_eq!(pieces[0]["source_info"]["Original"]["file_id"], 0);
1880
1881 assert_eq!(pieces[1]["offset_in_concat"], 10);
1883 assert_eq!(pieces[1]["length"], 10);
1884 assert_eq!(pieces[1]["source_info"]["Original"]["file_id"], 1);
1885
1886 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1888 assert_eq!(combined, deserialized);
1889 }
1890
1891 #[test]
1893 fn test_json_serialization_complex_nested() {
1894 let file_id = FileId(0);
1895
1896 let qmd_file = SourceInfo::from_range(
1898 file_id,
1899 Range {
1900 start: Location {
1901 offset: 0,
1902 row: 0,
1903 column: 0,
1904 },
1905 end: Location {
1906 offset: 500,
1907 row: 20,
1908 column: 0,
1909 },
1910 },
1911 );
1912
1913 let yaml_frontmatter = SourceInfo::substring(qmd_file.clone(), 4, 200);
1915
1916 let yaml_key = SourceInfo::substring(yaml_frontmatter.clone(), 10, 20);
1918
1919 let yaml_value = SourceInfo::substring(yaml_frontmatter, 25, 50);
1921
1922 let combined = yaml_key.combine(&yaml_value);
1924
1925 let json = serde_json::to_value(&combined).unwrap();
1926
1927 assert!(json.is_object());
1929 assert!(json["Concat"].is_object());
1930
1931 let deserialized: SourceInfo = serde_json::from_value(json).unwrap();
1933 assert_eq!(combined, deserialized);
1934 }
1935
1936 #[test]
1941 fn test_preimage_in_original_same_file() {
1942 let info = SourceInfo::original(FileId(0), 10, 25);
1943 assert_eq!(info.preimage_in(FileId(0)), Some(10..25));
1944 }
1945
1946 #[test]
1947 fn test_preimage_in_original_different_file_returns_none() {
1948 let info = SourceInfo::original(FileId(0), 10, 25);
1949 assert_eq!(info.preimage_in(FileId(1)), None);
1950 }
1951
1952 #[test]
1953 fn test_preimage_in_substring_composes_offsets() {
1954 let parent = SourceInfo::original(FileId(0), 100, 200);
1958 let info = SourceInfo::substring(parent, 5, 15);
1959 assert_eq!(info.preimage_in(FileId(0)), Some(105..115));
1960 }
1961
1962 #[test]
1963 fn test_preimage_in_substring_different_file_returns_none() {
1964 let parent = SourceInfo::original(FileId(0), 100, 200);
1965 let info = SourceInfo::substring(parent, 5, 15);
1966 assert_eq!(info.preimage_in(FileId(7)), None);
1967 }
1968
1969 #[test]
1970 fn test_preimage_in_substring_chain() {
1971 let root = SourceInfo::original(FileId(0), 1000, 2000);
1974 let mid = SourceInfo::substring(root, 100, 500);
1975 let leaf = SourceInfo::substring(mid, 10, 50);
1976 assert_eq!(leaf.preimage_in(FileId(0)), Some(1110..1150));
1977 }
1978
1979 #[test]
1980 fn test_preimage_in_concat_contiguous() {
1981 let a = SourceInfo::original(FileId(0), 10, 15);
1983 let b = SourceInfo::original(FileId(0), 15, 25);
1984 let info = SourceInfo::concat(vec![(a, 5), (b, 10)]);
1985 assert_eq!(info.preimage_in(FileId(0)), Some(10..25));
1986 }
1987
1988 #[test]
1989 fn test_preimage_in_concat_gappy_returns_none() {
1990 let a = SourceInfo::original(FileId(0), 10, 15);
1992 let b = SourceInfo::original(FileId(0), 20, 25);
1993 let info = SourceInfo::concat(vec![(a, 5), (b, 5)]);
1994 assert_eq!(info.preimage_in(FileId(0)), None);
1995 }
1996
1997 #[test]
1998 fn test_preimage_in_concat_overlapping_returns_none() {
1999 let a = SourceInfo::original(FileId(0), 10, 20);
2001 let b = SourceInfo::original(FileId(0), 15, 25);
2002 let info = SourceInfo::concat(vec![(a, 10), (b, 10)]);
2003 assert_eq!(info.preimage_in(FileId(0)), None);
2004 }
2005
2006 #[test]
2007 fn test_preimage_in_concat_mixed_files_returns_none() {
2008 let a = SourceInfo::original(FileId(0), 10, 15);
2011 let b = SourceInfo::original(FileId(1), 15, 25);
2012 let info = SourceInfo::concat(vec![(a, 5), (b, 10)]);
2013 assert_eq!(info.preimage_in(FileId(0)), None);
2014 }
2015
2016 #[test]
2024 fn test_preimage_in_substring_over_concat_parent_returns_none() {
2025 let it = SourceInfo::original(FileId(0), 1, 3);
2030 let escaped_quote = SourceInfo::original(FileId(0), 3, 5);
2031 let s = SourceInfo::original(FileId(0), 5, 6);
2032 let concat = SourceInfo::concat(vec![(it, 2), (escaped_quote, 1), (s, 1)]);
2033
2034 let sub = SourceInfo::substring(concat, 0, 4);
2038 assert_eq!(sub.preimage_in(FileId(0)), None);
2039 }
2040
2041 #[test]
2042 fn test_preimage_in_bare_concat_over_gap_free_pieces_is_gating() {
2043 let it = SourceInfo::original(FileId(0), 1, 3);
2047 let escaped_quote = SourceInfo::original(FileId(0), 3, 5);
2048 let s = SourceInfo::original(FileId(0), 5, 6);
2049 let concat = SourceInfo::concat(vec![(it, 2), (escaped_quote, 1), (s, 1)]);
2050
2051 assert_eq!(concat.preimage_in(FileId(0)), Some(1..6));
2052 }
2053
2054 #[test]
2055 fn test_preimage_in_cell_options_multi_option_shape_is_gating() {
2056 let opt_a = SourceInfo::original(FileId(0), 3, 4);
2062 let opt_b = SourceInfo::original(FileId(0), 10, 11);
2063 let concat = SourceInfo::concat(vec![(opt_a, 1), (opt_b, 1)]);
2064
2065 assert_eq!(concat.preimage_in(FileId(0)), None);
2066 let sub = SourceInfo::substring(concat, 0, 2);
2067 assert_eq!(sub.preimage_in(FileId(0)), None);
2068 }
2069
2070 #[test]
2071 fn test_preimage_in_cell_options_single_option_shape_through_substring_returns_none() {
2072 let opt = SourceInfo::original(FileId(0), 5, 8);
2080 let concat = SourceInfo::concat(vec![(opt, 3)]);
2081
2082 assert_eq!(concat.preimage_in(FileId(0)), Some(5..8));
2083
2084 let sub = SourceInfo::substring(concat, 0, 3);
2085 assert_eq!(sub.preimage_in(FileId(0)), None);
2086 }
2087
2088 #[test]
2089 fn test_preimage_in_concat_contiguous_hull_with_zero_content_piece() {
2090 let a = SourceInfo::original(FileId(0), 4, 7);
2097 let zero_content = SourceInfo::original(FileId(0), 7, 11);
2098 let b = SourceInfo::original(FileId(0), 11, 14);
2099 let concat = SourceInfo::concat(vec![(a, 3), (zero_content, 0), (b, 3)]);
2100 assert_eq!(concat.preimage_in(FileId(0)), Some(4..14));
2101
2102 let a2 = SourceInfo::original(FileId(0), 4, 7);
2105 let b2 = SourceInfo::original(FileId(0), 11, 14);
2106 let concat_missing_piece = SourceInfo::concat(vec![(a2, 3), (b2, 3)]);
2107 assert_eq!(concat_missing_piece.preimage_in(FileId(0)), None);
2108 }
2109
2110 #[test]
2111 fn test_preimage_in_generated_no_anchors_returns_none() {
2112 let info = SourceInfo::generated(By::sectionize());
2115 assert_eq!(info.preimage_in(FileId(0)), None);
2116 }
2117
2118 #[test]
2119 fn test_preimage_in_generated_with_invocation_in_target() {
2120 let token = SourceInfo::original(FileId(0), 50, 70);
2123 let mut info = SourceInfo::generated(By::shortcode("meta"));
2124 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
2125 assert_eq!(info.preimage_in(FileId(0)), Some(50..70));
2126 }
2127
2128 #[test]
2129 fn test_preimage_in_generated_with_invocation_outside_target() {
2130 let token = SourceInfo::original(FileId(0), 50, 70);
2132 let mut info = SourceInfo::generated(By::shortcode("meta"));
2133 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
2134 assert_eq!(info.preimage_in(FileId(1)), None);
2135 }
2136
2137 #[test]
2138 fn test_preimage_in_generated_walks_through_substring_in_invocation() {
2139 let root = SourceInfo::original(FileId(0), 100, 200);
2142 let token = SourceInfo::substring(root, 10, 30);
2143 let mut info = SourceInfo::generated(By::shortcode("meta"));
2144 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
2145 assert_eq!(info.preimage_in(FileId(0)), Some(110..130));
2146 }
2147
2148 #[test]
2153 fn test_preimage_in_generated_value_source_only_returns_none() {
2154 let meta_si = SourceInfo::original(FileId(0), 10, 25);
2158 let mut info = SourceInfo::generated(By::appendix());
2159 info.append_anchor(AnchorRole::ValueSource, Arc::new(meta_si));
2160 assert_eq!(info.preimage_in(FileId(0)), None);
2161 }
2162
2163 #[test]
2164 fn test_preimage_in_generated_other_only_returns_none() {
2165 let lua_si = SourceInfo::original(FileId(0), 10, 25);
2167 let mut info = SourceInfo::generated(By::filter("upper.lua", 14));
2168 info.append_anchor(
2169 AnchorRole::Other("ext/my-ext/dispatch".to_string()),
2170 Arc::new(lua_si),
2171 );
2172 assert_eq!(info.preimage_in(FileId(0)), None);
2173 }
2174
2175 #[test]
2176 fn test_preimage_in_generated_invocation_plus_value_source_walks_invocation_only() {
2177 let token = SourceInfo::original(FileId(0), 50, 70);
2183 let value = SourceInfo::original(FileId(1), 200, 215);
2184 let mut info = SourceInfo::generated(By::shortcode("meta"));
2185 info.append_anchor(AnchorRole::Invocation, Arc::new(token));
2186 info.append_anchor(AnchorRole::ValueSource, Arc::new(value));
2187
2188 assert_eq!(info.preimage_in(FileId(0)), Some(50..70));
2189 assert_eq!(info.preimage_in(FileId(1)), None);
2190 }
2191
2192 #[test]
2201 fn test_source_info_is_32_bytes() {
2202 use std::mem::size_of;
2203 assert_eq!(size_of::<SourceInfo>(), 32);
2204 assert_eq!(size_of::<Option<SourceInfo>>(), 32);
2205 }
2206
2207 #[test]
2212 fn test_generated_wire_shape_is_pinned() {
2213 fn roundtrip(si: &SourceInfo, expected: &str) {
2214 let json = serde_json::to_string(si).unwrap();
2215 assert_eq!(json, expected);
2216 let back: SourceInfo = serde_json::from_str(&json).unwrap();
2217 assert_eq!(&back, si);
2218 }
2219
2220 roundtrip(
2221 &SourceInfo::generated(By::sectionize()),
2222 r#"{"Generated":{"by":{"kind":"sectionize"}}}"#,
2223 );
2224 roundtrip(
2225 &SourceInfo::for_test(),
2226 r#"{"Generated":{"by":{"kind":"test-scaffold"}}}"#,
2227 );
2228
2229 let mut full = SourceInfo::generated(By::shortcode("meta"));
2230 full.append_anchor(
2231 AnchorRole::Invocation,
2232 Arc::new(SourceInfo::original(FileId(0), 3, 17)),
2233 );
2234 full.append_anchor(
2235 AnchorRole::Other("ext/x/role".to_string()),
2236 Arc::new(SourceInfo::original(FileId(1), 0, 2)),
2237 );
2238 roundtrip(
2239 &full,
2240 concat!(
2241 r#"{"Generated":{"by":{"kind":"shortcode","data":{"name":"meta"}},"#,
2242 r#""from":[{"role":"Invocation","source_info":{"Original":{"file_id":0,"start_offset":3,"end_offset":17}}},"#,
2243 r#"{"role":{"Other":"ext/x/role"},"source_info":{"Original":{"file_id":1,"start_offset":0,"end_offset":2}}}]}}"#,
2244 ),
2245 );
2246
2247 let sparse: SourceInfo =
2249 serde_json::from_str(r#"{"Generated":{"by":{"kind":"sectionize"}}}"#).unwrap();
2250 assert!(sparse.invocation_anchor().is_none());
2251 assert_eq!(sparse, SourceInfo::generated(By::sectionize()));
2252 }
2253
2254 #[test]
2255 fn test_generated_with_and_as_generated() {
2256 let anchor = Anchor {
2257 role: AnchorRole::Invocation,
2258 source_info: Arc::new(SourceInfo::original(FileId(0), 3, 17)),
2259 };
2260 let from_vec = SourceInfo::generated_with(By::shortcode("meta"), vec![anchor.clone()]);
2262 let from_sv =
2263 SourceInfo::generated_with(By::shortcode("meta"), smallvec::smallvec![anchor]);
2264 assert_eq!(from_vec, from_sv);
2265 assert_eq!(from_vec.resolve_byte_range(), Some((0, 3, 17)));
2266
2267 let g = from_vec.as_generated().expect("Generated");
2268 assert_eq!(g.by.kind, "shortcode");
2269 assert_eq!(g.from.len(), 1);
2270 assert!(
2271 SourceInfo::original(FileId(0), 0, 1)
2272 .as_generated()
2273 .is_none()
2274 );
2275
2276 assert_eq!(
2278 SourceInfo::generated(By::sectionize()),
2279 SourceInfo::generated_with(By::sectionize(), Vec::new())
2280 );
2281
2282 let mut si = SourceInfo::generated(By::sectionize());
2283 si.as_generated_mut().unwrap().by = By::appendix();
2284 assert_eq!(si.as_generated().unwrap().by.kind, "appendix");
2285 assert!(
2286 SourceInfo::original(FileId(0), 0, 1)
2287 .as_generated_mut()
2288 .is_none()
2289 );
2290 }
2291}