use crate::types::{
ApiSurface, BehavioralChangeKind, BodyAnalysisResult, BreakingVerdict, Caller, ChangedFunction,
EvidenceType, ExpectedChild, FunctionSpec, Reference, StructuralChange, Symbol, SymbolKind,
TestDiff, TestFile, Visibility,
};
use anyhow::Result;
use serde::{de::DeserializeOwned, Serialize};
use std::collections::{BTreeSet, HashMap};
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub trait BehaviorAnalyzer {
fn infer_spec(&self, function_body: &str, signature: &str) -> Result<FunctionSpec>;
fn infer_spec_with_test_context(
&self,
function_body: &str,
signature: &str,
test_context: &TestDiff,
) -> Result<FunctionSpec>;
fn specs_are_breaking(&self, old: &FunctionSpec, new: &FunctionSpec)
-> Result<BreakingVerdict>;
fn check_propagation(
&self,
caller_body: &str,
caller_signature: &str,
callee_name: &str,
evidence_description: &str,
) -> Result<bool>;
}
pub trait LanguageSemantics<M: Default + Clone + PartialEq = ()> {
fn is_member_addition_breaking(&self, container: &Symbol<M>, member: &Symbol<M>) -> bool;
fn same_family(&self, a: &Symbol<M>, b: &Symbol<M>) -> bool;
fn same_identity(&self, a: &Symbol<M>, b: &Symbol<M>) -> bool;
fn visibility_rank(&self, v: Visibility) -> u8;
fn parse_union_values(&self, _type_str: &str) -> Option<BTreeSet<String>> {
None
}
fn is_async_wrapper(&self, _type_str: &str) -> bool {
false
}
fn format_import_change(&self, symbol: &str, old_path: &str, new_path: &str) -> String {
format!(
"replace import of `{}` from `{}` with `{}`",
symbol, old_path, new_path,
)
}
fn should_skip_symbol(&self, _sym: &Symbol<M>) -> bool {
false
}
fn member_label(&self) -> &'static str {
"members"
}
fn extract_rename_fallback_key(&self, _sym: &Symbol<M>) -> Option<String> {
None
}
fn canonical_name_for_relocation(&self, qualified_name: &str) -> String {
qualified_name.to_string()
}
fn classify_relocation(&self, _old_qname: &str, _new_qname: &str) -> Option<&'static str> {
None
}
fn derive_import_subpath(&self, package: Option<&str>, _qualified_name: &str) -> String {
package.unwrap_or("unknown").to_string()
}
fn diff_language_data(&self, _old: &Symbol<M>, _new: &Symbol<M>) -> Vec<StructuralChange> {
vec![]
}
fn post_process(&self, _changes: &mut Vec<StructuralChange>) {}
fn hierarchy(&self) -> Option<&dyn HierarchySemantics<M>> {
None
}
fn renames(&self) -> Option<&dyn RenameSemantics> {
None
}
fn body_analyzer(&self) -> Option<&dyn BodyAnalysisSemantics> {
None
}
fn primitive_type_names(&self) -> &[&str] {
&["string", "number", "boolean", "void", "null"]
}
}
pub trait HierarchySemantics<M: Default + Clone + PartialEq = ()> {
fn family_source_paths(&self, repo: &Path, git_ref: &str, family_name: &str) -> Vec<String>;
fn family_name_from_symbols(&self, symbols: &[&Symbol<M>]) -> Option<String>;
fn cross_family_relationships(
&self,
repo: &Path,
git_ref: &str,
) -> Vec<(String, String, String)>;
fn related_family_content(
&self,
repo: &Path,
git_ref: &str,
family_name: &str,
relationship_names: &[String],
) -> Option<String>;
fn is_hierarchy_candidate(&self, sym: &Symbol<M>) -> bool;
fn min_components_for_hierarchy(&self) -> usize {
2
}
fn compute_deterministic_hierarchy(
&self,
new_surface: &ApiSurface<M>,
structural_changes: &[StructuralChange],
) -> HashMap<String, HashMap<String, Vec<ExpectedChild>>> {
let _ = (new_surface, structural_changes);
HashMap::new()
}
}
pub trait RenameSemantics {
fn sample_removed_constants<'a>(
&self,
removed: &[&'a str],
_added: &[&'a str],
) -> Vec<&'a str> {
removed.iter().take(30).copied().collect()
}
fn sample_added_constants<'a>(&self, _removed: &[&'a str], added: &[&'a str]) -> Vec<&'a str> {
added.iter().take(30).copied().collect()
}
fn min_removed_for_constant_inference(&self) -> usize {
50
}
fn min_removed_for_interface_inference(&self) -> usize {
2
}
}
pub trait BodyAnalysisSemantics {
fn analyze_changed_body(
&self,
old_body: &str,
new_body: &str,
func_name: &str,
file_path: &str,
) -> Vec<BodyAnalysisResult>;
}
pub trait MessageFormatter {
fn describe(&self, change: &StructuralChange) -> String;
}
pub trait WorktreeAccess: Send + Sync + 'static {
fn path(&self) -> &Path;
}
pub type ExtractionWithWorktree<M> = (ApiSurface<M>, Option<Arc<dyn WorktreeAccess>>);
#[derive(Debug, Clone)]
pub struct ExtendedAnalysisParams {
pub repo: PathBuf,
pub from_ref: String,
pub to_ref: String,
pub dep_dir: Option<PathBuf>,
pub removed_dep_components: Vec<String>,
pub dep_repo_packages: HashMap<String, String>,
pub from_worktree_path: Option<PathBuf>,
pub to_worktree_path: Option<PathBuf>,
pub dead_css_classes_after_swap: Vec<(String, String)>,
}
#[derive(Debug, Clone)]
pub struct LlmCategoryDefinition {
pub id: String,
pub label: String,
pub description: String,
}
pub trait Language:
LanguageSemantics<Self::SymbolData> + MessageFormatter + Send + Sync + 'static
{
type SymbolData: Debug
+ Clone
+ Default
+ PartialEq
+ Eq
+ Serialize
+ DeserializeOwned
+ Send
+ Sync;
type Category: Debug + Clone + Serialize + DeserializeOwned + Eq + std::hash::Hash + Send + Sync;
type ManifestChangeType: Debug
+ Clone
+ Serialize
+ DeserializeOwned
+ Eq
+ PartialEq
+ Send
+ Sync;
type Evidence: Debug + Clone + Serialize + DeserializeOwned + Send + Sync;
type ReportData: Debug + Clone + Serialize + DeserializeOwned + Send + Sync;
type AnalysisExtensions: Debug + Clone + Default + Serialize + DeserializeOwned + Send + Sync;
const RENAMEABLE_SYMBOL_KINDS: &'static [SymbolKind];
const NAME: &'static str;
const MANIFEST_FILES: &'static [&'static str];
fn discover_package_manifests(_repo: &Path, _git_ref: &str) -> Vec<(String, String)> {
vec![]
}
const SOURCE_FILE_PATTERNS: &'static [&'static str];
fn extract(
&self,
repo: &Path,
git_ref: &str,
degradation: Option<&crate::diagnostics::DegradationTracker>,
) -> Result<ApiSurface<Self::SymbolData>>;
fn extract_keeping_worktree(
&self,
repo: &Path,
git_ref: &str,
degradation: Option<&crate::diagnostics::DegradationTracker>,
) -> Result<ExtractionWithWorktree<Self::SymbolData>> {
let surface = self.extract(repo, git_ref, degradation)?;
Ok((surface, None))
}
fn parse_changed_functions(
&self,
repo: &Path,
from_ref: &str,
to_ref: &str,
) -> Result<Vec<ChangedFunction>>;
fn find_callers(&self, file: &Path, symbol_name: &str) -> Result<Vec<Caller>>;
fn find_references(&self, file: &Path, symbol_name: &str) -> Result<Vec<Reference>>;
fn find_tests(&self, repo: &Path, source_file: &Path) -> Result<Vec<TestFile>>;
fn diff_test_assertions(
&self,
repo: &Path,
test_file: &TestFile,
from_ref: &str,
to_ref: &str,
) -> Result<TestDiff>;
fn diff_manifest_content(old: &str, new: &str) -> Vec<crate::types::ManifestChange<Self>>
where
Self: Sized;
fn should_exclude_from_analysis(path: &Path) -> bool;
fn build_report(
&self,
results: &crate::types::AnalysisResult<Self>,
repo: &Path,
from_ref: &str,
to_ref: &str,
) -> crate::types::AnalysisReport<Self>
where
Self: Sized;
fn behavioral_change_kind(&self, _evidence_type: &EvidenceType) -> BehavioralChangeKind {
BehavioralChangeKind::Function
}
fn extract_referenced_symbols(&self, _description: &str) -> Vec<String> {
vec![]
}
fn display_name(&self, qualified_name: &str) -> String {
qualified_name.to_string()
}
fn llm_categories(&self) -> Vec<LlmCategoryDefinition> {
vec![]
}
fn run_extended_analysis(
&self,
_params: &ExtendedAnalysisParams,
) -> Result<Self::AnalysisExtensions> {
Ok(Self::AnalysisExtensions::default())
}
fn finalize_extensions(
&self,
_extensions: &mut Self::AnalysisExtensions,
structural_changes: Arc<Vec<StructuralChange>>,
_repo: &Path,
_from_ref: &str,
_to_ref: &str,
) -> Arc<Vec<StructuralChange>> {
structural_changes
}
fn extensions_log_summary(&self, _extensions: &Self::AnalysisExtensions) -> Vec<String> {
vec![]
}
}
pub fn diff_surfaces_with_semantics<M, S>(
old: &ApiSurface<M>,
new: &ApiSurface<M>,
semantics: &S,
) -> Vec<StructuralChange>
where
M: Default + Clone + PartialEq,
S: LanguageSemantics<M>,
{
crate::diff::diff_surfaces_with_semantics(old, new, semantics)
}
pub fn diff_surfaces<M: Default + Clone + PartialEq>(
old: &ApiSurface<M>,
new: &ApiSurface<M>,
) -> Vec<StructuralChange> {
crate::diff::diff_surfaces(old, new)
}