use super::{CodeGraphV2, TypeFlowGraphV2};
use crate::symbol::SymbolRegistry;
use crate::SymbolId;
use crate::SymbolKind;
#[derive(Debug, Clone)]
pub struct ReferenceIntegrityResult {
pub target_symbol: SymbolId,
pub issues: Vec<ReferenceIntegrityIssue>,
}
impl ReferenceIntegrityResult {
pub fn has_issues(&self) -> bool {
!self.issues.is_empty()
}
pub fn error_count(&self) -> usize {
self.issues.iter().filter(|i| i.is_error()).count()
}
pub fn warning_count(&self) -> usize {
self.issues.iter().filter(|i| !i.is_error()).count()
}
}
#[derive(Debug, Clone)]
pub enum ReferenceIntegrityIssue {
DanglingReference {
referrer: SymbolId,
deleted_symbol: SymbolId,
reference_count: usize,
},
MissingFieldInLiteral {
location: SymbolId,
struct_type: SymbolId,
missing_field: String,
},
RemovedFieldInLiteral {
location: SymbolId,
struct_type: SymbolId,
removed_field: String,
},
IncompatibleMethodCall {
caller: SymbolId,
method: SymbolId,
expected_args: usize,
actual_args: usize,
},
RenameWouldBreakReferences {
symbol: SymbolId,
referrers: Vec<SymbolId>,
},
UnusedAfterMutation {
symbol: SymbolId,
},
}
impl ReferenceIntegrityIssue {
pub fn is_error(&self) -> bool {
!matches!(self, ReferenceIntegrityIssue::UnusedAfterMutation { .. })
}
}
pub struct ReferenceIntegrityChecker<'a> {
graph: &'a CodeGraphV2,
typeflow: &'a TypeFlowGraphV2,
registry: &'a SymbolRegistry,
}
impl<'a> ReferenceIntegrityChecker<'a> {
pub fn new(
graph: &'a CodeGraphV2,
typeflow: &'a TypeFlowGraphV2,
registry: &'a SymbolRegistry,
) -> Self {
Self {
graph,
typeflow,
registry,
}
}
pub fn check_deletion_impact(&self, symbol_id: SymbolId) -> ReferenceIntegrityResult {
let mut issues = Vec::new();
let referrers: Vec<SymbolId> = self.typeflow.type_users(symbol_id).collect();
let callers: Vec<SymbolId> = self.graph.callers_of(symbol_id).collect();
let mut all_referrers: Vec<SymbolId> = referrers.clone();
all_referrers.extend(callers.iter().copied());
all_referrers.sort();
all_referrers.dedup();
if !all_referrers.is_empty() {
issues.push(ReferenceIntegrityIssue::DanglingReference {
referrer: all_referrers[0], deleted_symbol: symbol_id,
reference_count: all_referrers.len(),
});
}
let children: Vec<SymbolId> = self.graph.children_of(symbol_id).collect();
for child_id in children {
let child_result = self.check_deletion_impact(child_id);
issues.extend(child_result.issues);
}
ReferenceIntegrityResult {
target_symbol: symbol_id,
issues,
}
}
pub fn check_rename_impact(&self, symbol_id: SymbolId) -> ReferenceIntegrityResult {
let mut issues = Vec::new();
let mut referrers: Vec<SymbolId> = self.typeflow.type_users(symbol_id).collect();
referrers.extend(self.graph.callers_of(symbol_id));
referrers.sort();
referrers.dedup();
if !referrers.is_empty() {
issues.push(ReferenceIntegrityIssue::RenameWouldBreakReferences {
symbol: symbol_id,
referrers: referrers.clone(),
});
}
ReferenceIntegrityResult {
target_symbol: symbol_id,
issues,
}
}
pub fn check_field_addition_impact(
&self,
struct_id: SymbolId,
field_name: &str,
) -> ReferenceIntegrityResult {
let mut issues = Vec::new();
let users: Vec<SymbolId> = self.typeflow.type_users(struct_id).collect();
for user_id in users {
if let Some(kind) = self.registry.kind(user_id) {
if matches!(kind, SymbolKind::Function | SymbolKind::Method) {
issues.push(ReferenceIntegrityIssue::MissingFieldInLiteral {
location: user_id,
struct_type: struct_id,
missing_field: field_name.to_string(),
});
}
}
}
ReferenceIntegrityResult {
target_symbol: struct_id,
issues,
}
}
pub fn check_field_removal_impact(
&self,
struct_id: SymbolId,
field_name: &str,
) -> ReferenceIntegrityResult {
let mut issues = Vec::new();
let users: Vec<SymbolId> = self.typeflow.type_users(struct_id).collect();
for user_id in users {
if let Some(kind) = self.registry.kind(user_id) {
if matches!(kind, SymbolKind::Function | SymbolKind::Method) {
issues.push(ReferenceIntegrityIssue::RemovedFieldInLiteral {
location: user_id,
struct_type: struct_id,
removed_field: field_name.to_string(),
});
}
}
}
ReferenceIntegrityResult {
target_symbol: struct_id,
issues,
}
}
pub fn check_method_signature_change(
&self,
method_id: SymbolId,
new_arg_count: usize,
) -> ReferenceIntegrityResult {
let mut issues = Vec::new();
let current_param_count = self
.graph
.children_of(method_id)
.filter(|child_id| {
self.registry
.kind(*child_id)
.map(|k| matches!(k, SymbolKind::Parameter))
.unwrap_or(false)
})
.count();
if current_param_count != new_arg_count {
let callers: Vec<SymbolId> = self.graph.callers_of(method_id).collect();
for caller_id in callers {
issues.push(ReferenceIntegrityIssue::IncompatibleMethodCall {
caller: caller_id,
method: method_id,
expected_args: new_arg_count,
actual_args: current_param_count, });
}
}
ReferenceIntegrityResult {
target_symbol: method_id,
issues,
}
}
pub fn get_all_referrers(&self, symbol_id: SymbolId) -> Vec<SymbolId> {
let mut referrers: Vec<SymbolId> = self.typeflow.type_users(symbol_id).collect();
referrers.extend(self.graph.callers_of(symbol_id));
referrers.sort();
referrers.dedup();
referrers
}
pub fn is_symbol_unused(&self, symbol_id: SymbolId) -> bool {
self.graph.reference_count(symbol_id) == 0
&& self.typeflow.type_users(symbol_id).next().is_none()
}
pub fn reference_count(&self, symbol_id: SymbolId) -> usize {
self.graph.reference_count(symbol_id) + self.typeflow.usage_count(symbol_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query::{GraphBuilderV2, TypeFlowGraphV2};
use crate::symbol::SymbolPath;
fn create_test_setup() -> (CodeGraphV2, TypeFlowGraphV2, SymbolRegistry) {
let mut registry = SymbolRegistry::new();
let mut builder = GraphBuilderV2::new(&mut registry);
let config = builder
.add_symbol(
SymbolPath::parse("test::Config").unwrap(),
SymbolKind::Struct,
)
.unwrap();
let name_field = builder
.add_symbol(
SymbolPath::parse("test::Config::name").unwrap(),
SymbolKind::Field,
)
.unwrap();
let value_field = builder
.add_symbol(
SymbolPath::parse("test::Config::value").unwrap(),
SymbolKind::Field,
)
.unwrap();
builder.add_contains(config, name_field);
builder.add_contains(config, value_field);
let create_config = builder
.add_symbol(
SymbolPath::parse("test::create_config").unwrap(),
SymbolKind::Function,
)
.unwrap();
let init = builder
.add_symbol(
SymbolPath::parse("test::init").unwrap(),
SymbolKind::Function,
)
.unwrap();
builder.add_call(init, create_config);
let graph = builder.build();
let mut typeflow = TypeFlowGraphV2::new();
typeflow.add_usage(
crate::query::UsageContext::ReturnType,
crate::query::RefKind::Owned,
Some(config),
Some(create_config),
);
(graph, typeflow, registry)
}
#[test]
fn test_check_deletion_impact_with_references() {
let (graph, typeflow, registry) = create_test_setup();
let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, ®istry);
let create_config_id = registry.lookup_by_name("create_config").unwrap();
let result = checker.check_deletion_impact(create_config_id);
assert!(result.has_issues());
assert!(result.error_count() > 0);
}
#[test]
fn test_check_deletion_impact_no_references() {
let (graph, typeflow, registry) = create_test_setup();
let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, ®istry);
let init_id = registry.lookup_by_name("init").unwrap();
let result = checker.check_deletion_impact(init_id);
assert!(!result.has_issues());
}
#[test]
fn test_check_rename_impact() {
let (graph, typeflow, registry) = create_test_setup();
let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, ®istry);
let config_id = registry.lookup_by_name("Config").unwrap();
let result = checker.check_rename_impact(config_id);
assert!(result.has_issues());
}
#[test]
fn test_check_field_addition_impact() {
let (graph, typeflow, registry) = create_test_setup();
let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, ®istry);
let config_id = registry.lookup_by_name("Config").unwrap();
let result = checker.check_field_addition_impact(config_id, "timeout");
assert!(result.has_issues());
}
#[test]
fn test_get_all_referrers() {
let (graph, typeflow, registry) = create_test_setup();
let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, ®istry);
let create_config_id = registry.lookup_by_name("create_config").unwrap();
let referrers = checker.get_all_referrers(create_config_id);
let init_id = registry.lookup_by_name("init").unwrap();
assert!(referrers.contains(&init_id));
}
#[test]
fn test_is_symbol_unused() {
let (graph, typeflow, registry) = create_test_setup();
let checker = ReferenceIntegrityChecker::new(&graph, &typeflow, ®istry);
let init_id = registry.lookup_by_name("init").unwrap();
assert!(checker.is_symbol_unused(init_id));
let create_config_id = registry.lookup_by_name("create_config").unwrap();
assert!(!checker.is_symbol_unused(create_config_id));
}
}