use serde::{Deserialize, Serialize};
macro_rules! id_newtype {
($name:ident) => {
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct $name(pub u64);
};
}
id_newtype!(FileId);
id_newtype!(ScopeId);
id_newtype!(EntityId);
id_newtype!(AnchorId);
id_newtype!(OccurrenceId);
id_newtype!(EdgeId);
id_newtype!(DiagnosticId);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum EntityKind {
Package,
Class,
Role,
Subroutine,
Method,
Variable,
Constant,
Field,
Label,
Format,
Module,
GeneratedMember,
ExternalSymbol,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum OccurrenceKind {
Definition,
Reference,
Read,
Write,
Call,
MethodCall,
StaticMethodCall,
CoderefReference,
TypeglobReference,
Import,
Export,
Inheritance,
RoleComposition,
GeneratedUse,
DynamicBoundary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum EdgeKind {
Defines,
References,
Reads,
Writes,
Calls,
ImportsModule,
ImportsSymbol,
ExportsSymbol,
ExportsGroup,
Inherits,
ComposesRole,
MemberOf,
GeneratedFrom,
AliasOf,
DependsOn,
DynamicBoundary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Provenance {
ExactAst,
DesugaredAst,
SemanticAnalyzer,
FrameworkSynthesis,
ImportExportInference,
PragmaInference,
NameHeuristic,
SearchFallback,
DynamicBoundary,
LiteralRequireImport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Confidence {
High,
Medium,
Low,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnchorFact {
pub id: AnchorId,
pub file_id: FileId,
pub span_start_byte: u32,
pub span_end_byte: u32,
pub scope_id: Option<ScopeId>,
pub provenance: Provenance,
pub confidence: Confidence,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EntityFact {
pub id: EntityId,
pub kind: EntityKind,
pub canonical_name: String,
pub anchor_id: Option<AnchorId>,
pub scope_id: Option<ScopeId>,
pub provenance: Provenance,
pub confidence: Confidence,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OccurrenceFact {
pub id: OccurrenceId,
pub kind: OccurrenceKind,
pub entity_id: Option<EntityId>,
pub anchor_id: AnchorId,
pub scope_id: Option<ScopeId>,
pub provenance: Provenance,
pub confidence: Confidence,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EdgeFact {
pub id: EdgeId,
pub kind: EdgeKind,
pub from_entity_id: EntityId,
pub to_entity_id: EntityId,
pub via_occurrence_id: Option<OccurrenceId>,
pub provenance: Provenance,
pub confidence: Confidence,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiagnosticFact {
pub id: DiagnosticId,
pub code: Option<String>,
pub message: String,
pub primary_anchor_id: AnchorId,
pub related_anchor_ids: Vec<AnchorId>,
pub scope_id: Option<ScopeId>,
pub provenance: Provenance,
pub confidence: Confidence,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExportSet {
pub default_exports: Vec<String>,
pub optional_exports: Vec<String>,
pub tags: Vec<ExportTag>,
pub provenance: Provenance,
pub confidence: Confidence,
pub module_name: Option<String>,
pub anchor_id: Option<AnchorId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExportTag {
pub name: String,
pub members: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ImportSpec {
pub module: String,
pub kind: ImportKind,
pub symbols: ImportSymbols,
pub provenance: Provenance,
pub confidence: Confidence,
pub file_id: Option<FileId>,
pub anchor_id: Option<AnchorId>,
pub scope_id: Option<ScopeId>,
pub span_start_byte: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ImportKind {
Use,
UseEmpty,
UseExplicitList,
UseTag,
Require,
RequireThenImport,
UseConstant,
DynamicRequire,
ManualImport,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ImportSymbols {
Default,
None,
Explicit(Vec<String>),
Tags(Vec<String>),
Mixed { tags: Vec<String>, names: Vec<String> },
Dynamic,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VisibleSymbol {
pub name: String,
pub entity_id: Option<EntityId>,
pub source: VisibleSymbolSource,
pub confidence: Confidence,
pub context: Option<VisibleSymbolContext>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VisibleSymbolContext {
pub source_module: Option<String>,
pub source_import_anchor_id: Option<AnchorId>,
pub source_export_anchor_id: Option<AnchorId>,
}
impl VisibleSymbolContext {
pub fn new(
source_module: Option<String>,
source_import_anchor_id: Option<AnchorId>,
source_export_anchor_id: Option<AnchorId>,
) -> Self {
Self { source_module, source_import_anchor_id, source_export_anchor_id }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum VisibleSymbolSource {
LocalLexical,
LocalPackage,
ExplicitImport,
DefaultExport,
ExportTag,
Constant,
Generated,
External,
DynamicUnknown,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum DefinitionRank {
ExactQualified,
SamePackage,
ExplicitImport,
DefaultExport,
WorkspaceCandidate,
Heuristic,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DefinitionRankReason {
ExactQualifiedName,
SamePackage,
ExplicitImport { module: String },
DefaultExport { module: String },
WorkspaceSymbol,
HeuristicNameMatch,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DefinitionCandidate {
pub entity_id: EntityId,
pub anchor_id: AnchorId,
pub canonical_name: String,
pub display_name: String,
pub package: Option<String>,
pub kind: EntityKind,
pub provenance: Provenance,
pub confidence: Confidence,
pub rank: DefinitionRank,
pub rank_reason: DefinitionRankReason,
}
impl DefinitionCandidate {
#[allow(clippy::too_many_arguments)] pub fn new(
entity_id: EntityId,
anchor_id: AnchorId,
canonical_name: String,
display_name: String,
package: Option<String>,
kind: EntityKind,
provenance: Provenance,
confidence: Confidence,
rank: DefinitionRank,
rank_reason: DefinitionRankReason,
) -> Self {
Self {
entity_id,
anchor_id,
canonical_name,
display_name,
package,
kind,
provenance,
confidence,
rank,
rank_reason,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReferenceEdge {
pub occurrence_id: OccurrenceId,
pub anchor_id: AnchorId,
pub file_id: FileId,
pub symbol_key: String,
pub target_candidates: Vec<EntityId>,
pub kind: OccurrenceKind,
pub provenance: Provenance,
pub confidence: Confidence,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageNode {
pub entity_id: EntityId,
pub name: String,
pub kind: PackageKind,
pub anchor_id: Option<AnchorId>,
pub file_id: Option<FileId>,
}
impl PackageNode {
pub fn new(
entity_id: EntityId,
name: String,
kind: PackageKind,
anchor_id: Option<AnchorId>,
file_id: Option<FileId>,
) -> Self {
Self { entity_id, name, kind, anchor_id, file_id }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackageKind {
Package,
Class,
Role,
External,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageEdge {
pub from_package: String,
pub to_package: String,
pub kind: PackageEdgeKind,
pub anchor_id: Option<AnchorId>,
pub provenance: Provenance,
pub confidence: Confidence,
}
impl PackageEdge {
pub fn new(
from_package: String,
to_package: String,
kind: PackageEdgeKind,
anchor_id: Option<AnchorId>,
provenance: Provenance,
confidence: Confidence,
) -> Self {
Self { from_package, to_package, kind, anchor_id, provenance, confidence }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PackageEdgeKind {
Inherits,
ComposesRole,
DependsOn,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeneratedMember {
pub entity_id: EntityId,
pub name: String,
pub kind: GeneratedMemberKind,
pub source_anchor_id: AnchorId,
pub package: String,
pub provenance: Provenance,
pub confidence: Confidence,
}
impl GeneratedMember {
#[allow(clippy::too_many_arguments)] pub fn new(
entity_id: EntityId,
name: String,
kind: GeneratedMemberKind,
source_anchor_id: AnchorId,
package: String,
provenance: Provenance,
confidence: Confidence,
) -> Self {
Self { entity_id, name, kind, source_anchor_id, package, provenance, confidence }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GeneratedMemberKind {
Getter,
Setter,
Accessor,
Predicate,
Clearer,
Builder,
Constant,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ValueShape {
Unknown,
Scalar,
ArrayRef,
HashRef,
CodeRef,
PackageName {
package: String,
},
Object {
package: String,
confidence: Confidence,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum ProviderSurface {
Diagnostics,
Completion,
Hover,
Definition,
References,
Rename,
SafeDelete,
WorkspaceSymbols,
DocumentSymbols,
SemanticTokens,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum ProviderFactSourceKind {
ParserSyntax,
LegacyWorkspace,
SemanticFact,
CompilerFact,
FrameworkAdapter,
DynamicBoundary,
Fallback,
Unknown,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum ProviderFactFreshness {
Fresh,
Stale,
Unknown,
NotApplicable,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum ProviderFallbackState {
Primary,
Shadow,
Fallback,
Unavailable,
Blocked,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderFactTrace {
pub surface: ProviderSurface,
pub source: ProviderFactSourceKind,
pub provenance: Provenance,
pub confidence: Confidence,
pub freshness: ProviderFactFreshness,
pub fallback_state: ProviderFallbackState,
pub source_hash: Option<String>,
pub anchor_id: Option<AnchorId>,
pub model_version: Option<u32>,
}
impl ProviderFactTrace {
#[allow(clippy::too_many_arguments)] pub fn new(
surface: ProviderSurface,
source: ProviderFactSourceKind,
provenance: Provenance,
confidence: Confidence,
freshness: ProviderFactFreshness,
fallback_state: ProviderFallbackState,
source_hash: Option<String>,
anchor_id: Option<AnchorId>,
model_version: Option<u32>,
) -> Self {
Self {
surface,
source,
provenance,
confidence,
freshness,
fallback_state,
source_hash,
anchor_id,
model_version,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RenamePlan {
pub entity_id: EntityId,
pub old_name: String,
pub new_name: String,
pub edits: Vec<PlannedEdit>,
pub blockers: Vec<PlanBlocker>,
pub warnings: Vec<PlanWarning>,
}
impl RenamePlan {
pub fn new(
entity_id: EntityId,
old_name: String,
new_name: String,
edits: Vec<PlannedEdit>,
blockers: Vec<PlanBlocker>,
warnings: Vec<PlanWarning>,
) -> Self {
Self { entity_id, old_name, new_name, edits, blockers, warnings }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SafeDeletePlan {
pub entity_id: EntityId,
pub name: String,
pub blockers: Vec<PlanBlocker>,
pub warnings: Vec<PlanWarning>,
}
impl SafeDeletePlan {
pub fn new(
entity_id: EntityId,
name: String,
blockers: Vec<PlanBlocker>,
warnings: Vec<PlanWarning>,
) -> Self {
Self { entity_id, name, blockers, warnings }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlanBlocker {
pub reason: PlanBlockerReason,
pub anchor_id: Option<AnchorId>,
pub description: String,
}
impl PlanBlocker {
pub fn new(
reason: PlanBlockerReason,
anchor_id: Option<AnchorId>,
description: String,
) -> Self {
Self { reason, anchor_id, description }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PlanBlockerReason {
DynamicBoundary,
AmbiguousReference,
CrossModuleExport,
ImportedSymbol,
ExportedSymbol,
ReferencesExist,
GeneratedMember,
UnclassifiedOccurrence,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlanWarning {
pub message: String,
pub anchor_id: Option<AnchorId>,
}
impl PlanWarning {
pub fn new(message: String, anchor_id: Option<AnchorId>) -> Self {
Self { message, anchor_id }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlannedEdit {
pub anchor_id: AnchorId,
pub file_id: FileId,
pub category: PlannedEditCategory,
pub old_text: String,
pub new_text: String,
}
impl PlannedEdit {
pub fn new(
anchor_id: AnchorId,
file_id: FileId,
category: PlannedEditCategory,
old_text: String,
new_text: String,
) -> Self {
Self { anchor_id, file_id, category, old_text, new_text }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PlannedEditCategory {
Definition,
Reference,
ImportList,
ExportList,
}
impl ReferenceEdge {
#[allow(clippy::too_many_arguments)] pub fn new(
occurrence_id: OccurrenceId,
anchor_id: AnchorId,
file_id: FileId,
symbol_key: String,
target_candidates: Vec<EntityId>,
kind: OccurrenceKind,
provenance: Provenance,
confidence: Confidence,
) -> Self {
Self {
occurrence_id,
anchor_id,
file_id,
symbol_key,
target_candidates,
kind,
provenance,
confidence,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entity_fact_roundtrips_through_json() -> Result<(), serde_json::Error> {
let fact = EntityFact {
id: EntityId(100),
kind: EntityKind::Method,
canonical_name: "Foo::bar".to_string(),
anchor_id: Some(AnchorId(12)),
scope_id: Some(ScopeId(3)),
provenance: Provenance::SemanticAnalyzer,
confidence: Confidence::High,
};
let serialized = serde_json::to_string(&fact)?;
let decoded: EntityFact = serde_json::from_str(&serialized)?;
assert_eq!(decoded, fact);
Ok(())
}
#[test]
fn deterministic_debug_for_edge_fact() {
let fact = EdgeFact {
id: EdgeId(7),
kind: EdgeKind::Calls,
from_entity_id: EntityId(11),
to_entity_id: EntityId(22),
via_occurrence_id: Some(OccurrenceId(33)),
provenance: Provenance::ExactAst,
confidence: Confidence::Medium,
};
assert_eq!(
format!("{fact:?}"),
"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 }"
);
}
#[test]
fn pretty_json_for_anchor_fact_is_stable() -> Result<(), serde_json::Error> {
let fact = AnchorFact {
id: AnchorId(5),
file_id: FileId(1),
span_start_byte: 10,
span_end_byte: 15,
scope_id: None,
provenance: Provenance::DesugaredAst,
confidence: Confidence::Low,
};
let json = serde_json::to_string_pretty(&fact)?;
assert_eq!(
json,
"{\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}"
);
Ok(())
}
#[test]
fn occurrence_fact_with_null_entity_id_roundtrips() -> Result<(), serde_json::Error> {
let fact = OccurrenceFact {
id: OccurrenceId(42),
kind: OccurrenceKind::Call,
entity_id: None,
anchor_id: AnchorId(10),
scope_id: Some(ScopeId(2)),
provenance: Provenance::NameHeuristic,
confidence: Confidence::Low,
};
let serialized = serde_json::to_string(&fact)?;
let decoded: OccurrenceFact = serde_json::from_str(&serialized)?;
assert_eq!(decoded, fact);
assert!(
serialized.contains("\"entity_id\":null"),
"entity_id null must be explicit in JSON"
);
Ok(())
}
#[test]
fn id_u64_max_roundtrips() -> Result<(), serde_json::Error> {
let id = EntityId(u64::MAX);
let serialized = serde_json::to_string(&id)?;
let decoded: EntityId = serde_json::from_str(&serialized)?;
assert_eq!(decoded, id);
Ok(())
}
#[test]
fn import_spec_roundtrips_through_json() -> Result<(), serde_json::Error> {
let spec = ImportSpec {
module: "Foo::Bar".to_string(),
kind: ImportKind::RequireThenImport,
symbols: ImportSymbols::Mixed {
tags: vec!["all".to_string()],
names: vec!["$X".to_string(), "@Y".to_string()],
},
provenance: Provenance::ImportExportInference,
confidence: Confidence::Medium,
file_id: None,
anchor_id: None,
scope_id: None,
span_start_byte: None,
};
let serialized = serde_json::to_string(&spec)?;
let decoded: ImportSpec = serde_json::from_str(&serialized)?;
assert_eq!(decoded, spec);
Ok(())
}
#[test]
fn import_symbols_debug_is_deterministic() {
let symbols = ImportSymbols::Mixed {
tags: vec!["io".to_string(), "all".to_string()],
names: vec!["open".to_string(), "close".to_string()],
};
assert_eq!(
format!("{symbols:?}"),
"Mixed { tags: [\"io\", \"all\"], names: [\"open\", \"close\"] }"
);
}
#[test]
fn visible_symbol_pretty_json_is_stable() -> Result<(), serde_json::Error> {
let visible = VisibleSymbol {
name: "slurp".to_string(),
entity_id: Some(EntityId(17)),
source: VisibleSymbolSource::ExplicitImport,
confidence: Confidence::High,
context: None,
};
let json = serde_json::to_string_pretty(&visible)?;
assert_eq!(
json,
"{\n \"name\": \"slurp\",\n \"entity_id\": 17,\n \"source\": \"ExplicitImport\",\n \"confidence\": \"High\",\n \"context\": null\n}"
);
Ok(())
}
#[test]
fn reference_edge_roundtrips_through_json() -> Result<(), serde_json::Error> {
let edge = ReferenceEdge {
occurrence_id: OccurrenceId(50),
anchor_id: AnchorId(20),
file_id: FileId(3),
symbol_key: "Foo::bar".to_string(),
target_candidates: vec![EntityId(100), EntityId(200)],
kind: OccurrenceKind::Call,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
};
let serialized = serde_json::to_string(&edge)?;
let decoded: ReferenceEdge = serde_json::from_str(&serialized)?;
assert_eq!(decoded, edge);
Ok(())
}
#[test]
fn reference_edge_empty_candidates_roundtrips() -> Result<(), serde_json::Error> {
let edge = ReferenceEdge {
occurrence_id: OccurrenceId(51),
anchor_id: AnchorId(21),
file_id: FileId(4),
symbol_key: "unknown_sub".to_string(),
target_candidates: vec![],
kind: OccurrenceKind::Reference,
provenance: Provenance::NameHeuristic,
confidence: Confidence::Low,
};
let serialized = serde_json::to_string(&edge)?;
let decoded: ReferenceEdge = serde_json::from_str(&serialized)?;
assert_eq!(decoded, edge);
assert!(
serialized.contains("\"target_candidates\":[]"),
"empty target_candidates must be an empty JSON array"
);
Ok(())
}
#[test]
fn definition_rank_roundtrips_through_json() -> Result<(), serde_json::Error> {
let variants = [
DefinitionRank::ExactQualified,
DefinitionRank::SamePackage,
DefinitionRank::ExplicitImport,
DefinitionRank::DefaultExport,
DefinitionRank::WorkspaceCandidate,
DefinitionRank::Heuristic,
];
for variant in &variants {
let serialized = serde_json::to_string(variant)?;
let decoded: DefinitionRank = serde_json::from_str(&serialized)?;
assert_eq!(&decoded, variant);
}
Ok(())
}
#[test]
fn definition_rank_ordering_matches_design() {
assert!(DefinitionRank::ExactQualified < DefinitionRank::SamePackage);
assert!(DefinitionRank::SamePackage < DefinitionRank::ExplicitImport);
assert!(DefinitionRank::ExplicitImport < DefinitionRank::DefaultExport);
assert!(DefinitionRank::DefaultExport < DefinitionRank::WorkspaceCandidate);
assert!(DefinitionRank::WorkspaceCandidate < DefinitionRank::Heuristic);
}
#[test]
fn definition_rank_reason_roundtrips_through_json() -> Result<(), serde_json::Error> {
let reasons = [
DefinitionRankReason::ExactQualifiedName,
DefinitionRankReason::SamePackage,
DefinitionRankReason::ExplicitImport { module: "Foo::Bar".to_string() },
DefinitionRankReason::DefaultExport { module: "Baz::Qux".to_string() },
DefinitionRankReason::WorkspaceSymbol,
DefinitionRankReason::HeuristicNameMatch,
];
for reason in &reasons {
let serialized = serde_json::to_string(reason)?;
let decoded: DefinitionRankReason = serde_json::from_str(&serialized)?;
assert_eq!(&decoded, reason);
}
Ok(())
}
#[test]
fn definition_candidate_roundtrips_through_json() -> Result<(), serde_json::Error> {
let candidate = DefinitionCandidate {
entity_id: EntityId(300),
anchor_id: AnchorId(40),
canonical_name: "Foo::Bar::baz".to_string(),
display_name: "baz".to_string(),
package: Some("Foo::Bar".to_string()),
kind: EntityKind::Subroutine,
provenance: Provenance::ExactAst,
confidence: Confidence::High,
rank: DefinitionRank::ExactQualified,
rank_reason: DefinitionRankReason::ExactQualifiedName,
};
let serialized = serde_json::to_string(&candidate)?;
let decoded: DefinitionCandidate = serde_json::from_str(&serialized)?;
assert_eq!(decoded, candidate);
Ok(())
}
#[test]
fn definition_candidate_none_package_roundtrips() -> Result<(), serde_json::Error> {
let candidate = DefinitionCandidate {
entity_id: EntityId(301),
anchor_id: AnchorId(41),
canonical_name: "main::helper".to_string(),
display_name: "helper".to_string(),
package: None,
kind: EntityKind::Subroutine,
provenance: Provenance::NameHeuristic,
confidence: Confidence::Low,
rank: DefinitionRank::Heuristic,
rank_reason: DefinitionRankReason::HeuristicNameMatch,
};
let serialized = serde_json::to_string(&candidate)?;
let decoded: DefinitionCandidate = serde_json::from_str(&serialized)?;
assert_eq!(decoded, candidate);
assert!(serialized.contains("\"package\":null"), "package null must be explicit in JSON");
Ok(())
}
#[test]
fn definition_candidate_import_reason_roundtrips() -> Result<(), serde_json::Error> {
let candidate = DefinitionCandidate {
entity_id: EntityId(302),
anchor_id: AnchorId(42),
canonical_name: "List::Util::first".to_string(),
display_name: "first".to_string(),
package: Some("List::Util".to_string()),
kind: EntityKind::Subroutine,
provenance: Provenance::ImportExportInference,
confidence: Confidence::Medium,
rank: DefinitionRank::ExplicitImport,
rank_reason: DefinitionRankReason::ExplicitImport { module: "List::Util".to_string() },
};
let serialized = serde_json::to_string(&candidate)?;
let decoded: DefinitionCandidate = serde_json::from_str(&serialized)?;
assert_eq!(decoded, candidate);
Ok(())
}
#[test]
fn provider_fact_trace_roundtrips_through_json() -> Result<(), serde_json::Error> {
let trace = ProviderFactTrace::new(
ProviderSurface::Completion,
ProviderFactSourceKind::CompilerFact,
Provenance::ImportExportInference,
Confidence::High,
ProviderFactFreshness::Fresh,
ProviderFallbackState::Shadow,
Some("fixture-source-sha".to_string()),
Some(AnchorId(10)),
Some(1),
);
let serialized = serde_json::to_string(&trace)?;
let decoded: ProviderFactTrace = serde_json::from_str(&serialized)?;
assert_eq!(decoded, trace);
Ok(())
}
#[test]
fn provider_fact_trace_optional_metadata_roundtrips() -> Result<(), serde_json::Error> {
let trace = ProviderFactTrace::new(
ProviderSurface::Diagnostics,
ProviderFactSourceKind::Fallback,
Provenance::SearchFallback,
Confidence::Low,
ProviderFactFreshness::NotApplicable,
ProviderFallbackState::Fallback,
None,
None,
None,
);
let serialized = serde_json::to_string(&trace)?;
let decoded: ProviderFactTrace = serde_json::from_str(&serialized)?;
assert_eq!(decoded, trace);
assert!(
serialized.contains("\"source_hash\":null")
&& serialized.contains("\"anchor_id\":null")
&& serialized.contains("\"model_version\":null"),
"optional trace metadata should remain explicit for downstream consumers"
);
Ok(())
}
#[test]
fn provider_fact_trace_enums_roundtrip_through_json() -> Result<(), serde_json::Error> {
for surface in [
ProviderSurface::Diagnostics,
ProviderSurface::Completion,
ProviderSurface::Hover,
ProviderSurface::Definition,
ProviderSurface::References,
ProviderSurface::Rename,
ProviderSurface::SafeDelete,
ProviderSurface::WorkspaceSymbols,
ProviderSurface::DocumentSymbols,
ProviderSurface::SemanticTokens,
] {
let serialized = serde_json::to_string(&surface)?;
let decoded: ProviderSurface = serde_json::from_str(&serialized)?;
assert_eq!(decoded, surface);
}
for source in [
ProviderFactSourceKind::ParserSyntax,
ProviderFactSourceKind::LegacyWorkspace,
ProviderFactSourceKind::SemanticFact,
ProviderFactSourceKind::CompilerFact,
ProviderFactSourceKind::FrameworkAdapter,
ProviderFactSourceKind::DynamicBoundary,
ProviderFactSourceKind::Fallback,
ProviderFactSourceKind::Unknown,
] {
let serialized = serde_json::to_string(&source)?;
let decoded: ProviderFactSourceKind = serde_json::from_str(&serialized)?;
assert_eq!(decoded, source);
}
for freshness in [
ProviderFactFreshness::Fresh,
ProviderFactFreshness::Stale,
ProviderFactFreshness::Unknown,
ProviderFactFreshness::NotApplicable,
] {
let serialized = serde_json::to_string(&freshness)?;
let decoded: ProviderFactFreshness = serde_json::from_str(&serialized)?;
assert_eq!(decoded, freshness);
}
for fallback_state in [
ProviderFallbackState::Primary,
ProviderFallbackState::Shadow,
ProviderFallbackState::Fallback,
ProviderFallbackState::Unavailable,
ProviderFallbackState::Blocked,
] {
let serialized = serde_json::to_string(&fallback_state)?;
let decoded: ProviderFallbackState = serde_json::from_str(&serialized)?;
assert_eq!(decoded, fallback_state);
}
Ok(())
}
#[test]
fn package_edge_roundtrips_through_json() -> Result<(), serde_json::Error> {
let edge = PackageEdge {
from_package: "Child".to_string(),
to_package: "Parent".to_string(),
kind: PackageEdgeKind::Inherits,
anchor_id: Some(AnchorId(99)),
provenance: Provenance::ExactAst,
confidence: Confidence::High,
};
let serialized = serde_json::to_string(&edge)?;
let decoded: PackageEdge = serde_json::from_str(&serialized)?;
assert_eq!(decoded, edge);
Ok(())
}
#[test]
fn package_edge_kind_roundtrips_through_json() -> Result<(), serde_json::Error> {
let variants =
[PackageEdgeKind::Inherits, PackageEdgeKind::ComposesRole, PackageEdgeKind::DependsOn];
for variant in &variants {
let serialized = serde_json::to_string(variant)?;
let decoded: PackageEdgeKind = serde_json::from_str(&serialized)?;
assert_eq!(&decoded, variant);
}
Ok(())
}
#[test]
fn package_node_roundtrips_through_json() -> Result<(), serde_json::Error> {
let node = PackageNode {
entity_id: EntityId(500),
name: "My::Package".to_string(),
kind: PackageKind::Class,
anchor_id: Some(AnchorId(10)),
file_id: Some(FileId(2)),
};
let serialized = serde_json::to_string(&node)?;
let decoded: PackageNode = serde_json::from_str(&serialized)?;
assert_eq!(decoded, node);
Ok(())
}
#[test]
fn package_kind_roundtrips_through_json() -> Result<(), serde_json::Error> {
let variants =
[PackageKind::Package, PackageKind::Class, PackageKind::Role, PackageKind::External];
for variant in &variants {
let serialized = serde_json::to_string(variant)?;
let decoded: PackageKind = serde_json::from_str(&serialized)?;
assert_eq!(&decoded, variant);
}
Ok(())
}
#[test]
fn package_edge_none_anchor_roundtrips() -> Result<(), serde_json::Error> {
let edge = PackageEdge {
from_package: "App::Worker".to_string(),
to_package: "Unknown::External".to_string(),
kind: PackageEdgeKind::DependsOn,
anchor_id: None,
provenance: Provenance::NameHeuristic,
confidence: Confidence::Low,
};
let serialized = serde_json::to_string(&edge)?;
let decoded: PackageEdge = serde_json::from_str(&serialized)?;
assert_eq!(decoded, edge);
assert!(
serialized.contains("\"anchor_id\":null"),
"anchor_id null must be explicit in JSON"
);
Ok(())
}
#[test]
fn generated_member_roundtrips_through_json() -> Result<(), serde_json::Error> {
let member = GeneratedMember {
entity_id: EntityId(600),
name: "username".to_string(),
kind: GeneratedMemberKind::Getter,
source_anchor_id: AnchorId(50),
package: "MyApp::User".to_string(),
provenance: Provenance::FrameworkSynthesis,
confidence: Confidence::Medium,
};
let serialized = serde_json::to_string(&member)?;
let decoded: GeneratedMember = serde_json::from_str(&serialized)?;
assert_eq!(decoded, member);
Ok(())
}
#[test]
fn generated_member_kind_roundtrips_through_json() -> Result<(), serde_json::Error> {
let variants = [
GeneratedMemberKind::Getter,
GeneratedMemberKind::Setter,
GeneratedMemberKind::Accessor,
GeneratedMemberKind::Predicate,
GeneratedMemberKind::Clearer,
GeneratedMemberKind::Builder,
GeneratedMemberKind::Constant,
];
for variant in &variants {
let serialized = serde_json::to_string(variant)?;
let decoded: GeneratedMemberKind = serde_json::from_str(&serialized)?;
assert_eq!(&decoded, variant);
}
Ok(())
}
#[test]
fn generated_member_new_constructor() -> Result<(), serde_json::Error> {
let via_new = GeneratedMember::new(
EntityId(700),
"email".to_string(),
GeneratedMemberKind::Accessor,
AnchorId(60),
"MyApp::User".to_string(),
Provenance::FrameworkSynthesis,
Confidence::Medium,
);
let via_literal = GeneratedMember {
entity_id: EntityId(700),
name: "email".to_string(),
kind: GeneratedMemberKind::Accessor,
source_anchor_id: AnchorId(60),
package: "MyApp::User".to_string(),
provenance: Provenance::FrameworkSynthesis,
confidence: Confidence::Medium,
};
assert_eq!(via_new, via_literal);
Ok(())
}
#[test]
fn value_shape_unknown_roundtrips() -> Result<(), serde_json::Error> {
let shape = ValueShape::Unknown;
let serialized = serde_json::to_string(&shape)?;
let decoded: ValueShape = serde_json::from_str(&serialized)?;
assert_eq!(decoded, shape);
Ok(())
}
#[test]
fn value_shape_object_roundtrips() -> Result<(), serde_json::Error> {
let shape =
ValueShape::Object { package: "My::Class".to_string(), confidence: Confidence::High };
let serialized = serde_json::to_string(&shape)?;
let decoded: ValueShape = serde_json::from_str(&serialized)?;
assert_eq!(decoded, shape);
Ok(())
}
#[test]
fn value_shape_package_name_roundtrips() -> Result<(), serde_json::Error> {
let shape = ValueShape::PackageName { package: "Foo::Bar".to_string() };
let serialized = serde_json::to_string(&shape)?;
let decoded: ValueShape = serde_json::from_str(&serialized)?;
assert_eq!(decoded, shape);
Ok(())
}
#[test]
fn value_shape_all_variants_roundtrip() -> Result<(), serde_json::Error> {
let variants: Vec<ValueShape> = vec![
ValueShape::Unknown,
ValueShape::Scalar,
ValueShape::ArrayRef,
ValueShape::HashRef,
ValueShape::CodeRef,
ValueShape::PackageName { package: "Foo".to_string() },
ValueShape::Object { package: "Bar::Baz".to_string(), confidence: Confidence::Low },
];
for shape in &variants {
let serialized = serde_json::to_string(shape)?;
let decoded: ValueShape = serde_json::from_str(&serialized)?;
assert_eq!(&decoded, shape);
}
Ok(())
}
#[test]
fn rename_plan_roundtrips_through_json() -> Result<(), serde_json::Error> {
let plan = RenamePlan {
entity_id: EntityId(400),
old_name: "foo".to_string(),
new_name: "bar".to_string(),
edits: vec![
PlannedEdit {
anchor_id: AnchorId(80),
file_id: FileId(1),
category: PlannedEditCategory::Definition,
old_text: "foo".to_string(),
new_text: "bar".to_string(),
},
PlannedEdit {
anchor_id: AnchorId(81),
file_id: FileId(2),
category: PlannedEditCategory::Reference,
old_text: "foo".to_string(),
new_text: "bar".to_string(),
},
],
blockers: vec![PlanBlocker {
reason: PlanBlockerReason::DynamicBoundary,
anchor_id: Some(AnchorId(90)),
description: "reference crosses eval boundary".to_string(),
}],
warnings: vec![PlanWarning {
message: "symbol also appears in comments".to_string(),
anchor_id: None,
}],
};
let serialized = serde_json::to_string(&plan)?;
let decoded: RenamePlan = serde_json::from_str(&serialized)?;
assert_eq!(decoded, plan);
Ok(())
}
#[test]
fn rename_plan_empty_collections_roundtrip() -> Result<(), serde_json::Error> {
let plan = RenamePlan {
entity_id: EntityId(401),
old_name: "x".to_string(),
new_name: "y".to_string(),
edits: vec![],
blockers: vec![],
warnings: vec![],
};
let serialized = serde_json::to_string(&plan)?;
let decoded: RenamePlan = serde_json::from_str(&serialized)?;
assert_eq!(decoded, plan);
assert!(serialized.contains("\"edits\":[]"), "empty edits must be an empty JSON array");
assert!(
serialized.contains("\"blockers\":[]"),
"empty blockers must be an empty JSON array"
);
assert!(
serialized.contains("\"warnings\":[]"),
"empty warnings must be an empty JSON array"
);
Ok(())
}
#[test]
fn rename_plan_new_constructor() {
let via_new = RenamePlan::new(
EntityId(402),
"old".to_string(),
"new".to_string(),
vec![],
vec![],
vec![],
);
let via_literal = RenamePlan {
entity_id: EntityId(402),
old_name: "old".to_string(),
new_name: "new".to_string(),
edits: vec![],
blockers: vec![],
warnings: vec![],
};
assert_eq!(via_new, via_literal);
}
#[test]
fn safe_delete_plan_roundtrips_through_json() -> Result<(), serde_json::Error> {
let plan = SafeDeletePlan {
entity_id: EntityId(500),
name: "unused_sub".to_string(),
blockers: vec![
PlanBlocker {
reason: PlanBlockerReason::ReferencesExist,
anchor_id: Some(AnchorId(70)),
description: "3 references remain".to_string(),
},
PlanBlocker {
reason: PlanBlockerReason::ExportedSymbol,
anchor_id: Some(AnchorId(71)),
description: "symbol in @EXPORT_OK".to_string(),
},
],
warnings: vec![],
};
let serialized = serde_json::to_string(&plan)?;
let decoded: SafeDeletePlan = serde_json::from_str(&serialized)?;
assert_eq!(decoded, plan);
Ok(())
}
#[test]
fn safe_delete_plan_no_blockers_roundtrips() -> Result<(), serde_json::Error> {
let plan = SafeDeletePlan {
entity_id: EntityId(501),
name: "dead_code".to_string(),
blockers: vec![],
warnings: vec![PlanWarning {
message: "symbol appears in pod documentation".to_string(),
anchor_id: Some(AnchorId(72)),
}],
};
let serialized = serde_json::to_string(&plan)?;
let decoded: SafeDeletePlan = serde_json::from_str(&serialized)?;
assert_eq!(decoded, plan);
Ok(())
}
#[test]
fn safe_delete_plan_new_constructor() {
let via_new = SafeDeletePlan::new(EntityId(502), "helper".to_string(), vec![], vec![]);
let via_literal = SafeDeletePlan {
entity_id: EntityId(502),
name: "helper".to_string(),
blockers: vec![],
warnings: vec![],
};
assert_eq!(via_new, via_literal);
}
#[test]
fn plan_blocker_reason_roundtrips_through_json() -> Result<(), serde_json::Error> {
let variants = [
PlanBlockerReason::DynamicBoundary,
PlanBlockerReason::AmbiguousReference,
PlanBlockerReason::CrossModuleExport,
PlanBlockerReason::ImportedSymbol,
PlanBlockerReason::ExportedSymbol,
PlanBlockerReason::ReferencesExist,
PlanBlockerReason::GeneratedMember,
PlanBlockerReason::UnclassifiedOccurrence,
];
for variant in &variants {
let serialized = serde_json::to_string(variant)?;
let decoded: PlanBlockerReason = serde_json::from_str(&serialized)?;
assert_eq!(&decoded, variant);
}
Ok(())
}
#[test]
fn plan_blocker_none_anchor_roundtrips() -> Result<(), serde_json::Error> {
let blocker = PlanBlocker {
reason: PlanBlockerReason::GeneratedMember,
anchor_id: None,
description: "generated accessor without edit plan".to_string(),
};
let serialized = serde_json::to_string(&blocker)?;
let decoded: PlanBlocker = serde_json::from_str(&serialized)?;
assert_eq!(decoded, blocker);
assert!(
serialized.contains("\"anchor_id\":null"),
"anchor_id null must be explicit in JSON"
);
Ok(())
}
#[test]
fn plan_blocker_new_constructor() {
let via_new = PlanBlocker::new(
PlanBlockerReason::ImportedSymbol,
Some(AnchorId(99)),
"imported by other file".to_string(),
);
let via_literal = PlanBlocker {
reason: PlanBlockerReason::ImportedSymbol,
anchor_id: Some(AnchorId(99)),
description: "imported by other file".to_string(),
};
assert_eq!(via_new, via_literal);
}
#[test]
fn plan_warning_roundtrips_through_json() -> Result<(), serde_json::Error> {
let warning = PlanWarning {
message: "symbol also used in string interpolation".to_string(),
anchor_id: Some(AnchorId(85)),
};
let serialized = serde_json::to_string(&warning)?;
let decoded: PlanWarning = serde_json::from_str(&serialized)?;
assert_eq!(decoded, warning);
Ok(())
}
#[test]
fn plan_warning_new_constructor() {
let via_new = PlanWarning::new("check pod docs".to_string(), None);
let via_literal = PlanWarning { message: "check pod docs".to_string(), anchor_id: None };
assert_eq!(via_new, via_literal);
}
#[test]
fn planned_edit_roundtrips_through_json() -> Result<(), serde_json::Error> {
let edit = PlannedEdit {
anchor_id: AnchorId(60),
file_id: FileId(5),
category: PlannedEditCategory::ImportList,
old_text: "foo".to_string(),
new_text: "bar".to_string(),
};
let serialized = serde_json::to_string(&edit)?;
let decoded: PlannedEdit = serde_json::from_str(&serialized)?;
assert_eq!(decoded, edit);
Ok(())
}
#[test]
fn planned_edit_new_constructor() {
let via_new = PlannedEdit::new(
AnchorId(61),
FileId(6),
PlannedEditCategory::ExportList,
"old_sym".to_string(),
"new_sym".to_string(),
);
let via_literal = PlannedEdit {
anchor_id: AnchorId(61),
file_id: FileId(6),
category: PlannedEditCategory::ExportList,
old_text: "old_sym".to_string(),
new_text: "new_sym".to_string(),
};
assert_eq!(via_new, via_literal);
}
#[test]
fn planned_edit_category_roundtrips_through_json() -> Result<(), serde_json::Error> {
let variants = [
PlannedEditCategory::Definition,
PlannedEditCategory::Reference,
PlannedEditCategory::ImportList,
PlannedEditCategory::ExportList,
];
for variant in &variants {
let serialized = serde_json::to_string(variant)?;
let decoded: PlannedEditCategory = serde_json::from_str(&serialized)?;
assert_eq!(&decoded, variant);
}
Ok(())
}
}