use crate::intent::{
Goal, Intent, ItemKind, SelfParam as IntentSelfParam, SpecRelation as IntentSpecRelation,
SpecRelationKind as IntentSpecRelationKind, StmtInsertPosition as IntentStmtPosition,
Visibility,
};
use ryo_analysis::{SymbolKind, SymbolPath, SymbolRegistry};
use ryo_executor::{
InsertPosition, MutationSpec, MutationTargetSymbol, SelfParam, SpecRelation, SpecRelationKind,
StmtInsertPosition, VariantKind,
};
use ryo_symbol::SymbolId;
use std::collections::HashSet;
#[derive(Debug, thiserror::Error)]
pub enum PlanError {
#[error("Unsupported intent: {0}")]
UnsupportedIntent(String),
#[error("Invalid pattern: {0}")]
InvalidPattern(String),
#[error("Missing required field: {0}")]
MissingField(String),
#[error("Invalid target '{target}': {reason}")]
InvalidTarget { target: String, reason: String },
#[error("Symbol not found: {name} (kind: {kind:?})")]
SymbolNotFound {
name: String,
kind: Option<SymbolKind>,
},
#[error("Duplicate symbol: {name} (kind: {kind:?}, found {count} matches)")]
DuplicateSymbol {
name: String,
kind: Option<SymbolKind>,
count: usize,
},
#[error("SymbolRegistry not available to resolve '{target}'")]
RegistryNotAvailable { target: String },
#[error(
"Cannot resolve target for '{intent}'.\n\n\
Resolution requires one of:\n\
1. symbol_id (recommended): \"symbol_id\": \"42v1\"\n\
→ Use 'ryo discover' to find valid IDs\n\
2. symbol_path (canonical): \"symbol_path\": \"my_crate::module::Type\"\n\
→ Requires full path: crate_name::module::item\n\
→ NG: \"main\", \"crate::xxx\", \"self::xxx\"\n\
3. target_xxx (name only): \"target_type\": \"MyStruct\"\n\
→ Must be unique in workspace\n\n\
Tip: symbol_id is most reliable. Run:\n\
ryo discover \"Pattern*\" --format json\n\
to get symbol IDs for targeting."
)]
CannotResolve { intent: String },
#[error(
"Missing target module for '{intent}'.\n\n\
This intent requires 'symbol_path' to specify where to add the item.\n\n\
Valid symbol_path formats:\n\
✓ \"my_crate\" (crate root / lib.rs)\n\
✓ \"my_crate::module\" (submodule)\n\
✓ \"main::my_app\" (binary crate root / main.rs)\n\
✓ \"main::my_app::module\" (binary crate submodule)\n\n\
Invalid formats:\n\
✗ \"main\" (main:: requires crate name)\n\
✗ \"crate::xxx\" (use actual crate name)\n\
✗ \"self::xxx\", \"super::xxx\" (context-dependent)\n\n\
Example:\n\
{{\n\
\"type\": \"{intent}\",\n\
\"symbol_path\": \"my_crate::domain\",\n\
...\n\
}}"
)]
MissingTargetModule { intent: String },
#[error("Invalid module path: {message}")]
InvalidModulePath { message: String },
#[error("SymbolRegistry required: {message}")]
RegistryRequired { message: String },
#[error("Unknown crate '{crate_name}' in path '{path}'. Known crates: {known_crates}")]
UnknownCrate {
path: String,
crate_name: String,
known_crates: String,
},
}
pub type PlanResult<T> = Result<T, PlanError>;
pub struct Planner;
impl Planner {
pub fn plan(goal: &Goal, registry: Option<&SymbolRegistry>) -> PlanResult<Vec<MutationSpec>> {
let pending_symbols = Self::collect_pending_symbols(&goal.intents);
let mut all_specs = Vec::new();
for intent in &goal.intents {
let specs = Self::intent_to_specs(intent, registry, &pending_symbols)?;
all_specs.extend(specs);
}
Ok(Self::deduplicate_create_mods(all_specs))
}
fn collect_pending_symbols(intents: &[Intent]) -> HashSet<String> {
let mut pending = HashSet::new();
for intent in intents {
if let Intent::AddItem { content, .. } = intent {
if let Some(name) = extract_item_name_from_content(content) {
pending.insert(name);
}
}
}
pending
}
fn deduplicate_create_mods(specs: Vec<MutationSpec>) -> Vec<MutationSpec> {
use std::collections::HashSet;
let mut seen_create_mods: HashSet<(String, String)> = HashSet::new();
let mut result = Vec::with_capacity(specs.len());
for spec in specs {
match &spec {
MutationSpec::CreateMod {
target, mod_name, ..
} => {
let key = (format!("{:?}", target), mod_name.clone());
if seen_create_mods.insert(key) {
result.push(spec);
}
}
_ => {
result.push(spec);
}
}
}
result
}
fn intent_to_specs(
intent: &Intent,
registry: Option<&SymbolRegistry>,
pending_symbols: &HashSet<String>,
) -> PlanResult<Vec<MutationSpec>> {
match intent {
Intent::RenameIdent {
symbol_id,
symbol_path,
target_ident,
to,
..
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_ident.as_deref(),
"RenameIdent",
)?;
Ok(vec![MutationSpec::Rename {
target: MutationTargetSymbol::ById(resolved_id),
to: to.clone(),
scope: ryo_executor::Scope::default(),
}])
}
Intent::ChangeVisibility {
symbol_id,
symbol_path,
target_item,
to,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_item.as_deref(),
"ChangeVisibility",
)?;
Ok(vec![MutationSpec::ChangeVisibility {
target: MutationTargetSymbol::ById(resolved_id),
visibility: visibility_to_spec(*to),
}])
}
Intent::MoveItem {
symbol_id,
symbol_path,
target_item,
to_module,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_item.as_deref(),
"MoveItem",
)?;
let resolved_path =
registry.and_then(|r| r.path(resolved_id)).ok_or_else(|| {
PlanError::CannotResolve {
intent: "MoveItem".to_string(),
}
})?;
let _source = resolved_path
.parent()
.ok_or_else(|| PlanError::InvalidTarget {
target: resolved_path.to_string(),
reason: "Cannot get parent module of item".to_string(),
})?;
let crate_name = resolved_path.crate_name();
let to_path_str = if to_module == "test_crate" || to_module.starts_with("crate::") {
to_module.replacen("test_crate", crate_name, 1)
} else if to_module.contains("::") {
to_module.to_string()
} else {
format!("{}::{}", crate_name, to_module)
};
let to_path = if let Some(reg) = registry {
SymbolPath::parse_validated(&to_path_str, reg).map_err(|e| match e {
ryo_symbol::ParseError::UnknownCrate {
path,
crate_name,
known,
} => PlanError::UnknownCrate {
path,
crate_name,
known_crates: known,
},
other => PlanError::InvalidTarget {
target: to_path_str.clone(),
reason: format!("Invalid to_module: {}", other),
},
})?
} else {
SymbolPath::parse(&to_path_str).map_err(|e| PlanError::InvalidTarget {
target: to_path_str.clone(),
reason: format!("Invalid to_module: {}", e),
})?
};
Ok(vec![MutationSpec::MoveItem {
source: MutationTargetSymbol::ById(resolved_id),
target: MutationTargetSymbol::ByPath(Box::new(to_path)),
item_name: target_item.clone().unwrap_or_default(),
item_kind: ryo_executor::ItemKind::Struct,
add_use: true,
}])
}
Intent::ExtractTrait {
symbol_id,
symbol_path,
target_type,
trait_name,
methods,
} => {
let resolved_id = resolve_impl_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_type.as_deref(),
"ExtractTrait",
)?;
let methods_opt = if methods.is_empty() {
None
} else {
Some(methods.clone())
};
Ok(vec![MutationSpec::ExtractTrait {
target: MutationTargetSymbol::ById(resolved_id),
trait_name: trait_name.clone(),
methods: methods_opt,
}])
}
Intent::InlineTrait {
trait_symbol_id,
trait_symbol_path,
target_trait,
struct_symbol_id: _,
struct_symbol_path: _,
target_struct,
remove_trait,
} => {
let resolved_id = resolve_from_3fields(
registry,
trait_symbol_id.as_deref(),
trait_symbol_path.as_deref(),
target_trait.as_deref(),
"InlineTrait",
)?;
Ok(vec![MutationSpec::InlineTrait {
target: MutationTargetSymbol::ById(resolved_id),
struct_name: target_struct.clone().unwrap_or_default(),
remove_trait: *remove_trait,
}])
}
Intent::EnumToTrait {
symbol_id,
symbol_path,
target_enum,
new_trait_name,
remove_enum,
strategy,
match_handling,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_enum.as_deref(),
"EnumToTrait",
)?;
Ok(vec![MutationSpec::EnumToTrait {
target: MutationTargetSymbol::ById(resolved_id),
trait_name: new_trait_name.clone(),
remove_enum: *remove_enum,
strategy: *strategy,
match_handling: *match_handling,
}])
}
Intent::RemoveMod {
parent_mod,
mod_name,
} => Ok(vec![MutationSpec::RemoveMod {
target: MutationTargetSymbol::ByPath(Box::new(vec_to_symbol_path(
parent_mod, registry,
)?)),
mod_name: mod_name.clone(),
}]),
Intent::CreateMod {
parent_mod,
mod_name,
content,
is_pub,
} => Ok(vec![MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(vec_to_symbol_path(
parent_mod, registry,
)?)),
mod_name: mod_name.clone(),
content: content.clone(),
is_pub: *is_pub,
}]),
Intent::AddField {
symbol_id,
symbol_path,
target_struct,
field_name,
field_type,
is_pub,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_struct.as_deref(),
"AddField",
)?;
Ok(vec![MutationSpec::AddField {
target: MutationTargetSymbol::ById(resolved_id),
field_name: field_name.clone(),
field_type: field_type.clone(),
visibility: if *is_pub {
ryo_executor::Visibility::Pub
} else {
ryo_executor::Visibility::Private
},
}])
}
Intent::RemoveField {
symbol_id,
symbol_path,
target_struct,
field_name,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_struct.as_deref(),
"RemoveField",
)?;
Ok(vec![MutationSpec::RemoveField {
target: MutationTargetSymbol::ById(resolved_id),
field_name: field_name.clone(),
}])
}
Intent::AddDerive {
symbol_id,
symbol_path,
target_type,
derives,
} => {
let resolved_id = match resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_type.as_deref(),
"AddDerive",
) {
Ok(id) => Some(id),
Err(_)
if target_type
.as_ref()
.is_some_and(|n| pending_symbols.contains(n)) =>
{
None
}
Err(e) => return Err(e),
};
Ok(vec![MutationSpec::AddDerive {
target: match resolved_id {
Some(id) => MutationTargetSymbol::ById(id),
None => MutationTargetSymbol::ByKindAndName(
ryo_executor::ItemKind::Struct,
target_type.clone().unwrap_or_default(),
),
},
derives: derives.clone(),
}])
}
Intent::RemoveDerive {
symbol_id,
symbol_path,
target_type,
derives,
} => {
let resolved_id = match resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_type.as_deref(),
"RemoveDerive",
) {
Ok(id) => Some(id),
Err(_)
if target_type
.as_ref()
.is_some_and(|n| pending_symbols.contains(n)) =>
{
None
}
Err(e) => return Err(e),
};
Ok(vec![MutationSpec::RemoveDerive {
target: match resolved_id {
Some(id) => MutationTargetSymbol::ById(id),
None => MutationTargetSymbol::ByKindAndName(
ryo_executor::ItemKind::Struct,
target_type.clone().unwrap_or_default(),
),
},
derives: derives.clone(),
}])
}
Intent::AddEnum {
symbol_path,
name,
variants,
is_pub,
derives,
} => {
let target_path =
SymbolPath::parse(symbol_path).map_err(|e| PlanError::InvalidTarget {
target: symbol_path.clone(),
reason: format!("{}", e),
})?;
let vis = if *is_pub { "pub " } else { "" };
let derives_attr = if derives.is_empty() {
String::new()
} else {
format!("#[derive({})]\n", derives.join(", "))
};
let variants_str = if variants.is_empty() {
String::new()
} else {
variants.join(",\n ")
};
let content = format!(
"{}{}enum {} {{\n {}\n}}",
derives_attr, vis, name, variants_str
);
Ok(vec![MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(target_path)),
content,
position: InsertPosition::Bottom,
}])
}
Intent::AddVariant {
symbol_id,
symbol_path,
target_enum,
variant_name,
variant_type,
} => {
let resolved_id = match resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_enum.as_deref(),
"AddVariant",
) {
Ok(id) => Some(id),
Err(_)
if target_enum
.as_ref()
.is_some_and(|n| pending_symbols.contains(n)) =>
{
None
}
Err(e) => return Err(e),
};
let variant_kind = parse_variant_type(variant_type);
Ok(vec![MutationSpec::AddVariant {
target: match resolved_id {
Some(id) => MutationTargetSymbol::ById(id),
None => MutationTargetSymbol::ByKindAndName(
ryo_executor::ItemKind::Enum,
target_enum.clone().unwrap_or_default(),
),
},
variant_name: variant_name.clone(),
variant_kind,
}])
}
Intent::RemoveVariant {
symbol_id,
symbol_path,
target_enum,
variant_name,
} => {
let resolved_id = match resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_enum.as_deref(),
"RemoveVariant",
) {
Ok(id) => Some(id),
Err(_)
if target_enum
.as_ref()
.is_some_and(|n| pending_symbols.contains(n)) =>
{
None
}
Err(e) => return Err(e),
};
Ok(vec![MutationSpec::RemoveVariant {
target: match resolved_id {
Some(id) => MutationTargetSymbol::ById(id),
None => MutationTargetSymbol::ByKindAndName(
ryo_executor::ItemKind::Enum,
target_enum.clone().unwrap_or_default(),
),
},
variant_name: variant_name.clone(),
}])
}
Intent::AddMatchArm {
symbol_id,
symbol_path,
target_fn,
enum_name,
pattern,
body,
} => {
let target = resolve_target_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_fn.as_deref(),
"AddMatchArm",
)?;
Ok(vec![MutationSpec::AddMatchArm {
target,
enum_name: enum_name.clone(),
pattern: pattern.clone(),
body: body.clone(),
}])
}
Intent::RemoveMatchArm {
symbol_id,
symbol_path,
target_fn,
enum_name,
pattern,
} => {
let target = resolve_target_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_fn.as_deref(),
"RemoveMatchArm",
)?;
Ok(vec![MutationSpec::RemoveMatchArm {
target,
enum_name: enum_name.clone(),
pattern: pattern.clone(),
}])
}
Intent::ReplaceMatchArm {
symbol_id,
symbol_path,
target_fn,
enum_name,
old_pattern,
new_pattern,
new_body,
} => {
let target = resolve_target_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_fn.as_deref(),
"ReplaceMatchArm",
)?;
Ok(vec![MutationSpec::ReplaceMatchArm {
target,
enum_name: enum_name.clone(),
old_pattern: old_pattern.clone(),
new_pattern: new_pattern.clone(),
new_body: new_body.clone(),
}])
}
Intent::AddStructLiteralField {
symbol_id,
symbol_path,
target_struct,
field_name,
value,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_struct.as_deref(),
"AddStructLiteralField",
)?;
Ok(vec![MutationSpec::AddStructLiteralField {
target: MutationTargetSymbol::ById(resolved_id),
field_name: field_name.clone(),
value: value.clone(),
}])
}
Intent::RemoveStructLiteralField {
symbol_id,
symbol_path,
target_struct,
field_name,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_struct.as_deref(),
"RemoveStructLiteralField",
)?;
Ok(vec![MutationSpec::RemoveStructLiteralField {
target: MutationTargetSymbol::ById(resolved_id),
field_name: field_name.clone(),
}])
}
Intent::RemoveStruct {
symbol_id,
symbol_path,
target_struct,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_struct.as_deref(),
"RemoveStruct",
)?;
Ok(vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(resolved_id),
item_kind: ryo_executor::ItemKind::Struct,
}])
}
Intent::RemoveEnum {
symbol_id,
symbol_path,
target_enum,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_enum.as_deref(),
"RemoveEnum",
)?;
Ok(vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(resolved_id),
item_kind: ryo_executor::ItemKind::Enum,
}])
}
Intent::AddConst {
symbol_path,
name,
ty,
value,
is_pub,
} => {
let target_path =
SymbolPath::parse(symbol_path).map_err(|e| PlanError::InvalidTarget {
target: symbol_path.clone(),
reason: format!("{}", e),
})?;
let vis = if *is_pub { "pub " } else { "" };
let content = format!("{}const {}: {} = {};", vis, name, ty, value);
Ok(vec![MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(target_path)),
content,
position: InsertPosition::Bottom,
}])
}
Intent::AddTypeAlias {
symbol_path,
name,
ty,
is_pub,
} => {
let target_path =
SymbolPath::parse(symbol_path).map_err(|e| PlanError::InvalidTarget {
target: symbol_path.clone(),
reason: format!("{}", e),
})?;
let vis = if *is_pub { "pub " } else { "" };
let content = format!("{}type {} = {};", vis, name, ty);
Ok(vec![MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(target_path)),
content,
position: InsertPosition::Bottom,
}])
}
Intent::AddSpec {
symbol_id,
symbol_path,
target_type,
module_id,
module_path,
target_mod,
group,
alias_name,
relations,
} => {
let target_type_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_type.as_deref(),
"AddSpec target_type",
)?;
let resolved_module_id = resolve_from_3fields(
registry,
module_id.as_deref(),
module_path.as_deref(),
target_mod.as_deref(),
"AddSpec module",
)?;
let _target_path = symbol_path
.as_ref()
.and_then(|p| SymbolPath::parse(p).ok())
.or_else(|| registry.and_then(|r| r.path(target_type_id).cloned()));
let executor_relations: Vec<SpecRelation> = relations
.iter()
.map(intent_spec_relation_to_executor)
.collect();
Ok(vec![MutationSpec::AddSpec {
type_id: target_type_id,
module_id: resolved_module_id,
group: group.clone(),
alias_name: alias_name.clone(),
relations: executor_relations,
}])
}
Intent::AddMethod {
symbol_id,
symbol_path,
target_type,
method_name,
params,
return_type,
body,
is_pub,
self_param,
} => {
let target = if let Some(path_str) = symbol_path {
SymbolPath::parse(path_str).ok()
} else if let Some(id_str) = symbol_id {
if let Some(id) = ryo_analysis::SymbolId::parse(id_str) {
registry.and_then(|r| r.path(id).cloned())
} else {
None
}
} else {
None
};
Ok(vec![MutationSpec::AddMethod {
target: match target {
Some(path) => MutationTargetSymbol::ByPath(Box::new(path)),
None => {
let type_name =
strip_generics(&target_type.clone().unwrap_or_default());
MutationTargetSymbol::ByKindAndName(
ryo_executor::ItemKind::Struct,
type_name,
)
}
},
method_name: method_name.clone(),
params: params.clone(),
return_type: return_type.clone(),
body: body.clone(),
is_pub: *is_pub,
self_param: self_param.map(intent_self_param_to_spec),
}])
}
Intent::RemoveMethod {
symbol_id,
symbol_path,
target_type,
method_name,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_type.as_deref(),
"RemoveMethod",
)?;
Ok(vec![MutationSpec::RemoveMethod {
target: MutationTargetSymbol::ById(resolved_id),
method_name: method_name.clone(),
}])
}
Intent::RemoveConst {
symbol_id,
symbol_path,
target_const,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_const.as_deref(),
"RemoveConst",
)?;
Ok(vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(resolved_id),
item_kind: ryo_executor::ItemKind::Const,
}])
}
Intent::RemoveTypeAlias {
symbol_id,
symbol_path,
target_type_alias,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_type_alias.as_deref(),
"RemoveTypeAlias",
)?;
Ok(vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(resolved_id),
item_kind: ryo_executor::ItemKind::TypeAlias,
}])
}
Intent::RemoveUse {
symbol_id,
symbol_path,
target_use,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_use.as_deref(),
"RemoveUse",
)?;
Ok(vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(resolved_id),
item_kind: ryo_executor::ItemKind::Use,
}])
}
Intent::RemoveTrait {
symbol_id,
symbol_path,
target_trait,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_trait.as_deref(),
"RemoveTrait",
)?;
Ok(vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(resolved_id),
item_kind: ryo_executor::ItemKind::Trait,
}])
}
Intent::RemoveImpl {
symbol_id,
symbol_path,
target_type,
trait_name,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_type.as_deref(),
"RemoveImpl",
)?;
let _target = match (target_type.as_ref(), trait_name.as_ref()) {
(Some(s), Some(t)) => Some(format!("{} for {}", t, s)),
(Some(s), None) => Some(s.clone()),
_ => None,
};
Ok(vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(resolved_id),
item_kind: ryo_executor::ItemKind::Impl,
}])
}
Intent::AddItem {
symbol_id,
symbol_path,
target_mod,
content,
item_kind: _,
} => {
let target = resolve_target_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_mod.as_deref(),
"AddItem",
)?;
Ok(vec![MutationSpec::AddItem {
target,
content: content.clone(),
position: InsertPosition::Bottom,
}])
}
Intent::RemoveItem {
symbol_id,
symbol_path,
target_item,
item_kind,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_item.as_deref(),
"RemoveItem",
)?;
Ok(vec![MutationSpec::RemoveItem {
target: MutationTargetSymbol::ById(resolved_id),
item_kind: item_kind_to_spec(*item_kind),
}])
}
Intent::AddCode {
symbol_id,
symbol_path,
target_mod: _,
code,
} => {
let target = if let Some(ref id_str) = symbol_id {
let id = SymbolId::parse(id_str).ok_or_else(|| PlanError::InvalidTarget {
target: id_str.clone(),
reason: "Invalid SymbolId format".to_string(),
})?;
symbol_id_to_symbol_path(id, registry)?
} else if let Some(ref path) = symbol_path {
if let Ok(symbol_path) = SymbolPath::parse(path) {
symbol_path
} else {
let reg = registry.ok_or_else(|| PlanError::CannotResolve {
intent: "AddCode".to_string(),
})?;
let crate_name = reg
.iter()
.next()
.map(|(_, p)| p.crate_name())
.ok_or_else(|| PlanError::CannotResolve {
intent: "AddCode".to_string(),
})?;
file_path_to_symbol_path(path, crate_name)?
}
} else {
return Err(PlanError::MissingTargetModule {
intent: "AddCode".to_string(),
});
};
let mut specs = Vec::new();
let create_mods = if let Some(reg) = registry {
generate_create_mod_specs(&target, reg)
} else {
generate_create_mod_specs_without_registry(&target)
};
specs.extend(create_mods);
specs.push(MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(target)),
content: code.clone(),
position: InsertPosition::Bottom,
});
Ok(specs)
}
Intent::OrganizeImports {
target_mod: _,
deduplicate,
merge_groups,
} => Ok(vec![MutationSpec::OrganizeImports {
module_id: None, deduplicate: *deduplicate,
merge_groups: *merge_groups,
}]),
Intent::MergeImplBlocks {
target_mod: _,
target_type: _,
inherent_only: _,
} => {
Err(PlanError::UnsupportedIntent(
"MergeImplBlocks is not currently supported".to_string(),
))
}
Intent::LoopToIterator {
target_mod: _,
target_var,
} => Ok(vec![MutationSpec::LoopToIterator {
module_id: None, target_var: target_var.clone(),
}]),
Intent::UnwrapToQuestion {
target_mod: _,
target_fn,
include_expect,
} => {
if target_fn.is_some() {
return Err(PlanError::UnsupportedIntent(
"UnwrapToQuestion with target_fn name filtering is not currently supported. Use SymbolId-based targeting instead.".to_string(),
));
}
Ok(vec![MutationSpec::UnwrapToQuestion {
module_id: None,
target_fn: None,
include_expect: *include_expect,
}])
}
Intent::IntroduceVariable {
target_mod: _,
target_fn: _,
expr,
var_name,
} => Ok(vec![MutationSpec::IntroduceVariable {
module_id: None, fn_id: None, expr: expr.clone(),
var_name: var_name.clone(),
}]),
Intent::GenerateBuilder {
symbol_id: _,
symbol_path: _,
target_struct,
target_mod,
fields,
add_builder_method,
} => {
let target_mod_str =
target_mod
.as_deref()
.ok_or_else(|| PlanError::MissingTargetModule {
intent: "GenerateBuilder".to_string(),
})?;
let target = SymbolPath::parse(target_mod_str).map_err(|e| {
PlanError::InvalidModulePath {
message: format!("Failed to parse target_mod '{}': {}", target_mod_str, e),
}
})?;
let struct_name_str = target_struct
.clone()
.unwrap_or_else(|| "Unknown".to_string());
let builder_name = format!("{}Builder", struct_name_str);
let mut specs = Vec::new();
let builder_struct = Self::generate_builder_struct(&builder_name, fields);
specs.push(MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(target.clone())),
content: builder_struct,
position: InsertPosition::Bottom,
});
let builder_impl =
Self::generate_builder_impl(&struct_name_str, &builder_name, fields);
specs.push(MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(target.clone())),
content: builder_impl,
position: InsertPosition::Bottom,
});
if *add_builder_method {
let struct_path =
target
.child(&struct_name_str)
.map_err(|e| PlanError::InvalidTarget {
target: format!("{}::{}", target, struct_name_str),
reason: format!("Failed to create struct path: {}", e),
})?;
specs.push(MutationSpec::AddMethod {
target: MutationTargetSymbol::ByPath(Box::new(struct_path)),
method_name: "builder".to_string(),
params: vec![],
return_type: Some(builder_name.clone()),
body: format!("{}::new()", builder_name),
is_pub: true,
self_param: None,
});
}
Ok(specs)
}
Intent::ReplaceExpr {
target_mod: _,
target_fn: _,
old_expr,
new_expr,
replace_all,
symbol_path,
} => Ok(vec![MutationSpec::ReplaceExpr {
module_id: None, fn_id: None, old_expr: old_expr.clone(),
new_expr: new_expr.clone(),
replace_all: *replace_all,
symbol_path: symbol_path.clone(),
}]),
Intent::RemoveStatement {
target_mod: _,
target_fn: _,
pattern,
remove_all,
symbol_path,
} => Ok(vec![MutationSpec::RemoveStatement {
module_id: None, fn_id: None, pattern: pattern.clone(),
remove_all: *remove_all,
symbol_path: symbol_path.clone(),
}]),
Intent::InsertStatement {
target_mod: _,
target_fn,
stmt,
position,
reference_pattern,
symbol_path,
} => {
let fn_id = if let Some(reg) = registry {
resolve_symbol_by_name(target_fn, SymbolKind::Function, reg)?
} else {
return Err(PlanError::SymbolNotFound {
name: target_fn.clone(),
kind: Some(SymbolKind::Function),
});
};
Ok(vec![MutationSpec::InsertStatement {
module_id: None,
fn_id,
stmt: stmt.clone(),
position: intent_stmt_position_to_spec(position),
reference_pattern: reference_pattern.clone(),
symbol_path: symbol_path.clone(),
}])
}
Intent::ReplaceStatement {
target_mod: _,
target_fn: _,
old_stmt,
new_stmt,
symbol_path,
} => Ok(vec![MutationSpec::ReplaceStatement {
module_id: None, fn_id: None, old_stmt: old_stmt.clone(),
new_stmt: new_stmt.clone(),
symbol_path: symbol_path.clone(),
}]),
Intent::AssignOp {
target_mod: _,
target_fn: _,
} => Ok(vec![MutationSpec::AssignOp {
module_id: None, fn_id: None, }]),
Intent::BoolSimplify { target_mod: _ } => Ok(vec![MutationSpec::BoolSimplify {
module_id: None, }]),
Intent::CloneOnCopy { target_mod: _ } => Ok(vec![MutationSpec::CloneOnCopy {
module_id: None, }]),
Intent::CollapsibleIf { target_mod: _ } => Ok(vec![MutationSpec::CollapsibleIf {
module_id: None, }]),
Intent::ComparisonToMethod { target_mod: _ } => {
Ok(vec![MutationSpec::ComparisonToMethod {
module_id: None, }])
}
Intent::RedundantClosure { target_mod: _ } => {
Ok(vec![MutationSpec::RedundantClosure {
module_id: None, }])
}
Intent::ManualMap { target_mod: _ } => Ok(vec![MutationSpec::ManualMap {
module_id: None, }]),
Intent::MatchToIfLet { target_mod: _ } => Ok(vec![MutationSpec::MatchToIfLet {
module_id: None, }]),
Intent::FilterNext {
target_mod: _,
target_fn: _,
} => Ok(vec![MutationSpec::FilterNext {
module_id: None, fn_id: None, }]),
Intent::MapUnwrapOr {
target_mod: _,
target_fn: _,
} => Ok(vec![MutationSpec::MapUnwrapOr {
module_id: None, fn_id: None, }]),
Intent::DuplicateFunction {
symbol_id,
symbol_path,
target_fn,
to,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_fn.as_deref(),
"DuplicateFunction",
)?;
Ok(vec![MutationSpec::DuplicateFunction {
target: MutationTargetSymbol::ById(resolved_id),
to: to.clone(),
}])
}
Intent::DuplicateStruct {
symbol_id,
symbol_path,
target_struct,
to,
include_impls,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_struct.as_deref(),
"DuplicateStruct",
)?;
Ok(vec![MutationSpec::DuplicateStruct {
target: MutationTargetSymbol::ById(resolved_id),
to: to.clone(),
include_impls: *include_impls,
}])
}
Intent::DuplicateEnum {
symbol_id,
symbol_path,
target_enum,
to,
include_impls,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_enum.as_deref(),
"DuplicateEnum",
)?;
Ok(vec![MutationSpec::DuplicateEnum {
target: MutationTargetSymbol::ById(resolved_id),
to: to.clone(),
include_impls: *include_impls,
}])
}
Intent::DuplicateModTree {
symbol_id,
symbol_path,
target_mod,
to,
} => {
let resolved_id = resolve_from_3fields(
registry,
symbol_id.as_deref(),
symbol_path.as_deref(),
target_mod.as_deref(),
"DuplicateModTree",
)?;
Ok(vec![MutationSpec::DuplicateModTree {
target: MutationTargetSymbol::ById(resolved_id),
to: to.clone(),
}])
}
Intent::Custom { description, .. } => Err(PlanError::UnsupportedIntent(format!(
"Custom intent not directly supported: {}",
description
))),
#[cfg(feature = "wasm-plugin")]
Intent::Plugin {
name,
file_patterns,
} => Ok(vec![MutationSpec::PluginTransform {
plugin_name: name.clone(),
target: None,
file_patterns: file_patterns.clone(),
config: serde_json::Value::Null,
}]),
}
}
fn generate_builder_struct(builder_name: &str, fields: &[(String, String)]) -> String {
let mut code = format!("pub struct {} {{\n", builder_name);
for (name, ty) in fields {
code.push_str(&format!(" {}: Option<{}>,\n", name, ty));
}
code.push('}');
code
}
fn generate_builder_impl(
struct_name: &str,
builder_name: &str,
fields: &[(String, String)],
) -> String {
let mut code = format!("impl {} {{\n", builder_name);
code.push_str(" pub fn new() -> Self {\n");
code.push_str(" Self {\n");
for (name, _) in fields {
code.push_str(&format!(" {}: None,\n", name));
}
code.push_str(" }\n");
code.push_str(" }\n\n");
for (name, ty) in fields {
code.push_str(&format!(
" pub fn {}(mut self, {}: {}) -> Self {{\n",
name, name, ty
));
code.push_str(&format!(" self.{} = Some({});\n", name, name));
code.push_str(" self\n");
code.push_str(" }\n\n");
}
code.push_str(&format!(
" pub fn build(self) -> Result<{}, &'static str> {{\n",
struct_name
));
code.push_str(&format!(" Ok({} {{\n", struct_name));
for (name, _) in fields {
code.push_str(&format!(
" {}: self.{}.ok_or(\"{} is required\")?,\n",
name, name, name
));
}
code.push_str(" })\n");
code.push_str(" }\n");
code.push('}');
code
}
}
fn file_path_to_symbol_path(file_path: &str, crate_name: &str) -> Result<SymbolPath, PlanError> {
let path_str = file_path.trim_start_matches("src/");
let path_str = path_str.trim_end_matches(".rs");
let path_str = path_str.trim_end_matches("/mod");
if path_str == "lib" || path_str.is_empty() {
return SymbolPath::parse(crate_name).map_err(|e| PlanError::InvalidTarget {
target: crate_name.to_string(),
reason: format!("Invalid crate name: {}", e),
});
}
let symbol_str = format!("{}::{}", crate_name, path_str.replace('/', "::"));
SymbolPath::parse(&symbol_str).map_err(|e| PlanError::InvalidTarget {
target: symbol_str.clone(),
reason: format!("Invalid file path: {}", e),
})
}
fn resolve_symbol_by_path(
path: &SymbolPath,
registry: &SymbolRegistry,
) -> PlanResult<ryo_analysis::SymbolId> {
registry
.lookup(path)
.ok_or_else(|| PlanError::SymbolNotFound {
name: path.to_string(),
kind: None,
})
}
fn resolve_symbol_by_name(
name: &str,
kind: SymbolKind,
registry: &SymbolRegistry,
) -> PlanResult<ryo_analysis::SymbolId> {
let matches: Vec<_> = registry
.iter()
.filter(|(id, path)| path.name() == name && registry.kind(*id) == Some(kind))
.collect();
match matches.len() {
0 => Err(PlanError::SymbolNotFound {
name: name.to_string(),
kind: Some(kind),
}),
1 => Ok(matches[0].0),
count => Err(PlanError::DuplicateSymbol {
name: name.to_string(),
kind: Some(kind),
count,
}),
}
}
fn resolve_symbol_by_name_any_kind(
name: &str,
registry: &SymbolRegistry,
) -> PlanResult<ryo_analysis::SymbolId> {
if name.contains("::") {
if let Ok(path) = SymbolPath::parse(name) {
return resolve_symbol_by_path(&path, registry);
}
}
let matches: Vec<_> = registry
.iter()
.filter(|(_, path)| path.name() == name)
.collect();
match matches.len() {
0 => Err(PlanError::SymbolNotFound {
name: name.to_string(),
kind: None,
}),
1 => Ok(matches[0].0),
count => Err(PlanError::DuplicateSymbol {
name: name.to_string(),
kind: None,
count,
}),
}
}
fn resolve_from_3fields(
registry: Option<&SymbolRegistry>,
symbol_id: Option<&str>,
symbol_path: Option<&str>,
target_name: Option<&str>,
context: &str,
) -> PlanResult<ryo_analysis::SymbolId> {
if let Some(id_str) = symbol_id {
return ryo_analysis::SymbolId::parse(id_str).ok_or_else(|| PlanError::InvalidTarget {
target: id_str.to_string(),
reason: "invalid SymbolId format (expected 'NvM' format like '7v2')".to_string(),
});
}
if let Some(path_str) = symbol_path {
let reg = registry.ok_or_else(|| PlanError::RegistryNotAvailable {
target: path_str.to_string(),
})?;
let path = SymbolPath::parse(path_str).map_err(|e| PlanError::InvalidTarget {
target: path_str.to_string(),
reason: format!("invalid SymbolPath: {:?}", e),
})?;
return resolve_symbol_by_path(&path, reg);
}
if let Some(name) = target_name {
let reg = registry.ok_or_else(|| PlanError::RegistryNotAvailable {
target: name.to_string(),
})?;
return resolve_symbol_by_name_any_kind(name, reg);
}
Err(PlanError::CannotResolve {
intent: context.to_string(),
})
}
fn resolve_target_from_3fields(
registry: Option<&SymbolRegistry>,
symbol_id: Option<&str>,
symbol_path: Option<&str>,
module_name: Option<&str>,
context: &str,
) -> PlanResult<MutationTargetSymbol> {
if let Some(id_str) = symbol_id {
let id = ryo_analysis::SymbolId::parse(id_str).ok_or_else(|| PlanError::InvalidTarget {
target: id_str.to_string(),
reason: "invalid SymbolId format (expected 'NvM' format like '7v2')".to_string(),
})?;
return Ok(MutationTargetSymbol::ById(id));
}
if let Some(path_str) = symbol_path {
let path = if let Some(reg) = registry {
SymbolPath::parse_validated(path_str, reg).map_err(|e| match e {
ryo_symbol::ParseError::UnknownCrate {
path,
crate_name,
known,
} => PlanError::UnknownCrate {
path,
crate_name,
known_crates: known,
},
other => PlanError::InvalidTarget {
target: path_str.to_string(),
reason: format!("invalid SymbolPath: {:?}", other),
},
})?
} else {
SymbolPath::parse(path_str).map_err(|e| PlanError::InvalidTarget {
target: path_str.to_string(),
reason: format!("invalid SymbolPath: {:?}", e),
})?
};
return Ok(MutationTargetSymbol::ByPath(Box::new(path)));
}
if let Some(name) = module_name {
let reg = registry.ok_or_else(|| PlanError::RegistryNotAvailable {
target: name.to_string(),
})?;
let id = resolve_symbol_by_name_any_kind(name, reg)?;
return Ok(MutationTargetSymbol::ById(id));
}
Err(PlanError::CannotResolve {
intent: format!(
"{}: at least one of symbol_id, symbol_path, or target_mod must be specified",
context
),
})
}
fn resolve_impl_from_3fields(
registry: Option<&SymbolRegistry>,
symbol_id: Option<&str>,
symbol_path: Option<&str>,
target_type_name: Option<&str>,
context: &str,
) -> PlanResult<ryo_analysis::SymbolId> {
if let Some(id_str) = symbol_id {
return ryo_analysis::SymbolId::parse(id_str).ok_or_else(|| PlanError::InvalidTarget {
target: id_str.to_string(),
reason: "invalid SymbolId format (expected 'NvM' format like '7v2')".to_string(),
});
}
if let Some(path_str) = symbol_path {
let reg = registry.ok_or_else(|| PlanError::RegistryNotAvailable {
target: path_str.to_string(),
})?;
let path = SymbolPath::parse(path_str).map_err(|e| PlanError::InvalidTarget {
target: path_str.to_string(),
reason: format!("invalid SymbolPath: {:?}", e),
})?;
return resolve_symbol_by_path(&path, reg);
}
if let Some(type_name) = target_type_name {
let reg = registry.ok_or_else(|| PlanError::RegistryNotAvailable {
target: type_name.to_string(),
})?;
let impl_name = format!("<impl {}>", type_name);
let normalized_expected = normalize_generic_name(&impl_name);
let matches: Vec<_> = reg
.iter()
.filter(|(id, path)| {
reg.kind(*id) == Some(SymbolKind::Impl)
&& normalize_generic_name(path.name()) == normalized_expected
})
.collect();
return match matches.len() {
0 => Err(PlanError::SymbolNotFound {
name: impl_name,
kind: Some(SymbolKind::Impl),
}),
1 => Ok(matches[0].0),
count => Err(PlanError::DuplicateSymbol {
name: impl_name,
kind: Some(SymbolKind::Impl),
count,
}),
};
}
Err(PlanError::CannotResolve {
intent: context.to_string(),
})
}
fn normalize_generic_name(name: &str) -> String {
name.replace(" < ", "<")
.replace(" > ", ">")
.replace("< ", "<")
.replace(" >", ">")
.replace(", ", ",")
.replace(" ,", ",")
}
fn strip_generics(name: &str) -> String {
if let Some(idx) = name.find('<') {
name[..idx].to_string()
} else {
name.to_string()
}
}
fn vec_to_symbol_path(
segments: &[String],
registry: Option<&SymbolRegistry>,
) -> PlanResult<SymbolPath> {
if segments.iter().any(|s| s == "test_crate") {
return Err(PlanError::InvalidModulePath {
message: format!(
"Module path must NOT contain 'crate' literal (got: {:?}). \
Use empty array [] for crate root, or actual module names like ['infrastructure', 'memory']",
segments
),
});
}
let reg = registry.ok_or_else(|| PlanError::RegistryRequired {
message: "Cannot resolve module path without SymbolRegistry".to_string(),
})?;
let crate_name_str = reg
.iter()
.next()
.map(|(_, path)| path.crate_name().to_string())
.ok_or_else(|| PlanError::RegistryRequired {
message: "Registry is empty - cannot determine crate name".to_string(),
})?;
let crate_name = crate_name_str.as_str();
if segments.is_empty() {
SymbolPath::parse(crate_name).map_err(|e| PlanError::InvalidModulePath {
message: format!("Failed to create crate root path '{}': {}", crate_name, e),
})
} else {
let mut full_path = vec![crate_name];
full_path.extend(segments.iter().map(|s| s.as_str()));
SymbolPath::from_segments(full_path.iter().copied()).map_err(|e| {
PlanError::InvalidModulePath {
message: format!("Failed to create module path '{:?}': {}", full_path, e),
}
})
}
}
fn visibility_to_spec(vis: Visibility) -> ryo_executor::Visibility {
match vis {
Visibility::Private => ryo_executor::Visibility::Private,
Visibility::Pub => ryo_executor::Visibility::Pub,
Visibility::PubCrate => ryo_executor::Visibility::PubCrate,
Visibility::PubSuper => ryo_executor::Visibility::PubSuper,
}
}
fn intent_stmt_position_to_spec(pos: &IntentStmtPosition) -> StmtInsertPosition {
match pos {
IntentStmtPosition::Start => StmtInsertPosition::Start,
IntentStmtPosition::End => StmtInsertPosition::End,
IntentStmtPosition::BeforePattern => StmtInsertPosition::BeforePattern,
IntentStmtPosition::AfterPattern => StmtInsertPosition::AfterPattern,
}
}
fn item_kind_to_spec(kind: ItemKind) -> ryo_executor::ItemKind {
match kind {
ItemKind::Struct => ryo_executor::ItemKind::Struct,
ItemKind::Enum => ryo_executor::ItemKind::Enum,
ItemKind::Trait => ryo_executor::ItemKind::Trait,
ItemKind::Impl => ryo_executor::ItemKind::Impl,
ItemKind::Function => ryo_executor::ItemKind::Function,
ItemKind::Const => ryo_executor::ItemKind::Const,
ItemKind::Static => ryo_executor::ItemKind::Static,
ItemKind::TypeAlias => ryo_executor::ItemKind::TypeAlias,
ItemKind::Use => ryo_executor::ItemKind::Use,
ItemKind::Mod => ryo_executor::ItemKind::Mod,
ItemKind::Macro => ryo_executor::ItemKind::Macro,
ItemKind::Method => ryo_executor::ItemKind::Function,
ItemKind::Field | ItemKind::TupleField => ryo_executor::ItemKind::Struct,
ItemKind::Variant => ryo_executor::ItemKind::Enum,
ItemKind::LocalVar | ItemKind::Parameter => ryo_executor::ItemKind::Function,
ItemKind::Any | ItemKind::Other => ryo_executor::ItemKind::Struct, }
}
fn extract_item_name_from_content(content: &str) -> Option<String> {
let trimmed = content.trim();
let mut lines = trimmed.lines();
let mut decl_line = "";
for line in lines.by_ref() {
let line = line.trim();
if !line.starts_with('#') && !line.starts_with("//") && !line.is_empty() {
decl_line = line;
break;
}
}
let tokens: Vec<&str> = decl_line.split_whitespace().collect();
if tokens.is_empty() {
return None;
}
let mut idx = 0;
if tokens.get(idx) == Some(&"pub") {
idx += 1;
if let Some(t) = tokens.get(idx) {
if t.starts_with('(') {
idx += 1;
}
}
}
let keyword = tokens.get(idx)?;
idx += 1;
match *keyword {
"struct" | "enum" | "fn" | "type" | "const" | "static" | "trait" | "mod" => {
let name = tokens.get(idx)?;
let name = name.split('<').next().unwrap_or(name);
let name = name.trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_');
Some(name.to_string())
}
"impl" => {
None
}
_ => None,
}
}
fn symbol_id_to_symbol_path(
id: ryo_analysis::SymbolId,
registry: Option<&SymbolRegistry>,
) -> Result<SymbolPath, PlanError> {
if let Some(reg) = registry {
if let Some(path) = reg.resolve(id) {
return Ok(path.clone());
}
}
Err(PlanError::InvalidTarget {
target: format!("SymbolId({:?})", id),
reason: "SymbolId not found in registry. SymbolId is an internal identifier \
that requires SymbolRegistry for resolution. Ensure the registry is \
provided and contains this symbol (from Discover operation)."
.to_string(),
})
}
fn generate_create_mod_specs_without_registry(target: &SymbolPath) -> Vec<MutationSpec> {
let segments: Vec<&str> = target.segments().collect();
let mut specs = Vec::new();
for depth in 1..segments.len() {
let parent_path = segments[..depth].join("::");
let mod_name = segments[depth].to_string();
if let Ok(parent) = SymbolPath::parse(&parent_path) {
specs.push(MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(parent)),
mod_name,
content: String::new(),
is_pub: true, });
}
}
specs
}
fn generate_create_mod_specs(target: &SymbolPath, registry: &SymbolRegistry) -> Vec<MutationSpec> {
let segments: Vec<&str> = target.segments().collect();
let mut existing_depth = 0;
for depth in 1..=segments.len() {
let partial_path = segments[..depth].join("::");
if let Ok(path) = SymbolPath::parse(&partial_path) {
if registry.lookup(&path).is_some() {
existing_depth = depth;
} else {
break;
}
}
}
let mut specs = Vec::new();
for depth in existing_depth..segments.len() {
if depth == 0 {
continue;
}
let parent_path = segments[..depth].join("::");
let mod_name = segments[depth].to_string();
if let Ok(parent) = SymbolPath::parse(&parent_path) {
specs.push(MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(parent)),
mod_name,
content: String::new(),
is_pub: true, });
}
}
specs
}
fn parse_variant_type(variant_type: &str) -> VariantKind {
if variant_type == "unit" || variant_type.is_empty() {
VariantKind::Unit
} else if let Some(types) = variant_type.strip_prefix("tuple:") {
let types: Vec<String> = types.split(',').map(|s| s.trim().to_string()).collect();
VariantKind::Tuple { types }
} else if let Some(fields) = variant_type.strip_prefix("struct:") {
let fields: Vec<(String, String)> = fields
.split(',')
.filter_map(|f| {
let parts: Vec<&str> = f.trim().split(':').collect();
if parts.len() == 2 {
Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
} else {
None
}
})
.collect();
VariantKind::Struct { fields }
} else {
VariantKind::Unit
}
}
fn intent_self_param_to_spec(intent_param: IntentSelfParam) -> SelfParam {
match intent_param {
IntentSelfParam::Ref => SelfParam::Ref,
IntentSelfParam::Mut => SelfParam::Mut,
IntentSelfParam::Owned => SelfParam::Owned,
}
}
fn intent_spec_relation_to_executor(rel: &IntentSpecRelation) -> SpecRelation {
SpecRelation {
kind: intent_spec_relation_kind_to_executor(&rel.kind),
target: rel.target.clone(),
symbol_id: None,
target_path: None,
}
}
fn intent_spec_relation_kind_to_executor(kind: &IntentSpecRelationKind) -> SpecRelationKind {
match kind {
IntentSpecRelationKind::DependsOn => SpecRelationKind::DependsOn,
IntentSpecRelationKind::RelatedTo => SpecRelationKind::RelatedTo,
IntentSpecRelationKind::PartOf => SpecRelationKind::PartOf,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::intent::IdentKind;
#[test]
fn test_rename_intent() {
use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
let mut registry = SymbolRegistry::new();
let path = SymbolPath::parse("test_crate::foo").unwrap();
let symbol_id = registry.register(path, SymbolKind::Function).unwrap();
let goal = Goal::new(
"rename foo to bar".to_string(),
Intent::RenameIdent {
symbol_id: Some(format!("{:?}", symbol_id)),
symbol_path: None,
target_ident: Some("foo".to_string()),
to: "bar".to_string(),
kind: IdentKind::Any,
},
);
let specs = Planner::plan(&goal, Some(®istry)).unwrap();
assert_eq!(specs.len(), 1);
match &specs[0] {
MutationSpec::Rename { target, to, .. } => {
assert_eq!(*target, MutationTargetSymbol::ById(symbol_id));
assert_eq!(to, "bar");
}
_ => panic!("Expected Rename spec"),
}
}
#[test]
fn test_add_field_intent() {
let dummy_id = ryo_analysis::SymbolId::parse("0v1").expect("valid dummy id");
let goal = Goal::new(
"add field".to_string(),
Intent::AddField {
symbol_id: Some(format!("{:?}", dummy_id)),
symbol_path: None,
target_struct: Some("User".to_string()),
field_name: "email".to_string(),
field_type: "String".to_string(),
is_pub: true,
},
);
let specs = Planner::plan(&goal, None).unwrap();
assert_eq!(specs.len(), 1);
match &specs[0] {
MutationSpec::AddField {
field_name,
field_type,
visibility,
..
} => {
assert_eq!(field_name, "email");
assert_eq!(field_type, "String");
assert_eq!(*visibility, ryo_executor::Visibility::Pub);
}
_ => panic!("Expected AddField spec"),
}
}
#[test]
fn test_add_code_with_parent_symbol_path() {
let goal = Goal::new(
"add code".to_string(),
Intent::AddCode {
symbol_id: None,
symbol_path: Some("test_crate::domain::model".to_string()),
target_mod: None,
code: "pub struct User { pub id: u64 }".to_string(),
},
);
let specs = Planner::plan(&goal, None).unwrap();
assert_eq!(specs.len(), 3);
match specs.last().unwrap() {
MutationSpec::AddItem {
target,
content,
position,
} => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate::domain::model");
} else {
panic!("Expected ByPath target");
}
assert_eq!(content, "pub struct User { pub id: u64 }");
assert_eq!(*position, InsertPosition::Bottom);
}
_ => panic!("Expected AddItem spec"),
}
}
#[test]
fn test_add_code_with_nested_symbol_path() {
let goal = Goal::new(
"add code".to_string(),
Intent::AddCode {
symbol_id: None,
symbol_path: Some("test_crate::domain::model".to_string()),
target_mod: None,
code: "pub struct Order { pub id: u64 }".to_string(),
},
);
let specs = Planner::plan(&goal, None).unwrap();
assert_eq!(specs.len(), 3);
match specs.last().unwrap() {
MutationSpec::AddItem {
target, content, ..
} => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate::domain::model");
} else {
panic!("Expected ByPath target");
}
assert_eq!(content, "pub struct Order { pub id: u64 }");
}
_ => panic!("Expected AddItem spec"),
}
}
#[test]
fn test_add_code_with_parent_ref_symbol_path() {
let goal = Goal::new(
"add code".to_string(),
Intent::AddCode {
symbol_id: None,
symbol_path: Some("test_crate::usecase".to_string()),
target_mod: None,
code: "pub fn create_user() {}".to_string(),
},
);
let specs = Planner::plan(&goal, None).unwrap();
assert_eq!(specs.len(), 2);
match specs.last().unwrap() {
MutationSpec::AddItem {
target, content, ..
} => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate::usecase");
} else {
panic!("Expected ByPath target");
}
assert_eq!(content, "pub fn create_user() {}");
}
_ => panic!("Expected AddItem spec"),
}
}
#[test]
fn test_add_code_with_single_module_path() {
let goal = Goal::new(
"add code".to_string(),
Intent::AddCode {
symbol_id: None,
symbol_path: Some("test_crate::handlers".to_string()),
target_mod: None,
code: "pub fn handle() {}".to_string(),
},
);
let specs = Planner::plan(&goal, None).unwrap();
assert_eq!(specs.len(), 2);
match specs.last().unwrap() {
MutationSpec::AddItem {
target, content, ..
} => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate::handlers");
} else {
panic!("Expected ByPath target");
}
assert_eq!(content, "pub fn handle() {}");
}
_ => panic!("Expected AddItem spec"),
}
}
#[test]
fn test_add_code_to_crate_root() {
use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
let mut registry = SymbolRegistry::new();
let crate_root = SymbolPath::parse("test_crate").unwrap();
registry
.register(crate_root.clone(), SymbolKind::Mod)
.unwrap();
let goal = Goal::new(
"add code".to_string(),
Intent::AddCode {
symbol_id: None,
symbol_path: Some("test_crate".to_string()),
target_mod: None,
code: "pub const VERSION: &str = \"1.0\";".to_string(),
},
);
let specs = Planner::plan(&goal, Some(®istry)).unwrap();
assert_eq!(specs.len(), 1);
match &specs[0] {
MutationSpec::AddItem {
target, content, ..
} => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate");
} else {
panic!("Expected ByPath target");
}
assert_eq!(content, "pub const VERSION: &str = \"1.0\";");
}
_ => panic!("Expected AddItem spec"),
}
}
#[test]
fn test_add_code_missing_symbol_path_returns_error() {
let goal = Goal::new(
"add code".to_string(),
Intent::AddCode {
symbol_id: None,
symbol_path: None,
target_mod: None,
code: "pub const VERSION: &str = \"1.0\";".to_string(),
},
);
let result = Planner::plan(&goal, None);
assert!(matches!(
result,
Err(PlanError::MissingTargetModule { intent }) if intent == "AddCode"
));
}
#[test]
fn test_add_code_generates_create_mod_for_missing_modules() {
use ryo_analysis::{SymbolKind, SymbolRegistry};
let mut registry = SymbolRegistry::new();
let crate_path = SymbolPath::parse("test_crate").unwrap();
registry.register(crate_path, SymbolKind::Mod).unwrap();
let goal = Goal::new(
"add code to nested module".to_string(),
Intent::AddCode {
symbol_id: None,
symbol_path: Some("test_crate::domain::model".to_string()),
target_mod: None,
code: "pub struct Entity;".to_string(),
},
);
let specs = Planner::plan(&goal, Some(®istry)).unwrap();
assert_eq!(specs.len(), 3);
match &specs[0] {
MutationSpec::CreateMod {
target,
mod_name,
is_pub,
..
} => {
match target {
ryo_executor::MutationTargetSymbol::ByPath(path) => {
assert_eq!(path.to_string().as_str(), "test_crate");
}
_ => panic!("Expected ByPath variant"),
}
assert_eq!(mod_name, "domain");
assert!(*is_pub);
}
_ => panic!("Expected CreateMod spec for domain"),
}
match &specs[1] {
MutationSpec::CreateMod {
target,
mod_name,
is_pub,
..
} => {
match target {
ryo_executor::MutationTargetSymbol::ByPath(path) => {
assert_eq!(path.to_string().as_str(), "test_crate::domain");
}
_ => panic!("Expected ByPath variant"),
}
assert_eq!(mod_name, "model");
assert!(*is_pub);
}
_ => panic!("Expected CreateMod spec for model"),
}
match &specs[2] {
MutationSpec::AddItem {
target, content, ..
} => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate::domain::model");
} else {
panic!("Expected ByPath target");
}
assert_eq!(content, "pub struct Entity;");
}
_ => panic!("Expected AddItem spec"),
}
}
#[test]
fn test_add_code_no_create_mod_when_module_exists() {
use ryo_analysis::{SymbolKind, SymbolRegistry};
let mut registry = SymbolRegistry::new();
registry
.register(SymbolPath::parse("test_crate").unwrap(), SymbolKind::Mod)
.unwrap();
registry
.register(
SymbolPath::parse("test_crate::domain").unwrap(),
SymbolKind::Mod,
)
.unwrap();
let goal = Goal::new(
"add code to existing module".to_string(),
Intent::AddCode {
symbol_id: None,
symbol_path: Some("test_crate::domain".to_string()),
target_mod: None,
code: "pub struct User;".to_string(),
},
);
let specs = Planner::plan(&goal, Some(®istry)).unwrap();
assert_eq!(specs.len(), 1);
match &specs[0] {
MutationSpec::AddItem { target, .. } => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate::domain");
} else {
panic!("Expected ByPath target");
}
}
_ => panic!("Expected AddItem spec"),
}
}
#[test]
fn test_add_code_without_registry_generates_create_mods() {
let goal = Goal::new(
"add code without registry".to_string(),
Intent::AddCode {
symbol_id: None,
symbol_path: Some("test_crate::infrastructure::memory".to_string()),
target_mod: None,
code: "pub struct InMemoryRepo;".to_string(),
},
);
let specs = Planner::plan(&goal, None).unwrap();
assert_eq!(
specs.len(),
3,
"Expected 3 specs (2 CreateMod + 1 AddItem), got {}",
specs.len()
);
match &specs[0] {
MutationSpec::CreateMod {
target,
mod_name,
is_pub,
..
} => {
match target {
ryo_executor::MutationTargetSymbol::ByPath(path) => {
assert_eq!(path.to_string().as_str(), "test_crate");
}
_ => panic!("Expected ByPath variant"),
}
assert_eq!(mod_name, "infrastructure");
assert!(*is_pub);
}
_ => panic!(
"Expected CreateMod spec for infrastructure, got {:?}",
specs[0]
),
}
match &specs[1] {
MutationSpec::CreateMod {
target,
mod_name,
is_pub,
..
} => {
match target {
ryo_executor::MutationTargetSymbol::ByPath(path) => {
assert_eq!(path.to_string().as_str(), "test_crate::infrastructure");
}
_ => panic!("Expected ByPath variant"),
}
assert_eq!(mod_name, "memory");
assert!(*is_pub);
}
_ => panic!("Expected CreateMod spec for memory, got {:?}", specs[1]),
}
match &specs[2] {
MutationSpec::AddItem {
target, content, ..
} => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate::infrastructure::memory");
} else {
panic!("Expected ByPath target");
}
assert_eq!(content, "pub struct InMemoryRepo;");
}
_ => panic!("Expected AddItem spec, got {:?}", specs[2]),
}
}
#[test]
fn test_add_code_crate_root_no_create_mod() {
let goal = Goal::new(
"add code to crate root".to_string(),
Intent::AddCode {
symbol_id: None,
symbol_path: Some("test_crate".to_string()), target_mod: None,
code: "pub const VERSION: &str = \"1.0\";".to_string(),
},
);
let specs = Planner::plan(&goal, None).unwrap();
assert_eq!(specs.len(), 1);
match &specs[0] {
MutationSpec::AddItem { target, .. } => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate");
} else {
panic!("Expected ByPath target");
}
}
_ => panic!("Expected AddItem spec"),
}
}
#[test]
fn test_generate_builder_intent() {
let goal = Goal::new(
"generate builder".to_string(),
Intent::GenerateBuilder {
symbol_id: None,
symbol_path: None,
target_struct: Some("Config".to_string()),
target_mod: Some("test_crate::config".to_string()),
fields: vec![
("host".to_string(), "String".to_string()),
("port".to_string(), "u16".to_string()),
],
add_builder_method: true,
},
);
let specs = Planner::plan(&goal, None).unwrap();
assert_eq!(specs.len(), 3);
match &specs[0] {
MutationSpec::AddItem {
target,
content,
position,
} => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate::config");
} else {
panic!("Expected ByPath target");
}
assert!(content.contains("pub struct ConfigBuilder"));
assert!(content.contains("host: Option<String>"));
assert!(content.contains("port: Option<u16>"));
assert!(matches!(position, InsertPosition::Bottom));
}
_ => panic!("Expected AddItem spec for Builder struct"),
}
match &specs[1] {
MutationSpec::AddItem {
target, content, ..
} => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate::config");
} else {
panic!("Expected ByPath target");
}
assert!(content.contains("impl ConfigBuilder"));
assert!(content.contains("pub fn new()"));
assert!(content.contains("pub fn host("));
assert!(content.contains("pub fn port("));
assert!(content.contains("pub fn build("));
}
_ => panic!("Expected AddItem spec for Builder impl"),
}
match &specs[2] {
MutationSpec::AddMethod {
method_name,
return_type,
..
} => {
assert_eq!(method_name, "builder");
assert_eq!(return_type.as_deref(), Some("ConfigBuilder"));
}
_ => panic!("Expected AddMethod spec"),
}
}
#[test]
fn test_generate_builder_without_builder_method() {
let goal = Goal::new(
"generate builder".to_string(),
Intent::GenerateBuilder {
symbol_id: None,
symbol_path: None,
target_struct: Some("User".to_string()),
target_mod: Some("test_crate".to_string()), fields: vec![("name".to_string(), "String".to_string())],
add_builder_method: false,
},
);
let specs = Planner::plan(&goal, None).unwrap();
assert_eq!(specs.len(), 2);
match &specs[0] {
MutationSpec::AddItem { target, .. } => {
if let MutationTargetSymbol::ByPath(path) = target {
assert_eq!(path.to_string(), "test_crate");
} else {
panic!("Expected ByPath target");
}
}
_ => panic!("Expected AddItem spec"),
}
}
#[test]
fn test_generate_builder_missing_target_mod_returns_error() {
let goal = Goal::new(
"generate builder".to_string(),
Intent::GenerateBuilder {
symbol_id: None,
symbol_path: None,
target_struct: Some("User".to_string()),
target_mod: None,
fields: vec![("name".to_string(), "String".to_string())],
add_builder_method: false,
},
);
let result = Planner::plan(&goal, None);
assert!(matches!(
result,
Err(PlanError::MissingTargetModule { intent }) if intent == "GenerateBuilder"
));
}
}