1use serde::{Deserialize, Serialize};
10
11macro_rules! id_newtype {
12 ($name:ident) => {
13 #[derive(
14 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
15 )]
16 pub struct $name(pub u64);
17 };
18}
19
20id_newtype!(FileId);
21id_newtype!(ScopeId);
22id_newtype!(EntityId);
23id_newtype!(AnchorId);
24id_newtype!(OccurrenceId);
25id_newtype!(EdgeId);
26id_newtype!(DiagnosticId);
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
29pub enum EntityKind {
30 Package,
31 Class,
32 Role,
33 Subroutine,
34 Method,
35 Variable,
36 Constant,
37 Field,
38 Label,
39 Format,
40 Module,
41 GeneratedMember,
42 ExternalSymbol,
43 Unknown,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
47pub enum OccurrenceKind {
48 Definition,
49 Reference,
50 Read,
51 Write,
52 Call,
53 MethodCall,
54 StaticMethodCall,
55 CoderefReference,
56 TypeglobReference,
57 Import,
58 Export,
59 Inheritance,
60 RoleComposition,
61 GeneratedUse,
62 DynamicBoundary,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
66pub enum EdgeKind {
67 Defines,
68 References,
69 Reads,
70 Writes,
71 Calls,
72 ImportsModule,
73 ImportsSymbol,
74 ExportsSymbol,
75 ExportsGroup,
76 Inherits,
77 ComposesRole,
78 MemberOf,
79 GeneratedFrom,
80 AliasOf,
81 DependsOn,
82 DynamicBoundary,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
86pub enum Provenance {
87 ExactAst,
88 DesugaredAst,
89 SemanticAnalyzer,
90 FrameworkSynthesis,
91 ImportExportInference,
92 PragmaInference,
93 NameHeuristic,
94 SearchFallback,
95 DynamicBoundary,
96 LiteralRequireImport,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
104pub enum Confidence {
105 High,
106 Medium,
107 Low,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct AnchorFact {
112 pub id: AnchorId,
113 pub file_id: FileId,
114 pub span_start_byte: u32,
115 pub span_end_byte: u32,
116 pub scope_id: Option<ScopeId>,
117 pub provenance: Provenance,
118 pub confidence: Confidence,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct EntityFact {
123 pub id: EntityId,
124 pub kind: EntityKind,
125 pub canonical_name: String,
126 pub anchor_id: Option<AnchorId>,
127 pub scope_id: Option<ScopeId>,
128 pub provenance: Provenance,
129 pub confidence: Confidence,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct OccurrenceFact {
134 pub id: OccurrenceId,
135 pub kind: OccurrenceKind,
136 pub entity_id: Option<EntityId>,
137 pub anchor_id: AnchorId,
138 pub scope_id: Option<ScopeId>,
139 pub provenance: Provenance,
140 pub confidence: Confidence,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub struct EdgeFact {
145 pub id: EdgeId,
146 pub kind: EdgeKind,
147 pub from_entity_id: EntityId,
148 pub to_entity_id: EntityId,
149 pub via_occurrence_id: Option<OccurrenceId>,
150 pub provenance: Provenance,
151 pub confidence: Confidence,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub struct DiagnosticFact {
156 pub id: DiagnosticId,
157 pub code: Option<String>,
158 pub message: String,
159 pub primary_anchor_id: AnchorId,
160 pub related_anchor_ids: Vec<AnchorId>,
161 pub scope_id: Option<ScopeId>,
162 pub provenance: Provenance,
163 pub confidence: Confidence,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168pub struct ExportSet {
169 pub default_exports: Vec<String>,
171 pub optional_exports: Vec<String>,
173 pub tags: Vec<ExportTag>,
175 pub provenance: Provenance,
177 pub confidence: Confidence,
179 pub module_name: Option<String>,
181 pub anchor_id: Option<AnchorId>,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct ExportTag {
188 pub name: String,
190 pub members: Vec<String>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct ImportSpec {
197 pub module: String,
199 pub kind: ImportKind,
201 pub symbols: ImportSymbols,
203 pub provenance: Provenance,
205 pub confidence: Confidence,
207 pub file_id: Option<FileId>,
209 pub anchor_id: Option<AnchorId>,
211 pub scope_id: Option<ScopeId>,
213 pub span_start_byte: Option<u32>,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223pub enum ImportKind {
224 Use,
225 UseEmpty,
226 UseExplicitList,
227 UseTag,
228 Require,
229 RequireThenImport,
230 UseConstant,
231 DynamicRequire,
232 ManualImport,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub enum ImportSymbols {
243 Default,
244 None,
245 Explicit(Vec<String>),
246 Tags(Vec<String>),
247 Mixed { tags: Vec<String>, names: Vec<String> },
248 Dynamic,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253pub struct VisibleSymbol {
254 pub name: String,
256 pub entity_id: Option<EntityId>,
258 pub source: VisibleSymbolSource,
260 pub confidence: Confidence,
262 pub context: Option<VisibleSymbolContext>,
264}
265
266#[non_exhaustive]
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct VisibleSymbolContext {
274 pub source_module: Option<String>,
276 pub source_import_anchor_id: Option<AnchorId>,
278 pub source_export_anchor_id: Option<AnchorId>,
280}
281
282impl VisibleSymbolContext {
283 pub fn new(
288 source_module: Option<String>,
289 source_import_anchor_id: Option<AnchorId>,
290 source_export_anchor_id: Option<AnchorId>,
291 ) -> Self {
292 Self { source_module, source_import_anchor_id, source_export_anchor_id }
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
297pub enum VisibleSymbolSource {
298 LocalLexical,
299 LocalPackage,
300 ExplicitImport,
301 DefaultExport,
302 ExportTag,
303 Constant,
304 Generated,
305 External,
306 DynamicUnknown,
307}
308
309#[non_exhaustive]
317#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
318pub enum DefinitionRank {
319 ExactQualified,
320 SamePackage,
321 ExplicitImport,
322 DefaultExport,
323 WorkspaceCandidate,
324 Heuristic,
325}
326
327#[non_exhaustive]
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
333pub enum DefinitionRankReason {
334 ExactQualifiedName,
335 SamePackage,
336 ExplicitImport { module: String },
337 DefaultExport { module: String },
338 WorkspaceSymbol,
339 HeuristicNameMatch,
340}
341
342#[non_exhaustive]
347#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
348pub struct DefinitionCandidate {
349 pub entity_id: EntityId,
351 pub anchor_id: AnchorId,
353 pub canonical_name: String,
355 pub display_name: String,
357 pub package: Option<String>,
359 pub kind: EntityKind,
361 pub provenance: Provenance,
363 pub confidence: Confidence,
365 pub rank: DefinitionRank,
367 pub rank_reason: DefinitionRankReason,
369}
370
371impl DefinitionCandidate {
372 #[allow(clippy::too_many_arguments)] pub fn new(
378 entity_id: EntityId,
379 anchor_id: AnchorId,
380 canonical_name: String,
381 display_name: String,
382 package: Option<String>,
383 kind: EntityKind,
384 provenance: Provenance,
385 confidence: Confidence,
386 rank: DefinitionRank,
387 rank_reason: DefinitionRankReason,
388 ) -> Self {
389 Self {
390 entity_id,
391 anchor_id,
392 canonical_name,
393 display_name,
394 package,
395 kind,
396 provenance,
397 confidence,
398 rank,
399 rank_reason,
400 }
401 }
402}
403
404#[non_exhaustive]
411#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
412pub struct ReferenceEdge {
413 pub occurrence_id: OccurrenceId,
415 pub anchor_id: AnchorId,
417 pub file_id: FileId,
419 pub symbol_key: String,
421 pub target_candidates: Vec<EntityId>,
423 pub kind: OccurrenceKind,
425 pub provenance: Provenance,
427 pub confidence: Confidence,
429}
430
431#[non_exhaustive]
437#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
438pub struct PackageNode {
439 pub entity_id: EntityId,
441 pub name: String,
443 pub kind: PackageKind,
445 pub anchor_id: Option<AnchorId>,
447 pub file_id: Option<FileId>,
449}
450
451impl PackageNode {
452 pub fn new(
457 entity_id: EntityId,
458 name: String,
459 kind: PackageKind,
460 anchor_id: Option<AnchorId>,
461 file_id: Option<FileId>,
462 ) -> Self {
463 Self { entity_id, name, kind, anchor_id, file_id }
464 }
465}
466
467#[non_exhaustive]
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
470pub enum PackageKind {
471 Package,
473 Class,
475 Role,
477 External,
479}
480
481#[non_exhaustive]
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
487pub struct PackageEdge {
488 pub from_package: String,
490 pub to_package: String,
492 pub kind: PackageEdgeKind,
494 pub anchor_id: Option<AnchorId>,
496 pub provenance: Provenance,
498 pub confidence: Confidence,
500}
501
502impl PackageEdge {
503 pub fn new(
508 from_package: String,
509 to_package: String,
510 kind: PackageEdgeKind,
511 anchor_id: Option<AnchorId>,
512 provenance: Provenance,
513 confidence: Confidence,
514 ) -> Self {
515 Self { from_package, to_package, kind, anchor_id, provenance, confidence }
516 }
517}
518
519#[non_exhaustive]
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
522pub enum PackageEdgeKind {
523 Inherits,
525 ComposesRole,
527 DependsOn,
529}
530
531#[non_exhaustive]
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
536pub struct GeneratedMember {
537 pub entity_id: EntityId,
539 pub name: String,
541 pub kind: GeneratedMemberKind,
543 pub source_anchor_id: AnchorId,
545 pub package: String,
547 pub provenance: Provenance,
549 pub confidence: Confidence,
551}
552
553impl GeneratedMember {
554 #[allow(clippy::too_many_arguments)] pub fn new(
560 entity_id: EntityId,
561 name: String,
562 kind: GeneratedMemberKind,
563 source_anchor_id: AnchorId,
564 package: String,
565 provenance: Provenance,
566 confidence: Confidence,
567 ) -> Self {
568 Self { entity_id, name, kind, source_anchor_id, package, provenance, confidence }
569 }
570}
571
572#[non_exhaustive]
574#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
575pub enum GeneratedMemberKind {
576 Getter,
578 Setter,
580 Accessor,
582 Predicate,
584 Clearer,
586 Builder,
588 Constant,
590}
591
592#[non_exhaustive]
600#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601pub enum ValueShape {
602 Unknown,
604 Scalar,
606 ArrayRef,
608 HashRef,
610 CodeRef,
612 PackageName {
614 package: String,
616 },
617 Object {
619 package: String,
621 confidence: Confidence,
623 },
624}
625
626#[non_exhaustive]
630#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
631pub enum ProviderSurface {
632 Diagnostics,
633 Completion,
634 Hover,
635 Definition,
636 References,
637 Rename,
638 SafeDelete,
639 WorkspaceSymbols,
640 DocumentSymbols,
641 SemanticTokens,
642}
643
644#[non_exhaustive]
646#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
647pub enum ProviderFactSourceKind {
648 ParserSyntax,
650 LegacyWorkspace,
652 SemanticFact,
654 CompilerFact,
656 FrameworkAdapter,
658 DynamicBoundary,
660 Fallback,
662 Unknown,
664}
665
666#[non_exhaustive]
668#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
669pub enum ProviderFactFreshness {
670 Fresh,
671 Stale,
672 Unknown,
673 NotApplicable,
674}
675
676#[non_exhaustive]
678#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
679pub enum ProviderFallbackState {
680 Primary,
682 Shadow,
684 Fallback,
686 Unavailable,
688 Blocked,
690}
691
692#[non_exhaustive]
697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
698pub struct ProviderFactTrace {
699 pub surface: ProviderSurface,
701 pub source: ProviderFactSourceKind,
703 pub provenance: Provenance,
705 pub confidence: Confidence,
707 pub freshness: ProviderFactFreshness,
709 pub fallback_state: ProviderFallbackState,
711 pub source_hash: Option<String>,
713 pub anchor_id: Option<AnchorId>,
715 pub model_version: Option<u32>,
717}
718
719impl ProviderFactTrace {
720 #[allow(clippy::too_many_arguments)] pub fn new(
723 surface: ProviderSurface,
724 source: ProviderFactSourceKind,
725 provenance: Provenance,
726 confidence: Confidence,
727 freshness: ProviderFactFreshness,
728 fallback_state: ProviderFallbackState,
729 source_hash: Option<String>,
730 anchor_id: Option<AnchorId>,
731 model_version: Option<u32>,
732 ) -> Self {
733 Self {
734 surface,
735 source,
736 provenance,
737 confidence,
738 freshness,
739 fallback_state,
740 source_hash,
741 anchor_id,
742 model_version,
743 }
744 }
745}
746
747#[non_exhaustive]
754#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
755pub struct RenamePlan {
756 pub entity_id: EntityId,
758 pub old_name: String,
760 pub new_name: String,
762 pub edits: Vec<PlannedEdit>,
764 pub blockers: Vec<PlanBlocker>,
766 pub warnings: Vec<PlanWarning>,
768}
769
770impl RenamePlan {
771 pub fn new(
776 entity_id: EntityId,
777 old_name: String,
778 new_name: String,
779 edits: Vec<PlannedEdit>,
780 blockers: Vec<PlanBlocker>,
781 warnings: Vec<PlanWarning>,
782 ) -> Self {
783 Self { entity_id, old_name, new_name, edits, blockers, warnings }
784 }
785}
786
787#[non_exhaustive]
792#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
793pub struct SafeDeletePlan {
794 pub entity_id: EntityId,
796 pub name: String,
798 pub blockers: Vec<PlanBlocker>,
800 pub warnings: Vec<PlanWarning>,
802}
803
804impl SafeDeletePlan {
805 pub fn new(
810 entity_id: EntityId,
811 name: String,
812 blockers: Vec<PlanBlocker>,
813 warnings: Vec<PlanWarning>,
814 ) -> Self {
815 Self { entity_id, name, blockers, warnings }
816 }
817}
818
819#[non_exhaustive]
821#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
822pub struct PlanBlocker {
823 pub reason: PlanBlockerReason,
825 pub anchor_id: Option<AnchorId>,
827 pub description: String,
829}
830
831impl PlanBlocker {
832 pub fn new(
837 reason: PlanBlockerReason,
838 anchor_id: Option<AnchorId>,
839 description: String,
840 ) -> Self {
841 Self { reason, anchor_id, description }
842 }
843}
844
845#[non_exhaustive]
847#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
848pub enum PlanBlockerReason {
849 DynamicBoundary,
851 AmbiguousReference,
853 CrossModuleExport,
855 ImportedSymbol,
857 ExportedSymbol,
859 ReferencesExist,
861 GeneratedMember,
863 UnclassifiedOccurrence,
865}
866
867#[non_exhaustive]
869#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
870pub struct PlanWarning {
871 pub message: String,
873 pub anchor_id: Option<AnchorId>,
875}
876
877impl PlanWarning {
878 pub fn new(message: String, anchor_id: Option<AnchorId>) -> Self {
883 Self { message, anchor_id }
884 }
885}
886
887#[non_exhaustive]
889#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
890pub struct PlannedEdit {
891 pub anchor_id: AnchorId,
893 pub file_id: FileId,
895 pub category: PlannedEditCategory,
897 pub old_text: String,
899 pub new_text: String,
901}
902
903impl PlannedEdit {
904 pub fn new(
909 anchor_id: AnchorId,
910 file_id: FileId,
911 category: PlannedEditCategory,
912 old_text: String,
913 new_text: String,
914 ) -> Self {
915 Self { anchor_id, file_id, category, old_text, new_text }
916 }
917}
918
919#[non_exhaustive]
925#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
926pub enum PlannedEditCategory {
927 Definition,
929 Reference,
931 ImportList,
933 ExportList,
935}
936
937impl ReferenceEdge {
938 #[allow(clippy::too_many_arguments)] pub fn new(
944 occurrence_id: OccurrenceId,
945 anchor_id: AnchorId,
946 file_id: FileId,
947 symbol_key: String,
948 target_candidates: Vec<EntityId>,
949 kind: OccurrenceKind,
950 provenance: Provenance,
951 confidence: Confidence,
952 ) -> Self {
953 Self {
954 occurrence_id,
955 anchor_id,
956 file_id,
957 symbol_key,
958 target_candidates,
959 kind,
960 provenance,
961 confidence,
962 }
963 }
964}
965
966#[cfg(test)]
967mod tests {
968 use super::*;
969
970 #[test]
971 fn entity_fact_roundtrips_through_json() -> Result<(), serde_json::Error> {
972 let fact = EntityFact {
973 id: EntityId(100),
974 kind: EntityKind::Method,
975 canonical_name: "Foo::bar".to_string(),
976 anchor_id: Some(AnchorId(12)),
977 scope_id: Some(ScopeId(3)),
978 provenance: Provenance::SemanticAnalyzer,
979 confidence: Confidence::High,
980 };
981
982 let serialized = serde_json::to_string(&fact)?;
983 let decoded: EntityFact = serde_json::from_str(&serialized)?;
984 assert_eq!(decoded, fact);
985 Ok(())
986 }
987
988 #[test]
989 fn deterministic_debug_for_edge_fact() {
990 let fact = EdgeFact {
991 id: EdgeId(7),
992 kind: EdgeKind::Calls,
993 from_entity_id: EntityId(11),
994 to_entity_id: EntityId(22),
995 via_occurrence_id: Some(OccurrenceId(33)),
996 provenance: Provenance::ExactAst,
997 confidence: Confidence::Medium,
998 };
999
1000 assert_eq!(
1001 format!("{fact:?}"),
1002 "EdgeFact { id: EdgeId(7), kind: Calls, from_entity_id: EntityId(11), to_entity_id: EntityId(22), via_occurrence_id: Some(OccurrenceId(33)), provenance: ExactAst, confidence: Medium }"
1003 );
1004 }
1005
1006 #[test]
1007 fn pretty_json_for_anchor_fact_is_stable() -> Result<(), serde_json::Error> {
1008 let fact = AnchorFact {
1009 id: AnchorId(5),
1010 file_id: FileId(1),
1011 span_start_byte: 10,
1012 span_end_byte: 15,
1013 scope_id: None,
1014 provenance: Provenance::DesugaredAst,
1015 confidence: Confidence::Low,
1016 };
1017
1018 let json = serde_json::to_string_pretty(&fact)?;
1019 assert_eq!(
1020 json,
1021 "{\n \"id\": 5,\n \"file_id\": 1,\n \"span_start_byte\": 10,\n \"span_end_byte\": 15,\n \"scope_id\": null,\n \"provenance\": \"DesugaredAst\",\n \"confidence\": \"Low\"\n}"
1022 );
1023 Ok(())
1024 }
1025
1026 #[test]
1029 fn occurrence_fact_with_null_entity_id_roundtrips() -> Result<(), serde_json::Error> {
1030 let fact = OccurrenceFact {
1031 id: OccurrenceId(42),
1032 kind: OccurrenceKind::Call,
1033 entity_id: None,
1034 anchor_id: AnchorId(10),
1035 scope_id: Some(ScopeId(2)),
1036 provenance: Provenance::NameHeuristic,
1037 confidence: Confidence::Low,
1038 };
1039 let serialized = serde_json::to_string(&fact)?;
1040 let decoded: OccurrenceFact = serde_json::from_str(&serialized)?;
1041 assert_eq!(decoded, fact);
1042 assert!(
1044 serialized.contains("\"entity_id\":null"),
1045 "entity_id null must be explicit in JSON"
1046 );
1047 Ok(())
1048 }
1049
1050 #[test]
1054 fn id_u64_max_roundtrips() -> Result<(), serde_json::Error> {
1055 let id = EntityId(u64::MAX);
1056 let serialized = serde_json::to_string(&id)?;
1057 let decoded: EntityId = serde_json::from_str(&serialized)?;
1058 assert_eq!(decoded, id);
1059 Ok(())
1060 }
1061
1062 #[test]
1063 fn import_spec_roundtrips_through_json() -> Result<(), serde_json::Error> {
1064 let spec = ImportSpec {
1065 module: "Foo::Bar".to_string(),
1066 kind: ImportKind::RequireThenImport,
1067 symbols: ImportSymbols::Mixed {
1068 tags: vec!["all".to_string()],
1069 names: vec!["$X".to_string(), "@Y".to_string()],
1070 },
1071 provenance: Provenance::ImportExportInference,
1072 confidence: Confidence::Medium,
1073 file_id: None,
1074 anchor_id: None,
1075 scope_id: None,
1076 span_start_byte: None,
1077 };
1078
1079 let serialized = serde_json::to_string(&spec)?;
1080 let decoded: ImportSpec = serde_json::from_str(&serialized)?;
1081 assert_eq!(decoded, spec);
1082 Ok(())
1083 }
1084
1085 #[test]
1086 fn import_symbols_debug_is_deterministic() {
1087 let symbols = ImportSymbols::Mixed {
1088 tags: vec!["io".to_string(), "all".to_string()],
1089 names: vec!["open".to_string(), "close".to_string()],
1090 };
1091 assert_eq!(
1092 format!("{symbols:?}"),
1093 "Mixed { tags: [\"io\", \"all\"], names: [\"open\", \"close\"] }"
1094 );
1095 }
1096
1097 #[test]
1098 fn visible_symbol_pretty_json_is_stable() -> Result<(), serde_json::Error> {
1099 let visible = VisibleSymbol {
1100 name: "slurp".to_string(),
1101 entity_id: Some(EntityId(17)),
1102 source: VisibleSymbolSource::ExplicitImport,
1103 confidence: Confidence::High,
1104 context: None,
1105 };
1106
1107 let json = serde_json::to_string_pretty(&visible)?;
1108 assert_eq!(
1109 json,
1110 "{\n \"name\": \"slurp\",\n \"entity_id\": 17,\n \"source\": \"ExplicitImport\",\n \"confidence\": \"High\",\n \"context\": null\n}"
1111 );
1112 Ok(())
1113 }
1114
1115 #[test]
1117 fn reference_edge_roundtrips_through_json() -> Result<(), serde_json::Error> {
1118 let edge = ReferenceEdge {
1119 occurrence_id: OccurrenceId(50),
1120 anchor_id: AnchorId(20),
1121 file_id: FileId(3),
1122 symbol_key: "Foo::bar".to_string(),
1123 target_candidates: vec![EntityId(100), EntityId(200)],
1124 kind: OccurrenceKind::Call,
1125 provenance: Provenance::ExactAst,
1126 confidence: Confidence::High,
1127 };
1128
1129 let serialized = serde_json::to_string(&edge)?;
1130 let decoded: ReferenceEdge = serde_json::from_str(&serialized)?;
1131 assert_eq!(decoded, edge);
1132 Ok(())
1133 }
1134
1135 #[test]
1137 fn reference_edge_empty_candidates_roundtrips() -> Result<(), serde_json::Error> {
1138 let edge = ReferenceEdge {
1139 occurrence_id: OccurrenceId(51),
1140 anchor_id: AnchorId(21),
1141 file_id: FileId(4),
1142 symbol_key: "unknown_sub".to_string(),
1143 target_candidates: vec![],
1144 kind: OccurrenceKind::Reference,
1145 provenance: Provenance::NameHeuristic,
1146 confidence: Confidence::Low,
1147 };
1148
1149 let serialized = serde_json::to_string(&edge)?;
1150 let decoded: ReferenceEdge = serde_json::from_str(&serialized)?;
1151 assert_eq!(decoded, edge);
1152 assert!(
1154 serialized.contains("\"target_candidates\":[]"),
1155 "empty target_candidates must be an empty JSON array"
1156 );
1157 Ok(())
1158 }
1159
1160 #[test]
1162 fn definition_rank_roundtrips_through_json() -> Result<(), serde_json::Error> {
1163 let variants = [
1164 DefinitionRank::ExactQualified,
1165 DefinitionRank::SamePackage,
1166 DefinitionRank::ExplicitImport,
1167 DefinitionRank::DefaultExport,
1168 DefinitionRank::WorkspaceCandidate,
1169 DefinitionRank::Heuristic,
1170 ];
1171 for variant in &variants {
1172 let serialized = serde_json::to_string(variant)?;
1173 let decoded: DefinitionRank = serde_json::from_str(&serialized)?;
1174 assert_eq!(&decoded, variant);
1175 }
1176 Ok(())
1177 }
1178
1179 #[test]
1181 fn definition_rank_ordering_matches_design() {
1182 assert!(DefinitionRank::ExactQualified < DefinitionRank::SamePackage);
1183 assert!(DefinitionRank::SamePackage < DefinitionRank::ExplicitImport);
1184 assert!(DefinitionRank::ExplicitImport < DefinitionRank::DefaultExport);
1185 assert!(DefinitionRank::DefaultExport < DefinitionRank::WorkspaceCandidate);
1186 assert!(DefinitionRank::WorkspaceCandidate < DefinitionRank::Heuristic);
1187 }
1188
1189 #[test]
1192 fn definition_rank_reason_roundtrips_through_json() -> Result<(), serde_json::Error> {
1193 let reasons = [
1194 DefinitionRankReason::ExactQualifiedName,
1195 DefinitionRankReason::SamePackage,
1196 DefinitionRankReason::ExplicitImport { module: "Foo::Bar".to_string() },
1197 DefinitionRankReason::DefaultExport { module: "Baz::Qux".to_string() },
1198 DefinitionRankReason::WorkspaceSymbol,
1199 DefinitionRankReason::HeuristicNameMatch,
1200 ];
1201 for reason in &reasons {
1202 let serialized = serde_json::to_string(reason)?;
1203 let decoded: DefinitionRankReason = serde_json::from_str(&serialized)?;
1204 assert_eq!(&decoded, reason);
1205 }
1206 Ok(())
1207 }
1208
1209 #[test]
1211 fn definition_candidate_roundtrips_through_json() -> Result<(), serde_json::Error> {
1212 let candidate = DefinitionCandidate {
1213 entity_id: EntityId(300),
1214 anchor_id: AnchorId(40),
1215 canonical_name: "Foo::Bar::baz".to_string(),
1216 display_name: "baz".to_string(),
1217 package: Some("Foo::Bar".to_string()),
1218 kind: EntityKind::Subroutine,
1219 provenance: Provenance::ExactAst,
1220 confidence: Confidence::High,
1221 rank: DefinitionRank::ExactQualified,
1222 rank_reason: DefinitionRankReason::ExactQualifiedName,
1223 };
1224
1225 let serialized = serde_json::to_string(&candidate)?;
1226 let decoded: DefinitionCandidate = serde_json::from_str(&serialized)?;
1227 assert_eq!(decoded, candidate);
1228 Ok(())
1229 }
1230
1231 #[test]
1233 fn definition_candidate_none_package_roundtrips() -> Result<(), serde_json::Error> {
1234 let candidate = DefinitionCandidate {
1235 entity_id: EntityId(301),
1236 anchor_id: AnchorId(41),
1237 canonical_name: "main::helper".to_string(),
1238 display_name: "helper".to_string(),
1239 package: None,
1240 kind: EntityKind::Subroutine,
1241 provenance: Provenance::NameHeuristic,
1242 confidence: Confidence::Low,
1243 rank: DefinitionRank::Heuristic,
1244 rank_reason: DefinitionRankReason::HeuristicNameMatch,
1245 };
1246
1247 let serialized = serde_json::to_string(&candidate)?;
1248 let decoded: DefinitionCandidate = serde_json::from_str(&serialized)?;
1249 assert_eq!(decoded, candidate);
1250 assert!(serialized.contains("\"package\":null"), "package null must be explicit in JSON");
1252 Ok(())
1253 }
1254
1255 #[test]
1257 fn definition_candidate_import_reason_roundtrips() -> Result<(), serde_json::Error> {
1258 let candidate = DefinitionCandidate {
1259 entity_id: EntityId(302),
1260 anchor_id: AnchorId(42),
1261 canonical_name: "List::Util::first".to_string(),
1262 display_name: "first".to_string(),
1263 package: Some("List::Util".to_string()),
1264 kind: EntityKind::Subroutine,
1265 provenance: Provenance::ImportExportInference,
1266 confidence: Confidence::Medium,
1267 rank: DefinitionRank::ExplicitImport,
1268 rank_reason: DefinitionRankReason::ExplicitImport { module: "List::Util".to_string() },
1269 };
1270
1271 let serialized = serde_json::to_string(&candidate)?;
1272 let decoded: DefinitionCandidate = serde_json::from_str(&serialized)?;
1273 assert_eq!(decoded, candidate);
1274 Ok(())
1275 }
1276
1277 #[test]
1279 fn provider_fact_trace_roundtrips_through_json() -> Result<(), serde_json::Error> {
1280 let trace = ProviderFactTrace::new(
1281 ProviderSurface::Completion,
1282 ProviderFactSourceKind::CompilerFact,
1283 Provenance::ImportExportInference,
1284 Confidence::High,
1285 ProviderFactFreshness::Fresh,
1286 ProviderFallbackState::Shadow,
1287 Some("fixture-source-sha".to_string()),
1288 Some(AnchorId(10)),
1289 Some(1),
1290 );
1291
1292 let serialized = serde_json::to_string(&trace)?;
1293 let decoded: ProviderFactTrace = serde_json::from_str(&serialized)?;
1294 assert_eq!(decoded, trace);
1295 Ok(())
1296 }
1297
1298 #[test]
1300 fn provider_fact_trace_optional_metadata_roundtrips() -> Result<(), serde_json::Error> {
1301 let trace = ProviderFactTrace::new(
1302 ProviderSurface::Diagnostics,
1303 ProviderFactSourceKind::Fallback,
1304 Provenance::SearchFallback,
1305 Confidence::Low,
1306 ProviderFactFreshness::NotApplicable,
1307 ProviderFallbackState::Fallback,
1308 None,
1309 None,
1310 None,
1311 );
1312
1313 let serialized = serde_json::to_string(&trace)?;
1314 let decoded: ProviderFactTrace = serde_json::from_str(&serialized)?;
1315 assert_eq!(decoded, trace);
1316 assert!(
1317 serialized.contains("\"source_hash\":null")
1318 && serialized.contains("\"anchor_id\":null")
1319 && serialized.contains("\"model_version\":null"),
1320 "optional trace metadata should remain explicit for downstream consumers"
1321 );
1322 Ok(())
1323 }
1324
1325 #[test]
1327 fn provider_fact_trace_enums_roundtrip_through_json() -> Result<(), serde_json::Error> {
1328 for surface in [
1329 ProviderSurface::Diagnostics,
1330 ProviderSurface::Completion,
1331 ProviderSurface::Hover,
1332 ProviderSurface::Definition,
1333 ProviderSurface::References,
1334 ProviderSurface::Rename,
1335 ProviderSurface::SafeDelete,
1336 ProviderSurface::WorkspaceSymbols,
1337 ProviderSurface::DocumentSymbols,
1338 ProviderSurface::SemanticTokens,
1339 ] {
1340 let serialized = serde_json::to_string(&surface)?;
1341 let decoded: ProviderSurface = serde_json::from_str(&serialized)?;
1342 assert_eq!(decoded, surface);
1343 }
1344
1345 for source in [
1346 ProviderFactSourceKind::ParserSyntax,
1347 ProviderFactSourceKind::LegacyWorkspace,
1348 ProviderFactSourceKind::SemanticFact,
1349 ProviderFactSourceKind::CompilerFact,
1350 ProviderFactSourceKind::FrameworkAdapter,
1351 ProviderFactSourceKind::DynamicBoundary,
1352 ProviderFactSourceKind::Fallback,
1353 ProviderFactSourceKind::Unknown,
1354 ] {
1355 let serialized = serde_json::to_string(&source)?;
1356 let decoded: ProviderFactSourceKind = serde_json::from_str(&serialized)?;
1357 assert_eq!(decoded, source);
1358 }
1359
1360 for freshness in [
1361 ProviderFactFreshness::Fresh,
1362 ProviderFactFreshness::Stale,
1363 ProviderFactFreshness::Unknown,
1364 ProviderFactFreshness::NotApplicable,
1365 ] {
1366 let serialized = serde_json::to_string(&freshness)?;
1367 let decoded: ProviderFactFreshness = serde_json::from_str(&serialized)?;
1368 assert_eq!(decoded, freshness);
1369 }
1370
1371 for fallback_state in [
1372 ProviderFallbackState::Primary,
1373 ProviderFallbackState::Shadow,
1374 ProviderFallbackState::Fallback,
1375 ProviderFallbackState::Unavailable,
1376 ProviderFallbackState::Blocked,
1377 ] {
1378 let serialized = serde_json::to_string(&fallback_state)?;
1379 let decoded: ProviderFallbackState = serde_json::from_str(&serialized)?;
1380 assert_eq!(decoded, fallback_state);
1381 }
1382
1383 Ok(())
1384 }
1385
1386 #[test]
1390 fn package_edge_roundtrips_through_json() -> Result<(), serde_json::Error> {
1391 let edge = PackageEdge {
1392 from_package: "Child".to_string(),
1393 to_package: "Parent".to_string(),
1394 kind: PackageEdgeKind::Inherits,
1395 anchor_id: Some(AnchorId(99)),
1396 provenance: Provenance::ExactAst,
1397 confidence: Confidence::High,
1398 };
1399
1400 let serialized = serde_json::to_string(&edge)?;
1401 let decoded: PackageEdge = serde_json::from_str(&serialized)?;
1402 assert_eq!(decoded, edge);
1403 Ok(())
1404 }
1405
1406 #[test]
1408 fn package_edge_kind_roundtrips_through_json() -> Result<(), serde_json::Error> {
1409 let variants =
1410 [PackageEdgeKind::Inherits, PackageEdgeKind::ComposesRole, PackageEdgeKind::DependsOn];
1411 for variant in &variants {
1412 let serialized = serde_json::to_string(variant)?;
1413 let decoded: PackageEdgeKind = serde_json::from_str(&serialized)?;
1414 assert_eq!(&decoded, variant);
1415 }
1416 Ok(())
1417 }
1418
1419 #[test]
1421 fn package_node_roundtrips_through_json() -> Result<(), serde_json::Error> {
1422 let node = PackageNode {
1423 entity_id: EntityId(500),
1424 name: "My::Package".to_string(),
1425 kind: PackageKind::Class,
1426 anchor_id: Some(AnchorId(10)),
1427 file_id: Some(FileId(2)),
1428 };
1429
1430 let serialized = serde_json::to_string(&node)?;
1431 let decoded: PackageNode = serde_json::from_str(&serialized)?;
1432 assert_eq!(decoded, node);
1433 Ok(())
1434 }
1435
1436 #[test]
1438 fn package_kind_roundtrips_through_json() -> Result<(), serde_json::Error> {
1439 let variants =
1440 [PackageKind::Package, PackageKind::Class, PackageKind::Role, PackageKind::External];
1441 for variant in &variants {
1442 let serialized = serde_json::to_string(variant)?;
1443 let decoded: PackageKind = serde_json::from_str(&serialized)?;
1444 assert_eq!(&decoded, variant);
1445 }
1446 Ok(())
1447 }
1448
1449 #[test]
1451 fn package_edge_none_anchor_roundtrips() -> Result<(), serde_json::Error> {
1452 let edge = PackageEdge {
1453 from_package: "App::Worker".to_string(),
1454 to_package: "Unknown::External".to_string(),
1455 kind: PackageEdgeKind::DependsOn,
1456 anchor_id: None,
1457 provenance: Provenance::NameHeuristic,
1458 confidence: Confidence::Low,
1459 };
1460
1461 let serialized = serde_json::to_string(&edge)?;
1462 let decoded: PackageEdge = serde_json::from_str(&serialized)?;
1463 assert_eq!(decoded, edge);
1464 assert!(
1465 serialized.contains("\"anchor_id\":null"),
1466 "anchor_id null must be explicit in JSON"
1467 );
1468 Ok(())
1469 }
1470
1471 #[test]
1475 fn generated_member_roundtrips_through_json() -> Result<(), serde_json::Error> {
1476 let member = GeneratedMember {
1477 entity_id: EntityId(600),
1478 name: "username".to_string(),
1479 kind: GeneratedMemberKind::Getter,
1480 source_anchor_id: AnchorId(50),
1481 package: "MyApp::User".to_string(),
1482 provenance: Provenance::FrameworkSynthesis,
1483 confidence: Confidence::Medium,
1484 };
1485
1486 let serialized = serde_json::to_string(&member)?;
1487 let decoded: GeneratedMember = serde_json::from_str(&serialized)?;
1488 assert_eq!(decoded, member);
1489 Ok(())
1490 }
1491
1492 #[test]
1494 fn generated_member_kind_roundtrips_through_json() -> Result<(), serde_json::Error> {
1495 let variants = [
1496 GeneratedMemberKind::Getter,
1497 GeneratedMemberKind::Setter,
1498 GeneratedMemberKind::Accessor,
1499 GeneratedMemberKind::Predicate,
1500 GeneratedMemberKind::Clearer,
1501 GeneratedMemberKind::Builder,
1502 GeneratedMemberKind::Constant,
1503 ];
1504 for variant in &variants {
1505 let serialized = serde_json::to_string(variant)?;
1506 let decoded: GeneratedMemberKind = serde_json::from_str(&serialized)?;
1507 assert_eq!(&decoded, variant);
1508 }
1509 Ok(())
1510 }
1511
1512 #[test]
1514 fn generated_member_new_constructor() -> Result<(), serde_json::Error> {
1515 let via_new = GeneratedMember::new(
1516 EntityId(700),
1517 "email".to_string(),
1518 GeneratedMemberKind::Accessor,
1519 AnchorId(60),
1520 "MyApp::User".to_string(),
1521 Provenance::FrameworkSynthesis,
1522 Confidence::Medium,
1523 );
1524 let via_literal = GeneratedMember {
1525 entity_id: EntityId(700),
1526 name: "email".to_string(),
1527 kind: GeneratedMemberKind::Accessor,
1528 source_anchor_id: AnchorId(60),
1529 package: "MyApp::User".to_string(),
1530 provenance: Provenance::FrameworkSynthesis,
1531 confidence: Confidence::Medium,
1532 };
1533 assert_eq!(via_new, via_literal);
1534 Ok(())
1535 }
1536
1537 #[test]
1541 fn value_shape_unknown_roundtrips() -> Result<(), serde_json::Error> {
1542 let shape = ValueShape::Unknown;
1543 let serialized = serde_json::to_string(&shape)?;
1544 let decoded: ValueShape = serde_json::from_str(&serialized)?;
1545 assert_eq!(decoded, shape);
1546 Ok(())
1547 }
1548
1549 #[test]
1551 fn value_shape_object_roundtrips() -> Result<(), serde_json::Error> {
1552 let shape =
1553 ValueShape::Object { package: "My::Class".to_string(), confidence: Confidence::High };
1554 let serialized = serde_json::to_string(&shape)?;
1555 let decoded: ValueShape = serde_json::from_str(&serialized)?;
1556 assert_eq!(decoded, shape);
1557 Ok(())
1558 }
1559
1560 #[test]
1562 fn value_shape_package_name_roundtrips() -> Result<(), serde_json::Error> {
1563 let shape = ValueShape::PackageName { package: "Foo::Bar".to_string() };
1564 let serialized = serde_json::to_string(&shape)?;
1565 let decoded: ValueShape = serde_json::from_str(&serialized)?;
1566 assert_eq!(decoded, shape);
1567 Ok(())
1568 }
1569
1570 #[test]
1572 fn value_shape_all_variants_roundtrip() -> Result<(), serde_json::Error> {
1573 let variants: Vec<ValueShape> = vec![
1574 ValueShape::Unknown,
1575 ValueShape::Scalar,
1576 ValueShape::ArrayRef,
1577 ValueShape::HashRef,
1578 ValueShape::CodeRef,
1579 ValueShape::PackageName { package: "Foo".to_string() },
1580 ValueShape::Object { package: "Bar::Baz".to_string(), confidence: Confidence::Low },
1581 ];
1582 for shape in &variants {
1583 let serialized = serde_json::to_string(shape)?;
1584 let decoded: ValueShape = serde_json::from_str(&serialized)?;
1585 assert_eq!(&decoded, shape);
1586 }
1587 Ok(())
1588 }
1589
1590 #[test]
1594 fn rename_plan_roundtrips_through_json() -> Result<(), serde_json::Error> {
1595 let plan = RenamePlan {
1596 entity_id: EntityId(400),
1597 old_name: "foo".to_string(),
1598 new_name: "bar".to_string(),
1599 edits: vec![
1600 PlannedEdit {
1601 anchor_id: AnchorId(80),
1602 file_id: FileId(1),
1603 category: PlannedEditCategory::Definition,
1604 old_text: "foo".to_string(),
1605 new_text: "bar".to_string(),
1606 },
1607 PlannedEdit {
1608 anchor_id: AnchorId(81),
1609 file_id: FileId(2),
1610 category: PlannedEditCategory::Reference,
1611 old_text: "foo".to_string(),
1612 new_text: "bar".to_string(),
1613 },
1614 ],
1615 blockers: vec![PlanBlocker {
1616 reason: PlanBlockerReason::DynamicBoundary,
1617 anchor_id: Some(AnchorId(90)),
1618 description: "reference crosses eval boundary".to_string(),
1619 }],
1620 warnings: vec![PlanWarning {
1621 message: "symbol also appears in comments".to_string(),
1622 anchor_id: None,
1623 }],
1624 };
1625
1626 let serialized = serde_json::to_string(&plan)?;
1627 let decoded: RenamePlan = serde_json::from_str(&serialized)?;
1628 assert_eq!(decoded, plan);
1629 Ok(())
1630 }
1631
1632 #[test]
1634 fn rename_plan_empty_collections_roundtrip() -> Result<(), serde_json::Error> {
1635 let plan = RenamePlan {
1636 entity_id: EntityId(401),
1637 old_name: "x".to_string(),
1638 new_name: "y".to_string(),
1639 edits: vec![],
1640 blockers: vec![],
1641 warnings: vec![],
1642 };
1643
1644 let serialized = serde_json::to_string(&plan)?;
1645 let decoded: RenamePlan = serde_json::from_str(&serialized)?;
1646 assert_eq!(decoded, plan);
1647 assert!(serialized.contains("\"edits\":[]"), "empty edits must be an empty JSON array");
1648 assert!(
1649 serialized.contains("\"blockers\":[]"),
1650 "empty blockers must be an empty JSON array"
1651 );
1652 assert!(
1653 serialized.contains("\"warnings\":[]"),
1654 "empty warnings must be an empty JSON array"
1655 );
1656 Ok(())
1657 }
1658
1659 #[test]
1661 fn rename_plan_new_constructor() {
1662 let via_new = RenamePlan::new(
1663 EntityId(402),
1664 "old".to_string(),
1665 "new".to_string(),
1666 vec![],
1667 vec![],
1668 vec![],
1669 );
1670 let via_literal = RenamePlan {
1671 entity_id: EntityId(402),
1672 old_name: "old".to_string(),
1673 new_name: "new".to_string(),
1674 edits: vec![],
1675 blockers: vec![],
1676 warnings: vec![],
1677 };
1678 assert_eq!(via_new, via_literal);
1679 }
1680
1681 #[test]
1683 fn safe_delete_plan_roundtrips_through_json() -> Result<(), serde_json::Error> {
1684 let plan = SafeDeletePlan {
1685 entity_id: EntityId(500),
1686 name: "unused_sub".to_string(),
1687 blockers: vec![
1688 PlanBlocker {
1689 reason: PlanBlockerReason::ReferencesExist,
1690 anchor_id: Some(AnchorId(70)),
1691 description: "3 references remain".to_string(),
1692 },
1693 PlanBlocker {
1694 reason: PlanBlockerReason::ExportedSymbol,
1695 anchor_id: Some(AnchorId(71)),
1696 description: "symbol in @EXPORT_OK".to_string(),
1697 },
1698 ],
1699 warnings: vec![],
1700 };
1701
1702 let serialized = serde_json::to_string(&plan)?;
1703 let decoded: SafeDeletePlan = serde_json::from_str(&serialized)?;
1704 assert_eq!(decoded, plan);
1705 Ok(())
1706 }
1707
1708 #[test]
1710 fn safe_delete_plan_no_blockers_roundtrips() -> Result<(), serde_json::Error> {
1711 let plan = SafeDeletePlan {
1712 entity_id: EntityId(501),
1713 name: "dead_code".to_string(),
1714 blockers: vec![],
1715 warnings: vec![PlanWarning {
1716 message: "symbol appears in pod documentation".to_string(),
1717 anchor_id: Some(AnchorId(72)),
1718 }],
1719 };
1720
1721 let serialized = serde_json::to_string(&plan)?;
1722 let decoded: SafeDeletePlan = serde_json::from_str(&serialized)?;
1723 assert_eq!(decoded, plan);
1724 Ok(())
1725 }
1726
1727 #[test]
1729 fn safe_delete_plan_new_constructor() {
1730 let via_new = SafeDeletePlan::new(EntityId(502), "helper".to_string(), vec![], vec![]);
1731 let via_literal = SafeDeletePlan {
1732 entity_id: EntityId(502),
1733 name: "helper".to_string(),
1734 blockers: vec![],
1735 warnings: vec![],
1736 };
1737 assert_eq!(via_new, via_literal);
1738 }
1739
1740 #[test]
1742 fn plan_blocker_reason_roundtrips_through_json() -> Result<(), serde_json::Error> {
1743 let variants = [
1744 PlanBlockerReason::DynamicBoundary,
1745 PlanBlockerReason::AmbiguousReference,
1746 PlanBlockerReason::CrossModuleExport,
1747 PlanBlockerReason::ImportedSymbol,
1748 PlanBlockerReason::ExportedSymbol,
1749 PlanBlockerReason::ReferencesExist,
1750 PlanBlockerReason::GeneratedMember,
1751 PlanBlockerReason::UnclassifiedOccurrence,
1752 ];
1753 for variant in &variants {
1754 let serialized = serde_json::to_string(variant)?;
1755 let decoded: PlanBlockerReason = serde_json::from_str(&serialized)?;
1756 assert_eq!(&decoded, variant);
1757 }
1758 Ok(())
1759 }
1760
1761 #[test]
1763 fn plan_blocker_none_anchor_roundtrips() -> Result<(), serde_json::Error> {
1764 let blocker = PlanBlocker {
1765 reason: PlanBlockerReason::GeneratedMember,
1766 anchor_id: None,
1767 description: "generated accessor without edit plan".to_string(),
1768 };
1769
1770 let serialized = serde_json::to_string(&blocker)?;
1771 let decoded: PlanBlocker = serde_json::from_str(&serialized)?;
1772 assert_eq!(decoded, blocker);
1773 assert!(
1774 serialized.contains("\"anchor_id\":null"),
1775 "anchor_id null must be explicit in JSON"
1776 );
1777 Ok(())
1778 }
1779
1780 #[test]
1782 fn plan_blocker_new_constructor() {
1783 let via_new = PlanBlocker::new(
1784 PlanBlockerReason::ImportedSymbol,
1785 Some(AnchorId(99)),
1786 "imported by other file".to_string(),
1787 );
1788 let via_literal = PlanBlocker {
1789 reason: PlanBlockerReason::ImportedSymbol,
1790 anchor_id: Some(AnchorId(99)),
1791 description: "imported by other file".to_string(),
1792 };
1793 assert_eq!(via_new, via_literal);
1794 }
1795
1796 #[test]
1798 fn plan_warning_roundtrips_through_json() -> Result<(), serde_json::Error> {
1799 let warning = PlanWarning {
1800 message: "symbol also used in string interpolation".to_string(),
1801 anchor_id: Some(AnchorId(85)),
1802 };
1803
1804 let serialized = serde_json::to_string(&warning)?;
1805 let decoded: PlanWarning = serde_json::from_str(&serialized)?;
1806 assert_eq!(decoded, warning);
1807 Ok(())
1808 }
1809
1810 #[test]
1812 fn plan_warning_new_constructor() {
1813 let via_new = PlanWarning::new("check pod docs".to_string(), None);
1814 let via_literal = PlanWarning { message: "check pod docs".to_string(), anchor_id: None };
1815 assert_eq!(via_new, via_literal);
1816 }
1817
1818 #[test]
1820 fn planned_edit_roundtrips_through_json() -> Result<(), serde_json::Error> {
1821 let edit = PlannedEdit {
1822 anchor_id: AnchorId(60),
1823 file_id: FileId(5),
1824 category: PlannedEditCategory::ImportList,
1825 old_text: "foo".to_string(),
1826 new_text: "bar".to_string(),
1827 };
1828
1829 let serialized = serde_json::to_string(&edit)?;
1830 let decoded: PlannedEdit = serde_json::from_str(&serialized)?;
1831 assert_eq!(decoded, edit);
1832 Ok(())
1833 }
1834
1835 #[test]
1837 fn planned_edit_new_constructor() {
1838 let via_new = PlannedEdit::new(
1839 AnchorId(61),
1840 FileId(6),
1841 PlannedEditCategory::ExportList,
1842 "old_sym".to_string(),
1843 "new_sym".to_string(),
1844 );
1845 let via_literal = PlannedEdit {
1846 anchor_id: AnchorId(61),
1847 file_id: FileId(6),
1848 category: PlannedEditCategory::ExportList,
1849 old_text: "old_sym".to_string(),
1850 new_text: "new_sym".to_string(),
1851 };
1852 assert_eq!(via_new, via_literal);
1853 }
1854
1855 #[test]
1857 fn planned_edit_category_roundtrips_through_json() -> Result<(), serde_json::Error> {
1858 let variants = [
1859 PlannedEditCategory::Definition,
1860 PlannedEditCategory::Reference,
1861 PlannedEditCategory::ImportList,
1862 PlannedEditCategory::ExportList,
1863 ];
1864 for variant in &variants {
1865 let serialized = serde_json::to_string(variant)?;
1866 let decoded: PlannedEditCategory = serde_json::from_str(&serialized)?;
1867 assert_eq!(&decoded, variant);
1868 }
1869 Ok(())
1870 }
1871}