use crate::engine::ASTRegApply;
use crate::executor::spec::MutationSpec;
use ryo_analysis::{AnalysisContext, CheckError, SymbolPath};
use ryo_mutations::Mutation;
use ryo_symbol::{SymbolId, WorkspaceFilePath};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConvertError {
#[error("Unknown spec kind: {0}")]
UnknownSpec(String),
#[error("Type mismatch in converter: expected {expected}, got {actual}")]
TypeMismatch {
expected: &'static str,
actual: String,
},
#[error("Parse error: {0}")]
Parse(String),
#[error("Target not found: {0}")]
TargetNotFound(String),
#[error("Symbol not found in registry: {0:?}")]
SymbolNotFound(SymbolId),
#[error("Apply error: {0}")]
Apply(String),
#[error("Missing required field '{field}' in {spec_type}")]
MissingField {
field: &'static str,
spec_type: &'static str,
},
#[error("Pre-check failed: {0}")]
PreCheck(#[from] CheckError),
#[error("V2 conversion not supported for this spec kind")]
V2NotSupported,
}
#[derive(Debug, Clone)]
pub struct ApplyResult {
pub changes: usize,
pub affected_files: Vec<WorkspaceFilePath>,
}
impl ApplyResult {
pub fn new(changes: usize, affected_files: Vec<WorkspaceFilePath>) -> Self {
Self {
changes,
affected_files,
}
}
pub fn empty() -> Self {
Self {
changes: 0,
affected_files: vec![],
}
}
pub fn merge(&mut self, other: ApplyResult) {
self.changes += other.changes;
self.affected_files.extend(other.affected_files);
}
}
#[derive(Debug)]
pub struct ResolvedMutation {
pub mutation: Box<dyn Mutation>,
pub target_file: Option<WorkspaceFilePath>,
}
impl ResolvedMutation {
pub fn with_target(mutation: Box<dyn Mutation>, target_file: WorkspaceFilePath) -> Self {
Self {
mutation,
target_file: Some(target_file),
}
}
pub fn all_files(mutation: Box<dyn Mutation>) -> Self {
Self {
mutation,
target_file: None,
}
}
}
pub trait MutationConverter: Send + Sync {
fn spec_kinds(&self) -> &'static [&'static str];
fn can_handle(&self, spec: &MutationSpec) -> bool {
self.spec_kinds().contains(&spec.kind_name())
}
#[deprecated(
since = "0.1.0",
note = "Returns Box<dyn Mutation> for legacy apply(&mut PureFile). Use convert_v2() for ASTRegApply."
)]
fn convert(&self, _spec: &MutationSpec) -> Result<Box<dyn Mutation>, ConvertError> {
Err(ConvertError::V2NotSupported)
}
fn convert_v2(
&self,
_spec: &MutationSpec,
_ctx: &AnalysisContext,
) -> Result<Vec<Box<dyn ASTRegApply>>, ConvertError> {
Err(ConvertError::V2NotSupported)
}
}
pub fn resolve_file_path_from_symbol(
ctx: &AnalysisContext,
path: &SymbolPath,
) -> Result<WorkspaceFilePath, ConvertError> {
use ryo_symbol::FilePathResolver;
if path.is_main_symbol() {
return resolve_main_symbol_file(ctx, path);
}
let symbol_registry = ctx.registry();
let resolver = FilePathResolver::new(ctx.workspace_root.to_path_buf());
if let Some(symbol_id) = symbol_registry.lookup(path) {
if let Some(span) = symbol_registry.span(symbol_id) {
if ctx.files().contains_key(&span.file) {
return Ok(span.file.clone());
}
}
}
let candidates = resolver.resolve_candidates(path);
for candidate in candidates {
if ctx.files().contains_key(&candidate) {
return Ok(candidate);
}
}
Err(ConvertError::TargetNotFound(format!(
"File not found for symbol: {}",
path
)))
}
fn resolve_main_symbol_file(
ctx: &AnalysisContext,
path: &SymbolPath,
) -> Result<WorkspaceFilePath, ConvertError> {
let target_crate = path.main_target_crate().ok_or_else(|| {
ConvertError::TargetNotFound(format!(
"Invalid main symbol path (missing crate name): {}",
path
))
})?;
for file_path in ctx.files().keys() {
if file_path.is_binary_entry() && file_path.crate_name().as_str() == target_crate {
return Ok(file_path.clone());
}
}
Err(ConvertError::TargetNotFound(format!(
"Binary entry file not found for main symbol: {} (crate: {})",
path, target_crate
)))
}
pub fn opt_resolve_file_path_from_symbol(
ctx: &AnalysisContext,
path: &Option<SymbolPath>,
) -> Result<Option<WorkspaceFilePath>, ConvertError> {
match path {
Some(p) => resolve_file_path_from_symbol(ctx, p).map(Some),
None => Ok(None),
}
}