use incurs::command::{CommandContext, CommandDef, CommandHandler, Example};
use incurs::output::CommandResult;
use serde_json::{json, Value};
use super::engine_from_options;
#[derive(incurs::Args, serde::Deserialize)]
#[allow(dead_code)]
struct ImpactArgs {
file: String,
}
#[derive(incurs::Options, serde::Deserialize)]
#[allow(dead_code)]
struct ImpactOptions {
#[incurs(alias = "r", default = ".")]
repo: String,
#[incurs(alias = "d")]
data_dir: Option<String>,
#[incurs(default = "1")]
depth: i64,
}
struct ImpactHandler;
#[async_trait::async_trait]
impl CommandHandler for ImpactHandler {
async fn run(&self, ctx: CommandContext) -> CommandResult {
let file = match ctx.args.get("file").and_then(|v| v.as_str()) {
Some(f) => f.to_string(),
None => {
return CommandResult::Error {
code: "MISSING_ARG".into(),
message: "Missing required argument: file".into(),
retryable: false,
exit_code: Some(1),
cta: None,
};
}
};
let (mut engine, _repo_path) = match engine_from_options(&ctx.options) {
Ok(v) => v,
Err(e) => return e,
};
if let Err(e) = engine.load_code_tables(&["symbols", "imports"]) {
return CommandResult::Error {
code: "LOAD_ERROR".into(),
message: format!("Failed to load code tables: {e}"),
retryable: false,
exit_code: Some(1),
cta: None,
};
}
let exports_sql = format!(
"SELECT name, kind, line_start, signature, visibility \
FROM symbols \
WHERE file_path LIKE '%{file}%' \
AND (visibility = 'pub' OR visibility = 'public' OR visibility IS NULL) \
ORDER BY line_start"
);
let exported_symbols = match engine.query(&exports_sql) {
Ok(rows) => rows,
Err(e) => {
return CommandResult::Error {
code: "QUERY_ERROR".into(),
message: format!("Exports query failed: {e}"),
retryable: false,
exit_code: Some(1),
cta: None,
};
}
};
let symbol_names: Vec<String> = exported_symbols
.iter()
.filter_map(|s| s.get("name").and_then(|v| v.as_str()).map(String::from))
.collect();
let mut potential_dependents = Vec::new();
if !symbol_names.is_empty() {
let imports_sql = format!(
"SELECT DISTINCT file_path, source \
FROM imports \
WHERE source LIKE '%{file}%'"
);
if let Ok(import_rows) = engine.query(&imports_sql) {
for row in import_rows {
potential_dependents.push(row);
}
}
for name in &symbol_names {
let refs_sql = format!(
"SELECT DISTINCT file_path, name, kind \
FROM symbols \
WHERE file_path NOT LIKE '%{file}%' \
AND (parameters LIKE '%{name}%' OR return_type LIKE '%{name}%') \
LIMIT 20"
);
if let Ok(ref_rows) = engine.query(&refs_sql) {
for row in ref_rows {
potential_dependents.push(row);
}
}
}
}
CommandResult::Ok {
data: json!({
"file_pattern": file,
"exported_symbols": Value::Array(exported_symbols),
"potential_dependents": Value::Array(potential_dependents),
}),
cta: None,
}
}
}
pub fn build() -> CommandDef {
CommandDef::build("impact", ImpactHandler)
.description("Analyze a file's exported symbols and potential dependents")
.args::<ImpactArgs>()
.options::<ImpactOptions>()
.examples(vec![
Example {
command: "src/engine.rs --json".to_string(),
description: Some("Analyze impact of changes to engine.rs".to_string()),
},
])
.done()
}