use crate::edit::ResolvedEditChange;
use crate::graph::pdg::NodeType;
use crate::graph::ProgramDependenceGraph;
use crate::parse::traits::{CodeIntelligence, SignatureInfo};
use crate::validation::Location;
use crate::validation::ValidationError;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DriftType {
SignatureChanged,
VisibilityChanged,
TypeChanged,
Removed,
Added,
}
#[derive(Debug, Clone)]
pub struct DriftItem {
pub symbol_name: String,
pub drift_type: DriftType,
pub location: Location,
pub impact_description: String,
}
impl DriftItem {
pub fn new(
symbol_name: String,
drift_type: DriftType,
location: Location,
impact_description: String,
) -> Self {
Self {
symbol_name,
drift_type,
location,
impact_description,
}
}
pub fn signature_changed(
symbol_name: String,
location: Location,
old_sig: &str,
new_sig: &str,
) -> Self {
Self {
symbol_name,
drift_type: DriftType::SignatureChanged,
location,
impact_description: format!("Signature changed from '{}' to '{}'", old_sig, new_sig),
}
}
pub fn type_changed(symbol_name: String, location: Location, type_desc: String) -> Self {
Self {
symbol_name,
drift_type: DriftType::TypeChanged,
location,
impact_description: format!("Type changed: {}", type_desc),
}
}
pub fn visibility_changed(
symbol_name: String,
location: Location,
old_visibility: &str,
new_visibility: &str,
) -> Self {
Self {
symbol_name,
drift_type: DriftType::VisibilityChanged,
location,
impact_description: format!(
"Visibility changed from '{}' to '{}'",
old_visibility, new_visibility
),
}
}
pub fn removed(symbol_name: String, location: Location) -> Self {
Self {
impact_description: format!("Symbol '{}' was removed", symbol_name),
symbol_name,
drift_type: DriftType::Removed,
location,
}
}
pub fn added(symbol_name: String, location: Location) -> Self {
Self {
impact_description: format!("New symbol '{}' added", symbol_name),
symbol_name,
drift_type: DriftType::Added,
location,
}
}
pub fn is_breaking(&self) -> bool {
matches!(
self.drift_type,
DriftType::SignatureChanged | DriftType::TypeChanged | DriftType::Removed
)
}
}
#[derive(Debug, Clone)]
pub struct DriftReport {
pub breaking_changes: Vec<DriftItem>,
pub api_changes: Vec<DriftItem>,
}
impl DriftReport {
pub fn new() -> Self {
Self {
breaking_changes: Vec::new(),
api_changes: Vec::new(),
}
}
pub fn add_drift(&mut self, drift: DriftItem) {
if drift.is_breaking() {
self.breaking_changes.push(drift.clone());
}
self.api_changes.push(drift);
}
pub fn has_breaking_changes(&self) -> bool {
!self.breaking_changes.is_empty()
}
pub fn total_changes(&self) -> usize {
self.api_changes.len()
}
}
impl Default for DriftReport {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct SemanticDriftAnalyzer {
pdg: Arc<ProgramDependenceGraph>,
}
impl SemanticDriftAnalyzer {
pub fn new(pdg: Arc<ProgramDependenceGraph>) -> Self {
Self { pdg }
}
pub fn analyze_semantic_drift(
&self,
changes: &[ResolvedEditChange],
) -> Result<Vec<DriftItem>, ValidationError> {
let mut drift_items = Vec::new();
for change in changes {
let original_sigs = self.extract_signatures(change, &change.original_content)?;
let new_sigs = self.extract_signatures(change, &change.new_content)?;
drift_items.extend(self.compare_signatures(change, &original_sigs, &new_sigs)?);
}
Ok(drift_items)
}
fn extract_signatures(
&self,
change: &ResolvedEditChange,
content: &str,
) -> Result<Vec<SignatureInfo>, ValidationError> {
if content.is_empty() {
return Ok(Vec::new());
}
let lang = change.infer_language();
let source = content.as_bytes();
match lang {
"python" => {
use crate::parse::python::PythonParser;
let parser = PythonParser::new();
parser
.get_signatures(source)
.map_err(|e| ValidationError::Parse(format!("Failed to parse Python: {}", e)))
}
"javascript" => {
use crate::parse::javascript::JavaScriptParser;
let parser = JavaScriptParser::new();
parser.get_signatures(source).map_err(|e| {
ValidationError::Parse(format!("Failed to parse JavaScript: {}", e))
})
}
"typescript" => {
use crate::parse::javascript::TypeScriptParser;
let parser = TypeScriptParser::new();
parser.get_signatures(source).map_err(|e| {
ValidationError::Parse(format!("Failed to parse TypeScript: {}", e))
})
}
"rust" => {
use crate::parse::rust::RustParser;
let parser = RustParser::new();
parser
.get_signatures(source)
.map_err(|e| ValidationError::Parse(format!("Failed to parse Rust: {}", e)))
}
"go" => {
use crate::parse::go::GoParser;
let parser = GoParser::new();
parser
.get_signatures(source)
.map_err(|e| ValidationError::Parse(format!("Failed to parse Go: {}", e)))
}
"java" => {
use crate::parse::java::JavaParser;
let parser = JavaParser::new();
parser
.get_signatures(source)
.map_err(|e| ValidationError::Parse(format!("Failed to parse Java: {}", e)))
}
_ => {
Ok(Vec::new())
}
}
}
fn compare_signatures(
&self,
change: &ResolvedEditChange,
original: &[SignatureInfo],
new: &[SignatureInfo],
) -> Result<Vec<DriftItem>, ValidationError> {
let mut drift_items = Vec::new();
let original_map: HashMap<_, _> = original.iter().map(|sig| (&sig.name, sig)).collect();
let new_map: HashMap<_, _> = new.iter().map(|sig| (&sig.name, sig)).collect();
for name in original_map.keys() {
if !new_map.contains_key(name) {
let location =
self.find_signature_location(change, original_map.get(name).unwrap());
drift_items.push(DriftItem::removed(name.to_string(), location));
}
}
for name in new_map.keys() {
if !original_map.contains_key(name) {
let location = self.find_signature_location(change, new_map.get(name).unwrap());
drift_items.push(DriftItem::added(name.to_string(), location));
}
}
for name in original_map.keys() {
if let Some(new_sig) = new_map.get(name) {
if let Some(original_sig) = original_map.get(name) {
if let Some(drift) =
self.detect_signature_drift(change, original_sig, new_sig)?
{
drift_items.push(drift);
}
}
}
}
Ok(drift_items)
}
fn detect_signature_drift(
&self,
change: &ResolvedEditChange,
original: &SignatureInfo,
new: &SignatureInfo,
) -> Result<Option<DriftItem>, ValidationError> {
let location = self.find_signature_location(change, new);
if original.parameters != new.parameters {
return Ok(Some(DriftItem::signature_changed(
new.name.clone(),
location,
&format!("{:?}", original.parameters),
&format!("{:?}", new.parameters),
)));
}
if original.return_type != new.return_type {
return Ok(Some(DriftItem::type_changed(
new.name.clone(),
location,
format!(
"Return type changed from {:?} to {:?}",
original.return_type, new.return_type
),
)));
}
if original.visibility != new.visibility {
return Ok(Some(DriftItem::visibility_changed(
new.name.clone(),
location,
&format!("{:?}", original.visibility),
&format!("{:?}", new.visibility),
)));
}
Ok(None)
}
fn find_signature_location(
&self,
change: &ResolvedEditChange,
sig: &SignatureInfo,
) -> Location {
let byte_offset = sig.byte_range.0;
let mut line = 1;
let mut column = 1;
for (i, byte) in change.new_content.bytes().enumerate() {
if i == byte_offset {
break;
}
if byte == b'\n' {
line += 1;
column = 1;
} else {
column += 1;
}
}
Location { line, column }
}
pub fn is_public_api(&self, symbol_name: &str) -> bool {
if let Some(node_id) = self.pdg.find_by_symbol(symbol_name) {
if let Some(node) = self.pdg.get_node(node_id) {
return matches!(
node.node_type,
NodeType::Function | NodeType::Method | NodeType::Class
);
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_drift_type_equality() {
assert_eq!(DriftType::SignatureChanged, DriftType::SignatureChanged);
assert_ne!(DriftType::SignatureChanged, DriftType::TypeChanged);
}
#[test]
fn test_drift_item_signature_changed() {
let item = DriftItem::signature_changed(
"my_func".to_string(),
Location { line: 1, column: 1 },
"old_sig",
"new_sig",
);
assert_eq!(item.symbol_name, "my_func");
assert_eq!(item.drift_type, DriftType::SignatureChanged);
assert!(item.impact_description.contains("old_sig"));
assert!(item.impact_description.contains("new_sig"));
assert!(item.is_breaking());
}
#[test]
fn test_drift_item_type_changed() {
let item = DriftItem::type_changed(
"my_func".to_string(),
Location { line: 1, column: 1 },
"return type changed".to_string(),
);
assert_eq!(item.drift_type, DriftType::TypeChanged);
assert!(item.is_breaking());
}
#[test]
fn test_drift_item_visibility_changed() {
let item = DriftItem::visibility_changed(
"my_func".to_string(),
Location { line: 1, column: 1 },
"private",
"public",
);
assert_eq!(item.drift_type, DriftType::VisibilityChanged);
assert!(!item.is_breaking());
}
#[test]
fn test_drift_item_removed() {
let item = DriftItem::removed("my_func".to_string(), Location { line: 1, column: 1 });
assert_eq!(item.drift_type, DriftType::Removed);
assert!(item.is_breaking());
assert!(item.impact_description.contains("removed"));
}
#[test]
fn test_drift_item_added() {
let item = DriftItem::added("new_func".to_string(), Location { line: 1, column: 1 });
assert_eq!(item.drift_type, DriftType::Added);
assert!(!item.is_breaking()); assert!(item.impact_description.contains("added"));
}
#[test]
fn test_drift_report_new() {
let report = DriftReport::new();
assert!(report.breaking_changes.is_empty());
assert!(report.api_changes.is_empty());
assert!(!report.has_breaking_changes());
assert_eq!(report.total_changes(), 0);
}
#[test]
fn test_drift_report_default() {
let report = DriftReport::default();
assert!(report.breaking_changes.is_empty());
}
#[test]
fn test_drift_report_add_drift() {
let mut report = DriftReport::new();
let item = DriftItem::removed("foo".to_string(), Location { line: 1, column: 1 });
report.add_drift(item);
assert_eq!(report.total_changes(), 1);
assert!(report.has_breaking_changes());
assert_eq!(report.breaking_changes.len(), 1);
}
#[test]
fn test_drift_report_add_non_breaking() {
let mut report = DriftReport::new();
let item = DriftItem::added("foo".to_string(), Location { line: 1, column: 1 });
report.add_drift(item);
assert_eq!(report.total_changes(), 1);
assert!(!report.has_breaking_changes());
assert_eq!(report.breaking_changes.len(), 0);
}
#[test]
fn test_semantic_drift_analyzer_new() {
let pdg = Arc::new(ProgramDependenceGraph::new());
let _analyzer = SemanticDriftAnalyzer::new(pdg);
assert!(true);
}
#[test]
fn test_analyze_semantic_drift_empty_changes() {
let pdg = Arc::new(ProgramDependenceGraph::new());
let analyzer = SemanticDriftAnalyzer::new(pdg);
let changes: &[ResolvedEditChange] = &[];
let result = analyzer.analyze_semantic_drift(changes).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_is_public_api() {
let mut pdg = ProgramDependenceGraph::new();
let _node_id = pdg.add_node(crate::graph::Node {
id: "my_func".to_string(),
node_type: NodeType::Function,
name: "my_func".to_string(),
file_path: Arc::from("test.py"),
byte_range: (0, 10),
complexity: 1,
language: "python".to_string(),
});
let analyzer = SemanticDriftAnalyzer::new(Arc::new(pdg));
assert!(analyzer.is_public_api("my_func"));
assert!(!analyzer.is_public_api("nonexistent"));
}
#[test]
fn test_find_signature_location() {
let pdg = Arc::new(ProgramDependenceGraph::new());
let analyzer = SemanticDriftAnalyzer::new(pdg);
let change = ResolvedEditChange::new(
PathBuf::from("test.py"),
String::new(),
"def foo():\n pass".to_string(),
);
let sig = SignatureInfo {
name: "foo".to_string(),
qualified_name: "foo".to_string(),
parameters: vec![],
return_type: None,
visibility: crate::parse::traits::Visibility::Public,
is_async: false,
is_method: false,
docstring: None,
calls: vec![],
imports: vec![],
byte_range: (0, 14),
cyclomatic_complexity: 0,
};
let location = analyzer.find_signature_location(&change, &sig);
assert_eq!(location.line, 1);
assert_eq!(location.column, 1);
}
}